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 #258

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
214 changes: 214 additions & 0 deletions jsx/APIs/BaseAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
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 batchCreate(entities: T[]): Promise<T[]> {
return BaseAPI.fetchJSON<T[]>(`${this.baseUrl}/batch-create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(entities),
});
}

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),
});
}

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

static async fetchJSON<T>(url: string, options?: RequestInit): Promise<T> {
try {
const response = await fetch(url, { ...options });
ErrorHandler.handleResponse(response, { url, options });
const data = await response.json();
return data;
} catch (error) {
ErrorHandler.handleError(error, { url, options} );
}
}

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 logDetailedError(error: Error, context: { url: string; options?: RequestInit }) {
console.error(`Error requesting ${context.url} with options
${JSON.stringify(context.options)}: `, error);
}

static handleResponse(
response: Response,
context: {
url: string;
options?: RequestInit
}
) {
if (!response.ok) {
this.handleError(new Error(`HTTP error! Status: ${response.status}`), context);
}
return response;
}

static handleError(
error: any,
context: { url: string, options?: RequestInit },
) {
if (error instanceof DOMException && error.name === 'AbortError') {
// Log for informational purposes, but treat it as a non-critical error
console.info("Request was aborted by the client:", error);
return; // Exit early, do not throw further, as this is an expected scenario in abort cases
}

if (error instanceof Response && !error.ok) {
console.error(`HTTP error! Status: ${error.status}`);
} else {
this.logDetailedError(error, context); // Log detailed information about the error
throw new Error(`API Error: ${error.message || "An unknown error occurred"}`);
}
}
}
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