class CCPExecutionForm extends HTMLElement { #boot; #rootdoc; #data; #method; #serviceurl; #messages = { "en": { "confirm_form_overwrite": "Please confirm the overwrite of the previous execution form.", "Inputs": "Inputs", "Options": "Options", "Outputs": "Outputs", "Execute": "Execute", "execute_help": "Submit an execution request", "generate_code": "Generate code for", "generate_code_help": "Generate example code for a selectable programming language or runtime", "direct_link": "Direct link", "direct_link_help": "Navigate to the link for directly reopening this method in the execution form", "automatic_archive_option": "Select what you like to archive automatically when the execution is completed", "automatic_archive_provenance": "Automatically archive whole execution provenance", "automatic_archive_provenance_help": "Automatically archive the whole execution provenance to the workspace", "automatic_archive_outputs": "Automatically archive uncompressed outputs only", "automatic_archive_outputs_help": "Automatically archive only the outputs to the workspace", "automatic_archive_none": "Do not archive anything automatically", "automatic_archive_none_help": "Do not archive anything automatically", "execution_accepted": "Execution request accepted with id: ", "drop_method_invitation": "Drop a method here to request an execution", "err_execution_not_accepted": "Error. Execution has not been accepted by server", "err_code_generation": "Error while generating code", "err_load_method": "Error while loading method", "err_no_runtimes": "This method has no compatible runtimes available", "err_execute": "Error while sending execution request", "container_based_warning_confirm" : "The compatible architectures appear to be container based but no ccpimage input has been specified. This could cause errors that can be unpredictable on some infrastructures. Do you want to proceed anyway?" } } constructor() { super() this.#boot = document.querySelector("d4s-boot-2") this.#rootdoc = this.attachShadow({ "mode": "open" }) } connectedCallback() { this.#serviceurl = this.getAttribute("serviceurl") this.connectNewExecutionRequest() this.render() const params = new URLSearchParams(window.location.search) if (params.get('execution')) { const execution = { id: params.get('execution') } this.prepareFromExecution(execution) } else if (params.get('method')) { this.#method = params.get('method') this.loadMethod() } else { this.showMethod() } } static get observedAttributes() { return ["method"]; } getLabel(key, localehint) { const locale = localehint ? localehint : navigator.language const actlocale = this.#messages[locale] ? locale : "en" const msg = this.#messages[actlocale][key] return msg == null || msg == undefined ? key : this.#messages[actlocale][key] } attributeChangedCallback(name, oldValue, newValue) { //if((oldValue != newValue) && (name === "method")){ if (name === "method") { this.#method = newValue this.loadMethod() } } connectNewExecutionRequest() { document.addEventListener("newexecutionrequest", ev => { if (this.#data) { if (window.confirm(this.getLabel("confirm_form_overwrite"))) { this.setAttribute("method", ev.detail) this.parentElement.scrollIntoViewIfNeeded() } } else { this.setAttribute("method", ev.detail) this.parentElement.scrollIntoViewIfNeeded() } }) } render() { this.#rootdoc.innerHTML = `
` } lockRender() { const plexi = this.#rootdoc.querySelector(".plexiglass") plexi.innerHTML = `` plexi.classList.toggle("d-none") } unlockRender() { this.#rootdoc.querySelector(".plexiglass").classList.toggle("d-none") } writeToPlexi(message) { const plexi = this.#rootdoc.querySelector(".plexiglass") plexi.innerHTML = `
${message}
` } loadMethod() { return this.#boot.secureFetch(this.#serviceurl + "/processes/" + this.#method).then( (resp) => { if (resp.status === 200) { return resp.json() } else throw this.getLabel("err_load_method") } ).then(data => { this.#data = data const infra = this.#data.links.filter(l => l.rel === "compatibleWith")[0] return this.#boot.secureFetch(this.#serviceurl + "/" + infra.href) }).then(resp => { this.#data.executable = resp.status === 200 }).then(() => { this.showMethod() }).catch(err => alert(err)) } showEmpty(resp) { BSS.apply(this.#empty_executionform_bss, this.#rootdoc) } showMethod() { if (this.#method == null) this.showEmpty(); else { BSS.apply(this.#executionform_bss, this.#rootdoc) } } sendExecutionRequest() { this.lockRender() const url = this.#serviceurl + "/processes/" + this.#method + "/execution" const req = this.buildRequest() // Perform preliminary check on the possibility of infra being container based and ccpimage missing if(!req.inputs["ccpimage"]){ const reg = /docker|lxd/i const containerbased = this.#data.links.filter(l=>l.rel === "compatibleWith"). reduce((acc, l)=>{ return acc && (l.title.match(reg) || l.href.match(reg) ? true : false)}, true) if(containerbased && !window.confirm(this.getLabel("container_based_warning_confirm"))){ this.unlockRender() return; } } this.#boot.secureFetch( url, { method: "POST", body: JSON.stringify(req), headers: { "Content-Type": "application/json" } } ).then(reply => { if (reply.status !== 200 && reply.status !== 201) { throw this.getLabel("err_execute") } return reply.json() }).then(data => { if (data.status !== "accepted") { throw this.getLabel("err_execution_not_accepted") } const event = new CustomEvent('newexecution', { detail: data.jobID }); document.dispatchEvent(event) this.writeToPlexi(this.getLabel("execution_accepted") + " " + data.jobID) window.setTimeout(() => this.unlockRender(), 3000) }).catch(err => { alert(this.getLabel("err_execute") + ": " + err); this.unlockRender() }) } generateCode(mime, filename) { const url = this.#serviceurl + "/methods/" + this.#method + "/code" const req = this.buildRequest() this.#boot.secureFetch( url, { method: "POST", body: JSON.stringify(req), headers: { "Content-Type": "application/json", "Accept": mime } } ).then(reply => { if (reply.status !== 200) throw this.getLabel("err_code_generation"); return reply.blob() }).then(blob => { const objectURL = URL.createObjectURL(blob) var tmplnk = document.createElement("a") tmplnk.download = filename tmplnk.href = objectURL document.body.appendChild(tmplnk) tmplnk.click() document.body.removeChild(tmplnk) }).catch(err => { alert(err) }) } buildRequest() { let request = { inputs: {}, outputs: {}, response: "raw" } //fill inputs const inputs = this.getInputs() inputs.forEach(i => { request.inputs[i.name] = i.value }) //fill outputs const outputs = this.getOutputs() outputs.forEach(o => { if (o.enabled) request.outputs[o.name] = { transmissionMode: "value" }; }) const archiveoption = this.#rootdoc.querySelector("select[name='archive-selector']").value switch (archiveoption) { case 'execution': request.subscribers = [{ successUri: "http://registry:8080/executions/archive-to-folder" }] break; case 'outputs': request.subscribers = [{ successUri: "http://registry:8080/executions/outputs/archive-to-folder" }] break default: break } return request } getInputs() { return Array.prototype.slice.call(this.#rootdoc.querySelectorAll("d4s-ccp-input")) } getOutputs() { return Array.prototype.slice.call(this.#rootdoc.querySelectorAll("d4s-ccp-output")) } initInputValues(inputs) { Object.keys(inputs).forEach(k => { const w = this.#rootdoc.querySelector(`d4s-ccp-input[name=${k}]`) if (w) { w.value = (inputs[k]) } }) } initOptionValues(request) { const archiveselector = this.#rootdoc.querySelector("select[name='archive-selector']") if (request.subscribers) { if (request.subscribers.filter(s => s.successUri === "http://registry:8080/executions/archive-to-folder").length === 1) { archiveselector.value = "execution" } else if (request.subscribers.filter(s => s.successUri === "http://registry:8080/executions/outputs/archive-to-folder").length === 1) { archiveselector.value = "outputs" } else { archiveselector.value = "none" } } else { archiveselector.value = "none" } } prepareFromExecution(exec) { //if exec carries already full information then it comes from archive. Use this info instead of fetching. if (exec.fullrequest && exec.fullmethod && exec.fullinfrastructure) { this.#data = exec.fullmethod this.#method = this.#data.id this.#boot.secureFetch(this.#serviceurl + "/infrastructures/" + exec.fullinfrastructure.id) .then(resp => { if (resp.ok) { this.#data.executable = true this.showMethod() this.initInputValues(exec.fullrequest.inputs) this.initOptionValues(exec.fullrequest) } else throw ("Unable to find infrastructure") }).catch(err => alert(err)) return } //fetch method and request and validate infra let f1 = this.#boot.secureFetch(this.#serviceurl + `/executions/${exec.id}/metadata/method.json`) .then(resp => { if (resp.status === 200) { return resp.json() } else throw this.getLabel("err_load_method") } ).then(data => data) let f2 = this.#boot.secureFetch(this.#serviceurl + `/executions/${exec.id}/metadata/request.json`) .then(resp => { if (resp.status === 200) { return resp.json() } else throw this.getLabel("err_load_method") } ).then(data => data) var requestdata = null Promise.all([f1, f2]).then(m_and_r => { this.#data = m_and_r[0] requestdata = m_and_r[1] this.#method = this.#data.id const infra = this.#data.links.filter(l => l.rel === "compatibleWith")[0] return this.#boot.secureFetch(this.#serviceurl + "/" + infra.href) }).then(resp => { this.#data.executable = resp.status === 200 }).then(() => { this.showMethod() this.initInputValues(requestdata.inputs) this.initOptionValues(requestdata) }).catch(err => alert(err)) } #empty_executionform_bss = { template: "#EXECUTION_FORM_EMPTY_TEMPLATE", target: "div[name=execution_form]", on_drop: ev => { if (ev.dataTransfer) { if (ev.dataTransfer.getData('text/plain+ccpmethod')) { const id = ev.dataTransfer.getData('text/plain+ccpmethod') this.setAttribute("method", id); ev.preventDefault() ev.stopPropagation() ev.currentTarget.style.backgroundColor = "white" } else if (ev.dataTransfer.getData('text/plain+ccpexecution')) { this.prepareFromExecution(JSON.parse(ev.dataTransfer.getData('application/json+ccpexecution'))) ev.preventDefault() ev.stopPropagation() ev.currentTarget.style.backgroundColor = "white" } } }, on_dragover: ev => { ev.preventDefault() }, } #executionform_bss = { template: "#EXECUTION_FORM_TEMPLATE", target: "div[name=execution_form]", in: () => this.#data, on_drop: ev => { if (ev.dataTransfer) { if (ev.dataTransfer.getData('text/plain+ccpmethod')) { const id = ev.dataTransfer.getData('text/plain+ccpmethod'); this.setAttribute("method", id); ev.preventDefault() ev.stopPropagation() } else if (ev.dataTransfer.getData('text/plain+ccpexecution')) { this.prepareFromExecution(JSON.parse(ev.dataTransfer.getData('application/json+ccpexecution'))) ev.preventDefault() ev.stopPropagation() ev.currentTarget.style.backgroundColor = "white" } } }, on_dragover: ev => { ev.preventDefault() }, recurse: [ { target: ".ccp-method-title", apply: (e, d) => e.innerHTML = ` ${d.title} ${d.version} ${d.metadata.filter(md => md.role === 'author').map(a => `${a.title}`)} ` }, { target: "div.description", apply: (e, d) => { const maxchars = 200 const maxlines = 4 const s = d.description const lines = s.split("\n") if (lines.length <= maxlines && s.length <= maxchars) { e.innerHTML = s.replaceAll("\n", "
") } else if (lines.length > maxlines) { const lines1 = lines.slice(0, maxlines -1) const index = Math.min(maxchars, lines1.join("
").length) const sf = s.replaceAll("\n", "
") e.innerHTML = `
${sf.substring(0, index)}......${sf.substring(index)}
` } else { const sf = s.replaceAll("\n", "
") e.innerHTML = `
${sf.substring(0, maxchars)}......${sf.substring(maxchars)}
` } } }, { target: "div.ccp-inputs", in: (e, d) => d, recurse: [ { "in": (e, d) => { return Object.values(d.inputs) }, target: "div", apply: (e, d) => { e.innerHTML = `` } } ] }, { target: "div.ccp-outputs", in: (e, d) => d, recurse: [ { "in": (e, d) => { return Object.values(d.outputs) }, target: "div", apply: (e, d) => { e.innerHTML = `` } } ] }, { target: "div.ccp-runtimes *[name=refresh-runtimes]", on_click: ev => { BSS.apply(this.#executionform_bss) } }, { target: "#execute_method_button", on_click: ev => { ev.preventDefault() ev.stopPropagation() if (this.#data.executable) this.sendExecutionRequest(); else alert(this.getLabel("err_no_runtimes")) return false; } }, { target: "button[name=codegen]", on_click: ev => { ev.preventDefault() ev.stopPropagation() const langsel = ev.target.parentElement.querySelector("select[name=language-selector]") const lang = langsel.value const ext = langsel.selectedOptions[0].getAttribute("data-ext") this.generateCode(lang, `ccprequest.${ext}`) return false } }, { target: "a[name=direct_link_method]", apply: (e, d) => e.href = window.location.origin + window.location.pathname + "?method=" + d.id } ] } } window.customElements.define('d4s-ccp-executionform', CCPExecutionForm);