Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Biobank Module for LORIS V2 #261

Open
wants to merge 20 commits into
base: 24.1
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Binary file added css/._biobank.css
Binary file not shown.
84 changes: 0 additions & 84 deletions css/biobank.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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%;

Expand Down
205 changes: 205 additions & 0 deletions jsx/APIs/BaseAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
declare const loris: any;
import Query, { QueryParam } from './Query';
import fetchDataStream from 'jslib/fetchDataStream';

interface ApiResponse<T> {
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<T> {
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<U = T>(query?: Query): Promise<U[]> {
const path = this.subEndpoint ? `${this.baseUrl}/${this.subEndpoint}` : this.baseUrl;
const queryString = query ? query.build() : '';
const url = queryString ? `${path}?${queryString}` : path;
return BaseAPI.fetchJSON<U[]>(url);
}

async getLabels(...params: QueryParam[]): Promise<string[]> {
const query = new Query();
params.forEach(param => query.addParam(param));
return this.get<string>(query.addField('label'));
}

async getById(id: string): Promise<T> {
return BaseAPI.fetchJSON<T>(`${this.baseUrl}/${id}`);
}

async create(data: T): Promise<T> {
return BaseAPI.fetchJSON<T>(this.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
}

async update(id: string, data: T): Promise<T> {
return BaseAPI.fetchJSON<T>(`${this.baseUrl}/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
}

static async fetchJSON<T>(url: string, options?: RequestInit): Promise<T> {
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<void> {
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<void> {
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;
}
}
37 changes: 37 additions & 0 deletions jsx/APIs/ContainerAPI.ts
Original file line number Diff line number Diff line change
@@ -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<IContainer> {
constructor() {
super('containers'); // Provide the base URL for container-related API
}

async getTypes(queryParam?: QueryParam): Promise<string[]> {
this.setSubEndpoint(ContainerSubEndpoint.Types);
const query = new Query();
if (queryParam) {
query.addParam(queryParam);
}

return await this.get<string>(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<string[]> {
this.setSubEndpoint(ContainerSubEndpoint.Types);
const query = new Query();
if (queryParam) {
query.addParam(queryParam);
}

return await this.get<string>(query);
}
}
8 changes: 8 additions & 0 deletions jsx/APIs/LabelAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import BaseAPI from './BaseAPI';
import { Label } from '../types'; // Assuming you have a User type

export default class LabelAPI extends BaseAPI<Label> {
constructor() {
super('/labels'); // Provide the base URL for label-related API
}
}
8 changes: 8 additions & 0 deletions jsx/APIs/PoolAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import BaseAPI from './BaseAPI';
import { IPool } from '../entities';

export default class PoolAPI extends BaseAPI<IPool> {
constructor() {
super('pools');
}
}
Loading