550 lines
21 KiB
JavaScript
550 lines
21 KiB
JavaScript
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 = `
|
|
<div>
|
|
<link rel="canonical" href="https://getbootstrap.com/docs/5.0/components/modal/">
|
|
<link rel="stylesheet" href="https://cdn.cloud.d4science.org/ccp/css/common.css"></link>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" crossorigin="anonymous">
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/>
|
|
<style>
|
|
.ccp-execution-form{
|
|
position: relative;
|
|
}
|
|
</style>
|
|
<template id="EXECUTION_FORM_TEMPLATE">
|
|
<div class="ccp-execution-form" name="execution_form">
|
|
<h5 class="ccp-method-title"></h5>
|
|
<div class="description font-italic font-weight-lighter"></div>
|
|
<div class="plexiglass d-none"></div>
|
|
<form name="execution_form" class="d-flex flex-column gap-3" style="gap:5px">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5>${this.getLabel("Inputs")}</h5>
|
|
</div>
|
|
<div class="card-body ccp-inputs">
|
|
<div>
|
|
<div class="form-group"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5>${this.getLabel("Options")}</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="p-1">
|
|
<label class="small text-muted p-0">${this.getLabel("automatic_archive_option")}.</label>
|
|
<select name="archive-selector" class="form-select p-2" style="padding:2px">
|
|
<option value="execution" title="${this.getLabel("automatic_archive_provenance_help")}">${this.getLabel("automatic_archive_provenance")}</option>
|
|
<option value="outputs" title="${this.getLabel("automatic_archive_outputs_help")}">${this.getLabel("automatic_archive_outputs")}</option>
|
|
<option value="none" title="${this.getLabel("automatic_archive_none_help")}">${this.getLabel("automatic_archive_none")}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5>${this.getLabel("Outputs")}</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row ccp-outputs">
|
|
<div class="col form-group"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<button title="${this.getLabel("execute_help")}" id="execute_method_button" class="btn btn-info">${this.getLabel("Execute")}</button>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="mb-3" title="${this.getLabel("generate_code_help")}">
|
|
<label>${this.getLabel("generate_code")}</label>
|
|
<div class="d-flex">
|
|
<select name="language-selector" class="form-control" style="padding:2px">
|
|
<option value="text/python" data-ext="py" title="Generate plain Python3">Python 3</option>
|
|
<option value="text/plain+r" data-ext="r" title="Generate plain R">R</option>
|
|
<option value="application/vnd.jupyter+python" data-ext="ipynb" title="Generate Jupyter notebook with Python 3 cells">Jupyter Python3</option>
|
|
<option value="text/julia" data-ext="jl" title="Generate Julia 1.9.0 code (HTTP 1.10.0, JSON3 1.13.2)">Julia 1.9.0</option>
|
|
<option value="application/x-sh+curl" data-ext="sh" title="Generate Bash script (curl)">Bash (curl)</option>
|
|
<option value="application/x-sh+wget" data-ext="sh" title="Generate Bash script (wget)">Bash (wget)</option>
|
|
<option value="application/json+galaxy" data-ext="json" title="Generate JSON request for Galaxy">Galaxy CCP request (preview)</option>
|
|
<option value="application/xml+galaxy" data-ext="xml" title="Generate installable Galaxy tool">Galaxy tool (preview)</option>
|
|
</select>
|
|
<button name="codegen" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
|
|
<svg viewBox="0 96 960 960">
|
|
<path d="M320 814 80 574l242-242 43 43-199 199 197 197-43 43Zm318 2-43-43 199-199-197-197 43-43 240 240-242 242Z"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="d-flex">
|
|
<a title="${this.getLabel("direct_link_help")}" name="direct_link_method" class="text-truncate" href="${window.location.href}">${this.getLabel("direct_link")}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</template>
|
|
<template id="EXECUTION_FORM_EMPTY_TEMPLATE">
|
|
<div name="execution_form">
|
|
<i style="padding:3rem">${this.getLabel("drop_method_invitation")}!</i>
|
|
</div>
|
|
</template>
|
|
<div name="execution_form"></div>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
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 = `
|
|
<div class="d-flex" style="flex-direction: column;justify-content:center;position:relative;height:100%;align-items: center;">
|
|
<div class="mx-2 p-4 bg-success text-white border" style="border-radius: 5px;box-shadow: 2px 2px 2px darkgray;">
|
|
<div>${message}</h5></div>
|
|
</div>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
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} <span class="badge badge-primary ml-1">${d.version}</span>
|
|
${d.metadata.filter(md => md.role === 'author').map(a => `<span class="badge badge-warning ml-1">${a.title}</span>`)}
|
|
`
|
|
},
|
|
{
|
|
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", "<br/>")
|
|
} else if (lines.length > maxlines) {
|
|
const lines1 = lines.slice(0, maxlines -1)
|
|
const index = Math.min(maxchars, lines1.join("<br/>").length)
|
|
const sf = s.replaceAll("\n", "<br/>")
|
|
e.innerHTML = `<details><summary>${sf.substring(0, index)}...</summary>...${sf.substring(index)}</details>`
|
|
} else {
|
|
const sf = s.replaceAll("\n", "<br/>")
|
|
e.innerHTML = `<details><summary>${sf.substring(0, maxchars)}...</summary>...${sf.substring(maxchars)}</details>`
|
|
}
|
|
}
|
|
},
|
|
{
|
|
target: "div.ccp-inputs",
|
|
in: (e, d) => d,
|
|
recurse: [
|
|
{
|
|
"in": (e, d) => { return Object.values(d.inputs) },
|
|
target: "div",
|
|
apply: (e, d) => {
|
|
e.innerHTML = `<d4s-ccp-input name="${d.id}" input='${btoa(JSON.stringify(d))}'></d4s-ccp-input>`
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
target: "div.ccp-outputs",
|
|
in: (e, d) => d,
|
|
recurse: [
|
|
{
|
|
"in": (e, d) => { return Object.values(d.outputs) },
|
|
target: "div",
|
|
apply: (e, d) => {
|
|
e.innerHTML = `<d4s-ccp-output name="${d.id}" output='${btoa(JSON.stringify(d))}'></d4s-ccp-output>`
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
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);
|