diff --git a/ccp/js/infrastructurelistcontroller.js b/ccp/js/infrastructurelistcontroller.js index 601c231..bb2b9b8 100644 --- a/ccp/js/infrastructurelistcontroller.js +++ b/ccp/js/infrastructurelistcontroller.js @@ -2,7 +2,52 @@ class CCPInfrastructureList extends HTMLElement{ #boot; #socket; - #data = {}; + #infrastructures; + #runtimes; + #rootdoc; + + #style = ` + + + + `; #serviceurl = "https://nubis1.int.d4science.net:8080" #broadcasturl = "ws://nubis1.int.d4science.net:8989/ws/notification" @@ -10,61 +55,181 @@ class CCPInfrastructureList extends HTMLElement{ constructor(){ super() this.#boot = document.querySelector("d4s-boot-2") + this.#rootdoc = this.attachShadow({ "mode" : "open"}) } connectedCallback(){ - this.fetchInfrastructures() - } - - get data(){ - return this.#data + this.connectBroadcast() + this.fetchInfrastructures() } fetchInfrastructures(){ - console.log("Calling fetch infrastructures") - this.#boot.secureFetch(this.#serviceurl + "/infrastructures/cache"). - then(resp=>{ - return resp.json() - }).then(data=>{ - this.#data = data - /*this.updateList()*/ - }).catch(err=>{ - alert("Error while downloading methods: ", err) + const prom1 = this.#boot.secureFetch(this.#serviceurl + "/infrastructures/cache"). + then(resp=>{ + if(resp.status !== 200) throw "Unable to fetch infrastructure cache"; + return resp.json() + }).then(data=>{ + this.#infrastructures = data + }).catch(err=>{ + alert(err) + }) + + const prom2 = this.#boot.secureFetch(this.#serviceurl + "/runtimes"). + then(resp=>{ + if(resp.status !== 200) throw "Unable to fetch runtimes"; + return resp.json() + }).then(data=>{ + this.#runtimes = data + }).catch(err=>{ + alert(err) + }) + + Promise.all([prom1, prom2]).then(results=>{ + this.showList() + }) + } + + refreshInfrastructures(){ + this.#boot.secureFetch(this.#serviceurl + "/infrastructures/cache", { method : "HEAD"}) + } + + showList(){ + if(this.getAttribute("render") !== "true") { + this.#rootdoc.innerHTML = "" + return; + } + this.#rootdoc.innerHTML = ` +
+ ${this.#style} + ${this.toolbar()} + +
+ ` + this.#rootdoc.querySelector(".ccp_infrastructure_toolbar").addEventListener("click", ev=>{ + const evname = ev.target.getAttribute("name") + switch(evname){ + case "refresh": + this.refreshInfrastructures(); + break; + } + }) + + const forms = Array.prototype.slice.call(this.#rootdoc.querySelectorAll(".ccp_runtime_builder")) + forms.forEach(f=>{ + f.addEventListener("submit", ev=>{ + ev.stopPropagation() + ev.preventDefault() + const sel = ev.target.querySelector("select") + let url = ev.target.action + sel.value + this.#boot.secureFetch(url, { method : ev.target.method}) + return false + }) }) } -/* updateList(resp){ - this.connectBroadcast() - this.#infrastructures.forEach(i=>{ - this.#data[i.id] = { id : i.id, status : "unknown", type : i.type, name : i.name, runtimes : [] } - this.#boot.secureFetch(this.#serviceurl + `/infrastructures/${i.id}/status`).then( - ()=>console.log("Requested async status update for infrastructure ", i.id) - ) - }) + toolbar(){ + return ` +
+ +
+ ` } - */ -/* connectBroadcast(){ + showInfrastructures(){ + return this.#infrastructures.map(i => { + return ` +
  • +
    + + ${i.name} + ${this.showAge(i)} + ${this.showType(i)} + + ${this.showDeployableRuntimes(i)} + +
    +
  • + ` + }).join("\n") + } + + showDeployableRuntimes(infra){ + const alreadydeployed = infra.runtimes.map(r=>r["descriptor-id"]) + const options = this.#runtimes.filter(r=>{ + return infra.type === r.type && alreadydeployed.indexOf(r.id) === -1 + }).map(r=>{ + return `` + }).join("\n") + return ` +
    +
    + + +
    +
    + ` + } + + showRuntimes(infra){ + return infra.runtimes.map(r =>{ + return ` +
  • +
    + ${r.name} + +
    +
  • + ` + }).join("\n") + } + + showInstances(runtime){ + return runtime.instances.map(i =>{ + return ` +
  • ${i.name}
  • + ` + }).join("\n") + } + + showAge(infra){ + const age = Math.floor(((new Date()) - (new Date(infra.date))) / 3600000) + var cls = "badge-success" + var agetext = "fresh" + if(age > 24){ + cls = "badge-warning"; + agetext = "aged" + } else if (age > 72){ + cls = "badge-secondary" + agetext = "old" + } + return `${agetext}` + } + + showType(infra){ + return `${infra.type}` + } + + connectBroadcast(){ this.#socket = new WebSocket(this.#broadcasturl + "/infrastructures"); this.#socket.onmessage = event=>{ - const data = JSON.parse(event.data) - this.#data[data.infrastructure].status = data.status.toLowerCase() - if(data.type.toLowerCase() === "update" && data.status.toLowerCase() === "ok"){ - this.#data[data.infrastructure].runtimes = data.data.runtimes - }else{ - this.#data[data.infrastructure].runtimes = [] - } - console.log("New Infrastructure status", this.#data) + this.fetchInfrastructures() } window.setInterval( ()=>this.#socket.send("ping"), 30000) } - */ getCompatibleRuntimes(rts){ - const available = Object.keys(this.#data).reduce((acc, i) => { - const compatiblerts = this.#data[i].runtimes. + const available = Object.keys(this.#infrastructures).reduce((acc, i) => { + const compatiblerts = this.#infrastructures[i].runtimes. filter(r=>rts.indexOf(r["descriptor-id"]) >= 0). - map(cr=>{ return { runtime : cr, infrastructure : this.#data[i] } }) + map(cr=>{ return { runtime : cr, infrastructure : this.#infrastructures[i] } }) return acc.concat(compatiblerts) }, []) return available diff --git a/ccp/js/inputwidgeteditorcontroller.js b/ccp/js/inputwidgeteditorcontroller.js new file mode 100644 index 0000000..b4ed2e3 --- /dev/null +++ b/ccp/js/inputwidgeteditorcontroller.js @@ -0,0 +1,158 @@ +class CCPInputWidgetEditorController extends HTMLElement{ + + #input = null + #index = null + #type = null + + #delete_icon = ` + + + + ` + + constructor(){ + super(); + + } + + connectedCallback(){ + + } + + get index(){ + return this.#index + } + + render(input, i){ + this.#index = i + this.#input = input + this.#type = input.schema.enum ? "enum" : "string" + this.innerHTML = ` +
    + + + + +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + + Comma separated list of options +
    +
    +
    +
    + +
    +
    +
    +
    + ` + + this.addEventListener("input", ev=>{ + const val = ev.target.value + const ename = ev.target.getAttribute("name") + if(ename === "id"){ + this.#input.id = val + } + else if(ename === "title"){ + this.#input.title = val + } + else if(ename === "description"){ + this.#input.title = val + } + else if(ename === "minOccurs"){ + this.#input.minOccurs = val + } + else if(ename === "maxOccurs"){ + this.#input.maxOccurs = val + } + else if(ename === "format"){ + this.#input.schema.format = val + } + else if(ename === "contentMediaType"){ + this.#input.schema.contentMediaType = val + } + else if(ename === "options"){ + this.#input.schema.enum = val.split(",") + } + else if(ename === "default"){ + this.#input.schema.default = val + } + else if(ename === "type"){ + this.#type = ev.target.value + if(this.#type === "enum"){ + this.querySelector("div[name=string-input]").classList.add("d-none") + this.querySelector("div[name=enum-input]").classList.remove("d-none") + this.#input.schema.enum = this.querySelector("input[name=options]").value.split(",") + }else if(this.#type === "string"){ + this.querySelector("div[name=enum-input]").classList.add("d-none") + this.querySelector("div[name=string-input]").classList.remove("d-none") + delete this.#input.schema['enum'] + } + } + + console.log(this.#input) + }) + + } + +} +window.customElements.define('d4s-ccp-input-editor', CCPInputWidgetEditorController); \ No newline at end of file diff --git a/ccp/js/methodeditorcontroller.js b/ccp/js/methodeditorcontroller.js new file mode 100644 index 0000000..7eee553 --- /dev/null +++ b/ccp/js/methodeditorcontroller.js @@ -0,0 +1,666 @@ +class CCPMethodEditorController extends HTMLElement{ + + #boot; + #rootdoc; + + #serviceurl = "https://nubis1.int.d4science.net:8080" + #runtimes; + #locked = false; + + #tmp_inputs = [] + #tmp_outputs = [] + #current = null; + #method_template = { + id : "", + title : "New Method", + description : "New empty method", + version : "1.0.0", + jobControlOptions : "async-execute", + inputs : {}, + outputs : {}, + additionalParameters : { + parameters : [ + { + name : "deploy-script", + value : [ +` +- hosts: localhost + tasks: + - name: hookup instance + add_host: + name: "{ instancename }" + ansible_connection: lxd + groupname: "targets" +` + ] + }, + { + name : "execute-script", + value : [ +` +- hosts: localhost + tasks: + - name: hookup instance + add_host: + name: "{ instancename }" + ansible_connection: lxd + groupname: "targets" +` + ] + }, + { + name : "fetch-output-script", + value : [ +` +- hosts: localhost + tasks: + - name: hookup instance + add_host: + name: "{ instancename }" + ansible_connection: lxd + groupname: "targets" +` + ] + }, + { + name : "undeploy-script", + value : [ +` +- hosts: localhost + tasks: + - name: hookup instance + add_host: + name: "{ instancename }" + ansible_connection: lxd + groupname: "targets" +` + ] + }, + { + name : "cancel-script", + value : [ +` +- hosts: localhost + tasks: + - name: hookup instance + add_host: + name: "{ instancename }" + ansible_connection: lxd + groupname: "targets" +` + ] + } + ] + }, + links : [] + } + + #style = ` + + + + ` + #erase_icon = ` + + + ` + + #plus_icon = ` + + + + ` + #disc_icon = ` + + + + ` + #output_icon = ` + + + + ` + + #ansible_icon = ` + + + ` + + constructor(){ + super(); + this.#boot = document.querySelector("d4s-boot-2") + this.#rootdoc = this.attachShadow({ "mode" : "open"}) + this.fetchRuntimes() + } + + connectedCallback(){ + + } + + fetchRuntimes(){ + this.#boot.secureFetch(this.#serviceurl + "/runtimes"). + then(resp=>{ + if(resp.status !== 200) throw "Unable to fetch runtimes"; + return resp.json() + }).then(data=>{ + this.#runtimes = data + this.render() + }).catch(err=>{ + alert(err) + }) + } + + saveCurrent(){ + if(this.#current != null){ + this.adoptTemporaries() + console.log(this.#current) + } + } + + cloneMethod(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.#current.id = "" + 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(){ + if(this.#current == null){ + this.#current = JSON.parse(JSON.stringify(this.#method_template)) + this.#current.id = Math.random(10e6)|0 + } + this.#rootdoc.innerHTML = ` +
    +
    + + + +
    + ${this.#style} +
    +
    +
    +
    + ${this.#current.title} + ${this.renderSaveButton()} + ${this.renderResetButton()} +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    + ${this.renderKeywords()} +
    +
    +
    + + +
    + ${this.renderRuntimes()} +
    +
    +
    +
    +
    + + Inputs + ${this.renderPlusButton("add-input")} + +
    +
    +
    +
    + + Outputs + ${this.renderStandardOutputButtons()} + ${this.renderPlusButton("add-output")} + +
    + +
    +
    +
    + + Scripts + + ${ this.renderAnsibleIcon() } + +
    + ${this.renderScripts()} +
    +
    + + + ` + this.renderInputs() + this.renderOutputs() + + this.#rootdoc.addEventListener("drop", ev=>{ + if(ev.dataTransfer && ev.dataTransfer.getData('text/plain+ccpmethod')){ + const id = ev.dataTransfer.getData('text/plain+ccpmethod') + ev.preventDefault() + ev.stopPropagation() + this.cloneMethod(id) + } + }) + + this.#rootdoc.addEventListener("dragover", ev=>{ + ev.preventDefault() + }) + + this.#rootdoc.querySelector("button[name=reset]").addEventListener("click", ev=>{ + this.#current = this.#method_template + this.#tmp_inputs = [] + this.#tmp_outputs = [] + this.render() + ev.preventDefault() + ev.stopPropagation() + }) + + this.#rootdoc.querySelector("button[name=save]").addEventListener("click", ev=>{ + ev.preventDefault() + ev.stopPropagation() + this.saveCurrent() + }) + + 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=runtimes]").addEventListener("change", ev=>{ + if(ev.target.getAttribute("name") === "runtime-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 : "runtimes/" + id + } + if(!this.#current.links){ + this.#current.links = [link] + }else{ + this.#current.links.push(link) + } + this.reRenderRuntimes(ev.currentTarget) + } + }) + + this.#rootdoc.querySelector("div[name=runtime-list]").addEventListener("click", ev=>{ + ev.preventDefault() + ev.stopPropagation() + if(ev.target.getAttribute('name') === "delete-runtime"){ + const index = ev.target.getAttribute("data-index") + this.#current.links.splice(index, 1) + this.reRenderRuntimes() + } + }) + + 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 : null, + 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, + schema : { + type : "string", + contentMediaType : null + } + } + ) + 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, + 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, + 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[0] = ev.target.value; + }) + } + + renderAnsibleIcon(){ + return ` +
    + ${ this.#ansible_icon } +
    + ` + } + + renderSaveButton(){ + return ` + + ` + } + + renderResetButton(){ + return ` + + ` + } + + renderPlusButton(name){ + 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 "" + } + } + + reRenderRuntimes(){ + this.#rootdoc.querySelector("select[name=runtime-input]").innerHTML = this.renderRuntimeOptions() + this.#rootdoc.querySelector("div[name=runtime-list]").innerHTML = this.renderRuntimes() + } + + renderRuntimeOptions(){ + const selectedrts = this.#current.links.filter(l=>l.rel === "compatibleWith").map(r=>r.href.split("/")[1]) + const available = this.#runtimes.filter(r=>{ return selectedrts.indexOf(r.id) === -1 }) + return ` + + ${ + available.map(rt=>{ + return ` + + ` + }).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) => `` + ).join("\n") + } +} + +window.customElements.define('d4s-ccp-methodeditor', CCPMethodEditorController); diff --git a/ccp/js/outputwidgeteditorcontroller.js b/ccp/js/outputwidgeteditorcontroller.js new file mode 100644 index 0000000..a4d6ba4 --- /dev/null +++ b/ccp/js/outputwidgeteditorcontroller.js @@ -0,0 +1,82 @@ +class CCPOutputWidgetEditorController extends HTMLElement { + + #output = null + #index = null + #type = null + + #delete_icon = ` + + + + ` + + constructor(){ + super() + } + + connectedCallback(controller){ + + } + + get index(){ + return this.#index + } + + render(output, i){ + this.#index = i + this.#output = output + this.#type = output.schema.enum ? "enum" : "string" + this.innerHTML = ` +
    + + + + +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    + ` + } +} + +window.customElements.define('d4s-ccp-output-editor', CCPOutputWidgetEditorController); \ No newline at end of file