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 : [] } #scripts = ` ` #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"}) this.#serviceurl = this.getAttribute("serviceurl") this.initMethod() this.fetchInfrastructures() this.connectNewEditRequest() } connectedCallback(){ } 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); 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 = ` Choose whether you want to clone this method or edit it. ${this.#style} ${this.#current.title} ${this.renderSaveButton()} ${this.renderResetButton()} ${ this.#isupdate ? this.renderDeleteButton() : "" } ${this.renderAuthors()} Title Version Description Keywords ${this.renderKeywords()} Categories ${this.renderCategoryHints()} ${this.renderCategories()} Compatible Infrastrucures ${this.renderInfrastructureOptions()} ${this.renderInfrastructures()} Inputs ${this.renderStandardInputButtons()} ${this.renderPlusButton("add-input")} Outputs ${this.renderStandardOutputButtons()} ${this.renderPlusButton("add-output")} Scripts Deploy Execute Undeploy ${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.#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.#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.#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 const areas = Array.prototype.slice.call(this.#rootdoc.querySelectorAll("textarea.script-area")) areas.forEach(a=>{ if(a.getAttribute("name") === scriptname) a.classList.remove("d-none"); else a.classList.add("d-none") }) }) this.#rootdoc.querySelector("details[name=script-list]").addEventListener("input", ev=>{ ev.preventDefault() ev.stopPropagation() const scriptname = ev.target.getAttribute("name") const script = this.#current.additionalParameters.parameters.filter(p=>p.name === scriptname)[0] if(script) script.value = ev.target.value.split(/\r?\n/); }) } renderAuthors(){ return ` ${ this.#current.metadata. filter(md=>md.role === "author"). map(a=>`${a.title}`).join() } ` } renderCategoryHints(){ const ml = document.querySelector("d4s-ccp-methodlist") const cats = ml.getCategoryHints() if(cats.indexOf("Uncategorised") === -1) cats.push("Uncategorised") return ` ${cats.map(c=>`${c}`)} ` } renderContexts(){ return ` ${ this.#current.metadata. filter(md=>md.role === "context"). map(c=>`${c.title}`).join("") } ` } renderSaveButton(){ return ` ${this.#disc_icon} ` } renderDeleteButton(){ return ` ${this.#delete_icon} ` } renderResetButton(){ return ` ${this.#erase_icon} ` } renderPlusButton(name){ return ` ${this.#plus_icon} ` } renderStandardInputButtons(){ return ` ${this.#annotation_input_icon} ` } renderStandardOutputButtons(){ return ` ${this.#output_icon} ${this.#output_icon} ` } 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 ` ${infra.name} ` }).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) }) } renderScripts(){ const val = 'deploy-script' return this.#current.additionalParameters.parameters.map( (script, i) => { let code = script.value && script.value.length ? script.value.join("\r\n") : "" return ` ${code} ` } ).join("\n") } } window.customElements.define('d4s-ccp-methodeditor', CCPMethodEditorController);