class CCPExecutionHistory extends HTMLElement { #boot = null; #rootdoc = null; #serviceurl = null; #broadcasturl = null; #archive = null; #data = []; #pending = []; #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() } connectedCallback(){ this.connectBroadcastWithSubject() this.render() this.refreshExecutions() } render(){ this.#rootdoc.innerHTML = `
Execution Monitor
    ` 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("Please 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.ok) return reply.json(); else throw `Unable to load execution ${id}. Check console.` }).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.ok) this.refreshExecutions(); else throw `Unable to delete execution ${id}. Check console.` }).catch(err=>{ console.error(err)}) } refreshExecutions(){ this.#boot.secureFetch(`${this.#serviceurl}/executions`).then(reply =>{ if(reply.ok) return reply.json(); else throw "Unable to load executions. Check console." }).then(data=>{ this.#data = data this.updateList() }).catch(err=>{ console.error(err)}) } connectNewExecution(){ document.addEventListener("newexecution", ev=>{ this.#pending.push(ev.detail) }) } connectBroadcastWithSubject(){ var interval = window.setInterval( ()=>{ if(this.#boot.subject){ window.clearInterval(interval) this.connectBroadcast() } }, 1000) } connectBroadcast(){ this.#socket = new WebSocket(`${this.#broadcasturl}/unified?subject=${this.#boot.subject}`); this.#socket.onmessage = event=>{ const data = JSON.parse(event.data) if(data[0] && data[0].source){ //has to be logs this.appendLogs(data) return } let exec = this.#data.filter(e=>e.id === data.jobID)[0] if(exec){ this.refreshExecution(exec.id) }else{ this.#pending = this.#pending.filter(pe=>{ if(pe === data.jobID){ this.refreshExecution(pe) return false }else{ return true } }) } } this.#interval = window.setInterval( ()=>{ if(this.#socket.readyState === 3){ this.#socket.close() window.clearInterval(this.#interval) this.connectBroadcast() }else{ this.#socket.send("ping") } }, 30000) } appendLogs(data){ if(!data.length) return; const exid = data[0]["attrs"]["execution"] const lt = this.#rootdoc.querySelector(`d4s-ccp-logterminal[index='${exid}']`) if(!lt){ console.error("No terminal found for adding logs of " + exid) }else{ lt.addLines(data) } } download(url, name) { this.#boot.secureFetch(url).then(reply => { if (!reply.ok) { throw "Unable to download. Check console." } 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.ok) { 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.ok) { throw "Unable to re-execute. Check console." } 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.ok) { 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.ok) { 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.ok) { throw "Unable to archive" } }).catch(err=>{ alert(err)}) } toArchiveFolder(id){ this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/archive-to-folder`, { method: "POST" }) .then(reply =>{ if (!reply.ok) { 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.ok) { 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("Please 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("Please 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"){ if(window.confirm("Please confirm re-execution?")){ const id = ev.currentTarget.getAttribute("data-index") this.reexecute(id, 1) } } if(ev.target.getAttribute("name") === "archive"){ if(confirm(" Please confirm archiving of execution to workspace?")){ const id = ev.currentTarget.getAttribute("data-index") this.toArchive(id) } } if(ev.target.getAttribute("name") === "archivefolder"){ if(confirm(" Please confirm archiving of execution outputs to workspace?")){ const id = ev.currentTarget.getAttribute("data-index") this.toArchiveFolder(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)=>{ if(d.ccpnote){ e.textContent = `${d.ccpnote} (${d.methodversion})` }else{ 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)=>{ const rt = d.runtime ? d.runtime : "" const infratype = d.infrastructuretype ? d.infrastructuretype : "" e.textContent = rt + (d.replicas && d.replicas !== "1" ? ' x ' + d.replicas : '') const t = infratype.match(/docker/i) ? "docker" : null const t2 = !t && infratype.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 : "div[name=logterminalcontainer]", apply : (e,d)=>{ e.innerHTML = `` } }, { 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);