Compare commits

...

108 Commits

Author SHA1 Message Date
dcore94 7f36ebe941 Specify that number is of type integer 2024-10-25 15:09:45 +02:00
dcore94 a522a7a1cc fixed to handle scope init option 2024-10-18 12:50:06 +02:00
dcore94 b5758dadc4 fixed to handle scope init option 2024-10-18 12:44:53 +02:00
dcore94 3b6899ef1f d4sboot uses dynamic scope or UMA if explicitly requested 2024-10-18 11:42:51 +02:00
dcore94 224206adf7 added check to discourage execution requests with empty ccpimage when container based infra 2024-10-15 13:01:12 +02:00
dcore94 74c2819de8 ensure that true/false are returned as strings from boolean inputs 2024-10-11 18:25:13 +02:00
dcore94 7fae770186 scripts are always visible. fix missing cancel on geo widget. inputs show default preview when closed. Method description is now text area. Method list and execution form have limit on description length display 2024-10-11 13:12:30 +02:00
dcore94 9af993ac6c improved consistency between config changes and default value 2024-09-24 18:22:25 +02:00
dcore94 6752659166 moved boot query to connectcallback 2024-07-04 13:32:03 +02:00
dcore94 8748836d9f completed code and example 2024-07-04 11:20:11 +02:00
dcore94 f574b323ce changed reference to common stylesheet to point to prod cdn 2024-07-01 12:06:55 +02:00
dcore94 85e5c9b8e7 removed stopPopagation that made event no bubble to editor2 2024-06-14 12:43:01 +02:00
dcore94 94d1357ba2 moved iss access out of async context 2024-06-13 16:52:31 +02:00
dcore94 a4769a6281 moved iss access out of async context 2024-06-13 16:49:57 +02:00
dcore94 ef6ab169df made ws addres parametric to infrastructure 2024-06-13 14:51:51 +02:00
dcore94 c95b62b1d4 implemented resubmit for archived executions and removed unnecessary logs 2024-06-12 15:34:14 +02:00
dcore94 09114defad set executable flag 2024-06-12 12:51:06 +02:00
dcore94 423cfc7bed removed some logs from boot, it's now possible to drag and drop archived executions 2024-06-12 12:46:59 +02:00
dcore94 fd858f811a added different color theme for archived 2024-06-11 16:59:06 +02:00
dcore94 992e085a76 removed unnecessary details toggle 2024-06-11 16:50:07 +02:00
dcore94 59f76372b7 made archived view ooptional, refresh applies only to viewd tab, added delete for archived, improved labels, debugged 2024-06-11 16:14:00 +02:00
dcore94 9426adf4eb added archived executions panel 2024-06-10 18:05:23 +02:00
dcore94 d7b062828b added width 100% 2024-06-10 11:43:16 +02:00
dcore94 794443bbdd fixed some labels 2024-06-07 18:44:16 +02:00
dcore94 a013536dfb fixed some labels 2024-06-07 18:42:47 +02:00
dcore94 c7021a06b6 moved title to section 2024-06-07 17:41:41 +02:00
dcore94 43154c2e2b revised all tooltips and added support for i18n 2024-06-07 17:39:35 +02:00
dcore94 593429ca51 fixed setting from execution import 2024-06-05 19:07:54 +02:00
dcore94 592cafbb3e fixed setting from execution import 2024-06-05 19:05:13 +02:00
dcore94 01c4d8cf07 changed option to select 2024-06-05 18:57:15 +02:00
dcore94 be39c47217 fixed wring naming 2024-06-05 12:25:10 +02:00
dcore94 b982194b73 added extra option for storing whole execution 2024-06-04 17:23:02 +02:00
dcore94 ac10c3ba34 fixed naming 2024-06-04 17:04:19 +02:00
dcore94 8407f59013 fixed naming 2024-06-04 16:56:01 +02:00
dcore94 3e1dd1bf6d refactored according to new API 2024-06-04 16:53:01 +02:00
dcore94 0eb335cc17 added box shape, removed multiple geometries, tuned icons 2024-06-04 16:07:11 +02:00
dcore94 9ffdfa9a0b added input handler for direct modification 2024-06-03 21:17:40 +02:00
dcore94 ae38f41ac7 added input handler for direct modification 2024-06-03 21:12:10 +02:00
dcore94 ec93d76998 fix to use map 2024-06-03 17:58:04 +02:00
dcore94 5cad7c3d9a fix variable 2024-06-03 17:55:18 +02:00
dcore94 c8116426b9 project to EPSG:4326 2024-06-03 17:52:59 +02:00
dcore94 5694938485 fixed css rule 2024-06-03 16:27:55 +02:00
dcore94 2e55fa7510 added geo widget 2024-06-03 16:22:08 +02:00
dcore94 16c8a6068b exclude accounting info to save traffic 2024-05-31 15:25:20 +02:00
dcore94 47f83901c1 added options for galaxy code generation 2024-05-15 16:36:15 +02:00
dcore94 6fc96bfa3c minor visual improvements 2024-04-24 13:02:54 +02:00
dcore94 b254262f58 improved display 2024-04-23 16:46:47 +02:00
dcore94 f7cd0932b5 fire input event when file is read, fix deprecated func and improved display 2024-04-23 16:41:53 +02:00
dcore94 e3efc70bad fixed case of json for ccpimage 2024-04-23 12:50:58 +02:00
dcore94 96030670c2 cleaned up code and moved everything to bs5.0.2 2024-04-17 17:47:28 +02:00
dcore94 e8c71ecb55 overflow improvements 2024-04-17 17:12:30 +02:00
dcore94 4173ee650c position inside 2024-04-17 16:46:54 +02:00
dcore94 cc752abe40 Added select button for storage tree, added icons for well-known file types, corrected bug with direct input to ws link input field 2024-04-17 14:52:36 +02:00
dcore94 78a3009573 removed unnecessary style 2024-04-16 13:40:26 +02:00
dcore94 cc792a3e62 refactored method import to use preview widgets for input and scripts, added possibility to choose for public or protected link from workspace, fixed several bugs 2024-04-16 13:07:23 +02:00
dcore94 2f8f9483b8 added index test page for CCP webcomponents 2024-04-16 13:06:00 +02:00
dcore94 f4faebc9f1 changed gateway to next.dev 2024-04-16 13:05:06 +02:00
dcore94 687be71314 added some prevent defaults to events 2024-04-16 13:03:51 +02:00
dcore94 1078e7abbb removed redundant logs 2024-04-16 13:03:11 +02:00
dcore94 297b2f6ffe fix enum default selection 2024-04-11 17:25:41 +02:00
dcore94 05ef785cf0 added support for bash scripts generation 2024-04-10 12:28:47 +02:00
dcore94 79ec13cdc5 added support for generating bash scripts 2024-04-10 12:21:14 +02:00
dcore94 ff613b70c2 show place holder instead of full link 2024-04-09 13:22:14 +02:00
dcore94 f8902077ec show place holder instead of full link 2024-04-09 13:16:51 +02:00
dcore94 d275b087b8 fix to access correct uri 2024-02-09 11:59:40 +01:00
dcore94 4533b43773 fix to correct datasource 2024-02-09 11:57:23 +01:00
dcore94 6cb7fb7cae allow user to activate automatic archiving of outputs to workspace 2024-02-09 11:42:49 +01:00
dcore94 bcc02f680c fixed missing brackets for value expansions 2024-02-09 11:01:00 +01:00
dcore94 d4cae32fe2 fix show link for direct execution 2024-02-02 19:07:30 +01:00
dcore94 3f02f0a1a1 fix show link for direct execution 2024-02-02 19:06:01 +01:00
dcore94 c8b31cecc7 fix show link for direct execution 2024-02-02 18:44:01 +01:00
dcore94 d4f3ed98f5 show link for direct execution 2024-02-02 18:33:42 +01:00
dcore94 a6517301d5 fix 2024-02-02 18:12:35 +01:00
dcore94 0aee111148 fix 2024-02-02 18:10:43 +01:00
dcore94 556168a239 fix 2024-02-02 18:08:59 +01:00
dcore94 f69ee9b3e6 show direct execution url for method 2024-02-02 18:07:28 +01:00
dcore94 4f3a892eeb added support for initializing from execution 2024-02-02 17:45:04 +01:00
dcore94 a087e4904f fix 2024-02-02 17:35:59 +01:00
dcore94 cfea15251a fix 2024-02-02 17:32:51 +01:00
dcore94 0ce705bb3f added method inferring from url parametere 2024-02-02 17:28:05 +01:00
dcore94 19d7be6ea0 non authors can only derive a method 2024-01-31 16:37:17 +01:00
dcore94 ab07310252 removed commented code 2023-12-14 10:46:54 +01:00
dcore94 a4bbbc0ccf pass encoded description as attribute 2023-12-13 19:24:58 +01:00
dcore94 1381140c49 fixed typo 2023-12-13 19:19:36 +01:00
dcore94 a9ec721892 removed spaces 2023-12-13 19:17:04 +01:00
dcore94 125f5492c5 fixed typo 2023-12-13 19:10:11 +01:00
dcore94 ccd75c7e6e changed description to textarea and show it without trigger 2023-12-13 19:07:56 +01:00
dcore94 45ab5dccca encode to base64 before setting as attirbute to fix descriptions containing quotes 2023-12-13 18:29:44 +01:00
dcore94 f57057e307 added option for generating Julia code 2023-12-12 11:13:16 +01:00
dcore94 8c73d0c8c5 added prortotype for external app manager 2023-12-01 14:05:42 +01:00
dcore94 901abe5869 derivation as query-param 2023-11-16 13:57:26 +01:00
dcore94 7f76178458 added derivation button 2023-11-16 13:45:24 +01:00
dcore94 5476851d1f fix possible npe 2023-11-16 11:31:34 +01:00
dcore94 d374f62ae0 use title instead of name to avoid uuid appearing as labels 2023-10-27 16:21:10 +02:00
dcore94 bfea923d4b visual fix 2023-10-27 15:59:52 +02:00
dcore94 801e349e80 fixed query to avoid hierarchy clashes 2023-10-27 15:53:32 +02:00
dcore94 4818464409 fixed query to avoid hierarchy clashes 2023-10-27 15:52:35 +02:00
dcore94 07382c94a7 fixed typo 2023-10-27 15:48:44 +02:00
dcore94 28d609a011 draw SharedFolder as folder 2023-10-27 15:46:10 +02:00
dcore94 00d53aed50 scroll only content of tree widget 2023-10-27 15:44:24 +02:00
dcore94 1d6fe0beb8 moved tool addition for remote file to render time 2023-10-27 15:37:53 +02:00
dcore94 b891562705 moved tool addition for remote file to render time 2023-10-27 15:30:11 +02:00
dcore94 1d52dc9fb8 moved tool addition for remote file to render time 2023-10-27 15:28:23 +02:00
dcore94 b7bd25fa7a improved visual of storage tree 2023-10-25 19:05:47 +02:00
dcore94 528db74d03 improved visual of storage tree 2023-10-25 19:04:20 +02:00
dcore94 63af988a9d improved visual of storage tree 2023-10-25 19:02:57 +02:00
dcore94 71c6d94674 improved visual of storage tree 2023-10-25 19:01:42 +02:00
dcore94 2897f65a60 improved visual of storage tree 2023-10-25 18:59:36 +02:00
17 changed files with 2837 additions and 1232 deletions

View File

@ -21,6 +21,7 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement {
#queue = [] #queue = []
#interval = null #interval = null
#config = null #config = null
#uma = false
#rpt = null #rpt = null
constructor() { constructor() {
@ -63,8 +64,8 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement {
console.log("Keycloak initialized and user authenticated") console.log("Keycloak initialized and user authenticated")
//console.log("Token exp: " + this.expirationDate(this.#keycloak.tokenParsed.exp)) //console.log("Token exp: " + this.expirationDate(this.#keycloak.tokenParsed.exp))
//if an audience is provided then perform also authorization //if an audience is provided and UMA flow requested then perform also authorization
if (this.#audience) { if (this.#audience && this.#uma) {
return this.loadConfig() return this.loadConfig()
} else { } else {
Promise.resolve() Promise.resolve()
@ -101,20 +102,24 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement {
clientId: this.#clientId clientId: this.#clientId
}) })
return this.#keycloak.init({onLoad: 'login-required', checkLoginIframe: false }) const properties = {onLoad: 'login-required', checkLoginIframe: false}
if(this.#audience && !this.#uma){
properties["scope"] = `d4s-context:${this.#audience}`
}
return this.#keycloak.init(properties)
} }
startStateChecker() { startStateChecker() {
this.#interval = window.setInterval(() => { this.#interval = window.setInterval(() => {
if (this.#locked) { 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) { } else if (!this.authenticated) {
window.alert("Not authorized!") window.alert("Not authorized!")
} else { } else {
if (this.#queue.length > 0) { if (this.#queue.length > 0) {
this.#keycloak.updateToken(30).then(() => { this.#keycloak.updateToken(30).then(() => {
if (this.#audience) { if (this.#uma && this.#audience) {
console.log("Checking entitlement for audience", this.#audience) //console.log("Checking entitlement for audience", this.#audience)
const audience = encodeURIComponent(this.#audience) const audience = encodeURIComponent(this.#audience)
return this.entitlement(audience) return this.entitlement(audience)
} else { } else {
@ -122,16 +127,16 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement {
} }
}).then(token => { }).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 //transform all queued requests to fetches
console.log("All pending requests to promises") //console.log("All pending requests to promises")
let promises = this.#queue.map(r => { let promises = this.#queue.map(r => {
r.request.headers["Authorization"] = "Bearer " + token r.request.headers["Authorization"] = "Bearer " + token
return r.resolve( fetch(r.url, r.request) ) return r.resolve( fetch(r.url, r.request) )
}) })
//clear queue //clear queue
this.#queue = [] this.#queue = []
console.log("Resolving all fetches") //console.log("Resolving all fetches")
return Promise.all(promises) return Promise.all(promises)
}).catch(err => console.error("Unable to make calls: " + err)) // Sometimes throws: Unable to make calls: TypeError: Cannot read properties of undefined (reading 'split') }).catch(err => console.error("Unable to make calls: " + err)) // Sometimes throws: Unable to make calls: TypeError: Cannot read properties of undefined (reading 'split')
@ -156,18 +161,19 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement {
return d return d
} }
checkContext() { // TODO: Candidate for removal
const parseJwt = this.parseJwt // checkContext() {
const expDt = this.expirationDate // const parseJwt = this.parseJwt
const audience = encodeURIComponent(this.#audience) // const expDt = this.expirationDate
this.entitlement(audience).then(function (rpt) { // const audience = encodeURIComponent(this.#audience)
// onGrant callback function. // this.entitlement(audience).then(function (rpt) {
// If authorization was successful you'll receive an RPT // // onGrant callback function.
// with the necessary permissions to access the resource server // // If authorization was successful you'll receive an RPT
//console.log(rpt) // // with the necessary permissions to access the resource server
//console.log("rpt expires: " + expDt(parseJwt(rpt).exp)) // //console.log(rpt)
}) // //console.log("rpt expires: " + expDt(parseJwt(rpt).exp))
} // })
// }
secureFetch(url, request) { secureFetch(url, request) {
const p = new Promise((resolve, reject) => { const p = new Promise((resolve, reject) => {
@ -177,7 +183,7 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement {
} else { } else {
req.headers = { "Authorization" : null} req.headers = { "Authorization" : null}
} }
console.log("Queued request to url ", url) //console.log("Queued request to url ", url)
this.#queue.push({ url : url, request : req, resolve : resolve, reject : reject}) this.#queue.push({ url : url, request : req, resolve : resolve, reject : reject})
}) })
return p return p
@ -202,7 +208,7 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement {
.then(response => response.json()) .then(response => response.json())
.then(json => { .then(json => {
this.#config = json this.#config = json
console.log("Keycloak uma2 configuration loaded") //console.log("Keycloak uma2 configuration loaded")
resolve(true) resolve(true)
}) })
.catch(err => reject("Failed to fetch uma2-configuration from server: " + err)) .catch(err => reject("Failed to fetch uma2-configuration from server: " + err))
@ -291,7 +297,7 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement {
} }
static get observedAttributes() { static get observedAttributes() {
return ["url", "realm", "gateway", "redirect-url", "context"]; return ["url", "realm", "gateway", "redirect-url", "context", "uma"];
} }
attributeChangedCallback(name, oldValue, newValue) { attributeChangedCallback(name, oldValue, newValue) {
@ -312,10 +318,17 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement {
case "context": case "context":
this.#audience = newValue this.#audience = newValue
break break
case "uma":
this.#uma = newValue === "true" ? true : false
break
} }
} }
} }
get uma(){
return this.#uma
}
get authenticated(){ get authenticated(){
return this.#authenticated return this.#authenticated
} }

42
ccp/index.html Normal file
View File

@ -0,0 +1,42 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.cloud-dev.d4science.org/boot/d4s-boot.js"></script>
<script src="js/methodlistcontroller.js"></script>
<script src="js/methodeditorcontroller.js"></script>
<script src="js/inputwidgeteditorcontroller.js"></script>
<script src="js/outputwidgeteditorcontroller.js"></script>
<script src="js/executionformcontroller.js"></script>
<script src="js/executionhistorycontroller.js"></script>
<script src="js/logterminalcontroller.js"></script>
<script src="js/inputwidgetcontroller.js"></script>
<script src="js/outputwidgetcontroller.js"></script>
<link href="css/common.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<script src="https://cdn.cloud-dev.d4science.org/common/js/bss-min-1.2.6.js"></script>
<script src="../storage/d4s-storage.js"></script>
<script src="https://cdn.jsdelivr.net/npm/ol@v9.2.4/dist/ol.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
</head>
<body class="m-4">
<d4s-boot-2 clientid="https://next.dev.d4science.org" context="%2Fgcube%2Fdevsec%2FCCP" gateway="next.dev.d4science.org" redirect-url="http://localhost:8080/ccp/index.html" url="https://accounts.dev.d4science.org/auth"></d4s-boot-2>
<div class="row">
<div class="col">
<d4s-ccp-methodlist serviceurl="https://ccp.cloud-dev.d4science.org" allow-edit="true" allow-execute="true" archive="true"></d4s-ccp-methodlist>
</div>
<div class="col">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<d4s-ccp-methodeditor serviceurl="https://ccp.cloud-dev.d4science.org"></d4s-ccp-methodeditor>
</div>
<div class="col">
<d4s-ccp-executionform serviceurl="https://ccp.cloud-dev.d4science.org"></d4s-ccp-executionform>
</div>
<div class="col">
<d4s-ccp-executionhistory allow-archive="true" serviceurl="https://ccp.cloud-dev.d4science.org"></d4s-ccp-executionhistory>
</div>
</div>
</body>
</html>

View File

@ -1,50 +1,103 @@
class CCPExecutionForm extends HTMLElement{ class CCPExecutionForm extends HTMLElement {
#boot; #boot;
#rootdoc; #rootdoc;
#data; #data;
#method; #method;
#executionmonitor; #serviceurl;
#messages = {
#serviceurl; "en": {
"confirm_form_overwrite": "Please confirm the overwrite of the previous execution form.",
constructor(){ "Inputs": "Inputs",
"Options": "Options",
"Outputs": "Outputs",
"Execute": "Execute",
"execute_help": "Submit an execution request",
"generate_code": "Generate code for",
"generate_code_help": "Generate example code for a selectable programming language or runtime",
"direct_link": "Direct link",
"direct_link_help": "Navigate to the link for directly reopening this method in the execution form",
"automatic_archive_option": "Select what you like to archive automatically when the execution is completed",
"automatic_archive_provenance": "Automatically archive whole execution provenance",
"automatic_archive_provenance_help": "Automatically archive the whole execution provenance to the workspace",
"automatic_archive_outputs": "Automatically archive uncompressed outputs only",
"automatic_archive_outputs_help": "Automatically archive only the outputs to the workspace",
"automatic_archive_none": "Do not archive anything automatically",
"automatic_archive_none_help": "Do not archive anything automatically",
"execution_accepted": "Execution request accepted with id: ",
"drop_method_invitation": "Drop a method here to request an execution",
"err_execution_not_accepted": "Error. Execution has not been accepted by server",
"err_code_generation": "Error while generating code",
"err_load_method": "Error while loading method",
"err_no_runtimes": "This method has no compatible runtimes available",
"err_execute": "Error while sending execution request",
"container_based_warning_confirm" : "The compatible architectures appear to be container based but no ccpimage input has been specified. This could cause errors that can be unpredictable on some infrastructures. Do you want to proceed anyway?"
}
}
constructor() {
super() super()
this.#boot = document.querySelector("d4s-boot-2") this.#boot = document.querySelector("d4s-boot-2")
this.#rootdoc = this.attachShadow({ "mode": "open" })
}
connectedCallback() {
this.#serviceurl = this.getAttribute("serviceurl") this.#serviceurl = this.getAttribute("serviceurl")
this.#rootdoc = this.attachShadow({ "mode" : "open"})
this.connectNewExecutionRequest() this.connectNewExecutionRequest()
this.render() this.render()
this.showMethod()
const params = new URLSearchParams(window.location.search)
if (params.get('execution')) {
const execution = { id: params.get('execution') }
this.prepareFromExecution(execution)
} else if (params.get('method')) {
this.#method = params.get('method')
this.loadMethod()
} else {
this.showMethod()
}
} }
static get observedAttributes() { static get observedAttributes() {
return ["method"]; return ["method"];
} }
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]
}
attributeChangedCallback(name, oldValue, newValue) { attributeChangedCallback(name, oldValue, newValue) {
//if((oldValue != newValue) && (name === "method")){ //if((oldValue != newValue) && (name === "method")){
if(name === "method"){ if (name === "method") {
this.#method = newValue this.#method = newValue
this.loadMethod() this.loadMethod()
} }
} }
connectNewExecutionRequest(){ connectNewExecutionRequest() {
document.addEventListener("newexecutionrequest", ev=>{ document.addEventListener("newexecutionrequest", ev => {
if(window.confirm("Please confirm overwrite of execution form?")){ if (this.#data) {
if (window.confirm(this.getLabel("confirm_form_overwrite"))) {
this.setAttribute("method", ev.detail)
this.parentElement.scrollIntoViewIfNeeded()
}
} else {
this.setAttribute("method", ev.detail) this.setAttribute("method", ev.detail)
this.parentElement.scrollIntoViewIfNeeded() this.parentElement.scrollIntoViewIfNeeded()
} }
}) })
} }
render(){ render() {
this.#rootdoc.innerHTML = ` this.#rootdoc.innerHTML = `
<div> <div>
<link rel="stylesheet" href="https://cdn.dev.d4science.org/ccp/css/common.css"></link> <link rel="canonical" href="https://getbootstrap.com/docs/5.0/components/modal/">
<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"> <link rel="stylesheet" href="https://cdn.cloud.d4science.org/ccp/css/common.css"></link>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/>
<style> <style>
.ccp-execution-form{ .ccp-execution-form{
position: relative; position: relative;
@ -53,12 +106,12 @@ class CCPExecutionForm extends HTMLElement{
<template id="EXECUTION_FORM_TEMPLATE"> <template id="EXECUTION_FORM_TEMPLATE">
<div class="ccp-execution-form" name="execution_form"> <div class="ccp-execution-form" name="execution_form">
<h5 class="ccp-method-title"></h5> <h5 class="ccp-method-title"></h5>
<p class="description font-italic font-weight-lighter"></p> <div class="description font-italic font-weight-lighter"></div>
<div class="plexiglass d-none"></div> <div class="plexiglass d-none"></div>
<form name="execution_form" class="d-flex flex-column gap-3" style="gap:5px"> <form name="execution_form" class="d-flex flex-column gap-3" style="gap:5px">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5>Inputs</h5> <h5>${this.getLabel("Inputs")}</h5>
</div> </div>
<div class="card-body ccp-inputs"> <div class="card-body ccp-inputs">
<div> <div>
@ -68,32 +121,57 @@ class CCPExecutionForm extends HTMLElement{
</div> </div>
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5>Outputs</h5> <h5>${this.getLabel("Options")}</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="form-row ccp-outputs"> <div class="p-1">
<label class="small text-muted p-0">${this.getLabel("automatic_archive_option")}.</label>
<select name="archive-selector" class="form-select p-2" style="padding:2px">
<option value="execution" title="${this.getLabel("automatic_archive_provenance_help")}">${this.getLabel("automatic_archive_provenance")}</option>
<option value="outputs" title="${this.getLabel("automatic_archive_outputs_help")}">${this.getLabel("automatic_archive_outputs")}</option>
<option value="none" title="${this.getLabel("automatic_archive_none_help")}">${this.getLabel("automatic_archive_none")}</option>
</select>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5>${this.getLabel("Outputs")}</h5>
</div>
<div class="card-body">
<div class="row ccp-outputs">
<div class="col form-group"></div> <div class="col form-group"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="form-row"> <div class="row">
<div class="col-6"> <div class="col-6">
<button id="execute_method_button" class="btn btn-info">Execute</button> <button title="${this.getLabel("execute_help")}" id="execute_method_button" class="btn btn-info">${this.getLabel("Execute")}</button>
</div> </div>
<div class="col-6"> <div class="col-6">
<div class=""> <div class="mb-3" title="${this.getLabel("generate_code_help")}">
<label>Generate code for:</label> <label>${this.getLabel("generate_code")}</label>
<div class="d-flex"> <div class="d-flex">
<select name="language-selector" class="form-control" style="padding:2px"> <select name="language-selector" class="form-control" style="padding:2px">
<option value="text/python" data-ext="py" title="Generate plain Python3">Python 3</option> <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="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="application/vnd.jupyter+python" data-ext="ipynb" title="Generate Jupyter notebook with Python 3 cells">Jupyter Python3</option>
</select> <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>
<button name="codegen" title="Generate code" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small"> <option value="application/x-sh+curl" data-ext="sh" title="Generate Bash script (curl)">Bash (curl)</option>
<svg viewBox="0 96 960 960"> <option value="application/x-sh+wget" data-ext="sh" title="Generate Bash script (wget)">Bash (wget)</option>
<path d="M320 814 80 574l242-242 43 43-199 199 197 197-43 43Zm318 2-43-43 199-199-197-197 43-43 240 240-242 242Z"></path> <option value="application/json+galaxy" data-ext="json" title="Generate JSON request for Galaxy">Galaxy CCP request (preview)</option>
</svg> <option value="application/xml+galaxy" data-ext="xml" title="Generate installable Galaxy tool">Galaxy tool (preview)</option>
</button> </select>
<button name="codegen" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<svg viewBox="0 96 960 960">
<path d="M320 814 80 574l242-242 43 43-199 199 197 197-43 43Zm318 2-43-43 199-199-197-197 43-43 240 240-242 242Z"></path>
</svg>
</button>
</div>
</div>
<div>
<div class="d-flex">
<a title="${this.getLabel("direct_link_help")}" name="direct_link_method" class="text-truncate" href="${window.location.href}">${this.getLabel("direct_link")}</a>
</div> </div>
</div> </div>
</div> </div>
@ -103,25 +181,25 @@ class CCPExecutionForm extends HTMLElement{
</template> </template>
<template id="EXECUTION_FORM_EMPTY_TEMPLATE"> <template id="EXECUTION_FORM_EMPTY_TEMPLATE">
<div name="execution_form"> <div name="execution_form">
<i style="padding:3rem">Drop a method here!</i> <i style="padding:3rem">${this.getLabel("drop_method_invitation")}!</i>
</div> </div>
</template> </template>
<div name="execution_form"></div> <div name="execution_form"></div>
</div> </div>
` `
} }
lockRender(){ lockRender() {
const plexi = this.#rootdoc.querySelector(".plexiglass") const plexi = this.#rootdoc.querySelector(".plexiglass")
plexi.innerHTML = `` plexi.innerHTML = ``
plexi.classList.toggle("d-none") plexi.classList.toggle("d-none")
} }
unlockRender(){ unlockRender() {
this.#rootdoc.querySelector(".plexiglass").classList.toggle("d-none") this.#rootdoc.querySelector(".plexiglass").classList.toggle("d-none")
} }
writeToPlexi(message){ writeToPlexi(message) {
const plexi = this.#rootdoc.querySelector(".plexiglass") const plexi = this.#rootdoc.querySelector(".plexiglass")
plexi.innerHTML = ` plexi.innerHTML = `
<div class="d-flex" style="flex-direction: column;justify-content:center;position:relative;height:100%;align-items: center;"> <div class="d-flex" style="flex-direction: column;justify-content:center;position:relative;height:100%;align-items: center;">
@ -131,67 +209,80 @@ class CCPExecutionForm extends HTMLElement{
</div> </div>
` `
} }
loadMethod(){ loadMethod() {
return this.#boot.secureFetch(this.#serviceurl + "/processes/" + this.#method).then( return this.#boot.secureFetch(this.#serviceurl + "/processes/" + this.#method).then(
(resp)=>{ (resp) => {
if(resp.status === 200){ if (resp.status === 200) {
return resp.json() return resp.json()
}else throw "Error retrieving process" } else throw this.getLabel("err_load_method")
} }
).then(data=>{ ).then(data => {
this.#data = data this.#data = data
const infra = const infra =
this.#data.links.filter(l => l.rel === "compatibleWith")[0] this.#data.links.filter(l => l.rel === "compatibleWith")[0]
return this.#boot.secureFetch(this.#serviceurl + "/" + infra.href) return this.#boot.secureFetch(this.#serviceurl + "/" + infra.href)
}).then(resp=>{ }).then(resp => {
this.#data.executable = resp.status === 200 this.#data.executable = resp.status === 200
}).then(()=>{ }).then(() => {
this.showMethod() this.showMethod()
}).catch(err=>alert(err)) }).catch(err => alert(err))
} }
showEmpty(resp){ showEmpty(resp) {
BSS.apply(this.#empty_executionform_bss, this.#rootdoc) BSS.apply(this.#empty_executionform_bss, this.#rootdoc)
} }
showMethod(){ showMethod() {
if(this.#method == null) this.showEmpty(); if (this.#method == null) this.showEmpty();
else{ else {
BSS.apply(this.#executionform_bss, this.#rootdoc) BSS.apply(this.#executionform_bss, this.#rootdoc)
} }
} }
sendExecutionRequest(){ sendExecutionRequest() {
this.lockRender() this.lockRender()
const url = this.#serviceurl + "/processes/" + this.#method + "/execution" const url = this.#serviceurl + "/processes/" + this.#method + "/execution"
const req = this.buildRequest() const req = this.buildRequest()
// Perform preliminary check on the possibility of infra being container based and ccpimage missing
if(!req.inputs["ccpimage"]){
const reg = /docker|lxd/i
const containerbased =
this.#data.links.filter(l=>l.rel === "compatibleWith").
reduce((acc, l)=>{ return acc && (l.title.match(reg) || l.href.match(reg) ? true : false)}, true)
if(containerbased && !window.confirm(this.getLabel("container_based_warning_confirm"))){
this.unlockRender()
return;
}
}
this.#boot.secureFetch( this.#boot.secureFetch(
url, { method : "POST", body : JSON.stringify(req), headers : { "Content-Type" : "application/json"}} url, { method: "POST", body: JSON.stringify(req), headers: { "Content-Type": "application/json" } }
).then(reply=>{ ).then(reply => {
if(reply.status !== 200 && reply.status !== 201){ if (reply.status !== 200 && reply.status !== 201) {
throw "Error while requesting resource" throw this.getLabel("err_execute")
} }
return reply.json() return reply.json()
}).then(data=>{ }).then(data => {
if(data.status !== "accepted"){ if (data.status !== "accepted") {
throw "Execution has not been accepted by server" throw this.getLabel("err_execution_not_accepted")
} }
const event = new CustomEvent('newexecution', { detail: data.jobID }); const event = new CustomEvent('newexecution', { detail: data.jobID });
document.dispatchEvent(event) document.dispatchEvent(event)
this.writeToPlexi("Execution request accepted with id: " + data.jobID) this.writeToPlexi(this.getLabel("execution_accepted") + " " + data.jobID)
window.setTimeout(()=>this.unlockRender(), 3000) window.setTimeout(() => this.unlockRender(), 3000)
}).catch(err => {alert("Unable to call execute: " + err); this.unlockRender()}) }).catch(err => { alert(this.getLabel("err_execute") + ": " + err); this.unlockRender() })
} }
generateCode(mime, filename){ generateCode(mime, filename) {
const url = this.#serviceurl + "/methods/" + this.#method + "/code" const url = this.#serviceurl + "/methods/" + this.#method + "/code"
const req = this.buildRequest() const req = this.buildRequest()
this.#boot.secureFetch( this.#boot.secureFetch(
url, { method : "POST", body : JSON.stringify(req), headers : { "Content-Type" : "application/json", "Accept" : mime}} url, { method: "POST", body: JSON.stringify(req), headers: { "Content-Type": "application/json", "Accept": mime } }
).then(reply=>{ ).then(reply => {
if(reply.status !== 200) throw "Error while requesting code:"; if (reply.status !== 200) throw this.getLabel("err_code_generation");
return reply.blob() return reply.blob()
}).then(blob => { }).then(blob => {
const objectURL = URL.createObjectURL(blob) const objectURL = URL.createObjectURL(blob)
@ -201,91 +292,135 @@ class CCPExecutionForm extends HTMLElement{
document.body.appendChild(tmplnk) document.body.appendChild(tmplnk)
tmplnk.click() tmplnk.click()
document.body.removeChild(tmplnk) document.body.removeChild(tmplnk)
}).catch(err=>{ alert(err)}) }).catch(err => { alert(err) })
} }
buildRequest(){ buildRequest() {
let request = { inputs : {}, outputs : {}, response : "raw"} let request = { inputs: {}, outputs: {}, response: "raw" }
//fill inputs //fill inputs
const inputs = this.getInputs() const inputs = this.getInputs()
inputs.forEach(i=>{ inputs.forEach(i => {
request.inputs[i.name] = i.value request.inputs[i.name] = i.value
}) })
//fill outputs //fill outputs
const outputs = this.getOutputs() const outputs = this.getOutputs()
outputs.forEach(o=>{ outputs.forEach(o => {
if(o.enabled) request.outputs[o.name] = { transmissionMode : "value" }; if (o.enabled) request.outputs[o.name] = { transmissionMode: "value" };
}) })
const archiveoption = this.#rootdoc.querySelector("select[name='archive-selector']").value
switch (archiveoption) {
case 'execution':
request.subscribers = [{ successUri: "http://registry:8080/executions/archive-to-folder" }]
break;
case 'outputs':
request.subscribers = [{ successUri: "http://registry:8080/executions/outputs/archive-to-folder" }]
break
default:
break
}
return request return request
} }
getInputs(){ getInputs() {
return Array.prototype.slice.call(this.#rootdoc.querySelectorAll("d4s-ccp-input")) return Array.prototype.slice.call(this.#rootdoc.querySelectorAll("d4s-ccp-input"))
} }
getOutputs(){ getOutputs() {
return Array.prototype.slice.call(this.#rootdoc.querySelectorAll("d4s-ccp-output")) return Array.prototype.slice.call(this.#rootdoc.querySelectorAll("d4s-ccp-output"))
} }
initValues(inputs){ initInputValues(inputs) {
Object.keys(inputs).forEach(k=>{ Object.keys(inputs).forEach(k => {
const w = this.#rootdoc.querySelector(`d4s-ccp-input[name=${k}]`) const w = this.#rootdoc.querySelector(`d4s-ccp-input[name=${k}]`)
if(w){ if (w) {
w.value = (inputs[k]) w.value = (inputs[k])
} }
}) })
} }
prepareFromExecution(exec){ initOptionValues(request) {
let f1 = const archiveselector = this.#rootdoc.querySelector("select[name='archive-selector']")
if (request.subscribers) {
if (request.subscribers.filter(s => s.successUri === "http://registry:8080/executions/archive-to-folder").length === 1) {
archiveselector.value = "execution"
} else if (request.subscribers.filter(s => s.successUri === "http://registry:8080/executions/outputs/archive-to-folder").length === 1) {
archiveselector.value = "outputs"
} else {
archiveselector.value = "none"
}
} else {
archiveselector.value = "none"
}
}
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`) this.#boot.secureFetch(this.#serviceurl + `/executions/${exec.id}/metadata/method.json`)
.then(resp=>{ .then(resp => {
if(resp.status === 200){ if (resp.status === 200) {
return resp.json() return resp.json()
}else throw "Error retrieving process" } else throw this.getLabel("err_load_method")
} }
).then(data=>data) ).then(data => data)
let f2 = let f2 =
this.#boot.secureFetch(this.#serviceurl + `/executions/${exec.id}/metadata/request.json`) this.#boot.secureFetch(this.#serviceurl + `/executions/${exec.id}/metadata/request.json`)
.then(resp=>{ .then(resp => {
if(resp.status === 200){ if (resp.status === 200) {
return resp.json() return resp.json()
}else throw "Error retrieving process" } else throw this.getLabel("err_load_method")
} }
).then(data=>data) ).then(data => data)
var requestdata = null var requestdata = null
Promise.all([f1, f2]).then(m_and_r => { Promise.all([f1, f2]).then(m_and_r => {
this.#data = m_and_r[0] this.#data = m_and_r[0]
requestdata = m_and_r[1] requestdata = m_and_r[1]
this.#method = this.#data.id this.#method = this.#data.id
const infra = const infra =
this.#data.links.filter(l => l.rel === "compatibleWith")[0] this.#data.links.filter(l => l.rel === "compatibleWith")[0]
return this.#boot.secureFetch(this.#serviceurl + "/" + infra.href) return this.#boot.secureFetch(this.#serviceurl + "/" + infra.href)
}).then(resp=>{ }).then(resp => {
this.#data.executable = resp.status === 200 this.#data.executable = resp.status === 200
}).then(()=>{ }).then(() => {
this.showMethod() this.showMethod()
this.initValues(requestdata.inputs) this.initInputValues(requestdata.inputs)
}).catch(err=>alert(err)) this.initOptionValues(requestdata)
}).catch(err => alert(err))
} }
#empty_executionform_bss = { #empty_executionform_bss = {
template : "#EXECUTION_FORM_EMPTY_TEMPLATE", template: "#EXECUTION_FORM_EMPTY_TEMPLATE",
target : "div[name=execution_form]", target: "div[name=execution_form]",
on_drop : ev=>{ on_drop: ev => {
if(ev.dataTransfer){ if (ev.dataTransfer) {
if(ev.dataTransfer.getData('text/plain+ccpmethod')){ if (ev.dataTransfer.getData('text/plain+ccpmethod')) {
const id = ev.dataTransfer.getData('text/plain+ccpmethod') const id = ev.dataTransfer.getData('text/plain+ccpmethod')
this.setAttribute("method", id); this.setAttribute("method", id);
ev.preventDefault() ev.preventDefault()
ev.stopPropagation() ev.stopPropagation()
ev.currentTarget.style.backgroundColor = "white" ev.currentTarget.style.backgroundColor = "white"
}else if(ev.dataTransfer.getData('text/plain+ccpexecution')){ } else if (ev.dataTransfer.getData('text/plain+ccpexecution')) {
this.prepareFromExecution(JSON.parse(ev.dataTransfer.getData('application/json+ccpexecution'))) this.prepareFromExecution(JSON.parse(ev.dataTransfer.getData('application/json+ccpexecution')))
ev.preventDefault() ev.preventDefault()
ev.stopPropagation() ev.stopPropagation()
@ -293,23 +428,23 @@ class CCPExecutionForm extends HTMLElement{
} }
} }
}, },
on_dragover : ev=>{ on_dragover: ev => {
ev.preventDefault() ev.preventDefault()
}, },
} }
#executionform_bss = { #executionform_bss = {
template : "#EXECUTION_FORM_TEMPLATE", template: "#EXECUTION_FORM_TEMPLATE",
target : "div[name=execution_form]", target: "div[name=execution_form]",
in : ()=>this.#data, in: () => this.#data,
on_drop : ev=>{ on_drop: ev => {
if(ev.dataTransfer){ if (ev.dataTransfer) {
if(ev.dataTransfer.getData('text/plain+ccpmethod')){ if (ev.dataTransfer.getData('text/plain+ccpmethod')) {
const id = ev.dataTransfer.getData('text/plain+ccpmethod'); const id = ev.dataTransfer.getData('text/plain+ccpmethod');
this.setAttribute("method", id); this.setAttribute("method", id);
ev.preventDefault() ev.preventDefault()
ev.stopPropagation() ev.stopPropagation()
}else if(ev.dataTransfer.getData('text/plain+ccpexecution')){ } else if (ev.dataTransfer.getData('text/plain+ccpexecution')) {
this.prepareFromExecution(JSON.parse(ev.dataTransfer.getData('application/json+ccpexecution'))) this.prepareFromExecution(JSON.parse(ev.dataTransfer.getData('application/json+ccpexecution')))
ev.preventDefault() ev.preventDefault()
ev.stopPropagation() ev.stopPropagation()
@ -317,66 +452,82 @@ class CCPExecutionForm extends HTMLElement{
} }
} }
}, },
on_dragover : ev=>{ on_dragover: ev => {
ev.preventDefault() ev.preventDefault()
}, },
recurse : [ recurse: [
{ {
target: ".ccp-method-title", target: ".ccp-method-title",
apply : (e,d)=>e.innerHTML = ` apply: (e, d) => e.innerHTML = `
${d.title} <span class="badge badge-primary ml-1">${d.version}</span> ${d.title} <span class="badge badge-primary ml-1">${d.version}</span>
${ d.metadata.filter(md=>md.role === 'author').map(a=>`<span class="badge badge-warning ml-1">${a.title}</span>`)} ${d.metadata.filter(md => md.role === 'author').map(a => `<span class="badge badge-warning ml-1">${a.title}</span>`)}
` `
}, },
{ {
target: "p.description", target: "div.description",
apply : (e,d)=>e.textContent = d.description apply: (e, d) => {
const maxchars = 200
const maxlines = 4
const s = d.description
const lines = s.split("\n")
if (lines.length <= maxlines && s.length <= maxchars) {
e.innerHTML = s.replaceAll("\n", "<br/>")
} else if (lines.length > maxlines) {
const lines1 = lines.slice(0, maxlines -1)
const index = Math.min(maxchars, lines1.join("<br/>").length)
const sf = s.replaceAll("\n", "<br/>")
e.innerHTML = `<details><summary>${sf.substring(0, index)}...</summary>...${sf.substring(index)}</details>`
} else {
const sf = s.replaceAll("\n", "<br/>")
e.innerHTML = `<details><summary>${sf.substring(0, maxchars)}...</summary>...${sf.substring(maxchars)}</details>`
}
}
}, },
{ {
target: "div.ccp-inputs", target: "div.ccp-inputs",
in : (e,d)=>d, in: (e, d) => d,
recurse : [ recurse: [
{ {
"in" : (e,d)=>{ return Object.values(d.inputs) }, "in": (e, d) => { return Object.values(d.inputs) },
target : "div", target: "div",
apply : (e,d)=>{ apply: (e, d) => {
e.innerHTML = `<d4s-ccp-input name="${d.id}" input='${JSON.stringify(d)}'></d4s-ccp-input>` e.innerHTML = `<d4s-ccp-input name="${d.id}" input='${btoa(JSON.stringify(d))}'></d4s-ccp-input>`
} }
} }
] ]
}, },
{ {
target: "div.ccp-outputs", target: "div.ccp-outputs",
in : (e,d)=>d, in: (e, d) => d,
recurse : [ recurse: [
{ {
"in" : (e,d)=>{ return Object.values(d.outputs) }, "in": (e, d) => { return Object.values(d.outputs) },
target : "div", target: "div",
apply : (e,d)=>{ apply: (e, d) => {
e.innerHTML = `<d4s-ccp-output name="${d.id}" output='${JSON.stringify(d)}'></d4s-ccp-output>` e.innerHTML = `<d4s-ccp-output name="${d.id}" output='${btoa(JSON.stringify(d))}'></d4s-ccp-output>`
} }
} }
] ]
}, },
{ {
target: "div.ccp-runtimes *[name=refresh-runtimes]", target: "div.ccp-runtimes *[name=refresh-runtimes]",
on_click : ev=>{ on_click: ev => {
BSS.apply(this.#executionform_bss) BSS.apply(this.#executionform_bss)
} }
}, },
{ {
target: "#execute_method_button", target: "#execute_method_button",
on_click : ev=>{ on_click: ev => {
ev.preventDefault() ev.preventDefault()
ev.stopPropagation() ev.stopPropagation()
if(this.#data.executable) this.sendExecutionRequest(); if (this.#data.executable) this.sendExecutionRequest();
else alert("This method has no compatible runtimes available") else alert(this.getLabel("err_no_runtimes"))
return false; return false;
} }
}, },
{ {
target: "button[name=codegen]", target: "button[name=codegen]",
on_click : ev=>{ on_click: ev => {
ev.preventDefault() ev.preventDefault()
ev.stopPropagation() ev.stopPropagation()
const langsel = ev.target.parentElement.querySelector("select[name=language-selector]") const langsel = ev.target.parentElement.querySelector("select[name=language-selector]")
@ -386,9 +537,13 @@ class CCPExecutionForm extends HTMLElement{
return false return false
} }
}, },
{
target: "a[name=direct_link_method]",
apply: (e, d) => e.href = window.location.origin + window.location.pathname + "?method=" + d.id
}
] ]
} }
} }
window.customElements.define('d4s-ccp-executionform', CCPExecutionForm); window.customElements.define('d4s-ccp-executionform', CCPExecutionForm);

View File

@ -3,6 +3,7 @@ class CCPExecutionHistory extends HTMLElement {
#boot = null; #boot = null;
#rootdoc = null; #rootdoc = null;
#serviceurl = null; #serviceurl = null;
#wsurl = null;
#broadcasturl = null; #broadcasturl = null;
#archive = null; #archive = null;
#data = []; #data = [];
@ -11,34 +12,97 @@ class CCPExecutionHistory extends HTMLElement {
#socket = null; #socket = null;
#interval = null; #interval = null;
#searchfield = null; #searchfield = null;
#fileupload = null; // #fileupload = null;
#archiveupload = 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(){ constructor(){
super() super()
this.#boot = document.querySelector("d4s-boot-2") this.#boot = document.querySelector("d4s-boot-2")
this.#rootdoc = this.attachShadow({ "mode" : "open"}) this.#rootdoc = this.attachShadow({ "mode" : "open"})
}
connectedCallback(){
this.#serviceurl = this.getAttribute("serviceurl") this.#serviceurl = this.getAttribute("serviceurl")
this.#broadcasturl = this.getAttribute("broadcasturl") this.#broadcasturl = this.getAttribute("broadcasturl")
if(!this.#broadcasturl){ if(!this.#broadcasturl){
this.#broadcasturl = this.#serviceurl.replace(/^http/, "ws") this.#broadcasturl = this.#serviceurl.replace(/^http/, "ws")
} }
this.#broadcasturl = this.#broadcasturl + "/ws/notification" 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.connectNewExecution()
}
connectedCallback(){
this.connectBroadcastWithSubject() this.connectBroadcastWithSubject()
this.render() this.render()
this.refreshExecutions() this.refreshExecutions()
if(this.#archive) this.refreshArchivedExecutions();
} }
render(){ render(){
this.#rootdoc.innerHTML = ` this.#rootdoc.innerHTML = `
<link rel="stylesheet" href="https://cdn.dev.d4science.org/ccp/css/common.css"></link> <link rel="stylesheet" href="https://cdn.cloud.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"> <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> <style>
.galaxy{
background-color: #2c3143;
color: yellow;
}
.lxd{ .lxd{
background-color: #dd4814; background-color: #dd4814;
color: white; color: white;
@ -64,10 +128,10 @@ class CCPExecutionHistory extends HTMLElement {
<summary class="ccp-method-item-header noselect d-flex flex-wrap justify-content-between"> <summary class="ccp-method-item-header noselect d-flex flex-wrap justify-content-between">
<h5 class="text-primary d-inline"></h5> <h5 class="text-primary d-inline"></h5>
<div> <div>
<span name="failed" title="Failed executions" class="badge badge-danger float-right">Z</span> <span name="failed" title="${this.getLabel("failed_count_help")}" 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="successful" title="${this.getLabel("successful_count_help")}"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="running" title="${this.getLabel("running_count_help")}"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> <span name="accepted" title="${this.getLabel("accepted_count_help")}" class="badge badge-secondary float-right mr-1">X</span>
</div> </div>
</summary> </summary>
<ul class="ccp-execution-list list-group" style="list-style:none"> <ul class="ccp-execution-list list-group" style="list-style:none">
@ -78,24 +142,24 @@ class CCPExecutionHistory extends HTMLElement {
<span name="status" class="ml-1 badge"></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;"> <div class="d-flex float-right" style="gap: 3px 5px; max-width: 40%; min-width:60px; flex-wrap:wrap;">
${ this.#archive ? ` ${ this.#archive ? `
<button data-index="0" name="archive" title="Archive to workspace" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small"> <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> <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>`
: `` : ``
} }
<button data-index="0" name="provo" title="Export to Prov-o document" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small"> <!--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> <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-->
<!-- button data-index="0" name="zip" title="Download as zip archive" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small"> <!-- 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> <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-->
<button data-index="0" name="archivefolder" title="Archive outputs to workspace" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small"> <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> <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>
<button data-index="0" name="reexecute1" title="Re-submit this execution" class="btn btn-info ccp-toolbar-button ccp-toolbar-button-small"> <button data-index="0" name="reexecute1" title="${this.getLabel("re-submit_help")}" class="btn btn-info ccp-toolbar-button ccp-toolbar-button-small">
Re-submit ${this.getLabel("re-submit")}
</button> </button>
<button data-index="0" name="delete" title="Delete" class="btn btn-danger ccp-toolbar-button ccp-toolbar-button-small"> <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"> <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> <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> </svg>
@ -111,26 +175,36 @@ class CCPExecutionHistory extends HTMLElement {
</p> </p>
</summary> </summary>
<div class="d-flex flex-wrap" style="gap:3px; overflow:hidden"> <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="Infrastructure" title="Infrastructure"></span> <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="Runtime" title="Runtime"></span> <span class="badge" name="runtime" alt="${this.getLabel("runtime_help")}" title="${this.getLabel("runtime_help")}"></span>
</div> </div>
<div name="logterminalcontainer" style="margin:5px 0 5px 0"> <div name="logterminalcontainer" style="margin:5px 0 5px 0">
</div> </div>
<ul> <ul>
<li></li> <li></li>
</ul> </ul>
<div class="d-flex justify-content-end" style="gap: 3px;"> <div class="d-flex flex-column align-items-end" style="gap: 3px;">
<label>Generate code for:</label> <div class="d-flex align-items-center" style="gap:5px" title="${this.getLabel("generate_code_help")}">
<select name="language-selector" class="form-control" style="max-width:10rem;height:inherit;padding:2px"> <label style="text-wrap:nowrap">${this.getLabel("generate_code")}</label>
<option value="text/python" data-ext="py" title="Generate plain Python3">Python 3</option> <select name="language-selector" class="form-control">
<option value="text/plain+r" data-ext="r" title="Generate plain R">R</option> <option value="text/python" data-ext="py" title="Generate plain Python3">Python 3</option>
<option value="application/vnd.jupyter+python" data-ext="ipynb" title="Generate Jupyter notebook with Python 3 cells">Jupyter Python3</option> <option value="text/plain+r" data-ext="r" title="Generate plain R">R</option>
</select> <option value="application/vnd.jupyter+python" data-ext="ipynb" title="Generate Jupyter notebook with Python 3 cells">Jupyter Python3</option>
<button data-index="0" name="codegen" title="Generate code" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small"> <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>
<svg viewBox="0 96 960 960"> <option value="application/x-sh+curl" data-ext="sh" title="Generate Bash script (curl)">Bash (curl)</option>
<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"/> <option value="application/x-sh+wget" data-ext="sh" title="Generate Bash script (wget)">Bash (wget)</option>
</svg> </select>
</button> <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> </div>
</details> </details>
</li> </li>
@ -139,28 +213,73 @@ class CCPExecutionHistory extends HTMLElement {
</li> </li>
</ul> </ul>
</template> </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"> <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">
<div class="card-header"> <div class="card-header">
<div class="ccp-toolbar-header d-flex flex-wrap justify-content-between"> <div class="ccp-toolbar-header d-flex flex-wrap justify-content-between">
<div> <div>
<span name="header">Execution Monitor</span> <span name="header">${this.getLabel("execution_monitor")}</span>
</div> </div>
<div class="d-flex flex-wrap" style="gap:2px"> <div class="d-flex flex-wrap" style="gap:2px">
<button name="refresh" class="btn btn-primary ccp-toolbar-button" title="Refresh"> <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> <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> </button>
<label name="fileupload" class="btn btn-primary ccp-toolbar-button m-0" title="Upload from file"> <!--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> <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"/> <input type="file" class="d-none" multiple="multiple"/>
</label> </label>
<div class="d-flex" style="gap:2px"> <div class="d-flex" style="gap:2px">
<input type="text" class="form-control" placeholder="Paste link here"/> <input type="text" class="form-control" placeholder="${this.getLabel("paste_link")}"/>
<button name="archive" class="btn btn-primary ccp-toolbar-button m-0" title="Upload from 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> <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> </button>
</div> </div-->
</div> </div>
</div> </div>
</div> </div>
@ -168,41 +287,96 @@ class CCPExecutionHistory extends HTMLElement {
<div class="mb-3"> <div class="mb-3">
<input accept="application/zip" type="text" name="search" class="form-control" placeholder="Search"/> <input accept="application/zip" type="text" name="search" class="form-control" placeholder="Search"/>
</div> </div>
<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> <ul name="ccp_execution_list"></ul>
</div> `
}
</div> </div>
</div> </div>
` `
this.#rootdoc.querySelector("button[name=refresh]").addEventListener("click", ev=>{ this.#rootdoc.querySelector("button[name=refresh]").addEventListener("click", ev=>{
this.refreshExecutions() if(this.isShowingArchived()){
this.refreshArchivedExecutions()
}else{
this.refreshExecutions()
}
}) })
this.#searchfield = this.#rootdoc.querySelector("input[name=search]") this.#searchfield = this.#rootdoc.querySelector("input[name=search]")
this.#searchfield.addEventListener("input", ev=>{ this.#searchfield.addEventListener("input", ev=>{
this.updateList() this.updateList()
}) })
this.#fileupload = this.#rootdoc.querySelector("label[name=fileupload] > input[type=file]") if(this.#archive){
this.#fileupload.addEventListener("change", ev=>{ const linklive = this.#rootdoc.querySelector("a.nav-link[name=live_executions]")
const filelist = ev.target.files; const linkarch = this.#rootdoc.querySelector("a.nav-link[name=archived_executions]")
if (filelist.length > 0) { const tablive = this.#rootdoc.querySelector("div.tab-pane#live_executions")
const files = Array.prototype.slice.call(filelist) const tabarch = this.#rootdoc.querySelector("div.tab-pane#archived_executions")
this.importExecutions(files) this.#rootdoc.querySelector("#switch_executions").addEventListener("click", ev=>{
} const tgt = ev.target
}) if(tgt == linklive){
linklive.classList.add("active")
this.#archiveupload = this.#rootdoc.querySelector("button[name=archive]") linkarch.classList.remove("active")
this.#archiveupload.addEventListener("click", ev=>{ tablive.classList.add("show")
const link = ev.target.parentElement.querySelector("input").value tablive.classList.add("active")
if(link){ tabarch.classList.remove("show")
if(confirm("Please confirm importing of execution from link?")){ tabarch.classList.remove("active")
this.fromArchive(link) }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(){ updateList(){
const filter = this.#searchfield.value const filter = this.#searchfield.value
if(filter === "" || filter == null || filter == undefined){ if(filter === "" || filter == null || filter == undefined){
@ -228,7 +402,7 @@ class CCPExecutionHistory extends HTMLElement {
return catalog return catalog
}, {}) }, {})
} }
refreshExecution(id){ refreshExecution(id){
this.#boot.secureFetch(`${this.#serviceurl}/executions?id=${id}`).then(reply =>{ this.#boot.secureFetch(`${this.#serviceurl}/executions?id=${id}`).then(reply =>{
if(reply.ok) return reply.json(); if(reply.ok) return reply.json();
@ -365,11 +539,27 @@ class CCPExecutionHistory extends HTMLElement {
}).catch(err=>{ alert(err)}) }).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){ reexecute(id,level){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/level/${level}`, { method: "POST" }) this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/level/${level}`, { method: "POST" })
.then(reply =>{ .then(reply =>{
if (!reply.ok) { if (!reply.ok) {
throw "Unable to re-execute. Check console." throw this.getLabel("err_reexecute")
} }
return reply.json() return reply.json()
}).then(data=>{ }).then(data=>{
@ -400,7 +590,7 @@ class CCPExecutionHistory extends HTMLElement {
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/code`, this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/code`,
{ method: "GET", headers : { "Accept" : mime} }).then(reply =>{ { method: "GET", headers : { "Accept" : mime} }).then(reply =>{
if (!reply.ok) { if (!reply.ok) {
throw "Unable to generate code for " + mime throw this.getLabel("err_generate_code_for") + mime
} }
return reply.blob() return reply.blob()
}).then(blob => { }).then(blob => {
@ -414,30 +604,30 @@ class CCPExecutionHistory extends HTMLElement {
}).catch(err=>{ alert(err)}) }).catch(err=>{ alert(err)})
} }
toArchive(id){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/archive`, { method: "POST" })
.then(reply =>{
if (!reply.ok) {
throw "Unable to archive"
}
}).catch(err=>{ alert(err)})
}
toArchiveFolder(id){ toArchiveFolder(id){
this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/archive-to-folder`, { method: "POST" }) this.#boot.secureFetch(`${this.#serviceurl}/executions/${id}/archive-to-folder`, { method: "POST" })
.then(reply =>{ .then(reply =>{
if (!reply.ok) { if (!reply.ok) {
throw "Unable to archive" 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)}) }).catch(err=>{ alert(err)})
} }
fromArchive(url){ fromArchiveFolder(url){
if(url){ if(url){
this.#boot.secureFetch(`${this.#serviceurl}/executions/archive?url=${url}`) this.#boot.secureFetch(`${this.#serviceurl}/executions/archive?url=${url}`)
.then(reply =>{ .then(reply =>{
if (!reply.ok) { if (!reply.ok) {
throw "Unable to fetch from archive" throw "Unable to fetch from archive folder"
} }
return reply.text() return reply.text()
}).then(data=>{ }).then(data=>{
@ -445,6 +635,104 @@ class CCPExecutionHistory extends HTMLElement {
}).catch(err=>{ alert(err)}) }).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 = { #execution_list_bss = {
template : "#EXECUTIOM_LIST_TEMPLATE", template : "#EXECUTIOM_LIST_TEMPLATE",
@ -463,7 +751,7 @@ class CCPExecutionHistory extends HTMLElement {
if(ev.dataTransfer && ev.dataTransfer.files && ev.dataTransfer.files.length){ if(ev.dataTransfer && ev.dataTransfer.files && ev.dataTransfer.files.length){
const files = Array.prototype.slice.call(ev.dataTransfer.files) const files = Array.prototype.slice.call(ev.dataTransfer.files)
const zips = files.filter(f=>f.type === "application/zip") const zips = files.filter(f=>f.type === "application/zip")
if(confirm("Please confirm import of execution files?")){ if(confirm(this.getLabel("confirm_import_file") + "?")){
this.importExecutions(files) this.importExecutions(files)
} }
} }
@ -524,7 +812,7 @@ class CCPExecutionHistory extends HTMLElement {
}, },
on_click: ev=>{ on_click: ev=>{
if(ev.target.getAttribute("name") === "delete"){ if(ev.target.getAttribute("name") === "delete"){
if(window.confirm("Please confirm deletion of this execution?")){ if(window.confirm(this.getLabel("confirm_delete_execution"))){
const id = ev.currentTarget.getAttribute("data-index") const id = ev.currentTarget.getAttribute("data-index")
this.deleteExecution(id) this.deleteExecution(id)
} }
@ -538,23 +826,23 @@ class CCPExecutionHistory extends HTMLElement {
this.export(id, "application/prov-o+xml", id + ".xml") this.export(id, "application/prov-o+xml", id + ".xml")
} }
if(ev.target.getAttribute("name") === "reexecute1"){ 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") const id = ev.currentTarget.getAttribute("data-index")
this.reexecute(id, 1) this.reexecute(id, 1)
} }
} }
if(ev.target.getAttribute("name") === "archive"){ if(ev.target.getAttribute("name") === "archive"){
if(confirm(" Please confirm archiving of execution to workspace?")){ if(confirm(this.getLabel("confirm_archive_execution"))){
const id = ev.currentTarget.getAttribute("data-index")
this.toArchive(id)
}
}
if(ev.target.getAttribute("name") === "archivefolder"){
if(confirm(" Please confirm archiving of execution outputs to workspace?")){
const id = ev.currentTarget.getAttribute("data-index") const id = ev.currentTarget.getAttribute("data-index")
this.toArchiveFolder(id) 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 : [ recurse : [
{ {
@ -642,7 +930,8 @@ class CCPExecutionHistory extends HTMLElement {
e.textContent = rt + (d.replicas && d.replicas !== "1" ? ' x ' + d.replicas : '') e.textContent = rt + (d.replicas && d.replicas !== "1" ? ' x ' + d.replicas : '')
const t = infratype.match(/docker/i) ? "docker" : null const t = infratype.match(/docker/i) ? "docker" : null
const t2 = !t && infratype.match(/lxd/i) ? "lxd" : t 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)
} }
}, },
{ {
@ -656,6 +945,10 @@ class CCPExecutionHistory extends HTMLElement {
this.generateCode(id, lang, `${id}.${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]", target : "div[name=logterminalcontainer]",
apply : (e,d)=>{ apply : (e,d)=>{
@ -689,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); window.customElements.define('d4s-ccp-executionhistory', CCPExecutionHistory);

View File

@ -7,7 +7,7 @@ class CCPInfrastructureList extends HTMLElement{
#rootdoc; #rootdoc;
#style = ` #style = `
<link rel="stylesheet" href="https://cdn.dev.d4science.org/ccp/css/common.css"></link> <link rel="stylesheet" href="https://cdn.cloud.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"></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"></link>
<style> <style>
.ccp-infrastructure-list{ .ccp-infrastructure-list{

File diff suppressed because it is too large Load Diff

View File

@ -1,110 +1,135 @@
class CCPInputWidgetEditorController extends HTMLElement{ class CCPInputWidgetEditorController extends HTMLElement {
#input = null #input = null
#index = null #index = null
#type = null #type = null
#messages = {
"en": {
"input_id_help": "The Id of the input. This has to be unique accross all the inputs the method. This is used as variable expansion in scripts.",
"input_delete_help": "Delete this input",
"title": "Title",
"title_help": "Title of the input. This is how the input will appear in forms.",
"description": "Description",
"description_help": "A description for this input",
"min_occurs": "Min. count",
"max_occurs": "Max. count",
"min_occurs_help": "Minimum cardinality of this input",
"max_occurs_help": "Maximum cardinality of this input",
"type": "Type",
"type_help": "Set the type of the input. Either String or Enumeration",
"mime": "Mime",
"mime_help": "Set MIME type of expected input",
"format": "Format",
"format_help": "Set specific format to tune the widget that will be used in forms",
"readonly": "Read only",
"readonly_help": "If enabled this input will not be editable in forms",
"string": "String",
"enum": "Enumerated",
"options": "Options",
"options_help": "A comma separated list of options for this enumerated input",
"options_ext_help": "Comma separated list of options",
"choice": "Single choice",
"multichoice": "Multiple choices",
"default": "Default value",
"default_help": "The default value applied for this input when nothing is set explicitly"
}
}
#delete_icon = ` #delete_icon = `
<svg style="width:24px;height:24px;pointer-events: none;" viewBox="0 0 24 24"> <svg style="width:24px;height:24px;pointer-events: none;" 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 d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" />
</svg> </svg>
` `
constructor(){ constructor() {
super(); super();
}
connectedCallback(){
}
get index(){
return this.#index
}
computeDefaultInputType(){
if(this.#input.schema.format === "secret"){
return "password"
}else if(this.#input.schema.format === "date"){
return "date"
}else if(this.#input.schema.format === "time"){
return "time"
}else if(this.#input.schema.format === "dateTime"){
return "datetime-local"
}
return "text"
}
isSelectedFormat(fmt){
return this.#input.schema.format === fmt
}
renderDefaultByType(){
if(this.#input.schema.format === "code"){
return `
<textarea rows="5" name="default" class="form-control" placeholder="default">${this.#input.schema.default}</textarea>
`
} else {
return `
<input type="${this.computeDefaultInputType()}" value="${this.#input.schema.default}" name="default" class="form-control" placeholder="default"/>
`
}
} }
renderDeleteButton(){ connectedCallback() {
return `<button data-index="${this.#index}" name="delete-input" title="Delete" class="btn btn-danger ccp-toolbar-button">
}
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]
}
get index() {
return this.#index
}
isSelectedFormat(fmt) {
return this.#input.schema.format === fmt
}
renderDefaultByType() {
return `<d4s-ccp-input name="default" input="${btoa(JSON.stringify(this.#input))}"></d4s-ccp-input>`
}
renderDeleteButton() {
return `<button data-index="${this.#index}" name="delete-input" title="${this.getLabel("input_delete_help")}" class="btn btn-danger ccp-toolbar-button">
${this.#delete_icon} ${this.#delete_icon}
</button>` </button>`
} }
render(input, i, reopen){ render(input, i, reopen) {
this.#index = i this.#index = i
this.#input = input this.#input = input
this.#type = input.schema.enum ? "enum" : "string" this.#type = input.schema.enum ? "enum" : "string"
const minOccurs = input.minOccurs = Number(input.minOccurs) ? Number(input.minOccurs) : 0
const maxOccurs = input.maxOccurs = Number(input.maxOccurs) ? Number(input.maxOccurs) : 0
this.innerHTML = ` this.innerHTML = `
<details ${ reopen ? 'open' : ''}> <details ${reopen ? 'open' : ''}>
<summary class="mb-3"> <style>
<input class="form-control" style="width:auto;display:inline" required="required" name="id" value="${input.id}" title="Id of input" ${ input.id === 'ccpimage' ? 'readonly' : ''}/> details:not([open]) p[name=static_default] {
${ input.id !== 'ccpimage' ? this.renderDeleteButton() : ''} display: block !important;
}
</style>
<summary class="mb-3" style="position:relative">
<input class="form-control" style="width:auto;display:inline" required="required" name="id" value="${input.id}" title="${this.getLabel("input_id_help")}" ${input.id === 'ccpimage' ? 'readonly' : ''}/>
${input.id !== 'ccpimage' ? this.renderDeleteButton() : ''}
<p title="${input.schema.format === "secret" ? "*****" : input.schema.default}" name="static_default" class="m-1 px-2 d-none small text-truncate text-muted font-italic" style="user-select:none;max-width:30rem">${input.schema.format === "secret" ? "*****" : input.schema.default}</p>
</summary> </summary>
<div style="padding-left: 1rem;border-left: 1px solid gray;"> <div style="padding-left: 1rem;border-left: 1px solid gray;">
<div class="row mb-3"> <div class="row mb-3">
<div class="col"> <div class="col">
<div class="form-field" title="Title"> <div class="form-field" title="${this.getLabel("title_help")}">
<input name="title" class="form-control" placeholder="title" value="${input.title}" required="required" ${ input.id === 'ccpimage' ? 'readonly' : ''}/> <input name="title" class="form-control" placeholder="${this.getLabel("title")}" value="${input.title}" required="required" ${input.id === 'ccpimage' ? 'readonly' : ''}/>
</div> </div>
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<div class="form-field" title="description"> <div class="form-field" title="${this.getLabel("description_help")}">
<input name="description" class="form-control" placeholder="description" value="${input.description}" required="required" ${ input.id === 'ccpimage' ? 'readonly' : ''}/> <textarea rows="2" name="description" class="form-control" placeholder="${this.getLabel("description")}" required="required" ${input.id === 'ccpimage' ? 'readonly' : ''}>${input.description}</textarea>
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-3"> <div class="col-3">
<div class="form-field" title="Type"> <div class="form-field" title="${this.getLabel("type_help")}">
<label>Type</label> <label>${this.getLabel("type")}</label>
<select name="type" class="form-control" placeholder="type" value="${this.#type}" ${ input.id === 'ccpimage' ? 'readonly' : ''}> <select name="type" class="form-control" placeholder="${this.getLabel("type")}" value="${this.#type}">
<option value="string" ${this.#type === "string" ? "selected" : ""}>String</option> <option value="string" ${this.#type === "string" ? "selected" : ""}>${this.getLabel("string")}</option>
<option value="enum" ${this.#type === "enum" ? "selected" : ""}>Enum</option> ${input.id === 'ccpimage' ? '' : `<option value="enum" ${this.#type === "enum" ? "selected" : ""}>${this.getLabel("enum")}</option>`}
</select> </select>
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="form-field" title="Minimum occurrences"> <div class="form-field" title="${this.getLabel("min_occurs_help")}">
<label>Min occurs</label> <label>${this.getLabel("min_occurs")}</label>
<input value="${input.minOccurs}" type="number" min="0" step="1" name="minOccurs" value="${input.minOccurs}" required="required" class="form-control" placeholder="minOccurs" ${ input.id === 'ccpimage' ? 'readonly' : ''}/> <input value="${minOccurs}" type="number" min="0" step="1" name="minOccurs" value="${minOccurs}" required="required" class="form-control" placeholder="${this.getLabel("min_occurs")}" ${input.id === 'ccpimage' ? 'readonly' : ''}/>
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="form-field" title="Maximum occurrences"> <div class="form-field" title="${this.getLabel("max_occurs_help")}">
<label>Max occurs</label> <label>${this.getLabel("max_occurs")}</label>
<input value="${input.maxOccurs}" type="number" min="0" step="1" name="maxOccurs" value="${input.maxOccurs}" required="required" class="form-control" placeholder="maxOccurs" ${ input.id === 'ccpimage' ? 'readonly' : ''}/> <input value="${maxOccurs}" type="number" min="0" step="1" name="maxOccurs" value="${maxOccurs}" required="required" class="form-control" placeholder="${this.getLabel("max_occurs")}" ${input.id === 'ccpimage' ? 'readonly' : ''}/>
</div> </div>
</div> </div>
<div class="col" title="Read only"> <div class="col" title="${this.getLabel("readonly_help")}">
<label>Read only</label> <label>${this.getLabel("readonly")}</label>
<div class="form-field"> <div class="form-field">
<input type="checkbox" ${input.schema.readOnly ? 'checked' : ''} name="readonly" class="form-check-input"> <input type="checkbox" ${input.schema.readOnly ? 'checked' : ''} name="readonly" class="form-check-input">
</div> </div>
@ -112,124 +137,205 @@ class CCPInputWidgetEditorController extends HTMLElement{
</div> </div>
<div name="string-input" class="${this.#type === 'enum' ? 'd-none' : ''} row mb-3"> <div name="string-input" class="${this.#type === 'enum' ? 'd-none' : ''} row mb-3">
<div class="col-3"> <div class="col-3">
<div class="form-field" title="Format"> <div class="form-field" title="${this.getLabel("format_help")}">
<label>Format</label> <label>${this.getLabel("format")}</label>
<select value="${input.schema.format}" name="format" class="form-control" ${ input.id === 'ccpimage' ? 'readonly' : ''}> <select value="${input.schema.format}" name="format" class="form-control" ${input.id === 'ccpimage' ? 'readonly' : ''}>
<option value="none" ${this.isSelectedFormat('none') ? "selected" : ""}>None</option> <option value="none" ${this.isSelectedFormat('none') ? "selected" : ""}>None</option>
<option value="date" ${this.isSelectedFormat('date') ? "selected" : ""}>Date</option> <option value="date" ${this.isSelectedFormat('date') ? "selected" : ""}>Date</option>
<option value="time" ${this.isSelectedFormat('time') ? "selected" : ""}>Time</option> <option value="time" ${this.isSelectedFormat('time') ? "selected" : ""}>Time</option>
<option value="dateTime" ${this.isSelectedFormat('dateTime') ? "selected" : ""}>Date time</option> <option value="dateTime" ${this.isSelectedFormat('dateTime') ? "selected" : ""}>Date time</option>
<option value="number" ${this.isSelectedFormat('number') ? "selected" : ""}>Number</option> <option value="number" ${this.isSelectedFormat('number') ? "selected" : ""}>Integer Number</option>
<option value="boolean" ${this.isSelectedFormat('boolean') ? "selected" : ""}>True/False</option> <option value="boolean" ${this.isSelectedFormat('boolean') ? "selected" : ""}>True/False</option>
<option value="code" ${this.isSelectedFormat('code') ? "selected" : ""}>Code</option> <option value="code" ${this.isSelectedFormat('code') ? "selected" : ""}>Code</option>
<option value="file" ${this.isSelectedFormat('file') ? "selected" : ""}>File</option> <option value="file" ${this.isSelectedFormat('file') ? "selected" : ""}>File</option>
<option value="remotefile" ${this.isSelectedFormat('remotefile') ? "selected" : ""}>Remote file</option> <option value="remotefile" ${this.isSelectedFormat('remotefile') ? "selected" : ""}>Workspace file</option>
<option value="geo" ${this.isSelectedFormat('geo') ? "selected" : ""}>Geography</option>
<option value="secret" ${this.isSelectedFormat('secret') ? "selected" : ""}>Secret</option> <option value="secret" ${this.isSelectedFormat('secret') ? "selected" : ""}>Secret</option>
<option value="url" ${this.isSelectedFormat('url') ? "selected" : ""}>Url</option> <option value="url" ${this.isSelectedFormat('url') ? "selected" : ""}>Url</option>
</select> </select>
</div> </div>
</div> </div>
<div class="col" title="Mime type"> <div class="col" title="${this.getLabel("mime_help")}">
<label>Mime</label> <label>${this.getLabel("mime")}</label>
<div class="form-field"> <div class="form-field">
<input value="${input.schema.contentMediaType}" name="contentMediaType" class="form-control" placeholder="mime" ${ input.id === 'ccpimage' ? 'readonly' : ''}/> <input value="${input.schema.contentMediaType}" name="contentMediaType" class="form-control" placeholder="mime" ${input.id === 'ccpimage' ? 'readonly' : ''}/>
</div> </div>
</div> </div>
</div> </div>
<div name="enum-input" class="${this.#type !== 'enum' ? 'd-none' : ''} mb-3"> <div name="enum-input" class="${this.#type !== 'enum' ? 'd-none' : ''} mb-3">
<div class="form-field" title="options"> <div class="form-field" title="${this.getLabel("options_help")}">
<input name="options" class="form-control" type="text" placeholder="option" value="${input.schema.enum ? input.schema.enum.join(',') : ''}"/> <input name="options" class="form-control" type="text" placeholder="${this.getLabel("options")}" value="${input.schema.enum ? input.schema.enum.join(',') : ''}"/>
<small class="form-text text-muted">Comma separated list of options</small> <small class="form-text text-muted">${this.getLabel("options_ext_help")}</small>
</div> </div>
<div class="form-field" title="Format"> <div class="form-field" title="${this.getLabel("format_help")}">
<label>Format</label> <label>${this.getLabel("format")}</label>
<select value="${input.schema.format}" name="format" class="form-control"> <select value="${input.schema.format}" name="format" class="form-control">
<option value="select" ${this.isSelectedFormat('select') ? "selected" : ""}>Choice</option> <option value="select" ${this.isSelectedFormat('select') ? "selected" : ""}>${this.getLabel("choice")}</option>
<option value="checklist" ${this.isSelectedFormat('checklist') ? "selected" : ""}>Multi Choice</option> <option value="checklist" ${this.isSelectedFormat('checklist') ? "selected" : ""}>${this.getLabel("multi_choice")}</option>
</select> </select>
</div> </div>
</div> </div>
<div name="input-default" class="mb-3"> <div name="input-default" class="mb-3">
<div class="form-field" title="Default value"> <label>${this.getLabel("default")}</label>
${this.renderDefaultByType()} <div class="form-field border border-info px-2 py-1" style="background-color:#0dcaf022" title="${this.getLabel("default_help")}">
<div name="default-container">${this.renderDefaultByType()}</div>
<span style="user-select:none;position:relative;top:-1.6rem;float:right;cursor:pointer" name="password_toggle" class="${this.isSelectedFormat('secret') ? 'inline' : 'd-none'}">&#128065;</span> <span style="user-select:none;position:relative;top:-1.6rem;float:right;cursor:pointer" name="password_toggle" class="${this.isSelectedFormat('secret') ? 'inline' : 'd-none'}">&#128065;</span>
</div> </div>
</div> </div>
</div> </div>
</details> </details>
` `
this.addEventListener("click", ev=>{ this.addEventListener("click", ev => {
const ename = ev.target.getAttribute("name") const ename = ev.target.getAttribute("name")
if(ename === "password_toggle"){ if (ename === "password_toggle") {
const w = this.querySelector("div[name=input-default] input[name=default]") const w = this.querySelector("div[name=input-default] input[name=default]")
w.type = (w.type === "password" ? "" : "password") w.type = (w.type === "password" ? "" : "password")
ev.preventDefault() ev.preventDefault()
} }
}) })
this.addEventListener("input", ev=>{ const defaultinp = this.querySelector("div[name='input-default']")
defaultinp.addEventListener("input", ev => {
const inp = this.querySelector("d4s-ccp-input")
this.#input.schema.default = inp.value
const p = this.querySelector("p[name=static_default]")
p.innerHTML = p.title = inp.value
})
defaultinp.addEventListener("change", ev => {
const inp = this.querySelector("d4s-ccp-input")
this.#input.schema.default = inp.value
const p = this.querySelector("p[name=static_default]")
p.innerHTML = p.title = inp.value
})
defaultinp.addEventListener("click", ev => {
const src = ev.target.getAttribute("name")
if (src === "plus" || src === "minus") {
const inp = this.querySelector("d4s-ccp-input")
this.#input.schema.default = inp.value
const p = this.querySelector("p[name=static_default]")
p.innerHTML = p.title = inp.value
}
})
this.addEventListener("input", ev => {
const val = ev.target.value const val = ev.target.value
const ename = ev.target.getAttribute("name") const ename = ev.target.getAttribute("name")
if(ename === "id"){ const display = this.querySelector("div[name='default-container']")
if (ename === "id") {
this.#input.id = val this.#input.id = val
} }
else if(ename === "title"){ else if (ename === "title") {
this.#input.title = val this.#input.title = val
display.innerHTML = this.renderDefaultByType()
} }
else if(ename === "description"){ else if (ename === "description") {
this.#input.description = val this.#input.description = val
display.innerHTML = this.renderDefaultByType()
} }
else if(ename === "minOccurs"){ else if (ename === "minOccurs") {
this.#input.minOccurs = val this.#input.minOccurs = Number(val) ? Number(val) : 0
display.innerHTML = this.renderDefaultByType()
} }
else if(ename === "maxOccurs"){ else if (ename === "maxOccurs") {
this.#input.maxOccurs = val this.#input.maxOccurs = Number(val) ? Number(val) : 0
display.innerHTML = this.renderDefaultByType()
} }
else if(ename === "format"){ else if (ename === "format") {
this.#input.schema.default = this.formatConversion(this.#input.schema.format, this.#input.schema.default, val)
this.#input.schema.format = val this.#input.schema.format = val
//this.render(this.#input, this.#index, true) display.innerHTML = this.renderDefaultByType()
/*this.querySelector("div[name=input-default] span[name=password_toggle]").classList.add("d-none")
this.querySelector("div[name=input-default] input[name=default]").type = ""
if(this.#input.schema.format === "secret"){
this.querySelector("div[name=input-default] input[name=default]").type = "password"
this.querySelector("div[name=input-default] span[name=password_toggle]").classList.remove("d-none")
}else if(this.#input.schema.format === "date"){
this.querySelector("div[name=input-default] input[name=default]").type = "date"
}else if(this.#input.schema.format === "time"){
this.querySelector("div[name=input-default] input[name=default]").type = "time"
}else if(this.#input.schema.format === "dateTime"){
this.querySelector("div[name=input-default] input[name=default]").type = "dateTime"
}else if(this.#input.schema.format === "file"){
this.querySelector("div[name=input-default] input[name=default]").type = "file"
}*/
} }
else if(ename === "contentMediaType"){ else if (ename === "contentMediaType") {
this.#input.schema.contentMediaType = val this.#input.schema.contentMediaType = val
display.innerHTML = this.renderDefaultByType()
} }
else if(ename === "options"){ else if (ename === "options") {
this.#input.schema.enum = val.split(",") this.#input.schema.enum = val.split(",")
if (this.#input.schema.enum.indexOf(this.#input.schema.default) === -1) {
this.#input.schema.default = this.#input.schema.enum[0]
}
display.innerHTML = this.renderDefaultByType()
} }
else if(ename === "default"){ else if (ename === "readonly") {
this.#input.schema.default = val
}
else if(ename === "readonly"){
this.#input.schema.readOnly = ev.target.checked this.#input.schema.readOnly = ev.target.checked
display.innerHTML = this.renderDefaultByType()
} }
else if(ename === "type"){ else if (ename === "type") {
this.#type = ev.target.value this.#type = ev.target.value
if(this.#type === "enum"){ if (this.#type === "enum") {
this.querySelector("div[name=string-input]").classList.add("d-none") this.querySelector("div[name=string-input]").classList.add("d-none")
this.querySelector("div[name=enum-input]").classList.remove("d-none") this.querySelector("div[name=enum-input]").classList.remove("d-none")
this.querySelector("div[name=enum-input] select[name=format]").value = "select"
this.#input.schema.format = "select"
this.#input.schema.enum = this.querySelector("input[name=options]").value.split(",") this.#input.schema.enum = this.querySelector("input[name=options]").value.split(",")
}else if(this.#type === "string"){ if (this.#input.schema.enum.indexOf(this.#input.schema.default) === -1) {
this.#input.schema.default = this.#input.schema.enum[0]
}
} else if (this.#type === "string") {
this.querySelector("div[name=enum-input]").classList.add("d-none") this.querySelector("div[name=enum-input]").classList.add("d-none")
this.querySelector("div[name=string-input]").classList.remove("d-none") this.querySelector("div[name=string-input]").classList.remove("d-none")
this.#input.schema.format = "none"
delete this.#input.schema['enum'] delete this.#input.schema['enum']
} }
} display.innerHTML = this.renderDefaultByType()
}
}) })
} }
boolean2number(prev){
if(Array.isArray(prev)){
return prev.map(v=>(v === "true" || v === true ? "1" : "0"))
} else return prev === "true" || prev === true ? "1" : "0"
}
none2number(prev){
if(Array.isArray(prev)){
return prev.map(v=>(isNaN(Number(v)) ? "0" : v))
} else return isNaN(Number(prev)) ? "0" : prev
}
number2boolean(prev){
if(Array.isArray(prev)){
return prev.map(v=>(Number(v) !== 0 ? "true" : "false"))
} else return Number(prev) !== 0 ? "true" : "false"
}
none2boolean(prev){
if(Array.isArray(prev)){
return prev.map(v=>(["true", "false"].indexOf(v.toLowerCase()) !== -1 ? v.toLowerCase() : "false"))
} else return ["true", "false"].indexOf(prev.toLowerCase()) !== -1 ? prev.toLowerCase() : "false"
}
any2boolean(prev){
if(Array.isArray(prev)){
return prev.map(v=>String(!!v))
} else return String(!!v);
}
any2empty(prev){
if(Array.isArray(prev)){
return prev.map(v=>"")
} else return "";
}
formatConversion(prevformat, prevvalue, format){
if(format === "none"){
return prevvalue;
}
if(format === "number"){
if(prevformat === "boolean") return this.boolean2number(prevvalue);
if(!prevformat || prevformat === "none") return this.none2number(prevvalue);
return "0";
}
if(format === "boolean"){
if(prevformat === "number") return this.number2boolean(prevvalue)
if(!prevformat || prevformat === "none") return this.none2boolean(prevvalue)
return this.any2boolean(prevvalue)
}
return this.any2empty(prevvalue)
}
} }
window.customElements.define('d4s-ccp-input-editor', CCPInputWidgetEditorController); window.customElements.define('d4s-ccp-input-editor', CCPInputWidgetEditorController);

File diff suppressed because it is too large Load Diff

View File

@ -4,29 +4,47 @@ class CCPMethodList extends HTMLElement{
#rootdoc; #rootdoc;
#data = null; #data = null;
#filtered; #filtered;
#dragged = null;
#searchfield = null; #searchfield = null;
#allowedit = false; #allowedit = false;
#allowexecute = false; #allowexecute = false;
#archive = false; #archive = false;
#fileupload = null; //#fileupload = null;
#archiveupload = null; //#archiveupload = null;
#serviceurl; #serviceurl;
#messages = {
"en" : {
"Methods" : "Methods",
"refresh_help" : "Refresh the list of available methods",
"Search" : "Search",
"count_executable_help" : "Number of executable methods",
"count_not_executable_help" : "Number of non executable methods",
"export_category_help" : "Export all the methods in this category as JSON files",
"export_method_help" : "Export this method as JSON file",
"archive_method_help" : "Export this method as JSON file to your workspace",
"shared_help" : "This method is shared in this community",
"share_help" : "You can share this method in this community",
"execute_help" : "Open method's form to set inputs and execute",
"edit_help" : "Open method's form for cloning, deriving or editing"
}
}
constructor(){ constructor(){
super() super()
this.#boot = document.querySelector("d4s-boot-2") this.#boot = document.querySelector("d4s-boot-2")
this.#serviceurl = this.getAttribute("serviceurl")
this.#rootdoc = this.attachShadow({ "mode" : "open"}) this.#rootdoc = this.attachShadow({ "mode" : "open"})
this.#archive = this.getAttribute("archive") this.#archive = this.getAttribute("archive")
this.render()
this.fetchProcesses()
} }
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]
}
render(){ render(){
this.#rootdoc.innerHTML = ` this.#rootdoc.innerHTML = `
<link rel="stylesheet" href="https://cdn.dev.d4science.org/ccp/css/common.css"></link> <link rel="stylesheet" href="https://cdn.cloud.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"> <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> <style>
.ccp-process-category-list{ .ccp-process-category-list{
@ -50,9 +68,9 @@ class CCPMethodList extends HTMLElement{
<summary> <summary>
<h5 class="d-inline mr-2 text-primary"></h5> <h5 class="d-inline mr-2 text-primary"></h5>
<div class="float-right d-flex" style="gap:2px"> <div class="float-right d-flex" style="gap:2px">
<span name="count_notexecutables" title="Number of non executable methods" class="badge border border-danger text-danger"></span> <span name="count_notexecutables" title="${this.getLabel('count_not_executable_help')}" class="badge border border-danger text-danger"></span>
<span name="count_executables" title="Number of executable methods" class="badge border border-success text-success"></span> <span name="count_executables" title="${this.getLabel('count_executable_help')}" class="badge border border-success text-success"></span>
<button name="export_category" title="Export whole category" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small"> <button name="export_category" title="${this.getLabel('export_category_help')}" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<svg viewBox="0 0 24 24"><g><rect fill="none" height="24" width="24"/></g><g><path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"/></g></svg> <svg viewBox="0 0 24 24"><g><rect fill="none" height="24" width="24"/></g><g><path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"/></g></svg>
</button> </button>
</div> </div>
@ -64,31 +82,31 @@ class CCPMethodList extends HTMLElement{
<span name="version" class="badge badge-primary"></span> <span name="version" class="badge badge-primary"></span>
<span name="author" class="badge badge-warning"></span> <span name="author" class="badge badge-warning"></span>
<div class="float-right d-flex" style="gap:3px"> <div class="float-right d-flex" style="gap:3px">
<button name="executable" title="Execute" class="btn btn-success ccp-toolbar-button ccp-toolbar-button-small"> <button name="executable" title="${this.getLabel("execute_help")}" class="btn btn-success ccp-toolbar-button ccp-toolbar-button-small">
<svg viewBox="0 0 48 48" style="fill:white; stroke:white"> <svg viewBox="0 0 48 48" style="fill:white; stroke:white">
<path d="M10,10 v28 L38,24 Z"/> <path d="M10,10 v28 L38,24 Z"/>
</svg> </svg>
</button> </button>
<button name="export_method" title="Export this version" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small"> <button name="export_method" title="${this.getLabel("export_method_help")}" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<svg viewBox="0 0 24 24"><g><rect fill="none"/></g><g><path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"/></g></svg> <svg viewBox="0 0 24 24"><g><rect fill="none"/></g><g><path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"/></g></svg>
</button> </button>
<button name="edit" title="Edit" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small"> <button name="edit" title="${this.getLabel("edit_help")}" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small">
<svg viewBox="0 0 48 48"> <svg viewBox="0 0 48 48">
<path d="M9 39h2.2l22.15-22.15-2.2-2.2L9 36.8Zm30.7-24.3-6.4-6.4 2.1-2.1q.85-.85 2.1-.85t2.1.85l2.2 2.2q.85.85.85 2.1t-.85 2.1Zm-2.1 2.1L12.4 42H6v-6.4l25.2-25.2Zm-5.35-1.05-1.1-1.1 2.2 2.2Z"/> <path d="M9 39h2.2l22.15-22.15-2.2-2.2L9 36.8Zm30.7-24.3-6.4-6.4 2.1-2.1q.85-.85 2.1-.85t2.1.85l2.2 2.2q.85.85.85 2.1t-.85 2.1Zm-2.1 2.1L12.4 42H6v-6.4l25.2-25.2Zm-5.35-1.05-1.1-1.1 2.2 2.2Z"/>
</svg> </svg>
</button> </button>
${ this.#archive ? ` ${ this.#archive ? `
<button data-index="0" name="archive" title="Archive to workspace" class="btn btn-primary ccp-toolbar-button ccp-toolbar-button-small"> <button data-index="0" name="archive" title="${this.getLabel("archive_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> <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>`
: `` : ``
} }
<button data-index="0" name="publish" title="Share with vlab" class="btn btn-warning ccp-toolbar-button ccp-toolbar-button-small"> <button data-index="0" name="publish" title="${this.getLabel("share_help")}" class="btn btn-warning ccp-toolbar-button ccp-toolbar-button-small">
<svg style="fill:black" viewBox="0 -960 960 960"><path d="M727-80q-47.5 0-80.75-33.346Q613-146.693 613-194.331q0-6.669 1.5-16.312T619-228L316-404q-15 17-37 27.5T234-366q-47.5 0-80.75-33.25T120-480q0-47.5 33.25-80.75T234-594q23 0 44 9t38 26l303-174q-3-7.071-4.5-15.911Q613-757.75 613-766q0-47.5 33.25-80.75T727-880q47.5 0 80.75 33.25T841-766q0 47.5-33.25 80.75T727-652q-23.354 0-44.677-7.5T646-684L343-516q2 8 3.5 18.5t1.5 17.741q0 7.242-1.5 15Q345-457 343-449l303 172q15-14 35-22.5t46-8.5q47.5 0 80.75 33.25T841-194q0 47.5-33.25 80.75T727-80Zm.035-632Q750-712 765.5-727.535q15.5-15.535 15.5-38.5T765.465-804.5q-15.535-15.5-38.5-15.5T688.5-804.465q-15.5 15.535-15.5 38.5t15.535 38.465q15.535 15.5 38.5 15.5Zm-493 286Q257-426 272.5-441.535q15.5-15.535 15.5-38.5T272.465-518.5q-15.535-15.5-38.5-15.5T195.5-518.465q-15.5 15.535-15.5 38.5t15.535 38.465q15.535 15.5 38.5 15.5Zm493 286Q750-140 765.5-155.535q15.5-15.535 15.5-38.5T765.465-232.5q-15.535-15.5-38.5-15.5T688.5-232.465q-15.5 15.535-15.5 38.5t15.535 38.465q15.535 15.5 38.5 15.5ZM727-766ZM234-480Zm493 286Z"/></svg> <svg style="fill:black" viewBox="0 -960 960 960"><path d="M727-80q-47.5 0-80.75-33.346Q613-146.693 613-194.331q0-6.669 1.5-16.312T619-228L316-404q-15 17-37 27.5T234-366q-47.5 0-80.75-33.25T120-480q0-47.5 33.25-80.75T234-594q23 0 44 9t38 26l303-174q-3-7.071-4.5-15.911Q613-757.75 613-766q0-47.5 33.25-80.75T727-880q47.5 0 80.75 33.25T841-766q0 47.5-33.25 80.75T727-652q-23.354 0-44.677-7.5T646-684L343-516q2 8 3.5 18.5t1.5 17.741q0 7.242-1.5 15Q345-457 343-449l303 172q15-14 35-22.5t46-8.5q47.5 0 80.75 33.25T841-194q0 47.5-33.25 80.75T727-80Zm.035-632Q750-712 765.5-727.535q15.5-15.535 15.5-38.5T765.465-804.5q-15.535-15.5-38.5-15.5T688.5-804.465q-15.5 15.535-15.5 38.5t15.535 38.465q15.535 15.5 38.5 15.5Zm-493 286Q257-426 272.5-441.535q15.5-15.535 15.5-38.5T272.465-518.5q-15.535-15.5-38.5-15.5T195.5-518.465q-15.5 15.535-15.5 38.5t15.535 38.465q15.535 15.5 38.5 15.5Zm493 286Q750-140 765.5-155.535q15.5-15.535 15.5-38.5T765.465-232.5q-15.535-15.5-38.5-15.5T688.5-232.465q-15.5 15.535-15.5 38.5t15.535 38.465q15.535 15.5 38.5 15.5ZM727-766ZM234-480Zm493 286Z"/></svg>
</button> </button>
</div> </div>
</div> </div>
<p class="m-0 p-0 small" name="description"></p> <div class="m-0 p-0 small" name="description"></div>
<div> <div>
<span name="keyword" class="badge badge-pill badge-light border border-dark mr-1" style="opacity:.6"></span> <span name="keyword" class="badge badge-pill badge-light border border-dark mr-1" style="opacity:.6"></span>
</div> </div>
@ -105,13 +123,13 @@ class CCPMethodList extends HTMLElement{
<div class="card-header"> <div class="card-header">
<div class="ccp-toolbar-header d-flex flex-wrap justify-content-between"> <div class="ccp-toolbar-header d-flex flex-wrap justify-content-between">
<div> <div>
<span name="header">Methods</span> <span name="header">${this.getLabel("Methods")}</span>
</div> </div>
<div class="d-flex flex-wrap" style="gap:2px"> <div class="d-flex flex-wrap" style="gap:2px">
<button name="refresh" class="btn btn-primary ccp-toolbar-button" title="Refresh"> <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> <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> </button>
<label name="fileupload" class="btn btn-primary ccp-toolbar-button m-0" title="Upload from file"> <!--label name="fileupload" class="btn btn-primary ccp-toolbar-button m-0" title="Upload from file">
<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> <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"/> <input type="file" class="d-none" multiple="multiple"/>
</label> </label>
@ -120,13 +138,13 @@ class CCPMethodList extends HTMLElement{
<button name="archive" class="btn btn-primary ccp-toolbar-button m-0" title="Upload from link"> <button name="archive" class="btn btn-primary ccp-toolbar-button m-0" title="Upload from link">
<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> <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> </button>
</div> </div-->
</div> </div>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="mb-3"> <div class="mb-3">
<input type="text" name="search" class="form-control" placeholder="Search"/> <input type="text" name="search" class="form-control" placeholder="${this.getLabel("Search")}"/>
</div> </div>
<div> <div>
<ul name="process_category_list"></ul> <ul name="process_category_list"></ul>
@ -144,29 +162,32 @@ class CCPMethodList extends HTMLElement{
this.updateList() this.updateList()
}) })
this.#fileupload = this.#rootdoc.querySelector("label[name=fileupload] > input[type=file]") // this.#fileupload = this.#rootdoc.querySelector("label[name=fileupload] > input[type=file]")
this.#fileupload.addEventListener("change", ev=>{ // this.#fileupload.addEventListener("change", ev=>{
const filelist = ev.target.files; // const filelist = ev.target.files;
if (filelist.length > 0) { // if (filelist.length > 0) {
const files = Array.prototype.slice.call(filelist) // const files = Array.prototype.slice.call(filelist)
this.importMethods(files) // this.importMethods(files)
} // }
}) // })
this.#archiveupload = this.#rootdoc.querySelector("button[name=archive]") // this.#archiveupload = this.#rootdoc.querySelector("button[name=archive]")
this.#archiveupload.addEventListener("click", ev=>{ // this.#archiveupload.addEventListener("click", ev=>{
const link = ev.target.parentElement.querySelector("input").value // const link = ev.target.parentElement.querySelector("input").value
if(link){ // if(link){
if(confirm("Please confirm importing of method from link?")){ // if(confirm("Please confirm importing of method from link?")){
this.fromArchive(link) // this.fromArchive(link)
} // }
} // }
}) // })
} }
connectedCallback(){ connectedCallback(){
this.#allowedit = this.getAttribute("allow-edit") === "true" this.#allowedit = this.getAttribute("allow-edit") === "true"
this.#allowexecute = this.getAttribute("allow-execute") === "true" this.#allowexecute = this.getAttribute("allow-execute") === "true"
this.#serviceurl = this.getAttribute("serviceurl")
this.render()
this.fetchProcesses()
} }
fetchProcesses(){ fetchProcesses(){
@ -220,9 +241,9 @@ class CCPMethodList extends HTMLElement{
const f = filter.toLowerCase() const f = filter.toLowerCase()
this.#filtered = this.#data.filter(d=>{ this.#filtered = this.#data.filter(d=>{
return false || return false ||
(d.title.toLowerCase().indexOf(f) !== -1)|| (d.title && d.title.toLowerCase().indexOf(f) !== -1)||
(d.description.indexOf(f) !== -1) || (d.description && d.description.indexOf(f) !== -1) ||
(d.keywords.map(k=>k.toLowerCase()).filter(i=>i.indexOf(f) !== -1)).length (Array.isArray(d.keywords) && d.keywords.map(k=>k.toLowerCase()).filter(i=>i.indexOf(f) !== -1)).length
}) })
} }
this.groupBy() this.groupBy()
@ -457,6 +478,7 @@ class CCPMethodList extends HTMLElement{
span.classList.add("badge") span.classList.add("badge")
span.classList.add("badge-warning") span.classList.add("badge-warning")
span.textContent = "shared" span.textContent = "shared"
span.alt = span.title = this.getLabel("shared_help")
e.replaceWith(span) e.replaceWith(span)
}else if(!this.isAuthor(d)){ }else if(!this.isAuthor(d)){
e.parentElement.removeChild(e) e.parentElement.removeChild(e)
@ -479,9 +501,25 @@ class CCPMethodList extends HTMLElement{
apply : (e,d)=>{ e.alt = e.title = e.textContent = d.title } apply : (e,d)=>{ e.alt = e.title = e.textContent = d.title }
}, },
{ {
target : "p[name=description]", target : "div[name=description]",
apply : (e,d)=>{ e.textContent = d.description } apply : (e,d)=>{
}, const maxchars = 100
const maxlines = 2
const s = d.description
const lines = s.split("\n")
if (lines.length <= maxlines && s.length <= maxchars) {
e.innerHTML = s.replaceAll("\n", "<br/>")
} else if (lines.length > maxlines) {
const lines1 = lines.slice(0, maxlines -1)
const index = Math.min(maxchars, lines1.join("<br/>").length)
const sf = s.replaceAll("\n", "<br/>")
e.innerHTML = `<details><summary>${sf.substring(0, index)}...</summary>...${sf.substring(index)}</details>`
} else {
const sf = s.replaceAll("\n", "<br/>")
e.innerHTML = `<details><summary>${sf.substring(0, maxchars)}...</summary>...${sf.substring(maxchars)}</details>`
}
}
}
] ]
} }
] ]

View File

@ -4,7 +4,7 @@ class CCPOutputWidgetController extends HTMLElement {
constructor(){ constructor(){
super() super()
this.#output = JSON.parse(this.getAttribute("output")) this.#output = JSON.parse(atob(this.getAttribute("output")))
} }
connectedCallback(){ connectedCallback(){
@ -22,7 +22,7 @@ class CCPOutputWidgetController extends HTMLElement {
render(){ render(){
return ` return `
<div class="col form-check"> <div class="col form-check">
<input class="form-check-input" type="checkbox" name="this.#output.id" alt="${this.#output.description}" title="${this.#output.description}" checked="checked"/> <input class="form-check-input" type="checkbox" name="${this.#output.id}" alt="${this.#output.description}" title="${this.#output.description}" checked="checked"/>
<label class="form-check-label">${this.#output.title}</label> <label class="form-check-label">${this.#output.title}</label>
</div> </div>
` `

View File

@ -3,13 +3,32 @@ class CCPOutputWidgetEditorController extends HTMLElement {
#output = null #output = null
#index = null #index = null
#type = null #type = null
#messages = {
"en" : {
"output_id_help" : "The Id of the output. This has to be unique accross all the outputs the method",
"output_delete_help" : "Delete this output",
"output_title_help" : "Title of the output. This is how the output will appear in forms.",
"output_description_help" : "A description for this output",
"min_occurs_help" : "Minimum cardinality",
"max_occurs_help" : "Msximum cardinality",
"mimetype_help" : "Set MIME type of expected output",
"file_path" : "Path to file",
"file_path_help" : "Define the path to the file holding the output. This documents the location of the output in the zip archive returned by the execution. Paths in the execution runtime instead are infrastructure dependant."
}
}
#delete_icon = ` #delete_icon = `
<svg style="width:24px;height:24px;pointer-events: none;" viewBox="0 0 24 24"> <svg style="width:24px;height:24px;pointer-events: none;" 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 d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" />
</svg> </svg>
` `
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(){ constructor(){
super() super()
} }
@ -29,38 +48,38 @@ class CCPOutputWidgetEditorController extends HTMLElement {
this.innerHTML = ` this.innerHTML = `
<details> <details>
<summary class="mb-3"> <summary class="mb-3">
<input class="form-control" style="width:auto;display:inline" required="required" name="id" value="${this.#output.id}" title="Id of output"/> <input class="form-control" style="width:auto;display:inline" required="required" name="id" value="${this.#output.id}" title="${this.getLabel('output_id_help')}"/>
<button data-index="${this.#index}" name="delete-output" title="Delete" class="btn btn-danger ccp-toolbar-button"> <button data-index="${this.#index}" name="delete-output" title="${this.getLabel('output_delete_help')}" class="btn btn-danger ccp-toolbar-button">
${this.#delete_icon} ${this.#delete_icon}
</button> </button>
</summary> </summary>
<div style="padding-left: 1rem;border-left: 1px solid gray;"> <div style="padding-left: 1rem;border-left: 1px solid gray;">
<div class="row mb-3"> <div class="row mb-3">
<div class="col"> <div class="col">
<div class="form-field" title="Title"> <div class="form-field" title="${this.getLabel('output_title_help')}">
<input name="title" class="form-control" placeholder="title" value="${this.#output.title}" required="required"/> <input name="title" class="form-control" placeholder="title" value="${this.#output.title}" required="required"/>
</div> </div>
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<div class="form-field" title="Description"> <div class="form-field" title="${this.getLabel('output_description_help')}">
<input name="description" class="form-control" placeholder="description" value="${this.#output.description}" required="required"/> <textarea rows="1" name="description" class="form-control" placeholder="description" required="required">${this.#output.description}</textarea>
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3 d-none">
<div class="col"> <div class="col">
<div class="form-field" title="Mininum cardinality"> <div class="form-field" title="${this.getLabel("min_occurs_help")}">
<input value="${this.#output.minOccurs}" type="number" min="0" step="1" name="minOccurs" class="form-control" placeholder="minOccurs"/> <input value="${this.#output.minOccurs}" type="number" min="0" step="1" name="minOccurs" class="form-control" placeholder="minOccurs"/>
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="form-field" title="Maximum cardinality"> <div class="form-field" title="${this.getLabel("max_occurs_help")}">
<input value="${this.#output.maxOccurs}" type="number" min="0" step="1" name="maxOccurs" class="form-control" placeholder="maxOccurs"/> <input value="${this.#output.maxOccurs}" type="number" min="0" step="1" name="maxOccurs" class="form-control" placeholder="maxOccurs"/>
</div> </div>
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-3"> <div class="col d-none">
<div class="form-field" title="Type"> <div class="form-field" title="Type">
<select name="type" class="form-control" placeholder="type"> <select name="type" class="form-control" placeholder="type">
<option value="string">String</option> <option value="string">String</option>
@ -68,7 +87,7 @@ class CCPOutputWidgetEditorController extends HTMLElement {
</div> </div>
</div> </div>
<div class="col-3"> <div class="col-3">
<div class="form-field" title="Mime type"> <div class="form-field" title="${this.getLabel("mimetype_help")}">
<input value="${this.#output.schema.contentMediaType}" name="contentMediaType" class="form-control" placeholder="mime"/> <input value="${this.#output.schema.contentMediaType}" name="contentMediaType" class="form-control" placeholder="mime"/>
</div> </div>
</div> </div>
@ -110,8 +129,9 @@ class CCPOutputWidgetEditorController extends HTMLElement {
if(this.#output.metadata && this.#output.metadata.length > 0){ if(this.#output.metadata && this.#output.metadata.length > 0){
return ` return `
<div class="col"> <div class="col">
<div class="form-field" title="File path"> <div class="form-field" title="${this.getLabel("file_path_help")}">
<input value="${this.#output.metadata[0].href}" name="href" class="form-control" placeholder="file path"/>
<input value="${this.#output.metadata[0].href}" name="href" class="form-control" placeholder="${this.getLabel("file_path")}"/>
</div> </div>
</div> </div>
` `

View File

@ -217,6 +217,10 @@
} else { } else {
kc.enableLogging = false; kc.enableLogging = false;
} }
if (typeof initOptions.scope === 'string') {
kc.scope = initOptions.scope;
}
} }
if (!kc.responseMode) { if (!kc.responseMode) {
@ -438,16 +442,14 @@
baseUrl = kc.endpoints.authorize(); baseUrl = kc.endpoints.authorize();
} }
var scope; var scope = options && options.scope || kc.scope;
if (options && options.scope) { if (!scope) {
if (options.scope.indexOf("openid") != -1) { // if scope is not set, default to "openid"
scope = options.scope; scope = "openid";
} else { } else if (scope.indexOf("openid") === -1) {
scope = "openid " + options.scope; // if openid scope is missing, prefix the given scopes with it
} scope = "openid " + scope;
} else { }
scope = "openid";
}
var url = baseUrl var url = baseUrl
+ '?client_id=' + encodeURIComponent(kc.clientId) + '?client_id=' + encodeURIComponent(kc.clientId)

16
extapp/index.html Normal file
View File

@ -0,0 +1,16 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="js/rolescontroller.js"></script>
<script src="../common/js/keycloak.js" type="text/javascript"></script>
<script src="../boot/d4s-boot.js"></script>
</head>
<body>
<d4s-boot-2 context="%2Fgcube%2Fdevsec%2FCCP"
gateway="next.dev.d4science.org"
redirect-url="http://localhost:8080"
url="https://accounts.dev.d4science.org/auth">
</d4s-boot-2>
<d4s-extapp-role-manager groupid="8131621c-f896-4d41-81fd-3f6294bf1376" appid="076bfe7d-12a2-41d1-a720-05911f0ae527"></d4s-extapp-role-manager>
</body>
</html>

View File

@ -0,0 +1,246 @@
class ExtAppRoleController extends HTMLElement{
#boot;
#roles;
#users;
#serviceurl;
#appid;
#groupid;
#loading = false;
#header;
#section;
#userfilter = null;
#rolefilter = null;
constructor(){
super()
this.attachShadow({ mode: "open" });
}
connectedCallback(){
this.#appid = this.getAttribute("appid")
this.#groupid = this.getAttribute("groupid")
this.#boot = document.querySelector("d4s-boot-2")
this.#serviceurl = this.#boot.url
if(!this.#loading){
this.#loading = true
this.loadData()
}
this.renderStructure()
}
renderStructure(){
this.shadowRoot.innerHTML = `
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"/>
<style>
.dyn-opacity{
opacity: .8;
transition: opacity .5s;
}
.dyn-opacity:hover{
opacity: 1;
}
</style>
<header></header>
<section></section>
`
this.#header = this.shadowRoot.querySelector("header")
this.#section = this.shadowRoot.querySelector("section")
}
renderHeader(){
this.#header.innerHTML = `
<div class="row g-0 mb-3" style="gap: 1rem;align-items:baseline;">
<input class="col-3 form-control" id="search_user_filter" placeholder="Search by name" style="min-width: 10rem;max-width: 100rem;width: 30rem">
<div class="role-filter col-9 d-flex" style="gap:1rem;">
${
this.#roles.map(r=>{
return r.name === 'uma_protection' ? '' : `
<div class="form-check form-switch">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" name="${r.id}">
<span>${r.name}</span>
</label>
</div>
`
}).join("")
}
</div>
</div>`
this.attachHeaderEvents()
}
renderUsers(){
var users = this.#rolefilter && this.#rolefilter.length ?
this.#users.filter(u=>{
const assigned = u.mappings.map(m=>m.id)
return this.#rolefilter.reduce((a,f)=>{
return a && (assigned.indexOf(f) !== -1)
}, true)
}) : this.#users
users = this.#userfilter ?
this.#users.filter(u=>{
return u.username.toLowerCase().startsWith(this.#userfilter) ||
u.lastName.toLowerCase().startsWith(this.#userfilter) ||
u.firstName.toLowerCase().startsWith(this.#userfilter) ||
u.email.toLowerCase().startsWith(this.#userfilter)}) : users
const html = `
<ul name="users" style="list-style:none" class="d-flex flex-row flex-wrap gap-2 p-0">
${users.map( u=>{
return `
<li name="user" data-userid="${u.id}" style="user-select:none">
<div class="card" style="width:20rem;height:10rem">
<div class="card-header">
<span name="name" class="fw-bold">${u.firstName} ${u.lastName}</span>
</div>
<div class="card-body d-flex" style="overflow-y:auto;padding:.5rem">
<ul name="roles" style="list-style:none;min-width:70%;overflow-y:auto;align-items: flex-start" class="d-flex flex-row flex-wrap gap-1 pt-1 pb-1 ps-0 me-2">
${this.renderMappings(u)}
</ul>
</divExtAppRoleController>
</div>
</li>`
}).join('')}
</li>
</ul>
`
this.#section.innerHTML = html
this.attachSectionEvents()
}
renderMappings(u){
const html = `
<ul name="roles" style="list-style:none;min-width:70%;overflow-y:auto" class="d-flex flex-row flex-wrap gap-1 pt-1 pb-1 ps-0 me-2">
${this.#roles ? this.#roles.map(r=>{
const assigned = this.isAssigned(u, r)
return r.name !== 'uma_protection' ? `
<li data-userid="${u.id}" data-roleid="${r.id}" name="role" class="dyn-opacity btn m-0 p-0 ${assigned ? 'active' : ''}">
<span style="pointer-events: none" name="name" class="badge bg-${assigned ? 'success' : 'secondary'}">${r.name}</span>
</li>` : ''
}).join('') : ''}
</ul>
`
return html
}
isAssigned(u, r){
return u.mappings && u.mappings.filter(m=>m.id === r.id).length > 0
}
attachSectionEvents(){
this.#section.querySelector("ul[name=users]").addEventListener("click", ev=>{
const tgt = ev.target
if(tgt.getAttribute("name") === "role"){
const uid = tgt.getAttribute("data-userid")
const rid = tgt.getAttribute("data-roleid")
if(tgt.classList.contains("active")){
this.toggleMapping(uid, rid)
}else{
this.toggleMapping(uid, rid, true)
}
}
})
}
attachHeaderEvents(){
this.#header.querySelector("#search_user_filter").addEventListener("input", ev=>{
this.#userfilter = ev.target.value.toLowerCase()
this.renderUsers()
})
this.#header.querySelector("div.role-filter").addEventListener("change", ev=>{
this.#rolefilter = Array.prototype.slice.call(ev.currentTarget.querySelectorAll("input:checked")).map(r=>r.name)
this.renderUsers()
})
}
loadData(){
Promise.all([
this.loadEligibleUsers(), this.loadRoles()
]).then(()=>{
//console.log("Done. Ui should be complete now")
}).catch(err=>alert(err))
}
loadEligibleUsers(){
const url = this.#serviceurl + `/admin/realms/d4science/groups/${this.#groupid}/members?first=0&max=-1&briefRepresentation=true`
return this.#boot.secureFetch(url).then(resp=>{
if(resp.ok){
return resp.json()
}else if(resp.status === 403){
throw "Fetching users: You are not allowed to manage roles for this application."
}else{
throw "Fetching users: Unspecified error"
}
}).then(json=>{
this.#users = json
const prom = this.loadMappings()
this.renderUsers()
return prom
})
}
loadRoles(){
const url = this.#serviceurl + `/admin/realms/d4science/clients/${this.#appid}/roles`
return this.#boot.secureFetch(url).then(resp=>{
if(resp.ok){
return resp.json()
}else if(resp.status === 403){
throw "Fetching roles: You are not allowed to manage roles for this application."
}else{
throw "Fetching roles: Unspecified error"
}
}).then(json=>{
this.#roles = json
this.renderHeader()
})
}
loadMappings(){
return Promise.all(
this.#users.map(u => {
return this.loadMapping(u)
})
)
}
loadMapping(u){
const url = this.#serviceurl + `/admin/realms/d4science/users/${u.id}/role-mappings/clients/${this.#appid}`
return this.#boot.secureFetch(url).then(resp=>{
if(resp.ok){
return resp.json()
}else if(resp.status === 403){
throw "Fetching role mappings: You are not allowed to manage roles for this application."
}else{
throw "Fetching role mappings: Unspecified error"
}
}).then(json=>{
u.mappings = json
this.renderUsers()
})
}
toggleMapping(uid, rid, add){
const url = this.#serviceurl + `/admin/realms/d4science/users/${uid}/role-mappings/clients/${this.#appid}`
const role = this.#roles.filter(r=>r.id === rid)
if(role.length !== 1){
alert("Something went wrong with looking up the role")
return
}
this.#boot.secureFetch(url, { method : add ? "POST" : "DELETE", body : JSON.stringify(role), headers : { "Content-Type" : "application/json"}}).then(resp=>{
if(resp.ok){
return resp.text()
}else if(resp.status === 403){
throw "Assigning or removing role: You are not allowed to manage roles for this application."
}else{
throw "Assigning or removing role: Unspecified error"
}
}).then(()=>{
return this.loadMapping(this.#users.filter(u=>u.id === uid)[0])
}).catch(err=>alert(err))
}
}
window.customElements.define('d4s-extapp-role-manager', ExtAppRoleController);

View File

@ -12,10 +12,13 @@
<h3>CDN sandbox</h3> <h3>CDN sandbox</h3>
<p>Select the section you want to enter</p> <p>Select the section you want to enter</p>
</header> </header>
<div class="d-flex"> <div class="d-flex flex-column">
<div class="d-inline-block"> <div class="d-inline-block">
<a href="storage/index.html">D4S Workspace</a> (OIDC protected) <a href="storage/index.html">D4S Workspace</a> (OIDC protected)
</div> </div>
<div class="d-inline-block">
<a href="ccp/index.html">CCP</a> (OIDC protected)
</div>
</div> </div>
</body> </body>

View File

@ -16,10 +16,7 @@ class D4SStorageHtmlElement extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
let linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css');
this.shadowRoot.appendChild(linkElem);
} }
get srcBaseURL() { get srcBaseURL() {
@ -98,7 +95,7 @@ class D4SStorageToolbar extends D4SStorageHtmlElement {
class D4SStorageTree extends D4SStorageHtmlElement { class D4SStorageTree extends D4SStorageHtmlElement {
static tree_event_name = "d4s-toolbar"; static tree_event_name = "d4s-tree";
static folder_data_event_name = "d4s-folder-data"; static folder_data_event_name = "d4s-folder-data";
static dataid_attr = 'data-id'; static dataid_attr = 'data-id';
@ -226,8 +223,10 @@ class D4SStorageTree extends D4SStorageHtmlElement {
const div = document.createElement('div') const div = document.createElement('div')
div.innerHTML = /*css*/` div.innerHTML = /*css*/`
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<style> <style>
.selected > span { .selected > .li-title {
background-color: ${this.#selectedbgcolor}; background-color: ${this.#selectedbgcolor};
} }
ul.root { ul.root {
@ -238,6 +237,12 @@ class D4SStorageTree extends D4SStorageHtmlElement {
margin-bottom: 0; margin-bottom: 0;
padding-left: 0.6em; padding-left: 0.6em;
} }
li{
color: #444444;
}
li:hover{
color: #555555;
}
li { li {
cursor: pointer; cursor: pointer;
} }
@ -378,7 +383,7 @@ class D4SStorageTree extends D4SStorageHtmlElement {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent( new CustomEvent(
D4SStorageTree.tree_event_name, D4SStorageTree.tree_event_name,
{detail: {id: id}} {detail: {id: id, bubbles: true}}
) )
); );
} }
@ -417,15 +422,43 @@ class D4SStorageTree extends D4SStorageHtmlElement {
data data
.filter(item => this.showFiles || item['@class'].includes('FolderItem')) .filter(item => this.showFiles || item['@class'].includes('FolderItem'))
.forEach(item => { .forEach(item => {
ul.appendChild(this.createListItem(item.name, item.id, item['@class'], parentId)); ul.appendChild(this.createListItem(item, parentId));
}) })
} else { } else {
ul.appendChild(this.createListItem(data.displayName ? data.displayName : data.name, data.id, data['@class'], parentId)); ul.appendChild(this.createListItem(data, parentId));
} }
parentElement.appendChild(ul); parentElement.appendChild(ul);
} }
createListItem(label, id, type, parentId) { getIconByMIME(mime){
if(mime.startsWith("image")) return "image";
if(mime.startsWith("application/pdf")) return "picture_as_pdf";
if(mime.startsWith("application/zip")) return "archive";
if(mime.startsWith("application/xml")) return "code";
if(mime.match(/tar|compressed|gzip|gz|tgz/)) return 'archive';
return null;
}
getIconByExtension(name){
const tks = name.split(".")
if(tks.length === 0) return null;
const ext = tks[tks.length - 1]
if(ext.match(/java$|py$|^r$|^c$|ccp|lua|jl|^sh$|json|xml|xsl|xslt|md$|xq$|xqm$/i)) return "code";
if(ext.match(/js$/i)) return "javascript";
if(ext.match(/html$|css$|php$/i)) return ext;
if(ext.match(/kml$/i)) return "place";
if(ext.match(/ics$/i)) return "event";
if(ext.match(/csv$|xls$|ods$/i)) return "assessment";
if(ext.match(/txt$|odt$|rtf$|doc$|docx$/i)) return "description";
if(ext.match(/ppt$|odp$/i)) return "slideshow";
return null;
}
createListItem(item, parentId) {
const label = item.displayName ? item.displayName : item.title
const id = item.id
const type = item["@class"]
const mime = item.content && item.content.mimeType ? item.content.mimeType : null
const li = document.createElement('li'); const li = document.createElement('li');
li.setAttribute(D4SStorageTree.dataname_attr, label); li.setAttribute(D4SStorageTree.dataname_attr, label);
li.setAttribute(D4SStorageTree.dataid_attr, id); li.setAttribute(D4SStorageTree.dataid_attr, id);
@ -434,14 +467,30 @@ class D4SStorageTree extends D4SStorageHtmlElement {
if (parentId) { if (parentId) {
li.setAttribute(D4SStorageTree.parentid_attr, parentId); li.setAttribute(D4SStorageTree.parentid_attr, parentId);
} }
const icon = type.includes('FolderItem') ? "folder.svg" : "file-earmark.svg" //const icon = type.includes('FolderItem') || type.includes("SharedFolder")? "folder.svg" : "file-earmark.svg"
var icon = "insert_drive_file";
if(type.includes('FolderItem')){
icon = "folder"
}else if(type.includes("SharedFolder")){
icon = "folder_shared"
}else{
const im = mime ? this.getIconByMIME(mime) : null
if(im) icon = im;
else{
const ie = this.getIconByExtension(label)
if(ie) icon = ie;
}
}
li.innerHTML = ` li.innerHTML = `
<img class="px-1" src="${this.srcBaseURL}/img/${icon}"</img> <div class="li-title d-flex gap-1">
<span>${label}</span> <span class="material-icons">${icon}</span>
<span>${label}</span>
</div>
` `
li.addEventListener('click', (ev) => { li.addEventListener('click', (ev) => {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault()
this.select(ev.currentTarget.getAttribute(D4SStorageTree.dataid_attr)); this.select(ev.currentTarget.getAttribute(D4SStorageTree.dataid_attr));
}); });
@ -668,6 +717,7 @@ class D4SStorageFolder extends D4SStorageHtmlElement {
const isFolder = item['@class'].includes('FolderItem') const isFolder = item['@class'].includes('FolderItem')
filename.addEventListener('click', (ev) => { filename.addEventListener('click', (ev) => {
ev.stopPropagation() ev.stopPropagation()
ev.preventDefault()
if (isFolder) { if (isFolder) {
const span = ev.currentTarget.querySelector(':nth-child(2)') const span = ev.currentTarget.querySelector(':nth-child(2)')
if (span) { if (span) {
@ -786,7 +836,7 @@ class D4SWorkspace {
if (uri) { if (uri) {
url += uri.startsWith('/') ? uri : '/' + uri; url += uri.startsWith('/') ? uri : '/' + uri;
} }
console.log("Invoking WS API with: " + url); //console.log("Invoking WS API with: " + url);
return this.#d4sboot.secureFetch(url, req) return this.#d4sboot.secureFetch(url, req)
.then(resp => { .then(resp => {
if (resp.ok) { if (resp.ok) {
@ -840,7 +890,7 @@ class D4SWorkspace {
* @throws the error as string, if occurred * @throws the error as string, if occurred
*/ */
listFolder(folderId, extraHeaders) { listFolder(folderId, extraHeaders) {
const uri = "/items/" + folderId + "/children"; const uri = "/items/" + folderId + "/children?exclude=hl:accounting";
return this.#callWorkspace("GET", uri, null, null, extraHeaders) return this.#callWorkspace("GET", uri, null, null, extraHeaders)
.then(blob => { .then(blob => {
return blob.text().then(text => { return blob.text().then(text => {
@ -971,7 +1021,7 @@ class D4SWorkspace {
tmplnk.click(); tmplnk.click();
document.body.removeChild(tmplnk); document.body.removeChild(tmplnk);
} else { } else {
console.log("Skipping local download"); //console.log("Skipping local download");
} }
return data; return data;
}).catch(err => { }).catch(err => {

View File

@ -10,8 +10,8 @@
<body class="m-4"> <body class="m-4">
<d4s-boot-2 context="%2Fgcube%2Fdevsec%2FdevVRE" <d4s-boot-2 context="%2Fgcube%2Fdevsec%2FdevVRE"
gateway="next.d4science.org" gateway="next.dev.d4science.org"
redirect-url="https://cdn.dev.d4science.org/storage/" redirect-url="https://cdn.cloud-dev.d4science.org/storage/"
url="https://accounts.dev.d4science.org/auth"> url="https://accounts.dev.d4science.org/auth">
<!-- redirect-url="http://localhost:8080/storage/" --> <!-- redirect-url="http://localhost:8080/storage/" -->
<div class="row h-100"> <div class="row h-100">