Added `D4SWorkspace` class that interacts with WS WEB "rest" API and modificed the WEB components to use it. Some methods of the class, unused by web components at the moment, are still to be tested.

This commit is contained in:
Mauro Mugnaini 2023-07-26 18:14:11 +02:00
parent 53bb1f97b6
commit daa84c6d91
1 changed files with 443 additions and 79 deletions

View File

@ -9,10 +9,13 @@ class D4SStorageHtmlElement extends HTMLElement {
#baseurl = 'https://api.d4science.org/workspace' #baseurl = 'https://api.d4science.org/workspace'
//#baseurl = 'https://workspace-repository.dev.d4science.org/storagehub/workspace' //#baseurl = 'https://workspace-repository.dev.d4science.org/storagehub/workspace'
#boot = null #boot = null
#d4sworkspace = null;
#showroot = false; // To show WS root folder by default set it to true
constructor() { constructor() {
super() super()
this.#boot = document.querySelector('d4s-boot-2') this.#boot = document.querySelector('d4s-boot-2')
this.#d4sworkspace = new D4SWorkspace(this.#baseurl, this.#boot)
} }
get boot() { get boot() {
@ -43,7 +46,21 @@ class D4SStorageHtmlElement extends HTMLElement {
set baseUrl(url) { set baseUrl(url) {
this.#baseurl = url this.#baseurl = url
this.setAttribute("baseurl", url) this.setAttribute("baseurl", url)
this.#d4sworkspace = new D4SWorkspace(this.#baseurl, this.#boot)
} }
get d4sWorkspace() {
return this.#d4sworkspace;
}
get showRoot() {
return this.#showroot;
}
set showRoot(show) {
this.#showroot = show;
}
} }
@ -52,6 +69,7 @@ class D4SStorageHtmlElement extends HTMLElement {
* <d4s-storage-tree * <d4s-storage-tree
* /@filelist-id: identifier of d4s-storage-folder component * /@filelist-id: identifier of d4s-storage-folder component
* /@base-url: the base endpoint url [defaults to: https://api.d4science.org/workspace] * /@base-url: the base endpoint url [defaults to: https://api.d4science.org/workspace]
* /@show-root: false (or omitted) to show the VRE folder (default), true to show the root folder of the WS
*/ */
window.customElements.define('d4s-storage-tree', class extends D4SStorageHtmlElement { window.customElements.define('d4s-storage-tree', class extends D4SStorageHtmlElement {
@ -97,44 +115,33 @@ window.customElements.define('d4s-storage-tree', class extends D4SStorageHtmlEle
` `
div.innerHTML = style div.innerHTML = style
this.boot.secureFetch(this.baseUrl + '/vrefolder').then((reply) => { const initalFolderFunction = (cond) => {if (cond) return this.d4sWorkspace.getWorkspace(); else return this.d4sWorkspace.getVREFolder();}
if (reply.status !== 200) { initalFolderFunction(this.showRoot).then(data => {
throw "Unable to load folder" this.parseResponse(div, data);
} this.#shadowRoot.appendChild(div);
return reply.json()
}).then(data => {
this.parseResponse(div, data)
this.#shadowRoot.appendChild(div)
}).catch(err => console.error(err)) }).catch(err => console.error(err))
} }
parseResponse(parentElement, jresp) { parseResponse(parentElement, data) {
parentElement.classList.add(this.#loadedClass) parentElement.classList.add(this.#loadedClass);
var ul = document.createElement('ul') var ul = document.createElement('ul');
ul.classList.add('nested') ul.classList.add('nested');
if (jresp.item) { if (Array.isArray(data)) {
const itemname = jresp.item.displayName data
const itemid = jresp.item.id
const path = this.buildListUrl(itemid)
ul.appendChild(this.createListItem(itemname, itemid))
parentElement.appendChild(ul)
} else if (jresp.itemlist) {
jresp
.itemlist
.filter(item => item['@class'].includes('FolderItem')) .filter(item => item['@class'].includes('FolderItem'))
.forEach(item => { .forEach(item => {
const itemname = item.name const itemName = item.name;
const itemid = item.id const itemId = item.id;
const path = this.buildListUrl(itemid) const path = this.buildListUrl(itemId);
ul.appendChild(this.createListItem(itemname, itemid)) ul.appendChild(this.createListItem(itemName, itemId));
}) })
parentElement.appendChild(ul) parentElement.appendChild(ul);
} else { } else {
console.error(jresp) const itemName = data.displayName ? data.displayName : data.name;
const itemId = data.id;
const path = this.buildListUrl(itemId);
ul.appendChild(this.createListItem(itemName, itemId));
parentElement.appendChild(ul);
} }
} }
@ -178,15 +185,8 @@ window.customElements.define('d4s-storage-tree', class extends D4SStorageHtmlEle
return return
} }
this.boot.secureFetch(this.buildListUrl(id)).then(reply => { this.d4sWorkspace.listFolder(id).then(data => {
if (reply.status !== 200) {
throw "Unable to load folder"
}
return reply.json()
}).then(data => {
this.parseResponse(li, data) this.parseResponse(li, data)
}).catch(err => console.error(err)) }).catch(err => console.error(err))
} }
@ -208,7 +208,7 @@ window.customElements.define('d4s-storage-tree', class extends D4SStorageHtmlEle
} }
static get observedAttributes() { static get observedAttributes() {
return ["base-url", "filelist-id"] return ["base-url", "filelist-id", "show-root"]
} }
attributeChangedCallback(name, oldValue, newValue) { attributeChangedCallback(name, oldValue, newValue) {
@ -220,6 +220,8 @@ window.customElements.define('d4s-storage-tree', class extends D4SStorageHtmlEle
case "base-url": case "base-url":
this.baseUrl = newValue this.baseUrl = newValue
break break
case "show-root":
this.showRoot = newValue;
} }
} }
} }
@ -290,24 +292,17 @@ window.customElements.define('d4s-storage-folder', class extends D4SStorageHtmlE
} }
list(folderId) { list(folderId) {
this.boot.secureFetch(this.buildListUrl(folderId)).then(reply => { this.d4sWorkspace.listFolder(folderId).then(data => {
if (reply.status !== 200) { this.parseResponse(data);
throw "Unable to build list url" }).catch(err => console.error(err));
}
return reply.json()
}).then(data => {
this.parseResponse(data)
}).catch(err => console.error(err))
} }
parseResponse(jresp) { parseResponse(data) {
const root = this.createContainer() const root = this.createContainer();
if (jresp.itemlist) { if (Array.isArray(data)) {
jresp.itemlist.forEach(item => root.appendChild(this.createFileRow(item))) data.forEach(item => root.appendChild(this.createFileRow(item)));
} else { } else {
console.error(jresp) console.error("Returned data is not an array: " + data);
} }
} }
@ -331,7 +326,7 @@ window.customElements.define('d4s-storage-folder', class extends D4SStorageHtmlE
} }
} else { } else {
console.info("Download of " + item.id) console.info("Download of " + item.id)
this.download(this.buildDownloadUrl(item.id), item.name) this.d4sWorkspace.download(item.id, item.name).catch(err => {alert(err)});
} }
}) })
@ -351,29 +346,29 @@ window.customElements.define('d4s-storage-folder', class extends D4SStorageHtmlE
return '<span class="px-2">' + i + '</span>' return '<span class="px-2">' + i + '</span>'
} }
buildDownloadUrl(id) { // buildDownloadUrl(id) {
return this.baseUrl+ '/items/' + id + '/download' // return this.baseUrl+ '/items/' + id + '/download'
} // }
download(url, name) { // download(url, name) {
this.boot.secureFetch(url).then(reply => { // this.d4sWorkspace.download(url).then(reply => {
if (reply.status !== 200) { // if (reply.status !== 200) {
throw "Unable to download" // throw "Unable to download"
} // }
return reply.blob() // return reply.blob()
}).then(blob => { // }).then(blob => {
const objectURL = URL.createObjectURL(blob) // const objectURL = URL.createObjectURL(blob)
var tmplnk = document.createElement("a") // var tmplnk = document.createElement("a")
tmplnk.download = name // tmplnk.download = name
tmplnk.href = objectURL // tmplnk.href = objectURL
//tmplnk.target="_blank" // //tmplnk.target="_blank"
document.body.appendChild(tmplnk) // document.body.appendChild(tmplnk)
tmplnk.click() // tmplnk.click()
document.body.removeChild(tmplnk) // document.body.removeChild(tmplnk)
}).catch(err => console.error(err)) // }).catch(err => console.error(err))
} // }
static get observedAttributes() { static get observedAttributes() {
return ["base-url"]; return ["base-url"];
@ -390,3 +385,372 @@ window.customElements.define('d4s-storage-folder', class extends D4SStorageHtmlE
} }
}) })
/**
* Class that helps the interaction with the D4Science's Workspace via API and user's token as bearer authentication
*/
class D4SWorkspace {
#d4sboot = null;
#workspaceURL = null;
/**
* Creates the new D4SWorkspace object, the d4sboot parameter can be null, in this case it is searched in the DOM document via <code>querySelector()</code> method
* @param {string} workspaceURL the WS API base URL
* @param {d4s-boot-2} d4sboot the d4s-boot-2 instance
*/
constructor(workspaceURL, d4sboot) {
this.setWorkspaceURL(workspaceURL)
if (d4sboot) {
this.setD4SBoot(d4sboot)
} else {
this.setD4SBoot(document.querySelector("d4s-boot-2"));
}
}
/**
* Sets the Workspace API base URL
* @param {string} workspaceURL the WS API base URL
*/
setWorkspaceURL(workspaceURL) {
this.#workspaceURL = workspaceURL;
}
/**
* Sets the d4s-boot-2 instance to be used to perform the OIDC/UMA autenticated secure fetches
* @param {string} d4sboot the d4s-boot-2 instance
*/
setD4SBoot(d4sboot) {
this.#d4sboot = d4sboot;
}
/**
* Calls with a secure fetch the workspace with provided data.
* To upload data by using a <code>FormData</code> object you have to leave the <code>mime</code> attribute as <code>null</code>.
* @param {*} method the method to use for the call, default to <code>GET</code>
* @param {*} uri the uri to invoke, relative to <code>workspaceURL</code> provided in the object's constructor
* @param {*} body the payload to send
* @param {*} mime the mime type of the payload
* @param {*} extraHeaders extra HTTP headers to send
* @returns the reponse payload as a promise, JSON data promise if the returned data is "application/json", a String data promise if "text/*" or a blob data promise in other cases
* @throws the error as string, if occurred
*/
#callWorkspace(method, uri, body, mime, extraHeaders) {
let req = { };
if (method) {
req.method = method;
}
if (body) {
req.body = body;
}
if (extraHeaders) {
req.headers = extraHeaders;
}
if (mime) {
if (req.headers) {
req.headers["Content-Type"] = mime;
} else {
req.headers = { "Content-Type" : mime };
}
}
let url = this.#workspaceURL;
if (uri) {
url += uri.startsWith('/') ? uri : '/' + uri;
}
console.log("Invoking WS API with: " + url);
return this.#d4sboot.secureFetch(url, req)
.then(resp => {
if (resp.ok) {
const contentType = resp.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
return resp.json();
} else if (contentType && contentType.indexOf("text/") !== -1) {
return resp.text();
} else {
return resp.blob();
}
} else {
throw "Cannot invoke workspace via secure fetch for URL: " + url;
}
}).catch(err => {
console.error(err);
throw err;
});
}
/**
* Gets the user's Workspace root folder JSON item
* @param {map} extraHeaders extra HTTP headers to be used for the request
* @returns the WS root folder JSON item object
* @throws the error as string, if occurred
*/
getWorkspace(extraHeaders) {
return this.#callWorkspace("GET", null, null, null, extraHeaders)
.then(json => {
return json.item;
}).catch(err => {
const msg = "Cannot get workspace root ID. Error message: " + err;
console.error(msg);
throw msg;
});
}
/**
* Gets the user's Workspace root folder id (as string)
* @param {map} extraHeaders extra HTTP headers to be used for the request
* @returns the id of the WS root folder
* @throws the error as string, if occurred
*/
getWorkspaceId(extraHeaders) {
return this.getWorkspace(extraHeaders)
.then(json => {
return json.item.id;
});
}
/**
* Gets the contents of a folder, all sub-folders and files.
* @param {string} folderId the WS folder id to list
* @param {map} extraHeaders extra HTTP headers to be used for the request
* @returns the contents of the WS folder as json objects
* @throws the error as string, if occurred
*/
listFolder(folderId, extraHeaders) {
const uri = "/items/" + folderId + "/children";
return this.#callWorkspace("GET", uri, null, null, extraHeaders)
.then(json => {
return json.itemlist;
}).catch(err => {
const msg = "Cannot get folder's content list. Error message: " + err;
console.error(msg);
throw msg;
});
}
/**
* Gets the VRE folder JSON item that belongs to the context where the access token has been issued.
* @param {map} extraHeaders extra HTTP headers to be used for the request
* @returns the id of the WS VRE folder
* @throws the error as string, if occurred
*/
getVREFolder(extraHeaders) {
return this.#callWorkspace("GET", "/vrefolder", null, null, extraHeaders)
.then(json => {
return json.item;
}).catch(err => {
const msg = "Cannot get VRE folder ID. Error message: " + err;
console.error(msg);
throw msg;
});
}
/**
* Gets the VRE folder id (as string) that belongs to the context where the access token has been issued.
* @param {map} extraHeaders extra HTTP headers to be used for the request
* @returns the id of the WS VRE folder
* @throws the error as string, if occurred
*/
getVREFolderId(extraHeaders) {
return this.getVREFolder(extraHeaders)
.then(item => {
return item.id;
});
}
/**
* @deprecated please use getOrCreateFolder() function instead
* @param {string} parentFolderId the id of the parent folder where search fo the folder
* @param {string} name the name of the folder (characters not allowed: ":", [others still to be found])
* @param {string} description the description of the folder if it should be creaded
* @param {map} extraHeaders extra HTTP headers to be used for the request
* @returns the id of the folder, if it exists or it has been just created
* @throws the error as string, if occurred
*/
checkOrCreateFolder(parentFolderId, name, description, extraHeaders) {
return this.getOrCreateFolder(parentFolderId, name, description, extraHeaders);
}
/**
* Gets the id of the folder represented by the name parameter if exists, a new folder is created if it doesn't not exist
* @param {string} parentFolderId the id of the parent folder where search fo the folder
* @param {string} name the name of the folder (characters not allowed: ":", [others still to be found])
* @param {string} description the description of the folder if it should be creaded
* @param {map} extraHeaders extra HTTP headers to be used for the request
* @returns the id of the folder, if it exists or it has been just created
* @throws the error as string, if occurred
*/
getOrCreateFolder(parentFolderId, name, description, extraHeaders) {
const baseURI = "/items/" + parentFolderId;
console.log("Checking existance of folder: " + name);
let uri = baseURI + "/items/" + name;
return this.#callWorkspace("GET", uri, null, null, extraHeaders)
.then(json => {
if (json.itemlist[0]) {
const id = json.itemlist[0].id;
console.log("'" + name + "' folder exists with and has id: " + id);
return id;
} else {
console.info("'" + name + "' folder doesn't exist, creating it: " + name);
uri = baseURI + "/create/FOLDER";
const params = new URLSearchParams({ name : name, description : description, hidden : false });
return this.#callWorkspace("POST", uri, params.toString(), "application/x-www-form-urlencoded", extraHeaders).then(id => {
console.log("New '" + name + "' folder successfully created with id: " + id);
return id
});
}
});
};
/**
* Deletes a Workspace item (folder or file) represented by its id.
* @param {string} itemId the id of the item do delete
* @param {map} extraHeaders extra HTTP headers to be used for the request
* @returns <code>true</code> if the file has been correctly created, <code>false<c/ode> otherwise
*/
deleteItem(itemId, extraHeaders) {
return this.#callWorkspace("DELETE", "/workspace/items/" + itemId, null, null, extraHeaders)
.then(results => {
return true;
}).catch(err => {
console.error("Cannot DELETE workspace item with ID: " + itemId + ". Error message: " + err);
return false;
});
}
/**
* Downloads a Workspace item (folder or file) represented by its id.
* @param {string} itemId the WS item to download
* @param {string} name the name of the file to use for the download, if null the id is used
* @param {boolean} skipLocalDownload if provided and <code>true</code> returns the data to function caller, otherwise it also downloads it locally.
* @param {map} extraHeaders extra HTTP headers to be used for the request
* @returns the file's content if item is a file or a ZIP compressed archive if the item represents a folder
* @throws the error as string, if occurred
*/
download(itemId, name, skipLocalDownload, extraHeaders) {
return this.#callWorkspace("GET", "/items/" + itemId + "/download", null, null, extraHeaders)
.then(data => {
if (!skipLocalDownload) {
console.log("Donwloads item also locally to browser");
const objectURL = URL.createObjectURL(data);
var tmplnk = document.createElement("a");
tmplnk.download = name ? name : itemId;
tmplnk.href = objectURL;
//tmplnk.target="_blank";
document.body.appendChild(tmplnk);
tmplnk.click();
document.body.removeChild(tmplnk);
} else {
console.log("Skipping local download");
}
return data;
}).catch(err => {
const msg = "Cannot download item with ID: " + itemId + ". Error message: " + err;
console.error(msg);
throw msg;
});
}
/**
* Uploads a new file into the folder represented by its id
* @param {string} folderId the folder where to create the new file
* @param {string} name the name of the file to crete (characters not allowed: ":", [others still to be found])
* @param {object} data the archive data
* @param {string} contentType the content type of the file
* @param {map} extraHeaders extra HTTP headers to be used for the request
* @returns <code>true</code> if the file has been correctly created, <code>false<c/ode> otherwise
*/
uploadFile(folderId, name, description, data, contentType, extraHeaders) {
const uri = "/items/" + folderId + "/create/FILE";
const request = new FormData();
request.append("name", name);
request.append("description", description);
request.append(
"file",
new Blob([data], {
type: contentType
})
);
return this.#callWorkspace("POST", uri, request, null, extraHeaders)
.then(item => {
const id = item.id;
console.info("File '" + name + "' successfully uploaded and its id is: " + id);
return true;
}).catch(err => {
console.error("Cannot upload file '" + name + "'. Error: " + err);
return false;
});
};
/**
* Uploads a new archive and stores its content into the folder represented by its id
* @param {string} folderId the folder where to create the new file
* @param {string} parentFolderName the name of the folder to create (characters not allowed: ":", [others still to be found])
* @param {object} data the file's contents
* @param {map} extraHeaders extra HTTP headers to be used for the request
* @returns <code>true</code> if the file has been correctly created, <code>false<c/ode> otherwise
*/
uploadArchive(folderId, parentFolderName, description, data, extraHeaders) {
const uri = "/items/" + folderId + "/create/ARCHIVE";
const request = new FormData();
request.append("parentFolderName", parentFolderName);
request.append(
"file",
new Blob([data], {
type: "application/octet-stream"
})
);
return this.#callWorkspace("POST", uri, request, null, extraHeaders)
.then(item => {
const id = item.id;
console.info("Archive file successfully uploaded and extracted into the '" + parentFolderName + "' folder, its id is: " + id);
return true;
}).catch(err => {
console.error("Cannot upload archive file. Error: " + err);
return false;
});
};
/**
* Returns the public URL of a file
* @param {string} itemId the id of the item
* @param {int} version the optional file version
* @param {map} extraHeaders extra HTTP headers to be used for the request
* @returns the public URL of the item
* @throws the error as string, if occurred
*/
getPublicURL(itemId, version, extraHeaders) {
let uri = "/workspace/items/" + itemId + "/publiclink";
if (version) {
uri += "version=" + version;
}
return this.#callWorkspace("GET", uri, null, null, extraHeaders)
.then(publicURL => {
return publicURL;
}).catch(err => {
const message = "Cannot retrieve the item's public URL. Error message: " + err;
console.error(message);
throw message;
});
}
/**
* Finds into a folder all the file files having the name than matches a specific pattern.
* Wildcard (*) can be used at the start or the end of the pattern (eg. starts with *{name}, ends with = {name}*)
* @param {string} folderId the WS folder id where to search
* @param {string} pattern the pattern to use for the search. Wildcard '*' can be used at the start or the end of the pattern (eg. starts with *{name}, ends with = {name}*)
* @param {map} extraHeaders extra HTTP headers to be used for the request
* @returns the contents of the WS folder as json objects
* @throws the error as string, if occurred
*/
findByName(folderId, pattern, extraHeaders) {
const uri = "/items/" + folderId + "/items/" + pattern;
return this.#callWorkspace("GET", uri, null, null, extraHeaders)
.then(json => {
return json.itemlist;
}).catch(err => {
const msg = "Cannot get folder's content list. Error message: " + err;
console.error(msg);
throw msg;
});
}
}