class CCPMethodEditorController extends HTMLElement { #boot; #rootdoc; #serviceurl; #infrastructures; #locked = false; #isupdate = false; #cloneornew_dialog = null; #dragging_method = null; #tmp_inputs = [] #tmp_outputs = [] #current = null; #method_template = { id: "", title: "New Method", description: "New empty method", version: "1.0.0", jobControlOptions: "async-execute", metadata: [], inputs: {}, outputs: {}, additionalParameters: { parameters: [ { name: "deploy-script", value: [] }, { name: "execute-script", value: [] }, { name: "undeploy-script", value: [] } ] }, links: [] } #style = ` ` #erase_icon = ` ` #plus_icon = ` ` #disc_icon = ` ` #output_icon = ` ` #delete_icon = ` ` #annotation_input_icon = ` ` constructor() { super(); this.#boot = document.querySelector("d4s-boot-2") this.#rootdoc = this.attachShadow({ "mode": "open" }) } connectedCallback() { this.#serviceurl = this.getAttribute("serviceurl") this.initMethod() this.fetchInfrastructures() this.connectNewEditRequest() } connectNewEditRequest() { document.addEventListener("neweditrequest", ev => { this.cloneOrEditMethod(ev.detail) }) } isInfrastructureRegistered(infra) { return (this.#infrastructures.filter(i => i.id === infra.id)).length > 0 } fetchInfrastructures() { this.#boot.secureFetch(this.#serviceurl + "/infrastructures"). then(resp => { if (resp.status !== 200) throw "Unable to fetch infrastructures"; return resp.json() }).then(data => { this.#infrastructures = data this.render() }).catch(err => { alert(err) }) } saveMethod() { if (this.#locked) return; if (this.#current != null) { this.adoptTemporaries() const text = `Please 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.ok) { return resp.text() } else throw "Error saving process: " + resp.status }).then(data => { if (!this.#isupdate) { this.#current = JSON.parse(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() }).catch(err => { alert(err) this.unlockRender() }) } } } deleteMethod() { if (this.#locked) return; if (this.#current != null) { const text = `Please 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: " + resp.status }).then(data => { this.#isupdate = false this.initMethod() 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.#current.metadata = [] this.#tmp_inputs = [ { id: "ccpimage", title: "Runtime", description: "The image of the runtime to use for method execution. This depends on the infrastructure specific protocol for interacting with registries.", minOccurs: 1, maxOccurs: 1, schema: { type: "string", format: "url", contentMediaType: "text/plain", default: "", readonly: true, } } ] this.#tmp_outputs = [] } resetMethod() { this.initMethod() this.render() } cloneOrEditMethod(method) { const subject = this.#boot.subject const matchingauthors = method.metadata ? method.metadata.filter(md => md.role === "author" && md.href.endsWith("/" + subject)).length : 0 if (matchingauthors === 0) this.cloneMethod(method.id, true); else { this.#dragging_method = method.id this.#cloneornew_dialog.style.display = "block" this.#cloneornew_dialog.classList.add("show") } } cloneMethod(method, derivation) { if (this.#locked) return; this.lockRender() const derive = derivation ? "?derive=true" : "" this.#boot.secureFetch(this.#serviceurl + "/methods/" + method + "/clone" + derive).then( (resp) => { if (resp.status === 200) { return resp.json() } else throw "Error retrieving process: " + resp.status } ).then(data => { this.#current = data this.#isupdate = false 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 + "/methods/" + method + "/updatable").then( (resp) => { if (resp.status === 200) { return resp.json() } else throw "Error retrieving process: " + resp.status } ).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() }).catch(err => { this.unlockRender() }) } adoptTemporaries() { this.#current.inputs = {} this.#tmp_inputs.forEach(t => { this.#current.inputs[t.id] = t }) this.#current.outputs = {} this.#tmp_outputs.forEach(t => { this.#current.outputs[t.id] = t }) } getInputAt(i) { return this.#tmp_inputs[i] } deleteTmpInputAt(i) { this.#tmp_inputs.splice(i, 1) } deleteTmpOutputAt(i) { this.#tmp_outputs.splice(i, 1) } lockRender() { this.#locked = true this.#rootdoc.querySelector(".plexiglass").classList.toggle("d-none") } unlockRender() { this.#rootdoc.querySelector(".plexiglass").classList.toggle("d-none") this.render() this.#locked = false if (this.parentElement) this.parentElement.scrollIntoViewIfNeeded(); } render() { this.#rootdoc.innerHTML = `
${this.#style}
${this.#current.title}
${this.renderSaveButton()} ${this.renderResetButton()} ${this.#isupdate ? this.renderDeleteButton() : ""}
${this.renderAuthors()}
${this.renderKeywords()}
${this.renderCategoryHints()}
${this.renderCategories()}
${this.renderInfrastructures()}
Inputs
${this.renderStandardInputButtons()} ${this.renderPlusButton("add-input")}
Outputs
${this.renderStandardOutputButtons()} ${this.renderPlusButton("add-output")}
Scripts
${this.renderScripts()}
` this.renderInputs() this.renderOutputs() this.#cloneornew_dialog = this.#rootdoc.querySelector("#cloneornew") this.#rootdoc.querySelector("#cloneornew button[name=clone]").addEventListener("click", ev => { this.#cloneornew_dialog.classList.remove("show") this.#cloneornew_dialog.style.display = "none" this.cloneMethod(this.#dragging_method) this.#dragging_method = null }) this.#rootdoc.querySelector("#cloneornew button[name=derive]").addEventListener("click", ev => { this.#cloneornew_dialog.classList.remove("show") this.#cloneornew_dialog.style.display = "none" this.cloneMethod(this.#dragging_method, true) this.#dragging_method = null }) this.#rootdoc.querySelector("#cloneornew button[name=edit]").addEventListener("click", ev => { this.#cloneornew_dialog.classList.remove("show") this.#cloneornew_dialog.style.display = "none" this.editMethod(this.#dragging_method) this.#dragging_method = null }) 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 method = JSON.parse(ev.dataTransfer.getData('application/json+ccpmethod')) ev.stopImmediatePropagation() ev.preventDefault() ev.stopPropagation() this.cloneOrEditMethod(method) } }) this.#rootdoc.addEventListener("dragover", ev => { ev.preventDefault() }) this.#rootdoc.querySelector("button[name=reset]").addEventListener("click", ev => { if (window.confirm("All unsaved data will be lost. Proceed?")) { this.resetMethod() } ev.preventDefault() ev.stopPropagation() }) this.#rootdoc.querySelector("button[name=save]").addEventListener("click", ev => { ev.preventDefault() ev.stopPropagation() 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() ev.stopPropagation() const val = ev.target.value ev.target.value = null if (!this.#current.keywords) { this.#current.keywords = [val] } else { this.#current.keywords.push(val) } this.reRenderKeywords() } }) this.#rootdoc.querySelector("div[name=keyword-list]").addEventListener("click", ev => { ev.preventDefault() ev.stopPropagation() if (ev.target.getAttribute('name') === "delete-keyword") { const index = ev.target.getAttribute("data-index") this.#current.keywords.splice(index, 1) this.reRenderKeywords() this.#rootdoc.querySelector("input[name=keyword-input]").focus() } }) this.#rootdoc.querySelector("input[name=category-input]").addEventListener("keypress", ev => { if (ev.key === "Enter" || ev.which === 13) { ev.preventDefault() ev.stopPropagation() const val = ev.target.value ev.target.value = null if (this.#current.metadata.filter(md => md.role === "category" && md.title === val).length === 0) { this.#current.metadata.push({ role: "category", title: val }) this.reRenderCategories() } } }) this.#rootdoc.querySelector("div[name=category-list]").addEventListener("click", ev => { ev.preventDefault() ev.stopPropagation() if (ev.target.getAttribute('name') === "delete-category") { const val = ev.target.parentElement.title this.#current.metadata = this.#current.metadata.filter(md => md.role !== "category" || md.title !== val) this.reRenderCategories() this.#rootdoc.querySelector("input[name=category-input]").focus() } }) this.#rootdoc.querySelector("div[name=infrastructures]").addEventListener("change", ev => { if (ev.target.getAttribute("name") === "infrastructure-input" && ev.target.value) { ev.preventDefault() ev.stopPropagation() const id = ev.target.value const display = ev.target.options[ev.target.selectedIndex].text let link = { rel: "compatibleWith", title: display, href: "infrastructures/" + id } if (!this.#current.links) { this.#current.links = [link] } else { this.#current.links.push(link) } this.reRenderInfrastructures(ev.currentTarget) } }) this.#rootdoc.querySelector("div[name=infrastructure-list]").addEventListener("click", ev => { ev.preventDefault() ev.stopPropagation() if (ev.target.getAttribute('name') === "delete-infrastructure") { const index = ev.target.getAttribute("data-index") this.#current.links.splice(index, 1) this.reRenderInfrastructures() } }) this.#rootdoc.querySelector("button[name=add-input]").addEventListener("click", ev => { ev.preventDefault() ev.stopPropagation() this.#tmp_inputs.push( { id: "new_input", title: "New input", description: "A new input field", minOccurs: 1, maxOccurs: 1, schema: { type: "string", format: null, contentMediaType: "text/plain", default: "" } } ) this.renderInputs() this.reRenderScripts() }) this.#rootdoc.querySelector("button[name=add-ccpannotation]").addEventListener("click", ev => { ev.preventDefault() ev.stopPropagation() this.#tmp_inputs.push( { id: "ccpnote", title: "Annotations for execution", description: "The value of this parameter will be associated as annotation to the execution", minOccurs: 1, maxOccurs: 1, schema: { type: "string", format: null, contentMediaType: "text/plain", default: "" } } ) this.renderInputs() this.reRenderScripts() }) this.#rootdoc.querySelector("div[name=input-list]").addEventListener("click", ev => { const evname = ev.target.getAttribute('name') if (evname === "delete-input") { ev.preventDefault() ev.stopPropagation() const index = Number(ev.target.getAttribute("data-index")) console.log("deleting input at index", index) this.deleteTmpInputAt(index) this.renderInputs() this.reRenderScripts() } }) //when any input changes update scripts so that possibly variable get re-expanded this.#rootdoc.querySelector("div[name=input-list]").addEventListener("input", ev => { this.reRenderScripts() }) this.#rootdoc.querySelector("div[name=input-list]").addEventListener("change", ev => { this.reRenderScripts() }) this.#rootdoc.querySelector("div[name=input-list]").addEventListener("click", ev => { const src = ev.target.getAttribute("name") if (src === "plus" || src === "minus" || src === "maxOccurs" || src === "minOccurs") { this.reRenderScripts() } }) this.#rootdoc.querySelector("button[name=add-output]").addEventListener("click", ev => { ev.preventDefault() ev.stopPropagation() this.#tmp_outputs.push( { id: "new_output", title: "New ouput", description: "A new output field", minOccurs: 1, maxOccurs: 1, metadata: [ { "role": "file", "title": "newoutput.txt", "href": "newoutput.txt" } ], schema: { type: "string", contentMediaType: "text/plain" } } ) this.renderOutputs() }) this.#rootdoc.querySelector("button[name=add-stdout]").addEventListener("click", ev => { ev.preventDefault() ev.stopPropagation() this.#tmp_outputs.push( { id: "stdout", title: "Standard output", description: "Standard output channel", minOccurs: 1, maxOccurs: 1, metadata: [ { "role": "file", "title": "stdout", "href": "stdout" } ], schema: { type: "string", contentMediaType: "text/plain" } } ) this.renderOutputs() }) this.#rootdoc.querySelector("button[name=add-stderr]").addEventListener("click", ev => { ev.preventDefault() ev.stopPropagation() this.#tmp_outputs.push( { id: "stderr", title: "Standard error", description: "Standard error channel", minOccurs: 1, maxOccurs: 1, metadata: [ { "role": "file", "title": "stderr", "href": "stderr" } ], schema: { type: "string", contentMediaType: "text/plain" } } ) this.renderOutputs() }) this.#rootdoc.querySelector("div[name=output-list]").addEventListener("click", ev => { const evname = ev.target.getAttribute('name') if (evname === "delete-output") { ev.preventDefault() ev.stopPropagation() const index = Number(ev.target.getAttribute("data-index")) console.log("deleting output at index", index) this.deleteTmpOutputAt(index) this.renderOutputs() } }) this.#rootdoc.querySelector("select[name=script-selector]").addEventListener("change", ev => { ev.preventDefault() ev.stopPropagation() const scriptname = ev.target.value this.reRenderScripts(scriptname) }) this.#rootdoc.querySelector("div[name=scripts_container]").addEventListener("input", ev => { ev.preventDefault() ev.stopPropagation() const scriptname = this.getCurrentScriptName() const script = this.#current.additionalParameters.parameters.find(p => p.name === scriptname) if (script) script.value = ev.target.value.split(/\r?\n/); const preview = ev.currentTarget.querySelector("textarea[name=preview]") preview.value = this.codeToPreview(ev.target.value) }) } renderAuthors() { return ` ` } renderCategoryHints() { const ml = document.querySelector("d4s-ccp-methodlist") const cats = ml.getCategoryHints() if (cats.indexOf("Uncategorised") === -1) cats.push("Uncategorised") return ` ${cats.map(c => ``)} ` } renderContexts() { return ` ` } renderSaveButton() { return ` ` } renderDeleteButton() { return ` ` } renderResetButton() { return ` ` } renderPlusButton(name) { return ` ` } renderStandardInputButtons() { return ` ` } renderStandardOutputButtons() { return ` ` } reRenderKeywords() { this.#rootdoc.querySelector("div[name=keyword-list]").innerHTML = this.renderKeywords() } renderKeywords() { if (this.#current.keywords) { return this.#current.keywords.map((k, i) => { return `
${k} x
` }).join("\n") } else { return "" } } reRenderCategories() { this.#rootdoc.querySelector("div[name=category-list]").innerHTML = this.renderCategories() } renderCategories() { if (this.#current.metadata) { return this.#current.metadata.filter(md => md.role === "category").map((k, i) => { return `
${k.title} x
` }).join("\n") } else { return "" } } reRenderInfrastructures() { this.#rootdoc.querySelector("select[name=infrastructure-input]").innerHTML = this.renderInfrastructureOptions() this.#rootdoc.querySelector("div[name=infrastructure-list]").innerHTML = this.renderInfrastructures() } renderInfrastructureOptions() { const selectedinfras = this.#current.links.filter(l => l.rel === "compatibleWith").map(i => i.href.split("/")[1]) const available = this.#infrastructures.filter(i => { return selectedinfras.indexOf(i.id) === -1 }) return ` ${available.map(infra => { return ` ` }).join("\n") } ` } renderInfrastructures() { return this.#current.links.filter(l => l.rel === "compatibleWith").map((l, i) => { return `
${l.title} x
` }).join("\n") } renderInputs() { const parent = this.#rootdoc.querySelector("div[name=input-list]") parent.innerHTML = "" return this.#tmp_inputs.map((inp, i) => { const c = document.createElement("d4s-ccp-input-editor"); parent.appendChild(c) c.render(inp, i) }) } renderOutputs() { const parent = this.#rootdoc.querySelector("div[name=output-list]") parent.innerHTML = "" return this.#tmp_outputs.map((outp, i) => { const c = document.createElement("d4s-ccp-output-editor"); parent.appendChild(c) c.render(outp, i) }) } codeToPreview(code) { return this.#tmp_inputs.reduce((acc, i) => { const r = new RegExp(`{{\\s*${i.id}\\s*}}`, 'g'); var def = i.schema.default if(i.schema.format === "secret"){ def = Array.isArray(def) ? def : [def] def = def.map(v=>"***") } if(i.schema.format === "file"){ def = Array.isArray(def) ? def : [def] def = def.map(v=>v.substring(0,5) + "......" + v.substring(v.length-5)) } return acc.replaceAll(r, def) }, code) } getCurrentScriptName() { return this.#rootdoc.querySelector("div[name=scripts_container] div.script-area").getAttribute("name") } renderScripts(scriptname) { const val = scriptname ? scriptname : 'deploy-script' const script = this.#current.additionalParameters.parameters.find(s => s.name === val) if (!script) return ''; const code = script.value && script.value.length ? script.value.join("\r\n") : "" return `
Preview
` } reRenderScripts(scriptname) { const container = this.#rootdoc.querySelector("div[name=scripts_container]") const sn = scriptname ? scriptname : this.getCurrentScriptName() container.innerHTML = this.renderScripts(sn) } } window.customElements.define('d4s-ccp-methodeditor', CCPMethodEditorController);