395 lines
13 KiB
JavaScript
395 lines
13 KiB
JavaScript
class CCPExecutionForm extends HTMLElement{
|
|
|
|
#boot;
|
|
#rootdoc;
|
|
#data;
|
|
#method;
|
|
#executionmonitor;
|
|
|
|
#serviceurl;
|
|
|
|
constructor(){
|
|
super()
|
|
this.#boot = document.querySelector("d4s-boot-2")
|
|
this.#serviceurl = this.getAttribute("serviceurl")
|
|
this.#rootdoc = this.attachShadow({ "mode" : "open"})
|
|
|
|
this.connectNewExecutionRequest()
|
|
this.render()
|
|
this.showMethod()
|
|
}
|
|
|
|
static get observedAttributes() {
|
|
return ["method"];
|
|
}
|
|
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
//if((oldValue != newValue) && (name === "method")){
|
|
if(name === "method"){
|
|
this.#method = newValue
|
|
this.loadMethod()
|
|
}
|
|
}
|
|
|
|
connectNewExecutionRequest(){
|
|
document.addEventListener("newexecutionrequest", ev=>{
|
|
if(window.confirm("Please confirm overwrite of execution form?")){
|
|
this.setAttribute("method", ev.detail)
|
|
this.parentElement.scrollIntoViewIfNeeded()
|
|
}
|
|
})
|
|
}
|
|
|
|
render(){
|
|
this.#rootdoc.innerHTML = `
|
|
<div>
|
|
<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>
|
|
.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>
|
|
<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">
|
|
<div>
|
|
<div class="form-group"></div>
|
|
</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>
|
|
<div class="form-row">
|
|
<div class="col-6">
|
|
<button id="execute_method_button" class="btn btn-info">Execute</button>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="">
|
|
<label>Generate code for:</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>
|
|
</select>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</template>
|
|
<template id="EXECUTION_FORM_EMPTY_TEMPLATE">
|
|
<div name="execution_form">
|
|
<i style="padding:3rem">Drop a method here!</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 "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)
|
|
|
|
}).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()
|
|
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 "Error while requesting resource"
|
|
}
|
|
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 });
|
|
document.dispatchEvent(event)
|
|
this.writeToPlexi("Execution request accepted with id: " + data.jobID)
|
|
window.setTimeout(()=>this.unlockRender(), 3000)
|
|
}).catch(err => {alert("Unable to call 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 "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)})
|
|
}
|
|
|
|
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" };
|
|
})
|
|
|
|
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"))
|
|
}
|
|
|
|
initValues(inputs){
|
|
Object.keys(inputs).forEach(k=>{
|
|
const w = this.#rootdoc.querySelector(`d4s-ccp-input[name=${k}]`)
|
|
if(w){
|
|
w.value = (inputs[k])
|
|
}
|
|
})
|
|
}
|
|
|
|
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()
|
|
this.initValues(requestdata.inputs)
|
|
}).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: "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='${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='${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 method has no compatible runtimes available")
|
|
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
|
|
}
|
|
},
|
|
]
|
|
}
|
|
|
|
}
|
|
|
|
window.customElements.define('d4s-ccp-executionform', CCPExecutionForm);
|