overall fixes and refactorings for alpha testers

This commit is contained in:
dcore94 2022-07-18 18:08:53 +02:00
parent b2b1caf9db
commit c91a3dbdfe
7 changed files with 386 additions and 92 deletions

View File

@ -6,15 +6,15 @@ class CCPExecutionForm extends HTMLElement{
#method;
#executionmonitor;
#serviceurl = "https://nubis1.int.d4science.net:8080"
#cdnurl = "https://cdn.dev.d4science.org/ccp/executionformfragment.html"
//#cdnurl = "http://d4science-cdn-public:8984/resources/ccp/executionformfragment.html"
#serviceurl;
constructor(){
super()
this.#boot = document.querySelector("d4s-boot-2")
this.#serviceurl = this.getAttribute("serviceurl")
this.#rootdoc = this.attachShadow({ "mode" : "open"})
this.fetchMarkup()
this.render()
this.showMethod()
}
static get observedAttributes() {
@ -28,18 +28,68 @@ class CCPExecutionForm extends HTMLElement{
}
}
fetchMarkup(){
return fetch(this.#cdnurl).then(
(reply)=>{
if(reply.status === 200){
return reply.text()
}else{ throw new Exception("Unable to fetch markup") }
}).then(data=>{
this.#rootdoc.innerHTML = data
this.showMethod()
}).catch(err=>{
console.error(err)
})
render(){
this.#rootdoc.innerHTML = `
<div>
<link rel="stylesheet" href="https://cdn.dev.d4science.org/ccp/css/common.css"></link>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<style>
</style>
<template id="EXECUTION_FORM_TEMPLATE">
<div class="ccp-execution-form" name="execution_form">
<h5 class="ccp-method-title"></h5>
<p class="description font-italic font-weight-lighter"></p>
<div name="plexiglass" class="ccp-invisible">
<span name="status"></span>
<span class="fas fa-spinner fa-spin"></span>
</div>
<form name="execution_form" class="d-flex flex-column gap-3" style="gap:5px">
<div class="card">
<div class="card-header">
<h5>Inputs</h5>
</div>
<div class="card-body ccp-inputs">
<div class="card-body">
<div class="form-group"></div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5>Outputs</h5>
</div>
<div class="card-body">
<div class="form-row ccp-outputs">
<div class="col form-group"></div>
</div>
</div>
</div>
<!--div class="card">
<div class="card-header">
<h5>Runtimes <span alt="refresh" title="refresh" style="cursor:pointer" name="refresh-runtimes" class="text-info">&#8634;</span></h5>
</div>
<div class="card-body">
<div class="ccp-runtimes">
<div class="form-row">
<select class="form-control">
</select>
</div>
</div>
</div>
</div-->
<button id="execute_method_button" class="btn btn-info">Execute</button>
</form>
</div>
</template>
<template id="EXECUTION_FORM_EMPTY_TEMPLATE">
<div name="execution_form">
<i style="padding:3rem">Select a method</i>
</div>
</template>
<div name="execution_form"></div>
</div>
`
}
loadMethod(){

View File

@ -49,12 +49,18 @@ class CCPInfrastructureList extends HTMLElement{
</style>
`;
#serviceurl = "https://nubis1.int.d4science.net:8080"
#broadcasturl = "ws://nubis1.int.d4science.net:8989/ws/notification"
#serviceurl;
#broadcasturl;
constructor(){
super()
this.#boot = document.querySelector("d4s-boot-2")
this.#serviceurl = this.getAttribute("serviceurl")
this.#broadcasturl = this.getAttribute("broadcasturl")
if(!this.#broadcasturl){
this.#broadcasturl = this.#serviceurl.replace(/^http/, "ws")
}
this.#broadcasturl = this.#broadcasturl + "/ws/notification"
this.#rootdoc = this.attachShadow({ "mode" : "open"})
}

View File

@ -79,11 +79,16 @@ class Renderer{
}
static isCode(input){
return (input.schema.type === "string") && ("format" in input.schema) && (input.schema.format.toLowerCase() === "code")
return (input.schema.type === "string") &&
("format" in input.schema) &&
(input.schema.format != null) &&
(input.schema.format.toLowerCase() === "code")
}
static isDateTime(input){
return (input.schema.type === "string") && ("format" in input.schema) &&
return (input.schema.type === "string") &&
("format" in input.schema) &&
(input.schema.format != null) &&
(["date", "time", "datetime"].indexOf(input.schema.format.toLowerCase()) !== -1)
}
}

View File

@ -30,7 +30,7 @@ class CCPInputWidgetEditorController extends HTMLElement{
this.innerHTML = `
<details>
<summary class="mb-3">
<input class="form-control" style="width:auto;display:inline" required="required" name="id" value="${this.#input.id}"/>
<input class="form-control" style="width:auto;display:inline" required="required" name="id" value="${this.#input.id}" title="Id of input"/>
<button data-index="${this.#index}" name="delete-input" title="Delete" class="btn btn-danger ccp-toolbar-button">
${this.#delete_icon}
</button>
@ -38,19 +38,19 @@ class CCPInputWidgetEditorController extends HTMLElement{
<div style="padding-left: 1rem;border-left: 1px solid gray;">
<div class="row mb-3">
<div class="col">
<div class="form-field">
<div class="form-field" title="Title">
<input name="title" class="form-control" placeholder="title" value="${this.#input.title}" required="required"/>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-field">
<div class="form-field" title="description">
<input name="description" class="form-control" placeholder="description" value="${this.#input.description}" required="required"/>
</div>
</div>
<div class="row mb-3">
<div class="col-3">
<div class="form-field">
<div class="form-field" title="Type">
<label>Type</label>
<select name="type" class="form-control" placeholder="type" value="${this.#input.schema.type}">
<option value="string">String</option>
@ -59,13 +59,13 @@ class CCPInputWidgetEditorController extends HTMLElement{
</div>
</div>
<div class="col">
<div class="form-field">
<div class="form-field" title="Minimum">
<label>Min</label>
<input value="${this.#input.minOccurs}" type="number" min="0" step="1" name="minOccurs" value="${this.#input.minOccurs}" required="required" class="form-control" placeholder="minOccurs"/>
</div>
</div>
<div class="col">
<div class="form-field">
<div class="form-field" title="Maximum">
<label>Max</label>
<input value="${this.#input.maxOccurs}" type="number" min="0" step="1" name="maxOccurs" value="${this.#input.maxOccurs}" required="required" class="form-control" placeholder="maxOccurs"/>
</div>
@ -73,9 +73,9 @@ class CCPInputWidgetEditorController extends HTMLElement{
</div>
<div name="string-input" class="${this.#type === 'enum' ? 'd-none' : ''} row mb-3">
<div class="col-3">
<div class="form-field">
<div class="form-field" title="Format">
<label>Format</label>
<select value="${this.#input.schema.format}" name="format" class="form-control" value="${this.#input.schema.format}">
<select value="${this.#input.schema.format}" name="format" class="form-control">
<option value="none">None</option>
<option value="date">Date</option>
<option value="time">Time</option>
@ -84,7 +84,7 @@ class CCPInputWidgetEditorController extends HTMLElement{
</select>
</div>
</div>
<div class="col">
<div class="col" title="Mime type">
<label>Mime</label>
<div class="form-field">
<input value="${this.#input.schema.contentMediaType}" name="contentMediaType" class="form-control" placeholder="mime"/>
@ -92,13 +92,13 @@ class CCPInputWidgetEditorController extends HTMLElement{
</div>
</div>
<div name="enum-input" class="${this.#type !== 'enum' ? 'd-none' : ''} mb-3">
<div class="form-field">
<div class="form-field" title="options">
<input name="options" class="form-control" type="text" placeholder="option"/>
<small class="form-text text-muted">Comma separated list of options</small>
</div>
</div>
<div class="mb-3">
<div class="form-field">
<div class="form-field" title="Default value">
<input value="${this.#input.schema.default}" name="default" class="form-control" placeholder="default"/>
</div>
</div>

View File

@ -3,10 +3,12 @@ class CCPMethodEditorController extends HTMLElement{
#boot;
#rootdoc;
#serviceurl = "https://nubis1.int.d4science.net:8080"
#serviceurl;
#runtimes;
#locked = false;
#isupdate = false;
#tmp_inputs = []
#tmp_outputs = []
#current = null;
@ -103,6 +105,18 @@ class CCPMethodEditorController extends HTMLElement{
position: relative;
}
.ccp-toolbar-header {
display: inline-flex;
align-items: center;
gap: 5px;
padding: .2rem;
}
.ccp-toolbar-right {
position:absolute;
right: 1rem;
}
.ccp-toolbar-button {
font-weight: bold;
padding:.3rem;
@ -151,6 +165,11 @@ class CCPMethodEditorController extends HTMLElement{
<path d="M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z" />
</svg>
`
#delete_icon = `
<svg style="width:24px;height:24px;pointer-events: none;" viewBox="0 0 24 24">
<path d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" />
</svg>
`
#ansible_icon = `
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m4.1 15c-.19 0-.34-.1-.55-.27l-5.16-4.17l-1.73 4.34H7.17l4.37-10.51c.11-.28.35-.42.63-.42s.5.14.62.42l3.98 9.58c.04.11.07.22.07.29c-.01.42-.34.74-.74.74m-3.93-8.89l2.59 6.39l-3.91-3.08l1.32-3.31Z"></path>
@ -161,6 +180,8 @@ class CCPMethodEditorController extends HTMLElement{
super();
this.#boot = document.querySelector("d4s-boot-2")
this.#rootdoc = this.attachShadow({ "mode" : "open"})
this.#serviceurl = this.getAttribute("serviceurl")
this.initMethod()
this.fetchRuntimes()
}
@ -181,13 +202,72 @@ class CCPMethodEditorController extends HTMLElement{
})
}
saveCurrent(){
saveMethod(){
if(this.#locked) return;
if(this.#current != null){
this.adoptTemporaries()
console.log(this.#current)
const text = `Confirm ${this.#isupdate ? "updating" : "creation"} of ${this.#current.title} version ${this.#current.version}`
if(window.confirm(text)){
this.lockRender()
const url = this.#serviceurl + "/methods"
const args = {
body : JSON.stringify(this.#current),
method : this.#isupdate ? "PUT" : "POST",
headers : {"Content-type" : "application/json"}
}
this.#boot.secureFetch(url, args).then(
(resp)=>{
if(resp.status === 201 || resp.status === 204){
return resp.text()
}else throw "Error saving process"
}).then(data=>{
if(!this.#isupdate) this.#isupdate = true;
this.unlockRender()
}).catch(err=>{
alert(err)
this.unlockRender()
})
}
}
}
deleteMethod(){
if(this.#locked) return;
if(this.#current != null){
const text = `Confirm deletion of ${this.#current.title} version ${this.#current.version}`
if(window.confirm(text)){
this.lockRender()
const url = this.#serviceurl + "/methods/" + this.#current.id
const args = {
method : "DELETE"
}
this.#boot.secureFetch(url, args).then(
(resp)=>{
if(resp.status === 404 || resp.status === 204){
return null
}else throw "Error deleting method"
}).then(data=>{
this.unlockRender()
}).catch(err=>{
alert(err)
this.unlockRender()
})
}
}
}
initMethod(){
this.#current = JSON.parse(JSON.stringify(this.#method_template))
this.#current.id = Math.abs((Math.random() * 10e11)|0)
this.#tmp_inputs = []
this.#tmp_outputs = []
}
resetMethod(){
this.initMethod()
this.render()
}
cloneMethod(method){
if(this.#locked) return;
this.lockRender()
@ -199,7 +279,30 @@ class CCPMethodEditorController extends HTMLElement{
}
).then(data=>{
this.#current = data
this.#current.id = ""
this.#current.id = Math.abs((Math.random() * 10e11)|0)
this.#isupdate = false
this.#current.title = "Clone of " + this.#current.title
this.#current.description = "[Clone of] " + this.#current.description
this.#tmp_inputs = Object.keys(this.#current.inputs).map(k=>this.#current.inputs[k])
this.#tmp_outputs = Object.keys(this.#current.outputs).map(k=>this.#current.outputs[k])
this.unlockRender()
}).catch(err=>{
this.unlockRender()
})
}
editMethod(method){
if(this.#locked) return;
this.lockRender()
this.#boot.secureFetch(this.#serviceurl + "/processes/" + method).then(
(resp)=>{
if(resp.status === 200){
return resp.json()
}else throw "Error retrieving process"
}
).then(data=>{
this.#current = data
this.#isupdate = true
this.#tmp_inputs = Object.keys(this.#current.inputs).map(k=>this.#current.inputs[k])
this.#tmp_outputs = Object.keys(this.#current.outputs).map(k=>this.#current.outputs[k])
this.unlockRender()
@ -243,10 +346,6 @@ class CCPMethodEditorController extends HTMLElement{
}
render(){
if(this.#current == null){
this.#current = JSON.parse(JSON.stringify(this.#method_template))
this.#current.id = Math.random(10e6)|0
}
this.#rootdoc.innerHTML = `
<div class="ccp-method-editor">
<div class="d-none plexiglass">
@ -258,10 +357,15 @@ class CCPMethodEditorController extends HTMLElement{
<div class="ccp-method-editor-form">
<div class="card">
<div class="card-header">
<div>
<span class="mr-2">${this.#current.title}</span>
${this.renderSaveButton()}
${this.renderResetButton()}
<div class="ccp-toolbar-header">
<div>
<span name="header" class="mr-2">${this.#current.title}</span>
</div>
<div class="ccp-toolbar-right">
${this.renderSaveButton()}
${this.renderResetButton()}
${ this.#isupdate ? this.renderDeleteButton() : "" }
</div>
</div>
</div>
<div class="card-body">
@ -299,17 +403,29 @@ class CCPMethodEditorController extends HTMLElement{
</div>
<details class="card">
<summary class="card-header">
<span class="mr-2">Inputs</span>
${this.renderPlusButton("add-input")}
<div class="ccp-toolbar-header">
<div>
<span class="mr-2">Inputs</span>
</div>
<div class="ccp-toolbar-right">
${this.renderPlusButton("add-input")}
</div>
</div>
</summary>
<div class="card-body" name="input-list">
</div>
</details>
<details class="card">
<summary class="card-header" name="output-buttons">
<span class="mr-2">Outputs</span>
${this.renderStandardOutputButtons()}
${this.renderPlusButton("add-output")}
<div class="ccp-toolbar-header">
<div>
<span class="mr-2">Outputs</span>
</div>
<div class="ccp-toolbar-right">
${this.renderStandardOutputButtons()}
${this.renderPlusButton("add-output")}
</div>
</div>
</summary>
<div class="card-body" name="output-list">
<d4s-ccp-output-editor></d4s-ccp-output-editor>
@ -317,15 +433,22 @@ class CCPMethodEditorController extends HTMLElement{
</div>
<details class="card" name="script-list">
<summary class="card-header">
<span>Scripts</span>
<select name="script-selector" style="border:none; padding:.3rem">
<option value="deploy-script">Deploy</option>
<option value="execute-script">Execute</option>
<option value="fetch-output-script">Fetch output</option>
<option value="undeploy-script">Undeploy</option>
<option value="cancel-script">Cancel</option>
</select>
${ this.renderAnsibleIcon() }
<div class="ccp-toolbar-header">
<div>
<span>Scripts</span>
<select name="script-selector" style="border:none; padding:.3rem">
<option value=""></option>
<option value="deploy-script">Deploy</option>
<option value="execute-script">Execute</option>
<option value="fetch-output-script">Fetch output</option>
<option value="undeploy-script">Undeploy</option>
<option value="cancel-script">Cancel</option>
</select>
</div>
<div>
${ this.renderAnsibleIcon() }
</div>
</div>
</summary>
<div class="card-body" name="input_container">
${this.renderScripts()}
@ -337,12 +460,30 @@ class CCPMethodEditorController extends HTMLElement{
this.renderInputs()
this.renderOutputs()
this.#rootdoc.querySelector("input[name=title]").addEventListener("input", ev=>{
this.#current.title = ev.currentTarget.value
this.#rootdoc.querySelector("span[name=header]").innerText = this.#current.title
})
this.#rootdoc.querySelector("input[name=version]").addEventListener("input", ev=>{
this.#current.version = ev.currentTarget.value
})
this.#rootdoc.querySelector("input[name=description]").addEventListener("input", ev=>{
this.#current.description = ev.currentTarget.value
})
this.#rootdoc.addEventListener("drop", ev=>{
if(ev.dataTransfer && ev.dataTransfer.getData('text/plain+ccpmethod')){
const id = ev.dataTransfer.getData('text/plain+ccpmethod')
ev.stopImmediatePropagation()
ev.preventDefault()
ev.stopPropagation()
this.cloneMethod(id)
if(window.confirm("Do you want to create a new clone?")){
this.cloneMethod(id)
} else {
this.editMethod(id)
}
}
})
@ -351,10 +492,9 @@ class CCPMethodEditorController extends HTMLElement{
})
this.#rootdoc.querySelector("button[name=reset]").addEventListener("click", ev=>{
this.#current = this.#method_template
this.#tmp_inputs = []
this.#tmp_outputs = []
this.render()
if(window.confirm("All unsaved data will be lost. Proceed?")){
this.resetMethod()
}
ev.preventDefault()
ev.stopPropagation()
})
@ -362,9 +502,17 @@ class CCPMethodEditorController extends HTMLElement{
this.#rootdoc.querySelector("button[name=save]").addEventListener("click", ev=>{
ev.preventDefault()
ev.stopPropagation()
this.saveCurrent()
this.saveMethod()
})
if(this.#isupdate){
this.#rootdoc.querySelector("button[name=delete]").addEventListener("click", ev=>{
ev.preventDefault()
ev.stopPropagation()
this.deleteMethod()
})
}
this.#rootdoc.querySelector("input[name=keyword-input]").addEventListener("keypress", ev=>{
if(ev.key === "Enter" || ev.which === 13){
ev.preventDefault()
@ -432,7 +580,7 @@ class CCPMethodEditorController extends HTMLElement{
schema : {
type : "string",
format : null,
contentMediaType : null,
contentMediaType : "text/plain",
default : ""
}
}
@ -464,7 +612,7 @@ class CCPMethodEditorController extends HTMLElement{
maxOccurs : 1,
schema : {
type : "string",
contentMediaType : null
contentMediaType : "text/plain"
}
}
)
@ -543,7 +691,7 @@ class CCPMethodEditorController extends HTMLElement{
renderAnsibleIcon(){
return `
<div title="Ansible" style="width:2rem;height:2rem;display:inline-block;vertical-align:middle;position:absolute;right:0;margin-right:1rem;opacity:0.4;">
<div title="Ansible" style="width:2rem;height:2rem;position:absolute;right:5px;top:30%;opacity:0.4;">
${ this.#ansible_icon }
</div>
`
@ -557,6 +705,14 @@ class CCPMethodEditorController extends HTMLElement{
`
}
renderDeleteButton(){
return `
<button title="Delete" name="delete" class="btn btn-danger ccp-toolbar-button">
${this.#delete_icon}
</button>
`
}
renderResetButton(){
return `
<button name="reset" title="Reset" class="btn btn-primary ccp-toolbar-button">
@ -658,7 +814,7 @@ class CCPMethodEditorController extends HTMLElement{
renderScripts(){
return this.#current.additionalParameters.parameters.map(
(script, i) => `<textarea rows="5" class="script-area form-control ${ i > 0 ? 'd-none' : ''}" name="${script.name}">${script.value[0]}</textarea>`
(script, i) => `<textarea rows="5" class="script-area form-control d-none" name="${script.name}">${script.value[0]}</textarea>`
).join("\n")
}
}

View File

@ -6,29 +6,107 @@ class CCPMethodList extends HTMLElement{
#filtered;
#dragged = null;
#serviceurl = "https://nubis1.int.d4science.net:8080"
#cdnurl = "https://cdn.dev.d4science.org/ccp/methodlistfragment.html"
#serviceurl;
constructor(){
super()
this.#boot = document.querySelector("d4s-boot-2")
this.#serviceurl = this.getAttribute("serviceurl")
this.#rootdoc = this.attachShadow({ "mode" : "open"})
this.fetchMarkup()
this.render()
this.fetchProcesses()
}
fetchMarkup(){
return fetch(this.#cdnurl).then(
(reply)=>{
if(reply.status === 200){
return reply.text()
}else{ throw new Exception("Unable to fetch markup") }
}).then(data=>{
this.#rootdoc.innerHTML = data
console.log(this.#boot)
this.fetchProcesses()
}).catch(err=>{
console.error(err)
})
render(){
this.#rootdoc.innerHTML = `
<div>
<style>
.process_container{
display: flex;
flex-direction:column;
}
input[name=search]{
display: block;
padding: 0.375rem 0.75rem;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: 0.25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
input:focus{
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 0.2rem rgb(0 123 255 / 25%);
}
ul.process_list{
list-style: none;
padding-left: 0;
color: #495057;
display: flex;
flex-direction: column;
gap: .25rem;
user-select: none;
}
li.process_list_item{
display: flex;
flex-direction: column;
gap: 0.25rem;
cursor: pointer;
border: solid 1px rgba(0,0,0,.3);
padding: 0.3rem;
border-radius: 0.3rem;
box-shadow: #333333 1px 1px 4px;
background: #eeeeee;
transition: background .3s;
}
li.process_list_item:hover{
background: #ffffff;
}
ul.keyword_list{
list-style: none;
display: flex;
flex-direction: row;
gap:2px;
padding-left: 0;
font-size: x-small;
font-weight: 300;
}
li.keyword_list_item{
display: inline-block;
padding: 0.25em 0.4em;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25rem;
background-color: #eeffff;
color: #0099CC;
border: solid 1px #0099CC;
}
.process_list_item_header{
margin: 0;
}
</style>
<template id="PROCESS_LIST_TEMPLATE">
<ul class="process_list" name="process_list">
<li class="process_list_item" draggable="true">
<h4 class="process_list_item_header"></h4>
<ul name="keywords" class="keyword_list">
<li class="keyword_list_item"></li>
</ul>
<i></i>
</li>
</ul>
</template>
<div class="process_container">
<input name="search" type="text"/>
<ul name="process_list"></ul>
</div>
</div>
`
}
connectedCallback(){
@ -51,7 +129,6 @@ class CCPMethodList extends HTMLElement{
}
showList(){
//this.#data = JSON.parse(resp)
this.enableSearch()
this.updateList()
}

View File

@ -29,7 +29,7 @@ class CCPOutputWidgetEditorController extends HTMLElement {
this.innerHTML = `
<details>
<summary class="mb-3">
<input class="form-control" style="width:auto;display:inline" required="required" name="id" value="${this.#output.id}"/>
<input class="form-control" style="width:auto;display:inline" required="required" name="id" value="${this.#output.id}" title="Id of output"/>
<button data-index="${this.#index}" name="delete-output" title="Delete" class="btn btn-danger ccp-toolbar-button">
${this.#delete_icon}
</button>
@ -37,38 +37,38 @@ class CCPOutputWidgetEditorController extends HTMLElement {
<div style="padding-left: 1rem;border-left: 1px solid gray;">
<div class="row mb-3">
<div class="col">
<div class="form-field">
<div class="form-field" title="Title">
<input name="title" class="form-control" placeholder="title" value="${this.#output.title}" required="required"/>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-field">
<div class="form-field" title="Description">
<input name="description" class="form-control" placeholder="description" value="${this.#output.description}" required="required"/>
</div>
</div>
<div class="row mb-3">
<div class="col">
<div class="form-field">
<div class="form-field" title="Mininum cardinality">
<input value="${this.#output.minOccurs}" type="number" min="0" step="1" name="minOccurs" class="form-control" placeholder="minOccurs"/>
</div>
</div>
<div class="col">
<div class="form-field">
<div class="form-field" title="Maximum cardinality">
<input value="${this.#output.maxOccurs}" type="number" min="0" step="1" name="maxOccurs" class="form-control" placeholder="maxOccurs"/>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-3">
<div class="form-field">
<div class="form-field" title="Type">
<select name="type" class="form-control" placeholder="type">
<option value="string">String</option>
</select>
</div>
</div>
<div class="col">
<div class="form-field">
<div class="form-field" title="Mime type">
<input value="${this.#output.schema.contentMediaType}" name="contentMediaType" class="form-control" placeholder="mime"/>
</div>
</div>