diff --git a/css/._biobank.css b/css/._biobank.css new file mode 100644 index 00000000..b154692c Binary files /dev/null and b/css/._biobank.css differ diff --git a/css/biobank.css b/css/biobank.css index bbb6cbca..c5091c2c 100644 --- a/css/biobank.css +++ b/css/biobank.css @@ -38,23 +38,6 @@ margin: auto 0; } -.action { - display: inline-block; -} - -.action > * { - margin: 0 5px; -} - -.action-title { - font-size: 16px; - display: inline; -} - -.action-title > * { - margin: 0 5px; -} - .lifecycle { flex-basis: 73%; display: flex; @@ -367,73 +350,6 @@ font-size: 12px; } -.action-button .glyphicon { - font-size: 20px; - top: 0; -} - -.action-button { - font-size: 30px; - color: #FFFFFF; - border-radius: 50%; - height: 45px; - width: 45px; - cursor: pointer; - user-select: none; - - display: flex; - justify-content: center; - align-items: center; -} - -.action-button.add { - background-color: #0f9d58; - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); -} - -.action-button.disabled { - background-color: #dddddd; -} - -.action-button.pool { - background-color: #96398C; - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); -} - -.action-button.prepare { - background-color: #A6D3F5; - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); -} - -.action-button.search { - background-color: #E98430; - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); -} - -.action-button.add:hover, .pool:hover{ - box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.2), 0 8px 22px 0 rgba(0, 0, 0, 0.19); -} - -.action-button.update, .action-button.delete { - background-color: #FFFFFF; - color: #DDDDDD; - border: 2px solid #DDDDDD; -} - -.action-button.update:hover { - border: none; - background-color: #093782; - color: #FFFFFF; - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); -} - -.action-button.delete:hover { - border: none; - background-color: #BC1616; - color: #FFFFFF; - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); -} - .container-list { flex: 0 1 25%; diff --git a/jsx/APIs/BaseAPI.ts b/jsx/APIs/BaseAPI.ts new file mode 100644 index 00000000..717416eb --- /dev/null +++ b/jsx/APIs/BaseAPI.ts @@ -0,0 +1,205 @@ +declare const loris: any; +import Query, { QueryParam } from './Query'; +import fetchDataStream from 'jslib/fetchDataStream'; + +interface ApiResponse { + data: T, + // Other fields like 'message', 'status', etc., can be added here +} + +interface ApiError { + message: string, + code: number, + // Additional error details can be added here +} + +export default class BaseAPI { + protected baseUrl: string; + protected subEndpoint: string; + + constructor(baseUrl: string) { + this.baseUrl = loris.BaseURL+'/biobank/'+baseUrl; + } + + setSubEndpoint(subEndpoint: string): this { + this.subEndpoint = subEndpoint; + return this; + } + + async get(query?: Query): Promise { + const path = this.subEndpoint ? `${this.baseUrl}/${this.subEndpoint}` : this.baseUrl; + const queryString = query ? query.build() : ''; + const url = queryString ? `${path}?${queryString}` : path; + return BaseAPI.fetchJSON(url); + } + + async getLabels(...params: QueryParam[]): Promise { + const query = new Query(); + params.forEach(param => query.addParam(param)); + return this.get(query.addField('label')); + } + + async getById(id: string): Promise { + return BaseAPI.fetchJSON(`${this.baseUrl}/${id}`); + } + + async create(data: T): Promise { + return BaseAPI.fetchJSON(this.baseUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + } + + async update(id: string, data: T): Promise { + return BaseAPI.fetchJSON(`${this.baseUrl}/${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + } + + static async fetchJSON(url: string, options?: RequestInit): Promise { + try { + const response = await fetch(url, { ...options }); + let data: T; + + try { + data = await response.json(); + } catch (parseError) { + // Handle JSON parsing errors + ErrorHandler.handleError(parseError, { url, options }); + throw parseError; // Re-throw to be caught by the caller + } + + // Use ErrorHandler to log non-OK responses + if (!response.ok) { + ErrorHandler.handleResponse(response, data, { url, options }); + } + + // Return data regardless of response.ok + return data; + } catch (error) { + // Handle network errors + ErrorHandler.handleError(error, { url, options }); + throw error; // Re-throw to be handled by the caller + } + } + + + async fetchStream( + addEntity: (entity: T) => void, + setProgress: (progress: number) => void, + signal: AbortSignal + ): Promise { + const url = new URL(this.baseUrl); + url.searchParams.append('format', 'json'); + + try { + await this.streamData(url.toString(), addEntity, setProgress, signal); + } catch (error) { + if (signal.aborted) { + console.log('Fetch aborted'); + } else { + throw error; + } + } + } + + async streamData( + dataURL: string, + addEntity: (entity: T) => void, + setProgress: (progress: number) => void, + signal: AbortSignal + ): Promise { + const response = await fetch(dataURL, { + method: 'GET', + credentials: 'same-origin', + signal, + }); + + const reader = response.body.getReader(); + const utf8Decoder = new TextDecoder('utf-8'); + let remainder = ''; // For JSON parsing + let processedSize = 0; + const contentLength = +response.headers.get('Content-Length') || 0; + console.log('Content Length: '+contentLength); + + while (true) { + const { done, value } = await reader.read(); + + if (done) { + if (remainder.trim()) { + try { + console.log(remainder); + addEntity(JSON.parse(remainder)); + } catch (e) { + console.error("Failed to parse final JSON object:", e); + } + } + break; + } + + const chunk = utf8Decoder.decode(value, { stream: true }); + remainder += chunk; + + let boundary = remainder.indexOf('\n'); // Assuming newline-delimited JSON objects + while (boundary !== -1) { + const jsonStr = remainder.slice(0, boundary); + remainder = remainder.slice(boundary + 1); + + try { + addEntity(JSON.parse(jsonStr)); + } catch (e) { + console.error("Failed to parse JSON object:", e); + } + + boundary = remainder.indexOf('\n'); + } + + processedSize += value.length; + if (setProgress && contentLength > 0) { + setProgress(Math.min((processedSize / contentLength) * 100, 100)); + } + } + + setProgress(100); // Ensure progress is set to 100% on completion + } +} + +class ErrorHandler { + static handleResponse( + response: Response, + data: any, + context: { url: string; options?: RequestInit } + ): void { + if (!response.ok) { + if (response.status === 400 && data.status === 'error' && data.errors) { + // Validation error occurred + console.warn('Validation Error:', data.errors); + } else { + // Other HTTP errors + console.error(`HTTP Error! Status: ${response.status}`, { + url: context.url, + options: context.options, + responseData: data, + }); + } + } + // No need to throw an error here since we're returning data + } + + static handleError(error: any, context: { url: string; options?: RequestInit }) { + console.error('An error occurred:', { + url: context.url, + options: context.options, + error, + }); + // Re-throw the error to propagate it to the caller + throw error; + } +} diff --git a/jsx/APIs/ContainerAPI.ts b/jsx/APIs/ContainerAPI.ts new file mode 100644 index 00000000..57554e52 --- /dev/null +++ b/jsx/APIs/ContainerAPI.ts @@ -0,0 +1,37 @@ +import BaseAPI from './BaseAPI'; +import Query, { QueryParam } from './Query'; +import { IContainer } from '../entities'; + +export enum ContainerSubEndpoint { + Types = 'types', + Statuses = 'statuses', +} + +export default class ContainerAPI extends BaseAPI { + constructor() { + super('containers'); // Provide the base URL for container-related API + } + + async getTypes(queryParam?: QueryParam): Promise { + this.setSubEndpoint(ContainerSubEndpoint.Types); + const query = new Query(); + if (queryParam) { + query.addParam(queryParam); + } + + return await this.get(query); + } + + // TODO: to be updated to something more useful — status will probably no + // longer be something that you can select but rather something that is + // derived. + async getStatuses(queryParam?: QueryParam): Promise { + this.setSubEndpoint(ContainerSubEndpoint.Types); + const query = new Query(); + if (queryParam) { + query.addParam(queryParam); + } + + return await this.get(query); + } +} diff --git a/jsx/APIs/LabelAPI.ts b/jsx/APIs/LabelAPI.ts new file mode 100644 index 00000000..59fe9522 --- /dev/null +++ b/jsx/APIs/LabelAPI.ts @@ -0,0 +1,8 @@ +import BaseAPI from './BaseAPI'; +import { Label } from '../types'; // Assuming you have a User type + +export default class LabelAPI extends BaseAPI