diff --git a/bedhost/_version.py b/bedhost/_version.py index 777f190d..3e2f46a3 100644 --- a/bedhost/_version.py +++ b/bedhost/_version.py @@ -1 +1 @@ -__version__ = "0.8.0" +__version__ = "0.9.0" diff --git a/bedhost/data_models.py b/bedhost/data_models.py index cb524ebf..b81ed2bc 100644 --- a/bedhost/data_models.py +++ b/bedhost/data_models.py @@ -77,3 +77,7 @@ class BaseListResponse(BaseModel): limit: int offset: int results: list + + +class CreateBEDsetRequest(BaseModel): + registry_path: str diff --git a/bedhost/routers/bed_api.py b/bedhost/routers/bed_api.py index 67e8d852..72b79e12 100644 --- a/bedhost/routers/bed_api.py +++ b/bedhost/routers/bed_api.py @@ -15,8 +15,8 @@ BEDFileNotFoundError, TokenizeFileNotExistError, ) +from bbconf.models.bed_models import BedClassification # BedPEPHub, from bbconf.models.bed_models import ( - BedClassification, # BedPEPHub, BedEmbeddingResult, BedFiles, BedListResult, @@ -27,6 +27,8 @@ BedStatsModel, TokenizedBedResponse, TokenizedPathResponse, + QdrantSearchResult, + RefGenValidReturnModel, ) from fastapi import APIRouter, File, HTTPException, Query, UploadFile from fastapi.responses import PlainTextResponse @@ -193,6 +195,27 @@ async def get_bed_pephub( ) +@router.get( + "/{bed_id}/neighbours", + summary="Get nearest neighbours for a single BED record", + response_model=BedListSearchResult, + response_model_by_alias=False, + description=f"Returns most similar BED files in the database. " + f"Example\n bed_id: {EXAMPLE_BED}", +) +async def get_bed_neighbours( + bed_id: str = BedDigest, + limit: int = 10, + offset: int = 0, +): + try: + return bbagent.bed.get_neighbours(bed_id, limit=limit, offset=offset) + except BEDFileNotFoundError as _: + raise HTTPException( + status_code=404, + ) + + @router.get( "/{bed_id}/embedding", summary="Get embeddings for a single BED record", @@ -335,7 +358,52 @@ async def text_to_bed_search(query, limit: int = 10, offset: int = 0): Example: query="cancer" """ _LOGGER.info(f"Searching for: {query}") - results = bbagent.bed.text_to_bed_search(query, limit=limit, offset=offset) + + # results_sql = bbagent.bed.sql_search( + # query, limit=round(limit / 2, 0), offset=round(offset / 2, 0) + # ) + # + # if results_sql.count > results_sql.offset: + # qdrant_offset = offset - results_sql.offset + # else: + # qdrant_offset = offset - results_sql.count + # + # results_qdr = bbagent.bed.text_to_bed_search( + # query, limit=limit, offset=qdrant_offset - 1 if qdrant_offset > 0 else 0 + # ) + # + # results = BedListSearchResult( + # count=results_qdr.count, + # limit=limit, + # offset=offset, + # results=(results_sql.results + results_qdr.results)[0:limit], + # ) + spaceless_query = query.replace(" ", "") + if len(spaceless_query) == 32 and spaceless_query == query: + try: + similar_results = bbagent.bed.get_neighbours( + query, limit=limit, offset=offset + ) + + if similar_results.results and offset == 0: + + result = QdrantSearchResult( + id=query, + payload={}, + score=1.0, + metadata=bbagent.bed.get(query), + ) + + similar_results.results.insert(0, result) + return similar_results + except Exception as _: + pass + + results = bbagent.bed.text_to_bed_search( + query, + limit=limit, + offset=offset, + ) if results: return results @@ -414,3 +482,24 @@ async def get_tokens( status_code=404, detail="Tokenized file not found", ) + + +@router.get( + "/{bed_id}/genome-stats", + summary="Get reference genome validation results", + response_model=RefGenValidReturnModel, +) +async def get_ref_gen_results( + bed_id: str, +): + """ + Return reference genome validation results for a bed file + Example: bed: 0dcdf8986a72a3d85805bbc9493a1302 + """ + try: + return bbagent.bed.get_reference_validation(bed_id) + except BEDFileNotFoundError as _: + raise HTTPException( + status_code=404, + detail=f"Bed file {bed_id} not found", + ) diff --git a/bedhost/routers/bedset_api.py b/bedhost/routers/bedset_api.py index bcf05d3d..33650f89 100644 --- a/bedhost/routers/bedset_api.py +++ b/bedhost/routers/bedset_api.py @@ -1,6 +1,6 @@ import logging -from bbconf.exceptions import BedSetNotFoundError +from bbconf.exceptions import BedSetNotFoundError, BedSetTrackHubLimitError from bbconf.models.bedset_models import ( BedSetBedFiles, BedSetListResult, @@ -8,10 +8,12 @@ BedSetPlots, BedSetStats, ) +from pephubclient.helpers import is_registry_path, unwrap_registry_path from fastapi import APIRouter, HTTPException, Request, Response from ..const import EXAMPLE_BEDSET, PKG_NAME from ..main import bbagent +from ..data_models import CreateBEDsetRequest from ..utils import zip_pep router = APIRouter(prefix="/v1/bedset", tags=["bedset"]) @@ -165,22 +167,78 @@ async def get_trackDb_file_bedset(bedset_id: str): """ Generate trackDb file for the BED set track hub """ + # Response should be this type: + # trackDb_txt = ( + # trackDb_txt + f"track\t {metadata.name}\n" + # "type\t bigBed\n" + # f"bigDataUrl\t {metadata.files.bigbed_file.access_methods[0].access_url.url} \n" + # f"shortLabel\t {metadata.name}\n" + # f"longLabel\t {metadata.description}\n" + # "visibility\t full\n\n" + # ) + try: + trackDb_txt = bbagent.bedset.get_track_hub_file(bedset_id) + except BedSetTrackHubLimitError as _: + raise HTTPException( + status_code=400, + detail="Track hub limit reached. Please try smaller BEDset.", + ) + + return Response(trackDb_txt, media_type="text/plain") - hit = bbagent.bedset.get_bedset_bedfiles(bedset_id) - trackDb_txt = "" - for bed in hit.results: - metadata = bbagent.bed.get(bed.id, full=True) +@router.post( + "/create/", + description="Create a new bedset by providing registry path to the PEPhub project", +) +async def create_bedset(bedset: CreateBEDsetRequest): + """ + Create a new bedset + """ + # Validate the PEPhub project string + if not is_registry_path(bedset.registry_path): + raise HTTPException(status_code=406, detail="Invalid registry path") + + project_reg_path = unwrap_registry_path(bedset.registry_path) - if metadata.files.bigbed_file: + if project_reg_path.namespace not in ["databio", "bedbase", "pepkit"]: + raise HTTPException(status_code=403, detail="User is not in admin list") - trackDb_txt = ( - trackDb_txt + f"track\t {metadata.name}\n" - "type\t bigBed\n" - f"bigDataUrl\t {metadata.files.bigbed_file.access_methods[0].access_url.url} \n" - f"shortLabel\t {metadata.name}\n" - f"longLabel\t {metadata.description}\n" - "visibility\t full\n\n" - ) + try: + project = bbagent.config.phc.load_project(bedset.registry_path) + except Exception as _: + raise HTTPException( + status_code=404, detail=f"Project: '{bedset.registry_path}' not found" + ) + + bedfiles_list = [ + bedfile_id.get("record_identifier") or bedfile_id.sample_name + for bedfile_id in project.samples + ] + + if bbagent.bedset.exists(identifier=project.name): + raise HTTPException( + status_code=409, + detail=f"BEDset with identifier {project.name} already exists", + ) - return Response(trackDb_txt, media_type="text/plain") + try: + bbagent.bedset.create( + identifier=project.name, + name=project.name, + bedid_list=bedfiles_list, + statistics=True, + description=project.description, + annotation={ + "source": project.config.get("source", ""), + "author": project.config.get("author", project_reg_path.namespace), + }, + no_fail=False, + overwrite=False, + ) + except Exception as err: + raise HTTPException( + status_code=400, detail=f"Unable to create bedset. Error: {err}" + ) + + return {"status": "success"} diff --git a/interactive.py b/interactive.py new file mode 100644 index 00000000..88c6ed7d --- /dev/null +++ b/interactive.py @@ -0,0 +1,56 @@ +import bbconf + +bba = bbconf.BedBaseAgent("deployment/config/api-dev.bedbase.org.yaml") + +bba.config._b2bsi = bba.config._init_b2bsi_object() +bba.config._r2v = bba.config._init_r2v_object() +bba.config._bivec = bba.config._init_bivec_object() + + +# Here's some code to test the BiVectorSearchInterface + +from geniml.search.interfaces import BiVectorSearchInterface +from geniml.search.backends import BiVectorBackend + +from geniml.search.query2vec import Text2Vec + +search_backend = BiVectorBackend( + metadata_backend=self._qdrant_text_engine, bed_backend=self._qdrant_engine +) + +t2v = Text2Vec("sentence-transformers/all-MiniLM-L6-v2", v2v=None) + +bvsi = BiVectorSearchInterface() + +from langchain_huggingface.embeddings import HuggingFaceEmbeddings +import logging +from typing import Union + +import numpy as np +from langchain_huggingface.embeddings import HuggingFaceEmbeddings + +from geniml.text2bednn import Vec2VecFNN +from geniml.search.query2vec.abstract import Query2Vec + +# culprit: +te = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2") + +# Testing the sentence transformers: + + +from sentence_transformers import SentenceTransformer + +sentences = ["This is an example sentence", "Each sentence is converted"] + +model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") +embeddings = model.encode(sentences) +print(embeddings) + + +from fastembed import TextEmbedding + +model = TextEmbedding( + model_name="sentence-transformers/all-MiniLM-L6-v2", max_length=512 +) +sentences = ["This is an example sentence", "Each sentence is converted"] +embeddings = list(model.embed(sentences)) diff --git a/requirements/requirements-all.txt b/requirements/requirements-all.txt index 46423aa7..323d72b9 100644 --- a/requirements/requirements-all.txt +++ b/requirements/requirements-all.txt @@ -1,5 +1,5 @@ # bbconf @ git+https://github.com/databio/bbconf.git@dev#egg=bbconf -bbconf>=0.9.0 +bbconf>=0.10.0 fastapi>=0.103.0 logmuse>=0.2.7 markdown @@ -9,4 +9,4 @@ uvicorn yacman>=0.9.2 pephubclient>=0.4.1 psycopg[binary,pool] -python-multipart>=0.0.9 \ No newline at end of file +python-multipart>=0.0.9 diff --git a/ui/bedbase-types.d.ts b/ui/bedbase-types.d.ts index 410d2cbd..fbf47a76 100644 --- a/ui/bedbase-types.d.ts +++ b/ui/bedbase-types.d.ts @@ -78,6 +78,26 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/genomes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get available genomes + * @description Returns statistics + */ + get: operations["get_bedbase_db_stats_v1_genomes_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/service-info": { parameters: { query?: never; @@ -264,6 +284,27 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/bed/{bed_id}/neighbours": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get nearest neighbours for a single BED record + * @description Returns most similar BED files in the database. Example + * bed_id: bbad85f21962bb8d972444f7f9a3a932 + */ + get: operations["get_bed_neighbours_v1_bed__bed_id__neighbours_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/bed/{bed_id}/embedding": { parameters: { query?: never; @@ -304,6 +345,28 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/bed/missing_plots": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get missing plots for a bed file. + * @description Get missing plots for a bed file + * + * example -> plot_id: gccontent + */ + get: operations["missing_plots_v1_bed_missing_plots_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/bed/{bed_id}/regions/{chr_num}": { parameters: { query?: never; @@ -462,6 +525,27 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/bedset/{bedset_id}/pep": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Download PEP project for a single BEDset record + * @description Example + * bed_id: gse218680 + */ + get: operations["get_bedset_pep_v1_bedset__bedset_id__pep_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/bedset/{bedset_id}/metadata/plots": { parameters: { query?: never; @@ -525,6 +609,50 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/bedset/{bedset_id}/track_hub": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Track Hub Bedset + * @description Generate track hub files for the BED set + */ + get: operations["get_track_hub_bedset_v1_bedset__bedset_id__track_hub_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + /** + * Get Track Hub Bedset + * @description Generate track hub files for the BED set + */ + head: operations["get_track_hub_bedset_v1_bedset__bedset_id__track_hub_head"]; + patch?: never; + trace?: never; + }; + "/v1/bedset/create/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create Bedset + * @description Create a new bedset by providing registry path to the PEPhub project + */ + post: operations["create_bedset_v1_bedset_create__post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/objects/{object_id}": { parameters: { query?: never; @@ -626,6 +754,17 @@ export interface components { /** Headers */ headers?: Record | null; }; + /** BaseListResponse */ + BaseListResponse: { + /** Count */ + count: number; + /** Limit */ + limit: number; + /** Offset */ + offset: number; + /** Results */ + results: unknown[]; + }; /** BedClassification */ BedClassification: { /** Name */ @@ -960,6 +1099,7 @@ export interface components { widths_histogram?: components["schemas"]["FileModel"]; neighbor_distances?: components["schemas"]["FileModel"]; open_chromatin?: components["schemas"]["FileModel"]; + tss_distance?: components["schemas"]["FileModel"]; }; /** BedSetBedFiles */ BedSetBedFiles: { @@ -987,12 +1127,26 @@ export interface components { name: string; /** Md5Sum */ md5sum: string; + /** + * Submission Date + * Format: date-time + */ + submission_date?: string; + /** + * Last Update Date + * Format: date-time + */ + last_update_date?: string; statistics?: components["schemas"]["BedSetStats"] | null; plots?: components["schemas"]["BedSetPlots"] | null; /** Description */ description?: string; /** Bed Ids */ bed_ids?: string[]; + /** Author */ + author?: string | null; + /** Source */ + source?: string | null; }; /** BedSetMinimal */ BedSetMinimal: { @@ -1014,7 +1168,7 @@ export interface components { }; /** BedStatsModel */ BedStatsModel: { - /** Regions No */ + /** Number Of Regions */ number_of_regions?: number | null; /** Gc Content */ gc_content?: number | null; @@ -1080,6 +1234,11 @@ export interface components { /** Openapi Version */ openapi_version: string; }; + /** CreateBEDsetRequest */ + CreateBEDsetRequest: { + /** Registry Path */ + registry_path: string; + }; /** DRSModel */ DRSModel: { /** Id */ @@ -1144,7 +1303,7 @@ export interface components { /** Id */ id: string; /** Payload */ - payload: Record; + payload?: Record; /** Score */ score: number; metadata?: components["schemas"]["BedMetadataBasic"] | null; @@ -1418,6 +1577,26 @@ export interface operations { }; }; }; + get_bedbase_db_stats_v1_genomes_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BaseListResponse"]; + }; + }; + }; + }; service_info_v1_service_info_get: { parameters: { query?: never; @@ -1689,6 +1868,41 @@ export interface operations { }; }; }; + get_bed_neighbours_v1_bed__bed_id__neighbours_get: { + parameters: { + query?: { + limit?: number; + offset?: number; + }; + header?: never; + path: { + /** @description BED digest */ + bed_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BedListSearchResult"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; get_bed_embedding_v1_bed__bed_id__embedding_get: { parameters: { query?: never; @@ -1754,6 +1968,37 @@ export interface operations { }; }; }; + missing_plots_v1_bed_missing_plots_get: { + parameters: { + query: { + plot_id: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BaseListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; get_regions_for_bedfile_v1_bed__bed_id__regions__chr_num__get: { parameters: { query?: { @@ -2015,6 +2260,37 @@ export interface operations { }; }; }; + get_bedset_pep_v1_bedset__bedset_id__pep_get: { + parameters: { + query?: never; + header?: never; + path: { + bedset_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; get_bedset_metadata_v1_bedset__bedset_id__metadata_plots_get: { parameters: { query?: never; @@ -2108,6 +2384,101 @@ export interface operations { }; }; }; + get_track_hub_bedset_v1_bedset__bedset_id__track_hub_get: { + parameters: { + query?: never; + header?: never; + path: { + bedset_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_track_hub_bedset_v1_bedset__bedset_id__track_hub_head: { + parameters: { + query?: never; + header?: never; + path: { + bedset_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_bedset_v1_bedset_create__post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateBEDsetRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; get_drs_object_metadata_v1_objects__object_id__get: { parameters: { query?: never; diff --git a/ui/src/components/bed-splash-components/cards/mean-region-width-card.tsx b/ui/src/components/bed-splash-components/cards/mean-region-width-card.tsx index 3a4f2b30..f094631b 100644 --- a/ui/src/components/bed-splash-components/cards/mean-region-width-card.tsx +++ b/ui/src/components/bed-splash-components/cards/mean-region-width-card.tsx @@ -12,7 +12,7 @@ export const MeanRegionWidthCard = (props: Props) => { return ( diff --git a/ui/src/components/bed-splash-components/cards/no-regions-card.tsx b/ui/src/components/bed-splash-components/cards/no-regions-card.tsx index 998e1307..b38836ca 100644 --- a/ui/src/components/bed-splash-components/cards/no-regions-card.tsx +++ b/ui/src/components/bed-splash-components/cards/no-regions-card.tsx @@ -11,7 +11,7 @@ export const NoRegionsCard = (props: Props) => { const { metadata } = props; return ( diff --git a/ui/src/components/bed-splash-components/header.tsx b/ui/src/components/bed-splash-components/header.tsx index 92c37ab6..c8726472 100644 --- a/ui/src/components/bed-splash-components/header.tsx +++ b/ui/src/components/bed-splash-components/header.tsx @@ -27,13 +27,13 @@ export const BedSplashHeader = (props: Props) => { return (
-
-
-

- - {metadata?.id || 'No name available'} +
+
+

+ + {metadata?.id || 'No ID available'}

-

{metadata.name}

- -
-
+
+
{metadata.name}
+

{metadata?.description || 'No description available'}

+
+
+

{

} > -
+ {metadata?.genome_digest ? ( + +
+ + {metadata.genome_alias || 'No assembly available'} +
+
+ ) : (
- {metadata?.genome_alias || 'No assembly available'} + {metadata.genome_alias || 'No assembly available'}
- + )}

@@ -243,19 +253,19 @@ export const BedSplashHeader = (props: Props) => {
)}
-
-
- +
+
+

Created:{' '} {metadata?.submission_date ? formatDateTime(metadata?.submission_date) : 'No date available'}

-
- +
+

- Last update:{' '} + Updated:{' '} {metadata?.last_update_date ? formatDateTime(metadata?.last_update_date) : 'No date available'}

diff --git a/ui/src/components/bed-splash-components/plots.tsx b/ui/src/components/bed-splash-components/plots.tsx index a773d493..d04546ee 100644 --- a/ui/src/components/bed-splash-components/plots.tsx +++ b/ui/src/components/bed-splash-components/plots.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { Col, Image, Row } from 'react-bootstrap'; import { components } from '../../../bedbase-types'; -import { chunkArray, makeThumbnailImageLink } from '../../utils'; +import { chunkArray, makeThumbnailImageLink, makePDFImageLink } from '../../utils'; import { Fragment } from 'react'; import { FigureModal } from '../modals/figure-modal'; @@ -13,12 +13,13 @@ type PlotsProps = { type PlotProps = { src: string; + pdf: string; alt: string; title: string; }; const Plot = (props: PlotProps) => { - const { src, alt, title } = props; + const { src, pdf, alt, title } = props; const [show, setShow] = useState(false); return ( @@ -31,13 +32,13 @@ const Plot = (props: PlotProps) => { }} className="h-100 border rounded p-1 shadow-sm hover-border-primary transition-all" > -
- {title} +
+ {title} {/* */}
-
+
{alt}
{ }} title={title} src={src} + pdf={pdf} alt={alt} />
@@ -55,13 +57,14 @@ const Plot = (props: PlotProps) => { export const Plots = (props: PlotsProps) => { const { metadata } = props; + const plotNames = metadata.plots ? Object.keys(metadata.plots) : []; return ( -
+ {metadata.plots && chunkArray(plotNames, 3).map((chunk, idx) => ( - + {chunk.map((plotName) => { // this is for type checking const plotNameKey = plotName as keyof typeof metadata.plots; @@ -70,22 +73,23 @@ export const Plots = (props: PlotsProps) => { const title = plotExists ? metadata.plots[plotNameKey]?.title : plotName; const alt = plotExists ? // @ts-expect-error: type checking here is just too much - metadata.plots[plotNameKey]?.description || metadata.plots[plotNameKey].title + metadata.plots[plotNameKey]?.description || metadata.plots[plotNameKey].title : plotName; return ( ); })} - + ))} -
+
); }; diff --git a/ui/src/components/bedset-splash-components/beds-table.tsx b/ui/src/components/bedset-splash-components/beds-table.tsx index 69e2e002..0163d328 100644 --- a/ui/src/components/bedset-splash-components/beds-table.tsx +++ b/ui/src/components/bedset-splash-components/beds-table.tsx @@ -139,65 +139,73 @@ export const BedsTable = (props: Props) => { }); return ( -
+
setGlobalFilter(e.target.value)} />
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - (window.location.href = `/bed/${row.original.id}`)} - > - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} - -
- {header.isPlaceholder ? null : ( -
- {flexRender(header.column.columnDef.header, header.getContext())} - {{ - asc: ' 🔼', - desc: ' 🔽', - }[header.column.getIsSorted() as string] ?? null} -
- )} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + (window.location.href = `/bed/${row.original.id}`)} + > + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: , + }[header.column.getIsSorted() as string] ?? null} +
+ )} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
-
+
Showing diff --git a/ui/src/components/bedset-splash-components/cards/median-region-width.tsx b/ui/src/components/bedset-splash-components/cards/median-region-width.tsx index ca2a9ea7..b198fec2 100644 --- a/ui/src/components/bedset-splash-components/cards/median-region-width.tsx +++ b/ui/src/components/bedset-splash-components/cards/median-region-width.tsx @@ -11,7 +11,7 @@ export const MeanRegionWidthCard = (props: Props) => { const { metadata } = props; return ( - +

{formatNumberWithCommas(Math.round(metadata.statistics?.mean?.mean_region_width || 0))} bp diff --git a/ui/src/components/bedset-splash-components/header.tsx b/ui/src/components/bedset-splash-components/header.tsx index 1e7a3476..00ed080c 100644 --- a/ui/src/components/bedset-splash-components/header.tsx +++ b/ui/src/components/bedset-splash-components/header.tsx @@ -1,8 +1,11 @@ import { useState } from 'react'; +import { Dropdown } from 'react-bootstrap'; import { components } from '../../../bedbase-types'; import { useBedCart } from '../../contexts/bedcart-context'; import { DownloadBedSetModal } from '../modals/download-bedset-modal'; -import {useCopyToClipboard} from "@uidotdev/usehooks"; +import { useCopyToClipboard } from '@uidotdev/usehooks'; +import { formatDateTime } from '../../utils.ts'; + type BedSetMetadata = components['schemas']['BedSetMetadata']; type Props = { @@ -22,13 +25,13 @@ export const BedsetSplashHeader = (props: Props) => { return (
-
-
-

- +
+
+

+ {metadata?.id || 'No name available'}

- -
-

{metadata?.description || 'No description available'}

+
+

{metadata?.description || 'No description available'}

+

Author: {metadata?.author || 'None'}

+

Source: {metadata?.source || 'None'}

-
-
- - {metadata.md5sum} +
+
+

+

+ + {metadata.md5sum} +
+

+ {metadata.bed_ids && ( +

+

+ + {metadata.bed_ids?.length} BED files +
+

+ )}
-
- - {metadata.bed_ids?.length} BED files + +
+
+ +

+ Created:{' '} + {metadata?.submission_date ? formatDateTime(metadata?.submission_date) : 'No date available'} +

+
+ +
+ +

+ Updated:{' '} + {metadata?.last_update_date ? formatDateTime(metadata?.last_update_date) : 'No date available'} +

+
diff --git a/ui/src/components/bedset-splash-components/plots.tsx b/ui/src/components/bedset-splash-components/plots.tsx index d2575cc5..4e2cdce3 100644 --- a/ui/src/components/bedset-splash-components/plots.tsx +++ b/ui/src/components/bedset-splash-components/plots.tsx @@ -58,10 +58,10 @@ export const Plots = (props: PlotsProps) => { const plotNames = metadata.plots ? Object.keys(metadata.plots) : []; return ( -
+ {metadata.plots && chunkArray(plotNames, 3).map((chunk, idx) => ( - + {chunk.map((plotName) => { // this is for type checking const plotNameKey = plotName as keyof typeof metadata.plots; @@ -83,9 +83,9 @@ export const Plots = (props: PlotsProps) => { ); })} - + ))} -
+
); }; diff --git a/ui/src/components/layout.tsx b/ui/src/components/layout.tsx index 6d8ee6aa..f006bcec 100644 --- a/ui/src/components/layout.tsx +++ b/ui/src/components/layout.tsx @@ -27,6 +27,9 @@ const Footer = () => { bbconf {data?.component_versions?.bbconf_version || ''} + + geniml {data?.component_versions?.geniml_version || ''} + Python {data?.component_versions?.python_version || ''} diff --git a/ui/src/components/modals/create-bedset-modal.tsx b/ui/src/components/modals/create-bedset-modal.tsx new file mode 100644 index 00000000..30d0c96a --- /dev/null +++ b/ui/src/components/modals/create-bedset-modal.tsx @@ -0,0 +1,131 @@ +import { useCopyToClipboard } from '@uidotdev/usehooks'; +import { useState } from 'react'; +import { Modal } from 'react-bootstrap'; +import Markdown from 'react-markdown'; +import { generateBEDsetPEPMd, generateBEDsetPEPDownloadRaw } from '../../utils'; +import { useBedCart } from '../../contexts/bedcart-context'; +import rehypeHighlight from 'rehype-highlight'; +import axios from 'axios'; + +type Props = { + show: boolean; + setShow: (show: boolean) => void; +}; + +const API_BASE = import.meta.env.VITE_API_BASE || ''; +const API_ENDPOINT = `${API_BASE}/bedset/create`; + +export const generateBEDsetCreationDescription = () => { + const text = ` + **To create a new BEDset:** + + 1. Create PEP in [PEPhub](https://pephub.databio.org/), by copying the text below, and pasting it into the sample table. + The name of the PEP project will be used as the name and identifier for the BEDset. + 2. Add source, author, and other metadata to config file. e.g. + \`\`\`json + 'author': "BEDbase team", + 'source': "BEDbase", + \`\`\` + 3. Use 'Submit PEP' form below or BEDbase API ([${API_BASE}](${API_BASE}/docs#/bedset/create_bedset_v1_bedset_create__post)) and + create a new BEDset by providing the registry path in the Body of the request. (Registry path can be copied from the PEPhub): + \`\`\`json + { + "registry_path": "namespace/name:tag" + } + \`\`\` + **Note**: We currently only support PEPs from the bedbase team. If you want to create new bedset, please create an issue: [https://github.com/databio/bedbase](https://github.com/databio/bedbase/issues) + + `; + return text; +}; + +export const CreateBedSetModal = (props: Props) => { + const { show, setShow } = props; + const { cart } = useBedCart(); + const [, copyToClipboard] = useCopyToClipboard(); + const [copied, setCopied] = useState(false); + + const [inputValue, setInputValue] = useState(''); + const [message, setMessage] = useState(''); + + const handleInputRegistryPathChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + }; + + const handleSubmit = async () => { + try { + await axios.post(API_ENDPOINT, { registry_path: inputValue }); + setMessage('Successfully created BEDset!'); + } catch (error) { + const err = error as Error & { response?: { data?: { detail?: string } } }; + const errorMessage = err.response?.data?.detail || err.message; + setMessage(`! Unable to create BEDset. ${errorMessage}`); + } + }; + + + return ( + setShow(false)} + size="xl" + aria-labelledby="contained-modal-title-vcenter" + centered + > + + +
+

Create BEDset

+
+
+ + +
+ + {generateBEDsetCreationDescription()} + +
+ +
+ {generateBEDsetPEPMd(cart)} +
+ +
+
+ +
+ +
+ + Submit PEP + +
+ + +
+ {message &&
{message}
} + +
+ +
+
+ ); +}; diff --git a/ui/src/components/modals/figure-modal.tsx b/ui/src/components/modals/figure-modal.tsx index a60a7004..05d6a6fc 100644 --- a/ui/src/components/modals/figure-modal.tsx +++ b/ui/src/components/modals/figure-modal.tsx @@ -3,13 +3,15 @@ import { Modal } from 'react-bootstrap'; type Props = { title: string; src: string; + pdf?: string; alt: string; show: boolean; onHide: () => void; }; export const FigureModal = (props: Props) => { - const { title, src, alt, show, onHide } = props; + const { title, src, pdf, alt, show, onHide } = props; + return ( { link.click(); }} > - Download + Download PNG + {pdf && ( + + )} + diff --git a/ui/src/components/nav/nav-mobile.tsx b/ui/src/components/nav/nav-mobile.tsx index bc1fa4d7..4248f17b 100644 --- a/ui/src/components/nav/nav-mobile.tsx +++ b/ui/src/components/nav/nav-mobile.tsx @@ -6,7 +6,7 @@ export const MobileNav = () => { - + GitHub diff --git a/ui/src/components/search/bed2bed/b2b-search-results-table.tsx b/ui/src/components/search/bed2bed/b2b-search-results-table.tsx index 844a4271..e5802e51 100644 --- a/ui/src/components/search/bed2bed/b2b-search-results-table.tsx +++ b/ui/src/components/search/bed2bed/b2b-search-results-table.tsx @@ -28,6 +28,25 @@ type Props = { const columnHelper = createColumnHelper(); +const scoreTooltip = ( + +
+            Cosine similarity between files.
+            Score is between 0 an 100, where 100 is a perfect match.
+          
+ + } + > + + Score* + + +
+) + export const Bed2BedSearchResultsTable = (props: Props) => { const { beds } = props; const { cart, addBedToCart, removeBedFromCart } = useBedCart(); @@ -118,7 +137,7 @@ export const Bed2BedSearchResultsTable = (props: Props) => { ), footer: (info) => info.column.id, - header: 'Score', + header: () => scoreTooltip, id: 'score', }), columnHelper.accessor('metadata.id', { @@ -183,61 +202,67 @@ export const Bed2BedSearchResultsTable = (props: Props) => { getFilteredRowModel: getFilteredRowModel(), }); + const handleRowClick = (id?: string) => (e: React.MouseEvent) => { + if (!(e.target as HTMLElement).closest('button')) { + window.location.href = `/bed/${id}`; + } + }; + return ( -
+
setGlobalFilter(e.target.value)} />
- +
- {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - + {headerGroup.headers.map((header) => ( + - ))} - - ))} + : undefined + } + > + {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: , + }[header.column.getIsSorted() as string] ?? null} + + )} + + ))} + + ))} - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))}
- {header.isPlaceholder ? null : ( -
( +
+ {header.isPlaceholder ? null : ( +
- {flexRender(header.column.columnDef.header, header.getContext())} - {{ - asc: ' 🔼', - desc: ' 🔽', - }[header.column.getIsSorted() as string] ?? null} -
- )} -
{flexRender(cell.column.columnDef.cell, cell.getContext())}
{flexRender(cell.column.columnDef.cell, cell.getContext())}
-
-
+
+
Showing {table.getState().pagination.pageSize * table.getState().pagination.pageIndex + 1} to{' '} diff --git a/ui/src/components/search/search-bedset-table.tsx b/ui/src/components/search/search-bedset-table.tsx index 9417728a..01f68eb5 100644 --- a/ui/src/components/search/search-bedset-table.tsx +++ b/ui/src/components/search/search-bedset-table.tsx @@ -9,6 +9,12 @@ type Props = { export const SearchBedSetResultTable = (props: Props) => { const { results } = props; + const handleRowClick = (id?: string) => (e: React.MouseEvent) => { + if (!(e.target as HTMLElement).closest('button')) { + window.location.href = `/bedset/${id}`; + } + }; + return ( @@ -24,14 +30,14 @@ export const SearchBedSetResultTable = (props: Props) => { {results.results?.map((result) => ( - + +
{result?.id || 'Unknown Id'} {result?.name || 'Unknown Name'} {result?.description || 'Unknown Description'} {result?.bed_ids?.length || 0} - - diff --git a/ui/src/components/search/text2bed/t2b-search-results-table.tsx b/ui/src/components/search/text2bed/t2b-search-results-table.tsx index c5f97658..a19a5f44 100644 --- a/ui/src/components/search/text2bed/t2b-search-results-table.tsx +++ b/ui/src/components/search/text2bed/t2b-search-results-table.tsx @@ -7,51 +7,101 @@ import toast from 'react-hot-toast'; import YAML from 'js-yaml'; type SearchResponse = components['schemas']['BedListSearchResult']; +// type BedNeighboursResponse = components['schemas']['BedNeighboursResult']; type Props = { results: SearchResponse; + search_query?: string | undefined; +}; + +const IsUnique = (name: string, found_id: string, search_id: string) => { + if (found_id === search_id) { + return ( +
+ {name}   + +
+
Exact match
+
+ } + > +
+
+
+ +
+ ); + } else { + return name; + } }; export const Text2BedSearchResultsTable = (props: Props) => { - const { results } = props; + const { results, search_query } = props; const { cart, addBedToCart, removeBedFromCart } = useBedCart(); + + const handleRowClick = (id?: string) => (e: React.MouseEvent) => { + if (!(e.target as HTMLElement).closest('button')) { + window.location.href = `/bed/${id}`; + } + }; + return ( - - - - - - - - - {/**/} - {/**/} - - - - - {/* */} - - - - - {results.results?.map((result) => ( - - +
+
NameGenomeTissueCell LineCell TypeTarget AntibodyDescriptionAssayInfoScoreBEDbase ID - Actions -
{result?.metadata?.name || 'No name'}
+ + + + + + + + + + + + + + + + {results.results?.map((result) => ( + + - {/**/} - {/**/} - - {/**/} - - {/* */} - {/**/} ))} - -
NameGenomeTissueCell LineCell TypeDescriptionAssayInfo + +
+                      Cosine similarity between search term and bedfile.
+                      Score is between 0 an 100, where 100 is a perfect match.
+                    
+ + } + > + + Score* + + +
+ +
+ Actions +
{IsUnique(result?.metadata?.name || 'No name', result.id, search_query || '') || 'No name'} {result?.metadata?.genome_alias || 'N/A'} {result?.metadata?.annotation?.tissue || 'N/A'} {result?.metadata?.annotation?.cell_line || 'N/A'} {result?.metadata?.annotation?.cell_type || 'N/A'}{result?.metadata?.annotation?.target || 'N/A'}{result?.metadata?.annotation?.antibody || 'N/A'}{result?.metadata?.description || ''} {result?.metadata?.annotation?.assay || 'N/A'} { } > - + @@ -77,30 +127,17 @@ export const Text2BedSearchResultsTable = (props: Props) => { variant="primary" /> {result?.metadata?.id || 'No id'}*/} - {/* /!*{result?.metadata?.submission_date === undefined*!/*/} - {/* /!* ? 'No date'*!/*/} - {/* /!* : new Date(result.metadata?.submission_date).toLocaleDateString()}*!/\*/} - {/* */} - {/* - - {/**/} - {cart.includes(result?.metadata?.id || '') ? ( ) : (
+
+
); }; diff --git a/ui/src/components/search/text2bed/text2bed.tsx b/ui/src/components/search/text2bed/text2bed.tsx index 1c27b70a..b1541683 100644 --- a/ui/src/components/search/text2bed/text2bed.tsx +++ b/ui/src/components/search/text2bed/text2bed.tsx @@ -65,9 +65,9 @@ export const Text2Bed = () => { ) : (
{results ? ( -
+
- {' '} + {' '}
) : ( diff --git a/ui/src/components/search/text2bedset.tsx b/ui/src/components/search/text2bedset.tsx index b24a9fb2..f71607da 100644 --- a/ui/src/components/search/text2bedset.tsx +++ b/ui/src/components/search/text2bedset.tsx @@ -60,8 +60,10 @@ export const Text2BedSet = () => { ) : (
{results ? ( -
- +
+
+ +
{' '}
diff --git a/ui/src/custom.scss b/ui/src/custom.scss index 4bb6466b..7be2237c 100644 --- a/ui/src/custom.scss +++ b/ui/src/custom.scss @@ -298,4 +298,16 @@ a { .small-font { font-size: 0.9rem; /* Adjust the value as needed */ -} \ No newline at end of file +} + +th, +td { + padding-left: 1rem !important; + padding-right: 1rem !important; +} + +.markdown-bg code { + border-radius: 1rem !important; + color: #212529; + background: #e9ecef; +} diff --git a/ui/src/motions/landing-animations.tsx b/ui/src/motions/landing-animations.tsx index e1728718..b714e2f7 100644 --- a/ui/src/motions/landing-animations.tsx +++ b/ui/src/motions/landing-animations.tsx @@ -7,7 +7,9 @@ const STROKE_SPEAD = 0; export const InPaths = () => { return ( { export const OutPaths = () => { return ( { const [showDownloadModal, setShowDownloadModal] = useState(false); + const [showCreateBedsetModal, setCreateBedSetModal] = useState(false); const { cart, removeBedFromCart } = useBedCart(); + const handleRowClick = (id?: string) => (e: React.MouseEvent) => { + if (!(e.target as HTMLElement).closest('button')) { + window.location.href = `/bed/${id}`; + } + }; + if (cart.length === 0) { return ( @@ -14,7 +22,7 @@ export const BedCart = () => {

Your cart is empty

Try searching for some bedfiles!

-
- - +
+
+
+ - - - - {cart.map((item) => ( - - - - ))} - -
Item Action
{item} - - - - -
+ + + {cart.map((item) => ( + + {item} + + + + + ))} + + +
+ ); }; diff --git a/ui/src/pages/bed-splash.tsx b/ui/src/pages/bed-splash.tsx index cd5fb4c0..db18767e 100644 --- a/ui/src/pages/bed-splash.tsx +++ b/ui/src/pages/bed-splash.tsx @@ -13,6 +13,12 @@ import { Plots } from '../components/bed-splash-components/plots'; import { AxiosError } from 'axios'; import { GCContentCard } from '../components/bed-splash-components/cards/gc-content-card'; import { snakeToTitleCase } from '../utils'; +import { Text2BedSearchResultsTable } from '../components/search/text2bed/t2b-search-results-table'; +import { useBedNeighbours } from '../queries/useBedNeighbours'; +import type { components } from '../../bedbase-types.d.ts'; + +// Use the response type to properly type the metadata +type BedMetadata = components['schemas']['BedMetadataAll']; export const BedSplash = () => { const params = useParams(); @@ -28,6 +34,26 @@ export const BedSplash = () => { full: true, }); + const { data: neighbours } = useBedNeighbours({ + md5: bedId, + limit: 10, + offset: 0, + }); + + // Helper function to safely type the annotation keys + const getAnnotationValue = (data: BedMetadata | undefined, key: string) => { + if (!data?.annotation) return null; + return (data.annotation as Record)[key]; + }; + + // Helper function to get filtered keys + const getFilteredKeys = (data: BedMetadata | undefined) => { + if (!data?.annotation) return []; + return Object.keys(data.annotation).filter( + (k) => k !== 'input_file' && k !== 'file_name' && k !== 'sample_name' && getAnnotationValue(data, k), + ); + }; + if (isLoading) { return ( @@ -68,10 +94,10 @@ export const BedSplash = () => { >

Oh no!

-

+

We could not find BED with record identifier:
{bedId} -

+

@@ -108,96 +134,124 @@ export const BedSplash = () => { {metadata !== undefined ? : null} - + -

Overview

-
- - - - - - - - - {Object.keys(metadata?.annotation || {}).map((k) => { - if (k === 'input_file' || k === 'file_name' || k === 'sample_name') { - return null; - // @ts-expect-error wants to get mad because it could be an object and React cant render that (it wont be) - } else if (!metadata?.annotation[k]) { - return null; - } else { +

Overview

+
+
+
KeyValue
+ + + + + + + + {Object.keys(metadata?.annotation || {}).map((k) => { + if (k === 'input_file' || k === 'file_name' || k === 'sample_name') { + return null; + } + + const value = getAnnotationValue(metadata, k); + if (!value) { + return null; + } + return ( - ); - } - })} - -
KeyValue
{snakeToTitleCase(k)} - {/* @ts-expect-error wants to get mad because it could be an object and React cant render that (it wont be) */} - {metadata?.annotation[k] || 'N/A'} + {value ?? 'N/A'}
+ })} + + +
- -

BED Sets

-
-

Statistics

- + + +

Statistics

{metadata && ( - + )} - + - {/* */}
-

Plots

+ - +

Plots

+ + +
+ + {neighbours && ( + +

Similar BED Files

+ + + +
+ )}
); diff --git a/ui/src/pages/bedset-splash.tsx b/ui/src/pages/bedset-splash.tsx index d31ccf6a..8cdf0fca 100644 --- a/ui/src/pages/bedset-splash.tsx +++ b/ui/src/pages/bedset-splash.tsx @@ -64,17 +64,17 @@ export const BedsetSplash = () => { } else if (error) { if ((error as AxiosError)?.response?.status === 404) { return ( - +

Oh no!

-

+

We could not find BEDset with record identifier:
{bedsetId} -

+

@@ -102,46 +102,49 @@ export const BedsetSplash = () => { } } else { return ( - +

- - {metadata !== undefined ? : null} - + {metadata !== undefined ? : null} -

Statistics

- + + +

Statistics

{metadata && ( - - - - - - - - - - + + + + + )} + + +
-

Plots

+ - + +

Plots

+ +
-

BED files in this BED set

+ - {isLoadingBedfiles ? ( -
- - {Array.from({ length: 10 }).map((_, index) => ( -
- -
- ))} -
- ) : ( - bedfiles && - )} +

Constituent BED Files

+ + {isLoadingBedfiles ? ( +
+ + {Array.from({ length: 10 }).map((_, index) => ( +
+ +
+ ))} +
+ ) : ( + bedfiles && + )} +
diff --git a/ui/src/pages/home.tsx b/ui/src/pages/home.tsx index 23f82420..e3bb3323 100644 --- a/ui/src/pages/home.tsx +++ b/ui/src/pages/home.tsx @@ -51,8 +51,8 @@ export const Home = () => { {/*
*/}

Welcome to BEDbase

-
-

+

+

BEDbase is a unified platform for aggregating, analyzing, and serving genomic region data. BEDbase redefines the way to manage genomic region data and allows users to search for BED files of interest and create collections tailored to research needs. BEDbase is composed of a web server and an API. Users can explore @@ -60,7 +60,7 @@ export const Home = () => { interact with the data via an OpenAPI-compatible API.

-
+
{
-
- Or, explore an example BED file or a{' '} - example BED set +
+ Or, explore an example BED file + or an{' '} example BEDset
@@ -269,7 +269,7 @@ export const Home = () => { {/*
*/} - +

BEDbase client

@@ -338,7 +338,7 @@ export const Home = () => {

Data Availability Summary

-
+
  • Number of bed files available: diff --git a/ui/src/queries/useBedNeighbours.ts b/ui/src/queries/useBedNeighbours.ts new file mode 100644 index 00000000..82ca34a2 --- /dev/null +++ b/ui/src/queries/useBedNeighbours.ts @@ -0,0 +1,24 @@ +import { useQuery } from '@tanstack/react-query'; +import { useBedbaseApi } from '../contexts/api-context'; +import { components } from '../../bedbase-types'; + +type BedNeighboursResponse = components['schemas']['BedListSearchResult']; +type BedNeighboursQuery = { + md5?: string; + limit?: number; + offset?: number; +}; + +export const useBedNeighbours = (query: BedNeighboursQuery) => { + const { api } = useBedbaseApi(); + + const { md5, limit } = query; + + return useQuery({ + queryKey: ['neighbours', md5], + queryFn: async () => { + const { data } = await api.get(`/bed/${md5}/neighbours?limit=${limit}`); + return data; + }, + }); +}; diff --git a/ui/src/queries/useBedSetBedfiles.ts b/ui/src/queries/useBedSetBedfiles.ts index ffa5edcd..220edbd1 100644 --- a/ui/src/queries/useBedSetBedfiles.ts +++ b/ui/src/queries/useBedSetBedfiles.ts @@ -1,6 +1,6 @@ import type { components } from '../../bedbase-types'; import { useQuery } from '@tanstack/react-query'; -import { useBedbaseApi } from '../contexts/api-context.tsx'; +import { useBedbaseApi } from '../contexts/api-context'; type BedSetBedfilesResponse = components['schemas']['BedSetBedFiles']; @@ -11,11 +11,7 @@ type BedSetBedfilesQuery = { export const useBedsetBedfiles = (query: BedSetBedfilesQuery) => { const { api } = useBedbaseApi(); - const { id, autoRun } = query; - let enabled = false; - if (autoRun !== undefined && autoRun && id) { - enabled = true; - } + const { id } = query; return useQuery({ queryKey: ['bedset-bedfiles', id], @@ -23,7 +19,10 @@ export const useBedsetBedfiles = (query: BedSetBedfilesQuery) => { const { data } = await api.get(`/bedset/${id}/bedfiles`); return data; }, - enabled: enabled, - staleTime: 0, + enabled: Boolean(id), + staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes + gcTime: 30 * 60 * 1000, // This replaces cacheTime in newer versions + refetchOnWindowFocus: false, + refetchOnMount: false, }); }; diff --git a/ui/src/queries/useBedsetMetadata.ts b/ui/src/queries/useBedsetMetadata.ts index e847bf9f..f866de30 100644 --- a/ui/src/queries/useBedsetMetadata.ts +++ b/ui/src/queries/useBedsetMetadata.ts @@ -1,6 +1,6 @@ import type { components } from '../../bedbase-types'; import { useQuery } from '@tanstack/react-query'; -import { useBedbaseApi } from '../contexts/api-context.tsx'; +import { useBedbaseApi } from '../contexts/api-context'; type BedSetMetadataResponse = components['schemas']['BedSetMetadata']; @@ -11,11 +11,7 @@ type BedSetMetadataQuery = { export const useBedsetMetadata = (query: BedSetMetadataQuery) => { const { api } = useBedbaseApi(); - const { md5, autoRun } = query; - let enabled = false; - if (autoRun !== undefined && autoRun && md5) { - enabled = true; - } + const { md5 } = query; return useQuery({ queryKey: ['bedset-metadata', md5], @@ -23,7 +19,10 @@ export const useBedsetMetadata = (query: BedSetMetadataQuery) => { const { data } = await api.get(`/bedset/${md5}/metadata`); return data; }, - enabled: enabled, - staleTime: 0, + enabled: Boolean(md5), + staleTime: 5 * 60 * 1000, + gcTime: 30 * 60 * 1000, // This replaces cacheTime in newer versions + refetchOnWindowFocus: false, + refetchOnMount: false, }); }; diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 1af3a653..4cc01e5b 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -23,6 +23,11 @@ export const makeThumbnailImageLink = (md5: string, plotName: string, type: Obje return `${API_BASE}/objects/${type}.${md5}.${plotName}/access/http/thumbnail`; }; +export const makePDFImageLink = (md5: string, plotName: string, type: ObjectType) => { + const API_BASE = import.meta.env.VITE_API_BASE || ''; + return `${API_BASE}/objects/${type}.${md5}.${plotName}/access/http/bytes`; +}; + export const formatDateTime = (date: string) => { return new Date(date).toLocaleString(); }; @@ -34,6 +39,21 @@ export const bytesToSize = (bytes: number) => { return Math.round(bytes / Math.pow(1024, i)) + ' ' + sizes[i]; }; +export const generateBEDsetPEPMd = (md5List: string[]) => { + const script = ` + \`\`\`text + sample_name\n${md5List.join('\n')} + \`\`\` + `; + return script; +}; + +export const generateBEDsetPEPDownloadRaw = (md5List: string[]) => { + const script = `sample_name\n${md5List.join('\n')}`; + return script; +}; + + export const generateCurlScriptForCartDownloadMd = (md5List: string[]) => { const wgetCommands = md5List.map((md5, index) => { const downloadLink = makeHttpDownloadLink(md5);