diff --git a/ccp/js/methodeditorcontroller2.js b/ccp/js/methodeditorcontroller2.js new file mode 100644 index 0000000..caa359e --- /dev/null +++ b/ccp/js/methodeditorcontroller2.js @@ -0,0 +1,896 @@ +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 : { + runtime : { + id : "runtime", + 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", + default : "", + readonly : true, + } + } + }, + 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 = ` + + + + ` + + constructor(){ + super(); + this.#boot = document.querySelector("d4s-boot-2") + this.#rootdoc = this.attachShadow({ "mode" : "open"}) + this.#serviceurl = this.getAttribute("serviceurl") + this.initMethod() + this.fetchInfrastructures() + } + + connectedCallback(){ + + } + + 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 = `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: " + resp.status + }).then(data=>{ + if(!this.#isupdate){ + this.resetMethod() + } + 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: " + resp.status + }).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.#current.metadata = [] + this.#tmp_inputs = [] + 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){ + if(this.#locked) return; + this.lockRender() + this.#boot.secureFetch(this.#serviceurl + "/methods/" + method + "/clone").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 + } + + 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()} + + + ${this.renderContexts()} + + + + Title + + + + Version + + + + + Description + + + + Keywords + + + ${this.renderKeywords()} + + + + Compatible Infrastrucures + + ${this.renderInfrastructureOptions()} + + + ${this.renderInfrastructures()} + + + + + + + + + Inputs + + + ${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=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("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("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() } + + ` + } + + 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} + + ` + } + + 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 "" + } + } + + reRenderInfrastructures(){ + this.#rootdoc.querySelector("select[name=infrastructure-input]").innerHTML = this.renderInfrastructureOptions() + this.#rootdoc.querySelector("div[name=infrastructure-list]").innerHTML = this.renderInfrastructure() + } + + 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") + } + ` + } + + renderRuntimes(){ + 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(){ + return this.#current.additionalParameters.parameters.map( + (script, i) => { + let code = script.value.length ? script.value.join("\r\n") : "" + return ` + ${code} + ` + } + ).join("\n") + } +} + +window.customElements.define('d4s-ccp-methodeditor', CCPMethodEditorController);