web-components/ccp/js/executionformcontroller.js

444 lines
15 KiB
JavaScript
Raw Normal View History

2022-03-25 15:39:39 +01:00
class CCPExecutionForm extends HTMLElement{
#boot;
#rootdoc;
#data;
#method;
2022-05-05 12:19:06 +02:00
#executionmonitor;
2022-03-25 15:39:39 +01:00
#serviceurl;
2022-03-25 15:39:39 +01:00
constructor(){
super()
this.#boot = document.querySelector("d4s-boot-2")
this.#rootdoc = this.attachShadow({ "mode" : "open"})
2024-04-11 17:25:41 +02:00
}
connectedCallback(){
this.#serviceurl = this.getAttribute("serviceurl")
2023-03-17 10:49:21 +01:00
this.connectNewExecutionRequest()
this.render()
2024-02-02 17:32:51 +01:00
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')){
2024-02-02 17:35:59 +01:00
this.#method = params.get('method')
this.loadMethod()
} else {
this.showMethod()
2024-02-02 17:35:59 +01:00
}
2022-03-25 15:39:39 +01:00
}
2024-04-11 17:25:41 +02:00
2022-03-25 15:39:39 +01:00
static get observedAttributes() {
2022-05-05 12:19:06 +02:00
return ["method"];
2022-03-25 15:39:39 +01:00
}
attributeChangedCallback(name, oldValue, newValue) {
2023-06-15 10:36:25 +02:00
//if((oldValue != newValue) && (name === "method")){
if(name === "method"){
2022-03-25 15:39:39 +01:00
this.#method = newValue
this.loadMethod()
}
}
2023-03-17 10:49:21 +01:00
connectNewExecutionRequest(){
document.addEventListener("newexecutionrequest", ev=>{
if(window.confirm("Please confirm overwrite of execution form?")){
this.setAttribute("method", ev.detail)
this.parentElement.scrollIntoViewIfNeeded()
2023-03-17 10:49:21 +01:00
}
})
}
render(){
this.#rootdoc.innerHTML = `
<div>
<link rel="canonical" href="https://getbootstrap.com/docs/5.0/components/modal/">
<link rel="stylesheet" href="https://cdn.dev.d4science.org/ccp/css/common.css"></link>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<style>
2023-03-20 11:07:42 +01:00
.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>
<p class="description font-italic font-weight-lighter"></p>
2023-03-20 11:07:42 +01:00
<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>Inputs</h5>
</div>
<div class="card-body ccp-inputs">
2023-09-26 17:14:47 +02:00
<div>
<div class="form-group"></div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5>Options</h5>
</div>
<div class="card-body">
<div class="col form-check">
<input class="form-check-input" type="checkbox" name="auto-archive-outputs" alt="Automatically archive outputs to workspace" title="Automatically archive outputs to workspace" checked="checked">
<label class="form-check-label">Automatically archive outputs to workspace</label>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5>Outputs</h5>
</div>
<div class="card-body">
<div class="form-row ccp-outputs">
<div class="col form-group"></div>
</div>
</div>
</div>
2023-04-14 11:51:08 +02:00
<div class="form-row">
<div class="col-6">
2023-10-06 16:19:37 +02:00
<button id="execute_method_button" class="btn btn-info">Execute</button>
2023-03-20 11:07:42 +01:00
</div>
2023-04-14 11:51:08 +02:00
<div class="col-6">
2024-02-02 18:33:42 +01:00
<div class="mb-3">
2023-06-07 17:13:52 +02:00
<label>Generate code for:</label>
2023-06-07 17:24:07 +02:00
<div class="d-flex">
2024-02-02 18:12:35 +01:00
<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>
</select>
2024-02-02 18:12:35 +01:00
<button name="codegen" title="Generate code" 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>
2024-02-02 18:33:42 +01:00
</div>
<div>
<div class="d-flex">
2024-04-09 13:16:51 +02:00
<a name="direct_link_method" class="text-truncate" href="${window.location.href}">Direct link</a>
2023-06-07 17:24:07 +02:00
</div>
2023-03-20 11:07:42 +01:00
</div>
</div>
2023-04-14 11:51:08 +02:00
</div>
</form>
</div>
</template>
<template id="EXECUTION_FORM_EMPTY_TEMPLATE">
<div name="execution_form">
2022-07-19 09:54:40 +02:00
<i style="padding:3rem">Drop a method here!</i>
</div>
</template>
<div name="execution_form"></div>
</div>
`
2022-03-25 15:39:39 +01:00
}
2023-03-20 11:07:42 +01:00
lockRender(){
2023-05-08 11:10:33 +02:00
const plexi = this.#rootdoc.querySelector(".plexiglass")
plexi.innerHTML = ``
plexi.classList.toggle("d-none")
2023-03-20 11:07:42 +01:00
}
unlockRender(){
this.#rootdoc.querySelector(".plexiglass").classList.toggle("d-none")
}
2023-05-08 11:10:33 +02:00
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>
`
}
2022-03-25 15:39:39 +01:00
loadMethod(){
return this.#boot.secureFetch(this.#serviceurl + "/processes/" + this.#method).then(
2022-03-25 15:39:39 +01:00
(resp)=>{
2022-04-12 19:51:38 +02:00
if(resp.status === 200){
return resp.json()
}else throw "Error retrieving process"
}
).then(data=>{
this.#data = data
const infra =
this.#data.links.filter(l => l.rel === "compatibleWith")[0]
return this.#boot.secureFetch(this.#serviceurl + "/" + infra.href)
2022-05-05 12:19:06 +02:00
}).then(resp=>{
this.#data.executable = resp.status === 200
}).then(()=>{
2022-04-12 19:51:38 +02:00
this.showMethod()
}).catch(err=>alert(err))
2022-03-25 15:39:39 +01:00
}
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)
}
}
2022-04-12 19:51:38 +02:00
sendExecutionRequest(){
2023-03-20 11:07:42 +01:00
this.lockRender()
2022-04-12 19:51:38 +02:00
const url = this.#serviceurl + "/processes/" + this.#method + "/execution"
2022-05-05 12:19:06 +02:00
const req = this.buildRequest()
2022-04-12 19:51:38 +02:00
this.#boot.secureFetch(
2022-05-05 12:19:06 +02:00
url, { method : "POST", body : JSON.stringify(req), headers : { "Content-Type" : "application/json"}}
2022-04-12 19:51:38 +02:00
).then(reply=>{
if(reply.status !== 200 && reply.status !== 201){
throw "Error while requesting resource"
}
2022-05-05 12:19:06 +02:00
return reply.json()
}).then(data=>{
if(data.status !== "accepted"){
throw "Execution has not been accepted by server"
}
const event = new CustomEvent('newexecution', { detail: data.jobID });
2023-01-26 11:31:08 +01:00
document.dispatchEvent(event)
2023-05-08 11:10:33 +02:00
this.writeToPlexi("Execution request accepted with id: " + data.jobID)
window.setTimeout(()=>this.unlockRender(), 3000)
2023-03-20 11:07:42 +01:00
}).catch(err => {alert("Unable to call execute: " + err); this.unlockRender()})
2022-05-05 12:19:06 +02:00
}
2023-04-14 11:51:08 +02:00
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 "Error while requesting code:";
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)})
}
2022-05-05 12:19:06 +02:00
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 autoarchiveoption = this.#rootdoc.querySelector("input[name='auto-archive-outputs']")
if (autoarchiveoption.checked){
request.subscribers = [
{ successUri : "http://registry:8080/executions/archive-to-folder" }
]
}
2022-05-05 12:19:06 +02:00
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"))
2022-04-12 19:51:38 +02:00
}
2024-02-09 11:57:23 +01:00
initInputValues(inputs){
Object.keys(inputs).forEach(k=>{
const w = this.#rootdoc.querySelector(`d4s-ccp-input[name=${k}]`)
if(w){
w.value = (inputs[k])
}
})
}
2024-02-09 11:57:23 +01:00
initOptionValues(request){
const autoarchiveoption = this.#rootdoc.querySelector("input[name='auto-archive-outputs']")
2024-02-09 11:59:40 +01:00
autoarchiveoption.checked = request.subscribers && request.subscribers.filter(s=>s.successUri === "http://registry:8080/executions/archive-to-folder").length === 1
2024-02-09 11:57:23 +01:00
}
prepareFromExecution(exec){
let f1 =
this.#boot.secureFetch(this.#serviceurl + `/executions/${exec.id}/metadata/method.json`)
.then(resp=>{
if(resp.status === 200){
return resp.json()
}else throw "Error retrieving process"
}
).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 "Error retrieving process"
}
).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()
2024-02-09 11:57:23 +01:00
this.initInputValues(requestdata.inputs)
this.initOptionValues(requestdata)
}).catch(err=>alert(err))
}
2022-03-25 15:39:39 +01:00
#empty_executionform_bss = {
template : "#EXECUTION_FORM_EMPTY_TEMPLATE",
2022-03-28 16:42:02 +02:00
target : "div[name=execution_form]",
2022-03-25 15:39:39 +01:00
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"
}
2022-03-25 15:39:39 +01:00
}
},
on_dragover : ev=>{
ev.preventDefault()
},
}
#executionform_bss = {
template : "#EXECUTION_FORM_TEMPLATE",
2022-03-28 16:52:09 +02:00
target : "div[name=execution_form]",
2022-03-25 15:39:39 +01:00
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"
}
2022-03-25 15:39:39 +01:00
}
},
on_dragover : ev=>{
ev.preventDefault()
},
recurse : [
{
2022-05-05 16:51:47 +02:00
target: ".ccp-method-title",
2023-04-03 12:51:08 +02:00
apply : (e,d)=>e.innerHTML = `
${d.title} <span class="badge badge-primary ml-1">${d.version}</span>
2023-04-03 13:08:56 +02:00
${ d.metadata.filter(md=>md.role === 'author').map(a=>`<span class="badge badge-warning ml-1">${a.title}</span>`)}
2023-04-03 12:51:08 +02:00
`
2022-03-25 15:39:39 +01:00
},
{
target: "p.description",
apply : (e,d)=>e.textContent = d.description
},
{
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>`
2022-03-25 15:39:39 +01:00
}
}
]
2022-04-12 19:51:38 +02:00
},
{
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>`
2022-04-12 19:51:38 +02:00
}
}
]
},
{
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()
2022-05-05 12:19:06 +02:00
if(this.#data.executable) this.sendExecutionRequest();
else alert("This method has no compatible runtimes available")
2022-04-12 19:51:38 +02:00
return false;
}
},
2023-04-14 11:51:08 +02:00
{
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
}
},
2024-02-02 18:07:28 +01:00
{
target: "a[name=direct_link_method]",
2024-04-09 13:22:14 +02:00
apply : (e,d)=>e.href = window.location.origin + window.location.pathname + "?method=" + d.id
2024-02-02 18:07:28 +01:00
}
2022-03-25 15:39:39 +01:00
]
}
}
window.customElements.define('d4s-ccp-executionform', CCPExecutionForm);