class CCPExecutionHistory extends HTMLElement { #boot = null; #rootdoc = null; #serviceurl = null; #broadcasturl = null; #data = []; #filtered = []; #socket = null; #searchfield = 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.connectNewExecution() this.connectBroadcast() } connectedCallback(){ this.render() this.refreshExecutions() } render(){ this.#rootdoc.innerHTML = `
Execution history
    ` 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() }) } 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.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=>{ const exec = 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 = data.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=>{ this.#data = [ {id: ev.detail } ].concat(this.#data) }) } 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) } } window.setInterval( ()=>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=>{ console.error(err)}) } #execution_list_bss = { template : "#EXECUTIOM_LIST_TEMPLATE", target : "ul[name=ccp_execution_list]", in : ()=>this, 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") } }, 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 === "failure") e.classList.add("badge-danger"); else e.classList.add("badge-secondary"); } } }, { 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 : "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);