web-components/ccp/js/executionhistorycontroller.js

1132 lines
50 KiB
JavaScript

class CCPExecutionHistory extends HTMLElement {
#boot = null;
#rootdoc = null;
#serviceurl = null;
#wsurl = null;
#broadcasturl = null;
#archive = null;
#data = [];
#pending = [];
#filtered = [];
#socket = null;
#interval = null;
#searchfield = null;
// #fileupload = null;
// #archiveupload = null;
#ws = null;
#execfolderid = null
#archived = []
#messages = {
"en" : {
"search" : "Search",
"refresh_help" : "Refresh contents of Execution Monitor",
"paste_link" : "Paste your link here",
"execution_monitor" : "Execution Monitor",
"failed_count_help" : "Count of failed executions",
"successful_count_help" : "Count of successful executions",
"running_count_help" : "Count of running executions",
"accepted_count_help" : "Count of accepted executions",
"download_help" : "Click to download outputs to your local disk",
"archive_execution_help" : "Archive whole execution to workspace folder",
"archive_outputs_help" : "Archive only outputs to workspace folder",
"re-submit_help" : "Re submit this exact execution",
"re-submit" : "Re-submit",
"delete_help" : "Delete this execution",
"generate_code" : "Generate code for",
"generate_code_help" : "Generate code that replicates this exact execution on a chosen programming language or runtime",
"direct_link" : "Direct link",
"import_file_help" : "Import from a previosusly exported file on your local disk",
"import_link_help" : "Import execution from a link in your workspace",
"direct_link_help" : "Navigate to the direct link for opening this exact execution in the exeuction form",
"confirm_import_link" : "Please confirm importing of execution from link",
"confirm_import_file" : "Please confirm importing of execution from file",
"confirm_delete_execution" : "Please confirm deletion of this execution",
"confirm_delete_archived_execution" : "Please confirm deletion of this archived execution from your workspace.",
"confirm_archive_execution" : "Confirm archiving of this execution to workspace.",
"confirm_archive_outputs" : "Confirm archiving of outputs to workspace.",
"confirm_reexecution" : "Confirm resubmission of this execution",
"live_executions" : "Live executions",
"archived_executions" : "Archived executions",
"restore_execution_help" : "Restore this archived execution back to CCP",
"infrastructure_help" : "The infrastructure where this execution has been scheduled on",
"runtime_help" : "The runtime where this execution has been scheduled on",
"loading_archived_help" : "Loading archived executions from workspace. This can take a while.",
"err_reexecute" : "Unable to re-submit the execution. Please check console for further info",
"err_generate_code_for" : "Unable to generate code for "
}
}
getLabel(key, localehint){
const locale = localehint ? localehint : navigator.language
const actlocale = this.#messages[locale] ? locale : "en"
const msg = this.#messages[actlocale][key]
return msg == null || msg == undefined ? key : this.#messages[actlocale][key]
}
constructor(){
super()
this.#boot = document.querySelector("d4s-boot-2")
this.#rootdoc = this.attachShadow({ "mode" : "open"})
}
connectedCallback(){
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("allow-archive") && this.getAttribute("allow-archive").toLowerCase() === "true"
const iss = document.querySelector("d4s-boot-2").getAttribute("url");
const addresses = {
"https://accounts.dev.d4science.org/auth": "https://workspace-repository.dev.d4science.org/storagehub/workspace",
"https://accounts.pre.d4science.org/auth": "https://pre.d4science.org/workspace",
"https://accounts.d4science.org/auth": "https://api.d4science.org/workspace"
};
this.#wsurl = addresses[iss]
this.connectNewExecution()
this.connectBroadcastWithSubject()
this.render()
this.refreshExecutions()
if(this.#archive) this.refreshArchivedExecutions();
}
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>
.galaxy{
background-color: #2c3143;
color: yellow;
}
.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 d-flex flex-wrap justify-content-between">
<h5 class="text-primary d-inline"></h5>
<div>
<span name="failed" title="${this.getLabel("failed_count_help")}" class="badge badge-danger float-right">Z</span>
<span name="successful" title="${this.getLabel("successful_count_help")}"class="badge badge-success float-right mr-1">Y</span>
<span name="running" title="${this.getLabel("running_count_help")}"class="badge badge-primary float-right mr-1">Y</span>
<span name="accepted" title="${this.getLabel("accepted_count_help")}" class="badge badge-secondary float-right mr-1">X</span>
</div>
</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;">
${ this.#archive ? `
<button data-index="0" name="archive" title="${this.getLabel("archive_execution_help")}" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<svg viewBox="0 96 960 960"><path d="M140 796h680V516H140v280Zm540.118-90Q701 706 715.5 691.382q14.5-14.617 14.5-35.5Q730 635 715.382 620.5q-14.617-14.5-35.5-14.5Q659 606 644.5 620.618q-14.5 14.617-14.5 35.5Q630 677 644.618 691.5q14.617 14.5 35.5 14.5ZM880 456h-85L695 356H265L165 456H80l142-142q8-8 19.278-13 11.278-5 23.722-5h430q12.444 0 23.722 5T738 314l142 142ZM140 856q-24.75 0-42.375-17.625T80 796V456h800v340q0 24.75-17.625 42.375T820 856H140Z"/></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="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="archiveoutputs" title="${this.getLabel("archive_outputs_help")}" 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="reexecute1" title="${this.getLabel("re-submit_help")}" class="btn btn-info ccp-toolbar-button ccp-toolbar-button-small">
${this.getLabel("re-submit")}
</button>
<button data-index="0" name="delete" title="${this.getLabel("delete_help")}" 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 flex-wrap" style="gap:3px; overflow:hidden">
<span style="text-overflow:ellipsis" class="badge badge-light text-info border border-info" name="infrastructure" alt="${this.getLabel("infrastructure_help")}" title="${this.getLabel("infrastructure_help")}"></span>
<span class="badge" name="runtime" alt="${this.getLabel("runtime_help")}" title="${this.getLabel("runtime_help")}"></span>
</div>
<div name="logterminalcontainer" style="margin:5px 0 5px 0">
</div>
<ul>
<li></li>
</ul>
<div class="d-flex flex-column align-items-end" style="gap: 3px;">
<div class="d-flex align-items-center" style="gap:5px" title="${this.getLabel("generate_code_help")}">
<label style="text-wrap:nowrap">${this.getLabel("generate_code")}</label>
<select name="language-selector" class="form-control">
<option value="text/python" data-ext="py" title="Generate plain Python3">Python 3</option>
<option value="text/plain+r" data-ext="r" title="Generate plain R">R</option>
<option value="application/vnd.jupyter+python" data-ext="ipynb" title="Generate Jupyter notebook with Python 3 cells">Jupyter Python3</option>
<option value="text/julia" data-ext="jl" title="Generate Julia 1.9.0 code (HTTP 1.10.0, JSON3 1.13.2)">Julia 1.9.0</option>
<option value="application/x-sh+curl" data-ext="sh" title="Generate Bash script (curl)">Bash (curl)</option>
<option value="application/x-sh+wget" data-ext="sh" title="Generate Bash script (wget)">Bash (wget)</option>
</select>
<button data-index="0" name="codegen" title="${this.getLabel("generate_code_help")}" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<svg viewBox="0 96 960 960">
<path d="m384 721 43-43-101-102 101-101-43-43-144 144.5L384 721Zm192 0 145-145-144-144-43 43 101 101-102 102 43 43ZM180 936q-24.75 0-42.375-17.625T120 876V276q0-24.75 17.625-42.375T180 216h205q5-35 32-57.5t63-22.5q36 0 63 22.5t32 57.5h205q24.75 0 42.375 17.625T840 276v600q0 24.75-17.625 42.375T780 936H180Zm0-60h600V276H180v600Zm300-617q14 0 24.5-10.5T515 224q0-14-10.5-24.5T480 189q-14 0-24.5 10.5T445 224q0 14 10.5 24.5T480 259ZM180 876V276v600Z"/>
</svg>
</button>
</div>
<div class="d-flex align-items-middle" style="gap:5px">
<div class="d-flex">
<a title="${this.getLabel("direct_link_help")}" class="text-truncate" name="direct_link_execution" href="${window.location.href}">${this.getLabel("direct_link")}</a>
</div>
</div>
</div>
</details>
</li>
</ul>
</details>
</li>
</ul>
</template>
<template id="ARCHIVED_EXECUTION_LIST_TEMPLATE">
<ul name="ccp_archived_execution_list" class="ccp-execution-list list-group border border-2">
<li class="ccp-method-item ccp-archived-execution-method-item list-group-item list-group-item-light">
<details name="level1">
<summary class="ccp-method-item-header noselect d-flex flex-wrap justify-content-between">
<h5 class="text-primary d-inline"></h5>
</summary>
<ul class="ccp-execution-list list-group" style="list-style:none">
<li class="ccp-execution-item ccp-archived-execution-item list-group-item-secondary my-2 p-2" draggable="true">
<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;flex-wrap:wrap;">
<button data-index="0" name="reexecute2" title="${this.getLabel("re-submit_help")}" class="btn btn-info ccp-toolbar-button ccp-toolbar-button-small">
${this.getLabel("re-submit")}
</button>
<button name="restore" title="${this.getLabel("restore_execution_help")}" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<svg viewBox="0 -960 960 960" width="24px" fill="#5f6368">
<path d="M480-250q78 0 134-56t56-134q0-78-56-134t-134-56q-38 0-71 14t-59 38v-62h-60v170h170v-60h-72q17-18 41-29t51-11q54 0 92 38t38 92q0 54-38 92t-92 38q-44 0-77-25.5T356-400h-62q14 65 65.5 107.5T480-250ZM240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h320l240 240v480q0 33-23.5 56.5T720-80H240Zm0-80h480v-446L526-800H240v640Zm0 0v-640 640Z"/>
</svg>
</button>
<button name="delete" title="${this.getLabel("delete_help")}" 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>
<div class="d-flex flex-wrap" style="gap:3px; overflow:hidden">
<span style="text-overflow:ellipsis" class="badge badge-light text-info border border-info" name="infrastructure" alt="${this.getLabel("infrastructure_help")}" title="${this.getLabel("infrastructure_help")}"></span>
<span class="badge" name="runtime" alt="${this.getLabel("runtime_help")}" title="${this.getLabel("runtime_help")}"></span>
</div>
</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 d-flex flex-wrap justify-content-between">
<div>
<span name="header">${this.getLabel("execution_monitor")}</span>
</div>
<div class="d-flex flex-wrap" style="gap:2px">
<button name="refresh" class="btn btn-primary ccp-toolbar-button" title="${this.getLabel("refresh_help")}">
<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>
<!--label name="fileupload" class="btn btn-primary ccp-toolbar-button m-0" title="${this.getLabel("import_file_help")}">
<svg viewBox="0 96 960 960"><path d="M452 854h60V653l82 82 42-42-156-152-154 154 42 42 84-84v201ZM220 976q-24 0-42-18t-18-42V236q0-24 18-42t42-18h361l219 219v521q0 24-18 42t-42 18H220Zm331-554V236H220v680h520V422H551ZM220 236v186-186 680-680Z"/></svg>
<input type="file" class="d-none" multiple="multiple"/>
</label>
<div class="d-flex" style="gap:2px">
<input type="text" class="form-control" placeholder="${this.getLabel("paste_link")}"/>
<button name="archive" class="btn btn-primary ccp-toolbar-button m-0" title="${this.getLabel("import_link_help")}">
<svg viewBox="0 96 960 960">
<path d="M450 776H280q-83 0-141.5-58.5T80 576q0-83 58.5-141.5T280 376h170v60H280q-58.333 0-99.167 40.765-40.833 40.764-40.833 99Q140 634 180.833 675q40.834 41 99.167 41h170v60ZM324 606v-60h310v60H324Zm556-30h-60q0-58-40.833-99-40.834-41-99.167-41H510v-60h170q83 0 141.5 58.5T880 576ZM699 896V776H579v-60h120V596h60v120h120v60H759v120h-60Z"/>
</svg>
</button>
</div-->
</div>
</div>
</div>
<div class="card-body">
<div class="mb-3">
<input accept="application/zip" type="text" name="search" class="form-control" placeholder="Search"/>
</div>
${
this.#archive ? `
<ul id="switch_executions" class="nav nav-tabs">
<li class="nav-item">
<a name="live_executions" class="nav-link active" href="#">${this.getLabel("live_executions")}</a>
</li>
<li class="nav-item">
<a name="archived_executions" class="nav-link" href="#">${this.getLabel("archived_executions")}</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="live_executions">
<ul name="ccp_execution_list"></ul>
</div>
<div class="tab-pane fade" id="archived_executions">
<ul name="ccp_archived_execution_list"></ul>
</div>
</div>
` : `
<ul name="ccp_execution_list"></ul>
`
}
</div>
</div>
`
this.#rootdoc.querySelector("button[name=refresh]").addEventListener("click", ev=>{
if(this.isShowingArchived()){
this.refreshArchivedExecutions()
}else{
this.refreshExecutions()
}
})
this.#searchfield = this.#rootdoc.querySelector("input[name=search]")
this.#searchfield.addEventListener("input", ev=>{
this.updateList()
})
if(this.#archive){
const linklive = this.#rootdoc.querySelector("a.nav-link[name=live_executions]")
const linkarch = this.#rootdoc.querySelector("a.nav-link[name=archived_executions]")
const tablive = this.#rootdoc.querySelector("div.tab-pane#live_executions")
const tabarch = this.#rootdoc.querySelector("div.tab-pane#archived_executions")
this.#rootdoc.querySelector("#switch_executions").addEventListener("click", ev=>{
const tgt = ev.target
if(tgt == linklive){
linklive.classList.add("active")
linkarch.classList.remove("active")
tablive.classList.add("show")
tablive.classList.add("active")
tabarch.classList.remove("show")
tabarch.classList.remove("active")
}else if(tgt == linkarch){
linklive.classList.remove("active")
linkarch.classList.add("active")
tabarch.classList.add("show")
tabarch.classList.add("active")
tablive.classList.remove("show")
tablive.classList.remove("active")
}
ev.preventDefault();
ev.stopPropagation()
})
}
// 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(this.getLabel("confirm_import") + "?")){
// this.fromArchiveFolder(link)
// }
// }
// })
}
isShowingArchived(){
return this.#archive && this.#rootdoc.querySelector("#archived_executions").classList.contains("show")
}
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)})
}
reexecuteArchived(data, level){
this.#boot.secureFetch(`${this.#serviceurl}/executions/level/${level}`, {
method: "POST",
headers: {"Content-Type" : "application/json"},
body: JSON.stringify({ "request" : data.fullrequest, "method" : data.fullmethod, "infrastructure" : data.fullinfrastructure})
})
.then(reply =>{
if (!reply.ok) {
throw this.getLabel("err_reexecute")
}
return reply.json()
}).then(data=>{
this.refreshExecution(data.jobID)
}).catch(err=>{ alert(err)})
}
reexecute(id,level){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/level/${level}`, { method: "POST" })
.then(reply =>{
if (!reply.ok) {
throw this.getLabel("err_reexecute")
}
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 this.getLabel("err_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)})
}
toArchiveFolder(id){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/archive-to-folder`, { method: "POST" })
.then(reply =>{
if (!reply.ok) {
throw "Unable to archive execution to folder"
}
}).catch(err=>{ alert(err)})
}
toOutputsArchiveFolder(id){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/outputs/archive-to-folder`, { method: "POST" })
.then(reply =>{
if (!reply.ok) {
throw "Unable to archive outputs to folder"
}
}).catch(err=>{ alert(err)})
}
fromArchiveFolder(url){
if(url){
this.#boot.secureFetch(`${this.#serviceurl}/executions/archive?url=${url}`)
.then(reply =>{
if (!reply.ok) {
throw "Unable to fetch from archive folder"
}
return reply.text()
}).then(data=>{
this.refreshExecutions()
}).catch(err=>{ alert(err)})
}
}
async deleteArchiveFolder(id){
if(id){
await this.initWorkspace()
await this.#ws.deleteItem(id)
if(this.#archive) await this.refreshArchivedExecutions();
}
}
async initWorkspace(){
if(!this.#ws){
this.#ws = new D4SWorkspace(this.#wsurl, this.#boot)
}
if(!this.#execfolderid){
const rootws = await this.#ws.getWorkspace()
const ccpfolder = await this.#ws.findByName(rootws.id, "CCP")
if(ccpfolder.length === 1){
const execfolder = await this.#ws.findByName(ccpfolder[0].id, "executions")
if(execfolder.length === 1){
this.#execfolderid = execfolder[0].id
}
}
}
}
showLoading(el){
el.innerHTML = `
<div class="d-flex justify-content-center" title="${this.getLabel("loading_archived_help")}">
<style>
.spinning {
animation:spin 4s linear infinite;
}
@keyframes spin {
100% {
-webkit-transform: rotate(-360deg);
transform:rotate(-360deg);
}
}
</style>
<svg class="spinning" style="width:10%" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M160-160v-80h110l-16-14q-52-46-73-105t-21-119q0-111 66.5-197.5T400-790v84q-72 26-116 88.5T240-478q0 45 17 87.5t53 78.5l10 10v-98h80v240H160Zm400-10v-84q72-26 116-88.5T720-482q0-45-17-87.5T650-648l-10-10v98h-80v-240h240v80H690l16 14q49 49 71.5 106.5T800-482q0 111-66.5 197.5T560-170Z"/></svg>
<div>
`
}
async refreshArchivedExecutions(){
this.showLoading(this.#rootdoc.querySelector("ul[name=ccp_archived_execution_list]"))
await this.initWorkspace()
if(this.#execfolderid){
const folders = await this.#ws.listFolder(this.#execfolderid)
this.#archived = {}
for(let i=0; i < folders.length; i++){
const methodfolder = folders[i]
const executionfolders = await this.#ws.listFolder(methodfolder.id)
for(let j=0; j < executionfolders.length; j++){
const metadatafolder = await this.#ws.findByName(executionfolders[j].id, "metadata")
if(metadatafolder.length === 1){
//parse all metada content that is useful to build up an entry
const metadata = await this.#ws.download(metadatafolder[0].id, null, true)
const zip = new JSZip()
const req = JSON.parse(await (zip.loadAsync(metadata).then(z=>z.file("metadata/request.json").async("string"))))
const status = JSON.parse(await (zip.loadAsync(metadata).then(z=>z.file("metadata/jobStatus.json").async("string"))))
const meth = JSON.parse(await (zip.loadAsync(metadata).then(z=>z.file("metadata/method.json").async("string"))))
const infra = JSON.parse(await (zip.loadAsync(metadata).then(z=>z.file("metadata/infrastructure.json").async("string"))))
const parser = new DOMParser()
// Fasted way to get context is from prov-o. If it doesn't match the context in boot skip this execution
const provo = parser.parseFromString(await zip.loadAsync(metadata).then(z=>z.file("metadata/prov-o.xml").async("string")), "text/xml")
const context = provo.querySelector("entity[*|id='d4s:VRE']>value").textContent
if(context !== this.#boot.context && context.replaceAll("/", "%2F") !== this.#boot.context) continue;
//build entry from downloaded data
const entry = {
id : req.id,
status : status.status,
created : status.created,
started : status.started,
updated : status.updated,
message : status.message,
method : meth.title,
methodversion : meth.version,
infrastructure: infra.name,
infrastructuretype : infra.type,
ccpnote : req.inputs.ccpnote ? req.inputs.ccpnote : "",
runtime : req.inputs.ccpimage,
replicas : req.inputs.ccpreplicas ? req.inputs.ccpreplicas : 1,
href : `items/${executionfolders[j].id}/download`,
wsid : executionfolders[j].id,
fullrequest : req,
fullmethod : meth,
fullinfrastructure : infra
}
if(this.#archived[entry.method]) this.#archived[entry.method].push(entry);
else this.#archived[entry.method] = [entry]
}
}
}
}
BSS.apply(this.#archived_execution_list_bss, this.#rootdoc)
}
#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(this.getLabel("confirm_import_file") + "?")){
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(this.getLabel("confirm_delete_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(this.getLabel("confirm_reexecution"))){
const id = ev.currentTarget.getAttribute("data-index")
this.reexecute(id, 1)
}
}
if(ev.target.getAttribute("name") === "archive"){
if(confirm(this.getLabel("confirm_archive_execution"))){
const id = ev.currentTarget.getAttribute("data-index")
this.toArchiveFolder(id)
}
}
if(ev.target.getAttribute("name") === "archiveoutputs"){
if(confirm(this.getLabel("confirm_archive_outputs"))){
const id = ev.currentTarget.getAttribute("data-index")
this.toOutputsArchiveFolder(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
const t3 = !t2 && infratype.match(/galaxy/i) ? "galaxy" : t
e.classList.add(t3)
}
},
{
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 : "a[name=direct_link_execution]",
apply : (e,d)=>e.href = window.location.origin + window.location.pathname + "?execution=" + d.id
},
{
target : "div[name=logterminalcontainer]",
apply : (e,d)=>{
e.innerHTML = `<d4s-ccp-logterminal index="${d.id}" maxstoredlines="100" maxlines="10"></d4s-ccp-logterminal>`
}
},
{
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>`
}
}
]
}
]
}
]
}
]
}
#archived_execution_list_bss = {
template : "#ARCHIVED_EXECUTION_LIST_TEMPLATE",
target : "ul[name=ccp_archived_execution_list]",
in : ()=>this,
recurse:[
{
target : "li.ccp-method-item",
"in" : (e,d)=>Object.keys(this.#archived),
recurse: [
{
target : "details[name=level1]",
apply : (e,d)=>{
e.alt = e.title = d
if(sessionStorage.getItem("arch_" + d) === "open") e.open = "open";
else e.removeAttribute("open");
},
on_toggle : ev=>{
if(ev.target.open){
sessionStorage.setItem("arch_" + ev.currentTarget.alt, 'open')
}else{
sessionStorage.removeItem("arch_" + ev.currentTarget.alt)
}
},
},
{
target : "summary.ccp-method-item-header h5",
apply : (e,d) => { e.textContent = d }
},
{
target : "li.ccp-execution-item",
"in" : (e,d)=>this.#archived[d],
apply : (e,d)=>{
e.setAttribute("data-index", d.id)
e.setAttribute("data-href", d.href)
e.setAttribute("data-wsid", d.wsid)
},
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") === "restore"){
if(window.confirm(this.getLabel("confirm_restore_execution"))){
const href = ev.currentTarget.getAttribute("data-href")
this.fromArchiveFolder(href)
}
}else if(ev.target.getAttribute("name") === "delete"){
if(window.confirm(this.getLabel("confirm_delete_archived_execution"))){
const wsid = ev.currentTarget.getAttribute("data-wsid")
this.deleteArchiveFolder(wsid)
}
}else if(ev.target.getAttribute("name") === "reexecute2"){
if(window.confirm(this.getLabel("confirm_reexecution"))){
this.reexecuteArchived(ev.currentTarget.bss_input.data, 1)
}
}
},
recurse : [
{
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
const t3 = !t2 && infratype.match(/galaxy/i) ? "galaxy" : t
e.classList.add(t3)
}
}
]
}
]
}
]
}
}
window.customElements.define('d4s-ccp-executionhistory', CCPExecutionHistory);