diff --git a/boot/d4s-boot.js b/boot/d4s-boot.js index f0a174a..21c3a8d 100644 --- a/boot/d4s-boot.js +++ b/boot/d4s-boot.js @@ -107,14 +107,14 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement { startStateChecker() { this.#interval = window.setInterval(() => { if (this.#locked) { - console.log("Still locked. Currently has " + this.#queue.length + " pending requests.") + //console.log("Still locked. Currently has " + this.#queue.length + " pending requests.") } else if (!this.authenticated) { window.alert("Not authorized!") } else { if (this.#queue.length > 0) { this.#keycloak.updateToken(30).then(() => { if (this.#audience) { - console.log("Checking entitlement for audience", this.#audience) + //console.log("Checking entitlement for audience", this.#audience) const audience = encodeURIComponent(this.#audience) return this.entitlement(audience) } else { @@ -122,7 +122,7 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement { } }).then(token => { - console.log("Authorized. Token exp: " + this.expirationDate(this.parseJwt(token).exp)) + //console.log("Authorized. Token exp: " + this.expirationDate(this.parseJwt(token).exp)) //transform all queued requests to fetches //console.log("All pending requests to promises") let promises = this.#queue.map(r => { diff --git a/ccp/index.html b/ccp/index.html index 1d9d6ee..a8e5cf6 100644 --- a/ccp/index.html +++ b/ccp/index.html @@ -2,6 +2,7 @@ + @@ -16,10 +17,11 @@ + - +
@@ -32,7 +34,7 @@
- +
diff --git a/ccp/js/executionformcontroller.js b/ccp/js/executionformcontroller.js index 54cc4ee..e5e8e4d 100644 --- a/ccp/js/executionformcontroller.js +++ b/ccp/js/executionformcontroller.js @@ -343,6 +343,23 @@ class CCPExecutionForm extends HTMLElement{ } prepareFromExecution(exec){ + //if exec carries already full information then it comes from archive. Use this info instead of fetching. + if(exec.fullrequest && exec.fullmethod && exec.fullinfrastructure){ + this.#data = exec.fullmethod + this.#method = this.#data.id + this.#boot.secureFetch(this.#serviceurl + "/infrastructures/" + exec.fullinfrastructure.id) + .then(resp=>{ + if(resp.ok){ + this.#data.executable = true + this.showMethod() + this.initInputValues(exec.fullrequest.inputs) + this.initOptionValues(exec.fullrequest) + }else throw("Unable to find infrastructure") + }).catch(err=>alert(err)) + return + } + + //fetch method and request and validate infra let f1 = this.#boot.secureFetch(this.#serviceurl + `/executions/${exec.id}/metadata/method.json`) .then(resp=>{ diff --git a/ccp/js/executionhistorycontroller.js b/ccp/js/executionhistorycontroller.js index c33b789..ac392b4 100644 --- a/ccp/js/executionhistorycontroller.js +++ b/ccp/js/executionhistorycontroller.js @@ -3,6 +3,7 @@ class CCPExecutionHistory extends HTMLElement { #boot = null; #rootdoc = null; #serviceurl = null; + #wsurl = null; #broadcasturl = null; #archive = null; #data = []; @@ -11,8 +12,11 @@ class CCPExecutionHistory extends HTMLElement { #socket = null; #interval = null; #searchfield = null; - #fileupload = null; - #archiveupload = null; + // #fileupload = null; + // #archiveupload = null; + #ws = null; + #execfolderid = null + #archived = [] #messages = { "en" : { "search" : "Search", @@ -27,6 +31,7 @@ class CCPExecutionHistory extends HTMLElement { "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", @@ -36,7 +41,19 @@ class CCPExecutionHistory extends HTMLElement { "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_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 " } } @@ -60,11 +77,21 @@ class CCPExecutionHistory extends HTMLElement { this.#broadcasturl = this.#serviceurl.replace(/^http/, "ws") } this.#broadcasturl = this.#broadcasturl + "/ws/notification" - this.#archive = this.getAttribute("archive") + 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(){ @@ -72,6 +99,10 @@ class CCPExecutionHistory extends HTMLElement { + +
+ ` + } + + 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", @@ -582,19 +826,19 @@ class CCPExecutionHistory extends HTMLElement { this.export(id, "application/prov-o+xml", id + ".xml") } if(ev.target.getAttribute("name") === "reexecute1"){ - if(window.confirm("Please confirm re-execution?")){ + 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(" Please confirm archiving of execution to workspace folder?")){ + 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(" Please confirm archiving of execution outputs to workspace folder?")){ + if(confirm(this.getLabel("confirm_archive_outputs"))){ const id = ev.currentTarget.getAttribute("data-index") this.toOutputsArchiveFolder(id) } @@ -686,7 +930,8 @@ class CCPExecutionHistory extends HTMLElement { 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) + const t3 = !t2 && infratype.match(/galaxy/i) ? "galaxy" : t + e.classList.add(t3) } }, { @@ -737,5 +982,150 @@ class CCPExecutionHistory extends HTMLElement { } ] } + + #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); diff --git a/ccp/js/inputwidgetcontroller.js b/ccp/js/inputwidgetcontroller.js index 4eb48a9..95a2c10 100644 --- a/ccp/js/inputwidgetcontroller.js +++ b/ccp/js/inputwidgetcontroller.js @@ -709,7 +709,6 @@ class CCPRemoteFileInputWidgetController extends CCPBaseInputWidgetController { }) this.addEventListener("input", ev => { - ev.stopPropagation() if (ev.target.getAttribute("name") == this.name && ev.target.getAttribute("data-index") != null) { this.value[Number(ev.target.getAttribute("data-index"))] = ev.target.value } @@ -808,7 +807,7 @@ class CCPGeoInputWidgetController extends CCPBaseInputWidgetController { -
+
@@ -825,7 +824,7 @@ class CCPGeoInputWidgetController extends CCPBaseInputWidgetController {
Draw and export geometry
-
+