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 = `
-
-
${ this.#archive ? `
`
: ``
}
`
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);