support for prov-o exports and completed visual revision to be homogeneous with method list

This commit is contained in:
dcore94 2023-02-01 16:07:15 +01:00
parent 5d9d5451a5
commit a81804b0f6
1 changed files with 234 additions and 111 deletions

View File

@ -1,11 +1,13 @@
class CCPExecutionHistory extends HTMLElement {
#boot = null;
#rootdoc = null;
#serviceurl = null;
#broadcasturl = null;
#executions = [];
#data = [];
#filtered = [];
#socket = null;
#searchfield = null;
constructor(){
super()
@ -22,12 +24,12 @@ class CCPExecutionHistory extends HTMLElement {
}
connectedCallback(){
this.#rootdoc.innerHTML = this.render()
this.render()
this.refreshExecutions()
}
render(){
return `
this.#rootdoc.innerHTML = `
<style>
.noselect{
user-select: none;
@ -40,12 +42,27 @@ class CCPExecutionHistory extends HTMLElement {
color: white;
background-color: #2496ed;
}
.ccp-toolbar-header {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 0.2rem;
min-width: 20rem;
}
.ccp-toolbar-right {
position: absolute;
right: 1rem;
}
.ccp-toolbar-button {
font-weight: bold;
padding: 0.3rem !important;
line-height: .8rem !important;
cursor: pointer;
}
.ccp-toolbar-button-small {
padding: 0.1rem !important;
line-height: .6rem !important;
}
.ccp-toolbar-button svg {
fill: white;
width: 24px;
@ -54,36 +71,105 @@ class CCPExecutionHistory extends HTMLElement {
}
</style>
<template id="EXECUTIOM_LIST_TEMPLATE">
<ol name="ccp_execution_list" class="ccp-execution-list list-group">
<li class="ccp-execution-item list-group-item" style="border: none">
<details open>
<summary class="noselect">
<span name="runtime" class="badge"></span>
<h5 style="display:inline"></h5>
<span name="status" class="badge"></span>
<button data-index="0" name="zip" title="Download as zip archive" class="btn btn-primary ccp-toolbar-button">
<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="delete" title="Delete" class="btn btn-danger ccp-toolbar-button">
<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>
<ul name="ccp_execution_list" class="ccp-method-list list-group">
<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>
<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>
<ul class="list-group">
<li class="list-group-item" style="border: none"></li>
<ul class="ccp-execution-list list-group" style="list-style:none">
<li class="ccp-execution-item list-group-item-secondary my-2 p-2">
<details>
<summary>
<span name="version" class="badge badge-primary"></span>
<span name="status" class="ml-1 badge"></span>
<button data-index="0" name="delete" title="Delete" class="btn btn-danger ccp-toolbar-button ccp-toolbar-button-small float-right m-1">
<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>
<button data-index="0" name="provo" title="Export to Prov-o document" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small float-right m-1">
<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="zip" title="Download as zip archive" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small float-right m-1">
<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>
<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>
<ul>
<li></li>
</ul>
</details>
</li>
</ul>
</details>
</li>
</ol>
</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">
<ol name="ccp_execution_list" style="display:none"></ol>
<div class="card">
<div class="card-header">
<div class="ccp-toolbar-header">
<div>
<span name="header" class="mr-2">Execution history</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" style="display:none"></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.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){
@ -92,16 +178,16 @@ class CCPExecutionHistory extends HTMLElement {
else throw `Unable to load execution ${id}`
}).then(data=>{
const exec = data[0]
for(var i=0; i < this.#executions.length; i++){
if(this.#executions[i].id == exec.id){
this.#executions[i] = exec
for(var i=0; i < this.#data.length; i++){
if(this.#data[i].id == exec.id){
this.#data[i] = exec
break
}
if(i === this.#executions.length){
this.#executions = data.concat(this.#executions)
if(i === this.#data.length){
this.#data = data.concat(this.#data)
}
}
BSS.apply(this.execution_list_bss, this.#rootdoc)
this.updateList()
}).catch(err=>{ console.error(err)})
}
@ -117,14 +203,14 @@ class CCPExecutionHistory extends HTMLElement {
if(reply.status === 200) return reply.json();
else throw "Unable to load executions"
}).then(data=>{
this.#executions = data
BSS.apply(this.execution_list_bss, this.#rootdoc)
this.#data = data
this.updateList()
}).catch(err=>{ console.error(err)})
}
connectNewExecution(){
document.addEventListener("newexecution", ev=>{
this.#executions = [ {id: ev.detail } ].concat(this.#executions)
this.#data = [ {id: ev.detail } ].concat(this.#data)
})
}
@ -132,7 +218,7 @@ class CCPExecutionHistory extends HTMLElement {
this.#socket = new WebSocket(this.#broadcasturl + "/executions");
this.#socket.onmessage = event=>{
const data = JSON.parse(event.data)
let exec = this.#executions.filter(e=>e.id === data.jobID)[0]
let exec = this.#data.filter(e=>e.id === data.jobID)[0]
if(exec){
this.refreshExecution(exec.id)
}
@ -159,17 +245,17 @@ class CCPExecutionHistory extends HTMLElement {
}).catch(err => console.error(err))
}
downloadZIP(id){
export(id, mime, filename){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}`,
{ method: "GET", headers : { "Accept" : "application/zip"} }).then(reply =>{
{ method: "GET", headers : { "Accept" : mime} }).then(reply =>{
if (reply.status !== 200) {
throw "Unable to download"
throw "Unable to export " + mime
}
return reply.blob()
}).then(blob => {
const objectURL = URL.createObjectURL(blob)
var tmplnk = document.createElement("a")
tmplnk.download = id + ".zip"
tmplnk.download = filename
tmplnk.href = objectURL
document.body.appendChild(tmplnk)
tmplnk.click()
@ -177,33 +263,20 @@ class CCPExecutionHistory extends HTMLElement {
}).catch(err=>{ console.error(err)})
}
execution_list_bss = {
#execution_list_bss = {
template : "#EXECUTIOM_LIST_TEMPLATE",
target : "ol[name=ccp_execution_list]",
in : ()=>{ return {executions : this.#executions} },
target : "ul[name=ccp_execution_list]",
in : ()=>this,
recurse:[
{
target : "li",
in : (e,d)=>d.executions,
apply: (e,d,i)=>e.setAttribute("data-index", i),
on_click: ev=>{
if(ev.target.getAttribute("name") === "delete"){
if(window.confirm("Confirm deletion of this execution?")){
const index = Number(ev.currentTarget.getAttribute("data-index"))
this.deleteExecution(this.#executions[index].id)
}
}
if(ev.target.getAttribute("name") === "zip"){
const index = Number(ev.currentTarget.getAttribute("data-index"))
this.downloadZIP(this.#executions[index].id)
}
},
recurse : [
target : "li.ccp-method-item",
"in" : (e,d)=>Object.keys(this.#filtered),
recurse: [
{
target : "details",
target : "details[name=level1]",
apply : (e,d)=>{
e.alt = e.title = d.id
if(sessionStorage.getItem(d.id) === "open") e.open = "open";
e.alt = e.title = d
if(sessionStorage.getItem(d) === "open") e.open = "open";
else e.removeAttribute("open");
},
on_toggle : ev=>{
@ -212,68 +285,118 @@ class CCPExecutionHistory extends HTMLElement {
}else{
sessionStorage.removeItem(ev.currentTarget.alt)
}
}
},
},
{
target : "span[name=runtime]",
apply : (e,d)=>{
e.classList.add(d.infrastructuretype)
e.title = e.alt = `${d.runtime} (image: ${d.runtimeimage}) on ${d.infrastructure}`
e.textContent = d.runtime
}
target : "summary.ccp-method-item-header h5",
apply : (e,d) => { e.textContent = d }
},
{
target : "h5",
apply : (e,d)=>{
e.textContent = `${d.method} v. ${d.methodversion}`
}
target : "summary.ccp-method-item-header span[name=accepted]",
apply : (e,d) => { e.textContent = this.#filtered[d].filter(x=>x.status === 'accepted').length }
},
{
target : "span[name=updated]",
apply : (e,d)=>{
const dt = new Date(d.updated)
e.textContent = `Last update ${dt.toLocaleDateString()} @ ${dt.toLocaleTimeString()}`
}
target : "summary.ccp-method-item-header span[name=failed]",
apply : (e,d) => { e.textContent = this.#filtered[d].filter(x=>x.status === 'failed').length }
},
{
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 : "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_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)
}
}
}
},
{
target : "span[name=message]",
apply : (e,d)=>{
if(d.message){
e.textContent = d.message
if(ev.target.getAttribute("name") === "zip"){
const id = ev.currentTarget.getAttribute("data-index")
this.export(id, "application/zip", id + ".zip")
}
}
},
{
target : "ul",
if(ev.target.getAttribute("name") === "provo"){
const id = ev.currentTarget.getAttribute("data-index")
this.export(id, "application/prov-o+xml", id + ".xml")
}
},
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)
},
target : "details",
apply : (e,d)=>{
e.innerHTML = `<a href="${d.href}" onclick="event.preventDefault()">${d.path}</a>`
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 = `<a href="${d.href}" onclick="event.preventDefault()">${d.path}</a>`
}
}
]
}
]
}