Skip to content
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