517 lines
21 KiB
JavaScript
517 lines
21 KiB
JavaScript
class CCPExecutionHistory extends HTMLElement {
|
|
|
|
#boot = null;
|
|
#rootdoc = null;
|
|
#serviceurl = null;
|
|
#broadcasturl = null;
|
|
#data = [];
|
|
#filtered = [];
|
|
#socket = null;
|
|
#interval = 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 = `
|
|
<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>
|
|
.lxd{
|
|
background-color: #dd4814;
|
|
color: white;
|
|
}
|
|
.docker{
|
|
color: white;
|
|
background-color: #2496ed;
|
|
}
|
|
.ccp-execution-list {
|
|
min-height: 3rem;
|
|
}
|
|
.ccp-execution-list .lxd{
|
|
background-color: #dd4814;
|
|
}
|
|
.ccp-execution-list .docker{
|
|
background-color: #2496ed;
|
|
}
|
|
</style>
|
|
<template id="EXECUTIOM_LIST_TEMPLATE">
|
|
<ul name="ccp_execution_list" class="ccp-execution-list list-group border border-2">
|
|
<li class="ccp-method-item list-group-item list-group-item-dark">
|
|
<details name="level1">
|
|
<summary class="ccp-method-item-header noselect">
|
|
<h5 class="text-primary d-inline mr-2"></h5>
|
|
<span name="failed" title="Failed executions" class="badge badge-danger float-right">Z</span>
|
|
<span name="successful" title="Successful executions"class="badge badge-success float-right mr-1">Y</span>
|
|
<span name="running" title="Running executions"class="badge badge-primary float-right mr-1">Y</span>
|
|
<span name="accepted" title="Accepted executions" class="badge badge-secondary float-right mr-1">X</span>
|
|
</summary>
|
|
<ul class="ccp-execution-list list-group" style="list-style:none">
|
|
<li class="ccp-execution-item list-group-item-secondary my-2 p-2" draggable="true">
|
|
<details>
|
|
<summary>
|
|
<span name="version" class="badge badge-primary"></span>
|
|
<span name="status" class="ml-1 badge"></span>
|
|
<div class="d-flex float-right" style="gap: 3px 5px; max-width: 40%; min-width:60px; flex-wrap:wrap;">
|
|
<button data-index="0" name="zip" title="Download as zip archive" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
|
|
<svg viewBox="0 0 48 48"><path d="M7 40q-1.15 0-2.075-.925Q4 38.15 4 37V11q0-1.15.925-2.075Q5.85 8 7 8h14l3 3h17q1.15 0 2.075.925Q44 12.85 44 14v23q0 1.15-.925 2.075Q42.15 40 41 40Zm25-3h9V14h-9v4.6h4.6v4.6H32v4.6h4.6v4.6H32ZM7 37h20.4v-4.6H32v-4.6h-4.6v-4.6H32v-4.6h-4.6V14h-4.65l-3-3H7v26Zm0-23v-3 26-23Z"/></svg>
|
|
</button>
|
|
<button data-index="0" name="provo" title="Export to Prov-o document" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
|
|
<svg viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M13 3H6v18h4v-6h3c3.31 0 6-2.69 6-6s-2.69-6-6-6zm.2 8H10V7h3.2c1.1 0 2 .9 2 2s-.9 2-2 2z"/></svg>
|
|
</button>
|
|
<button data-index="0" name="reexecute1" title="Level 1 re-execution" class="btn btn-info ccp-toolbar-button ccp-toolbar-button-small">
|
|
<svg enable-background="new 0 0 24 24" viewBox="0 0 24 24"><g><rect fill="none" height="24" width="24"/><rect fill="none" height="24" width="24"/><rect fill="none" height="24" width="24"/></g><g><g/><path d="M12,5V1L7,6l5,5V7c3.31,0,6,2.69,6,6s-2.69,6-6,6s-6-2.69-6-6H4c0,4.42,3.58,8,8,8s8-3.58,8-8S16.42,5,12,5z"/></g></svg>
|
|
</button>
|
|
<button data-index="0" name="reexecute2" title="Level 2 re-execution" class="btn btn-info ccp-toolbar-button ccp-toolbar-button-small">
|
|
<svg enable-background="new 0 0 24 24" viewBox="0 0 24 24"><g><rect fill="none" height="24" width="24"/></g><g><path d="M12,2C6.48,2,2,6.48,2,12s4.48,10,10,10s10-4.48,10-10S17.52,2,12,2z M12,18.5c-3.31,0-6-2.69-6-6h2c0,2.21,1.79,4,4,4 s4-1.79,4-4c0-2.24-1.85-4.09-4.16-3.99l1.57,1.57L12,11.5l-4-4l4-4l1.41,1.41l-1.6,1.6C15.28,6.4,18,9.18,18,12.5 C18,15.81,15.31,18.5,12,18.5z"/></g></svg>
|
|
</button>
|
|
<button data-index="0" name="delete" title="Delete" class="btn btn-danger ccp-toolbar-button ccp-toolbar-button-small">
|
|
<svg viewBox="0 0 24 24">
|
|
<path d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<p name="createstart" class="font-weight-light font-italic" style="margin-top:revert">
|
|
<span name="created"></span>.
|
|
<span name="started" class="ml-1"></span>
|
|
</p>
|
|
<p name="message" class="font-weight-light font-italic" style="margin-top:revert">
|
|
<span name="updated"></span>:
|
|
<span name="message" class="ml-1"></span>
|
|
</p>
|
|
</summary>
|
|
<div class="d-flex mb-1" style="gap:3px">
|
|
<span class="badge badge-light text-info border border-info" name="infrastructure" alt="Infrastructure" title="Infrastructure"></span>
|
|
<span class="badge" name="runtime" alt="Runtime" title="Runtime"></span>
|
|
</div>
|
|
<ul>
|
|
<li></li>
|
|
</ul>
|
|
</details>
|
|
</li>
|
|
</ul>
|
|
</details>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
<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">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="ccp-toolbar-header">
|
|
<div>
|
|
<span name="header" class="mr-2">Execution Monitor</span>
|
|
</div>
|
|
<div class="ccp-toolbar-right">
|
|
<button name="refresh" class="btn btn-primary ccp-toolbar-button" title="Refresh">
|
|
<svg viewBox="0 0 48 48"><path d="M24 40q-6.65 0-11.325-4.675Q8 30.65 8 24q0-6.65 4.675-11.325Q17.35 8 24 8q4.25 0 7.45 1.725T37 14.45V8h3v12.7H27.3v-3h8.4q-1.9-3-4.85-4.85Q27.9 11 24 11q-5.45 0-9.225 3.775Q11 18.55 11 24q0 5.45 3.775 9.225Q18.55 37 24 37q4.15 0 7.6-2.375 3.45-2.375 4.8-6.275h3.1q-1.45 5.25-5.75 8.45Q29.45 40 24 40Z"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<input type="text" name="search" class="form-control" placeholder="Search"/>
|
|
</div>
|
|
<div>
|
|
<ul name="ccp_execution_list"></ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
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.#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=>{
|
|
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)
|
|
}
|
|
}
|
|
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) })
|
|
}
|
|
}
|
|
|
|
#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") === "reexecute2"){
|
|
const id = ev.currentTarget.getAttribute("data-index")
|
|
this.reexecute(id, 2)
|
|
}
|
|
},
|
|
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 : "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 = `<a href="${d.href}" onclick="event.preventDefault()">${d.path}</a>`
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
window.customElements.define('d4s-ccp-executionhistory', CCPExecutionHistory);
|