class CCPMethodList extends HTMLElement{ #boot; #rootdoc; #data = null; #filtered; #dragged = null; #searchfield = null; #allowedit = false; #allowexecute = false; #archive = false; #fileupload = null; #archiveupload = null; #serviceurl; constructor(){ super() this.#boot = document.querySelector("d4s-boot-2") this.#rootdoc = this.attachShadow({ "mode" : "open"}) this.#archive = this.getAttribute("archive") } render(){ this.#rootdoc.innerHTML = `
Methods
    ` this.#rootdoc.querySelector("button[name=refresh]").addEventListener("click", ev=>{ this.fetchProcesses() }) this.#searchfield = this.#rootdoc.querySelector("input[name=search]") this.#searchfield.addEventListener("input", ev=>{ this.updateList() }) 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.importMethods(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("Please confirm importing of method from link?")){ this.fromArchive(link) } } }) } connectedCallback(){ this.#allowedit = this.getAttribute("allow-edit") === "true" this.#allowexecute = this.getAttribute("allow-execute") === "true" this.#serviceurl = this.getAttribute("serviceurl") this.render() this.fetchProcesses() } fetchProcesses(){ console.log("Calling fetch processes") this.#boot.secureFetch(this.#serviceurl + "/methods"). then(resp=>{ return resp.json() }).then(data=>{ this.#data = data this.updateList() return this.fetchInfrastructures() }).then(d => { this.updateList() }).catch(err=>{ alert("Error while downloading methods") console.error("Error while downloading methods: " + err) }) } fetchInfrastructures(){ const url = this.#serviceurl + "/infrastructures" this.#boot.secureFetch(url). then(resp=>{ if(resp.status !== 200) throw "Unable to fetch infrastructures " + resp.status; else return resp.json() }).then(infras=>{ for(let m=0; m < this.#data.length; m++){ const method = this.#data[m] method["executable"] = false for(let i=0; i < infras.length; i++){ const infra = infras[i] const matches = method.links.filter(l => l.rel === "compatibleWith" && l.href === "infrastructures/" + infra.id) if(matches.length > 0){ method["executable"] = true break } } } this.updateList() }).catch(err=>{ alert("Error while checking runtimes for method") console.error("Error while checking runtimes for method: " + err) }) } updateList(){ const filter = this.#searchfield.value if(filter === "" || filter == null || filter == undefined){ this.#filtered = this.#data }else{ const f = filter.toLowerCase() this.#filtered = this.#data.filter(d=>{ return false || (d.title && d.title.toLowerCase().indexOf(f) !== -1)|| (d.description && d.description.indexOf(f) !== -1) || (Array.isArray(d.keywords) && d.keywords.map(k=>k.toLowerCase()).filter(i=>i.indexOf(f) !== -1)).length }) } this.groupBy() BSS.apply(this.#process_list_bss, this.#rootdoc) } groupBy(){ this.#filtered = this.#filtered.reduce((catalog, meth)=>{ const categories = meth.metadata.filter(md=>md.role === "category").map(c=>c.title) if(categories.length === 0){ catalog["Uncategorised"].push(meth) }else{ for(let c in categories){ const category = categories[c] catalog[category] = catalog[category] ?? [] catalog[category].push(meth) } } return catalog }, { "Uncategorised" : []}) } getCategoryHints(){ const s = new Set() if(this.#data === null) return []; for(let i=0; i < this.#data.length; i++){ const cat = this.#data[i].metadata.filter(md=>md.role === "category").map(md=>md.title) cat.forEach(c=>s.add(c)) } return [...s] } exportCategory(category){ Promise.all( this.#filtered[category].map(m=>this.exportMethod(m.id)) ) } exportMethod(method){ this.#boot.secureFetch(this.#serviceurl + "/methods/" + method + "/shareable").then( (resp)=>{ if(resp.status === 200){ return resp.json() }else throw "Error exporting sharable process: " + resp.status } ).then(data=>{ const filename = data.title + "-" + data.version + ".json" const datastr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data)); var tmplnk = document.createElement("a") tmplnk.download = filename tmplnk.href = datastr document.body.appendChild(tmplnk) tmplnk.click() document.body.removeChild(tmplnk) }).catch(err=>{ console.log(err) }) } importMethods(files){ if(files && files.length) { let formdata = new FormData(); files.reduce((formdata, f)=>{ formdata.append("files[]", f) return formdata }, formdata) this.#boot.secureFetch(`${this.#serviceurl}/methods`, { body: formdata, method : "POST"}) .then(reply=>{ if (reply.status !== 200) { throw "Unable to import" }else return reply.text() }).then(data=>{ this.fetchProcesses() }).catch(err=>{ alert(err) }) } } toArchive(id){ this.#boot.secureFetch(`${this.#serviceurl}/methods/${id}/archive`, { method: "POST" }) .then(reply =>{ if (!reply.ok) { throw "Unable to archive" } }).catch(err=>{ alert(err)}) } fromArchive(url){ if(url){ this.#boot.secureFetch(`${this.#serviceurl}/methods/archive?url=${url}`) .then(reply =>{ if (!reply.ok) { throw "Unable to fetch from archive" } return reply.text() }).then(data=>{ this.fetchProcesses() }).catch(err=>{ alert(err)}) } } publish(id){ this.#boot.secureFetch(`${this.#serviceurl}/methods/${id}/publish`, { method: "POST" }) .then(reply =>{ if (!reply.ok) { throw `Unable to archive (${reply.status})` } }).catch(err=>{ alert(err)}) } isShared(process){ return 0 < process.metadata.filter(md=>{ return md.role === "context" && md.title === this.#boot.context}).length } isAuthor(m){ const href = `${this.#boot.url}/admin/realms/${this.#boot.realm}/users/${this.#boot.subject}` const found = m.metadata.filter(md=>{return md.role === "author" && md.href === href}) return found.length > 0 } #process_list_bss = { template : "#PROCESS_LIST_TEMPLATE", target : "ul[name=process_category_list]", "in" : this, on_dragover : (ev)=> ev.preventDefault(), on_dragenter : (ev)=> ev.target.classList.toggle("border-info"), on_dragleave : (ev)=> ev.target.classList.toggle("border-info"), on_drop : (ev)=>{ if(ev.dataTransfer && ev.dataTransfer.files && ev.dataTransfer.files.length){ const files = Array.prototype.slice.call(ev.dataTransfer.files) const jsons = files.filter(f=>f.type === "application/json") if(confirm("Please confirm import of method files?")){ this.importMethods(files) } ev.preventDefault() ev.stopPropagation() } ev.target.classList.toggle("border-info") }, recurse : [ { target : "li.ccp-process-category", "in" : (e,d)=>Object.keys(this.#filtered), on_click : ev=>{ if(ev.target.getAttribute("name") === "export_category"){ this.exportCategory(ev.currentTarget.bss_input.data) } }, recurse : [ { target : "summary", apply : (e,d)=>{ const executables = this.#filtered[d].filter(m=>m.executable).length e.querySelector("h5").textContent = d e.querySelector("span[name=count_notexecutables]").textContent = this.#filtered[d].length - executables e.querySelector("span[name=count_executables]").textContent = executables } }, { target : "details", apply : (e,d)=>{ e.alt = e.title = d if(sessionStorage.getItem(d) === "open") e.open = "open"; else e.removeAttribute("open"); }, on_toggle : ev=>{ if(ev.target.open){ sessionStorage.setItem(ev.currentTarget.alt, 'open') }else{ sessionStorage.removeItem(ev.currentTarget.alt) } } }, { target : "details ul[name=process_list]", in : (e,d)=>d, recurse: [ { target : "li.ccp-process", "in" : (e,d)=>this.#filtered[d], apply : (e,d)=>{ e.alt = e.title = `${d.title} (${d.id})` }, on_click : ev=>{ const id = ev.currentTarget.bss_input.data.id if(ev.target.getAttribute("name") === "export_method"){ this.exportMethod(id) }else if(ev.target.getAttribute("name") === "executable"){ const event = new CustomEvent('newexecutionrequest', { detail: id }); document.dispatchEvent(event) }else if(ev.target.getAttribute("name") === "edit"){ const event = new CustomEvent('neweditrequest', { detail: ev.currentTarget.bss_input.data }); document.dispatchEvent(event) }else if(ev.target.getAttribute("name") === "archive"){ if(confirm("Please confirm archiving of method to workspace?")){ this.toArchive(id) } }else if(ev.target.getAttribute("name") === "publish"){ if(confirm("Please confirm publication of method to Vlab?")){ this.publish(id) } } }, on_dragstart : ev=>{ ev.dataTransfer.effectAllowed = 'move' ev.dataTransfer.setData('text/html', ev.currentTarget.innerHTML) ev.dataTransfer.setData('text/plain+ccpmethod', ev.currentTarget.bss_input.data.id) ev.dataTransfer.setData('application/json+ccpmethod', JSON.stringify(ev.currentTarget.bss_input.data)) }, on_dragend : ev=>{ ev.preventDefault() }, recurse : [ { target: "span[name=title]", apply : (e,d)=>{ e.textContent = d.title } }, { target: "span[name=version]", apply : (e,d)=>{ e.textContent = d.version } }, { target: "button[name=executable]", apply : (e,d)=>{ e.style.display = d.executable && this.#allowexecute ? "revert" : "none" } }, { target: "button[name=edit]", apply : (e,d)=>{ e.style.display = this.#allowedit ? "revert" : "none" } }, { target: "button[name=publish]", apply : (e,d)=>{ if(this.isShared(d)){ const span = document.createElement("span") span.classList.add("badge") span.classList.add("badge-warning") span.textContent = "shared" e.replaceWith(span) }else if(!this.isAuthor(d)){ e.parentElement.removeChild(e) } } }, { target : "span[name=author]", "in" : (e,d)=>d.metadata.filter(md=>md.role === "author"), apply : (e,d)=>{ e.textContent = d.title; e.alt = e.title = "author" } }, { target : "span[name=keyword]", "in" : (e,d)=>d.keywords, apply : (e,d)=>{ e.alt = e.title = e.textContent = d } }, { target : "span[name=infrastructures]", "in" : (e,d)=>d.links.filter(md=>md.rel === "compatibleWith"), apply : (e,d)=>{ e.alt = e.title = e.textContent = d.title } }, { target : "p[name=description]", apply : (e,d)=>{ e.textContent = d.description } }, ] } ] } ] } ] } } window.customElements.define('d4s-ccp-methodlist', CCPMethodList);