class CCPExecutionHistory extends HTMLElement {
#boot = null;
#rootdoc = null;
#serviceurl = null;
#broadcasturl = null;
#archive = null;
#data = [];
#filtered = [];
#socket = null;
#interval = null;
#searchfield = null;
#fileupload = null;
#archiveupload = null;
constructor(){
super()
this.#boot = document.querySelector("d4s-boot-2")
this.#rootdoc = this.attachShadow({ "mode" : "open"})
this.#serviceurl = this.getAttribute("serviceurl")
this.#broadcasturl = this.getAttribute("broadcasturl")
if(!this.#broadcasturl){
this.#broadcasturl = this.#serviceurl.replace(/^http/, "ws")
}
this.#broadcasturl = this.#broadcasturl + "/ws/notification"
this.#archive = this.getAttribute("archive")
this.connectNewExecution()
this.connectBroadcast()
}
connectedCallback(){
this.render()
this.refreshExecutions()
}
render(){
this.#rootdoc.innerHTML = `
-
-
${ this.#archive ? `
`
: ``
}
.
:
`
this.#rootdoc.querySelector("button[name=refresh]").addEventListener("click", ev=>{
this.refreshExecutions()
})
this.#searchfield = this.#rootdoc.querySelector("input[name=search]")
this.#searchfield.addEventListener("input", ev=>{
this.updateList()
})
this.#fileupload = this.#rootdoc.querySelector("label[name=fileupload] > input[type=file]")
this.#fileupload.addEventListener("change", ev=>{
const filelist = ev.target.files;
if (filelist.length > 0) {
const files = Array.prototype.slice.call(filelist)
this.importExecutions(files)
}
})
this.#archiveupload = this.#rootdoc.querySelector("button[name=archive]")
this.#archiveupload.addEventListener("click", ev=>{
const link = ev.target.parentElement.querySelector("input").value
if(link){
if(confirm("Confirm importing of execution from link?")){
this.fromArchive(link)
}
}
})
}
updateList(){
const filter = this.#searchfield.value
if(filter === "" || filter == null || filter == undefined){
this.#filtered = this.#data
}else{
const f = filter.toLowerCase()
this.#filtered = this.#data.filter(d=>{
return false ||
(d.status.toLowerCase().indexOf(f) !== -1)||
(d.method.indexOf(f) !== -1)
})
}
this.#filtered.sort((a,b)=>(new Date(b.updated)) - (new Date(a.updated)))
this.groupBy()
BSS.apply(this.#execution_list_bss, this.#rootdoc)
}
groupBy(){
this.#filtered = this.#filtered.reduce((catalog, exec)=>{
const category = exec.method
catalog[category] = catalog[category] ?? []
catalog[category].push(exec)
return catalog
}, {})
}
refreshExecution(id){
this.#boot.secureFetch(`${this.#serviceurl}/executions?id=${id}`).then(reply =>{
if(reply.status === 200) return reply.json();
else throw `Unable to load execution ${id}`
}).then(data=>{
//this may occur for timing issues since workflow start is async
const exec = data.length === 0 ? { id : id} : data[0]
for(var i=0; i < this.#data.length; i++){
if(this.#data[i].id == exec.id){
this.#data[i] = exec
break
}
}
if(i === this.#data.length){
this.#data = [exec].concat(this.#data)
}
this.updateList()
}).catch(err=>{ console.error(err)})
}
deleteExecution(id){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}`, { method: "DELETE"}).then(reply =>{
if(reply.status === 200) this.refreshExecutions();
else throw `Unable to delete execution ${id}`
}).catch(err=>{ console.error(err)})
}
refreshExecutions(){
this.#boot.secureFetch(`${this.#serviceurl}/executions`).then(reply =>{
if(reply.status === 200) return reply.json();
else throw "Unable to load executions"
}).then(data=>{
this.#data = data
this.updateList()
}).catch(err=>{ console.error(err)})
}
connectNewExecution(){
document.addEventListener("newexecution", ev=>{
const s = ev.detail
const dummy = { id : s.jobID, method : s.method, status : "accepted", created : s.updated, updated : s.updated, resources : []}
this.#data = [ dummy ].concat(this.#data)
this.updateList()
})
}
connectBroadcast(){
this.#socket = new WebSocket(this.#broadcasturl + "/executions");
this.#socket.onmessage = event=>{
const data = JSON.parse(event.data)
let exec = this.#data.filter(e=>e.id === data.jobID)[0]
if(exec){
this.refreshExecution(exec.id)
}
}
this.#interval = window.setInterval( ()=>{
if(this.#socket.readyState === 3){
this.#socket.close()
window.clearInterval(this.#interval)
this.connectBroadcast()
}else{
this.#socket.send("ping")
}
}, 30000)
}
download(url, name) {
this.#boot.secureFetch(url).then(reply => {
if (reply.status !== 200) {
throw "Unable to download"
}
return reply.blob()
}).then(blob => {
const objectURL = URL.createObjectURL(blob)
var tmplnk = document.createElement("a")
tmplnk.download = name
tmplnk.href = objectURL
document.body.appendChild(tmplnk)
tmplnk.click()
document.body.removeChild(tmplnk)
}).catch(err => console.error(err))
}
export(id, mime, filename){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}`,
{ method: "GET", headers : { "Accept" : mime} }).then(reply =>{
if (reply.status !== 200) {
throw "Unable to export " + mime
}
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)})
}
reexecute(id,level){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/level/${level}`, { method: "POST" })
.then(reply =>{
if (reply.status !== 200) {
throw "Unable to re-execute"
}
return reply.json()
}).then(data=>{
console.log(data)
this.refreshExecution(data.jobID)
}).catch(err=>{ alert(err)})
}
importExecutions(files){
if(files && files.length) {
let formdata = new FormData();
files.reduce((formdata, f)=>{
formdata.append("files[]", f)
return formdata
}, formdata)
this.#boot.secureFetch(`${this.#serviceurl}/executions`, { body: formdata, method : "POST"})
.then(reply=>{
if (reply.status !== 200) {
throw "Unable to import"
}else return reply.text()
}).then(data=>{
this.refreshExecutions()
}).catch(err=>{ alert(err) })
}
}
generateCode(id, mime, filename){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/code`,
{ method: "GET", headers : { "Accept" : mime} }).then(reply =>{
if (reply.status !== 200) {
throw "Unable to generate code for " + mime
}
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)})
}
toArchive(id){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/archive`, { method: "POST" })
.then(reply =>{
if (reply.status !== 200) {
throw "Unable to archive"
}
}).catch(err=>{ alert(err)})
}
fromArchive(url){
if(url){
this.#boot.secureFetch(`${this.#serviceurl}/executions/archive?url=${url}`)
.then(reply =>{
if (reply.status !== 200) {
throw "Unable to fetch from archive"
}
return reply.text()
}).then(data=>{
this.refreshExecutions()
}).catch(err=>{ alert(err)})
}
}
#execution_list_bss = {
template : "#EXECUTIOM_LIST_TEMPLATE",
target : "ul[name=ccp_execution_list]",
in : ()=>this,
on_dragover : (ev)=>{
ev.preventDefault()
},
on_dragenter : (ev)=>{
ev.target.classList.toggle("border-info")
},
on_dragleave : (ev)=>{
ev.target.classList.toggle("border-info")
},
on_drop : (ev)=>{
if(ev.dataTransfer && ev.dataTransfer.files && ev.dataTransfer.files.length){
const files = Array.prototype.slice.call(ev.dataTransfer.files)
const zips = files.filter(f=>f.type === "application/zip")
if(confirm("Confirm import of execution files?")){
this.importExecutions(files)
}
}
ev.target.classList.toggle("border-info")
},
recurse:[
{
target : "li.ccp-method-item",
"in" : (e,d)=>Object.keys(this.#filtered),
recurse: [
{
target : "details[name=level1]",
apply : (e,d)=>{
e.alt = e.title = d
if(sessionStorage.getItem(d) === "open") e.open = "open";
else e.removeAttribute("open");
},
on_toggle : ev=>{
if(ev.target.open){
sessionStorage.setItem(ev.currentTarget.alt, 'open')
}else{
sessionStorage.removeItem(ev.currentTarget.alt)
}
},
},
{
target : "summary.ccp-method-item-header h5",
apply : (e,d) => { e.textContent = d }
},
{
target : "summary.ccp-method-item-header span[name=accepted]",
apply : (e,d) => { e.textContent = this.#filtered[d].filter(x=>x.status === 'accepted').length }
},
{
target : "summary.ccp-method-item-header span[name=failed]",
apply : (e,d) => { e.textContent = this.#filtered[d].filter(x=>x.status === 'failed').length }
},
{
target : "summary.ccp-method-item-header span[name=successful]",
apply : (e,d) => { e.textContent = this.#filtered[d].filter(x=>x.status === 'successful').length }
},
{
target : "summary.ccp-method-item-header span[name=running]",
apply : (e,d) => { e.textContent = this.#filtered[d].filter(x=>x.status === 'running').length }
},
{
target : "li.ccp-execution-item",
"in" : (e,d)=>this.#filtered[d],
apply : (e,d)=>e.setAttribute("data-index", d.id),
on_dragstart : ev=>{
ev.dataTransfer.effectAllowed = 'move'
ev.dataTransfer.setData('text/html', ev.currentTarget.innerHTML)
ev.dataTransfer.setData('text/plain+ccpexecution', ev.currentTarget.getAttribute("data-index"))
ev.dataTransfer.setData('application/json+ccpexecution', JSON.stringify(ev.currentTarget.bss_input.data))
},
on_dragend : ev=>{
ev.preventDefault()
},
on_click: ev=>{
if(ev.target.getAttribute("name") === "delete"){
if(window.confirm("Confirm deletion of this execution?")){
const id = ev.currentTarget.getAttribute("data-index")
this.deleteExecution(id)
}
}
if(ev.target.getAttribute("name") === "zip"){
const id = ev.currentTarget.getAttribute("data-index")
this.export(id, "application/zip", id + ".zip")
}
if(ev.target.getAttribute("name") === "provo"){
const id = ev.currentTarget.getAttribute("data-index")
this.export(id, "application/prov-o+xml", id + ".xml")
}
if(ev.target.getAttribute("name") === "reexecute1"){
const id = ev.currentTarget.getAttribute("data-index")
this.reexecute(id, 1)
}
if(ev.target.getAttribute("name") === "archive"){
if(confirm("Confirm aarchiving of execution to workspace?")){
const id = ev.currentTarget.getAttribute("data-index")
this.toArchive(id)
}
}
},
recurse : [
{
target : "details",
apply : (e,d)=>{
e.alt = e.title = d.id
if(sessionStorage.getItem(d.id) === "open") e.open = "open";
else e.removeAttribute("open");
},
on_toggle : ev=>{
if(ev.target.open){
sessionStorage.setItem(ev.currentTarget.alt, 'open')
}else{
sessionStorage.removeItem(ev.currentTarget.alt)
}
}
},
{
target : "span[name=version]",
apply : (e,d)=>{
e.textContent = `${d.methodversion}`
}
},
{
target : "span[name=status]",
apply : (e,d)=>{
if(d.status){
const status = d.status
e.textContent = status
if (status === "running") e.classList.add("badge-primary");
else if (status === "successful") e.classList.add("badge-success");
else if (status === "failed") e.classList.add("badge-danger");
else e.classList.add("badge-secondary");
}
}
},
{
target : "span[name=created]",
apply : (e,d)=>{
if(d.created){
const dt = new Date(d.created)
e.textContent = `Accepted ${dt.toLocaleDateString()} @ ${dt.toLocaleTimeString()}`
}
}
},
{
target : "span[name=started]",
apply : (e,d)=>{
if(d.started){
const dt = new Date(d.started)
e.textContent = `Started ${dt.toLocaleDateString()} @ ${dt.toLocaleTimeString()}`
}
}
},
{
target : "span[name=updated]",
apply : (e,d)=>{
const dt = new Date(d.updated)
e.textContent = `Last update ${dt.toLocaleDateString()} @ ${dt.toLocaleTimeString()}`
}
},
{
target : "span[name=message]",
apply : (e,d)=>{
if(d.message){
e.textContent = d.message
}
}
},
{
target : "span[name=infrastructure]",
apply : (e,d)=>{
e.textContent = d.infrastructure
}
},
{
target : "span[name=runtime]",
apply : (e,d)=>{
e.textContent = d.runtime + (d.replicas && d.replicas !== "1" ? ' x ' + d.replicas : '')
const t = d.infrastructuretype.match(/docker/i) ? "docker" : null
const t2 = !t && d.infrastructuretype.match(/lxd/i) ? "lxd" : t
e.classList.add(t2)
}
},
{
target : "button[name=codegen]",
apply : (e,d)=>e.setAttribute("data-index", d.id),
on_click: (ev)=>{
const id = ev.target.getAttribute("data-index")
const langsel = ev.target.parentElement.querySelector("select[name=language-selector]")
const lang = langsel.value
const ext = langsel.selectedOptions[0].getAttribute("data-ext")
this.generateCode(id, lang, `${id}.${ext}`)
}
},
{
target : "ul",
recurse : [
{
target : "li",
"in" : (e,d)=>{
return d.resources.map(l=>{
return { href : this.#serviceurl + "/executions/" + d.id + "/" + l.path, path : l.path}
})
},
on_click : ev=>{
const href = ev.currentTarget.bss_input.data.href
const name = ev.currentTarget.bss_input.data.path
this.download(href, name)
},
apply : (e,d)=>{
e.innerHTML = `${d.path}`
}
}
]
}
]
}
]
}
]
}
}
window.customElements.define('d4s-ccp-executionhistory', CCPExecutionHistory);