From a7662f4683cab59b86d4b7a8f0a2b522e1e7ac5c Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 21 Nov 2024 11:46:08 -0500 Subject: [PATCH] feat: add reference service slice + gene search hook --- src/js/components/BentoAppRouter.tsx | 2 + src/js/constants/configConstants.ts | 4 +- src/js/features/reference/hooks.ts | 45 +++++++++++++++ src/js/features/reference/reference.store.ts | 60 ++++++++++++++++++++ src/js/features/reference/types.ts | 41 +++++++++++++ src/js/store.ts | 2 + src/js/types/ontology.ts | 4 ++ 7 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 src/js/features/reference/hooks.ts create mode 100644 src/js/features/reference/reference.store.ts create mode 100644 src/js/features/reference/types.ts create mode 100644 src/js/types/ontology.ts diff --git a/src/js/components/BentoAppRouter.tsx b/src/js/components/BentoAppRouter.tsx index c68f8278..c2545bb5 100644 --- a/src/js/components/BentoAppRouter.tsx +++ b/src/js/components/BentoAppRouter.tsx @@ -13,6 +13,7 @@ import { fetchGohanData, fetchKatsuData } from '@/features/ingestion/lastIngesti import { makeGetDataTypes } from '@/features/dataTypes/dataTypes.store'; import { useMetadata } from '@/features/metadata/hooks'; import { getProjects, markScopeSet, selectScope } from '@/features/metadata/metadata.store'; +import { getGenomes } from '@/features/reference/reference.store'; import Loader from '@/components/Loader'; import DefaultLayout from '@/components/Util/DefaultLayout'; @@ -107,6 +108,7 @@ const BentoAppRouter = () => { dispatch(fetchGohanData()); dispatch(makeGetServiceInfoRequest()); dispatch(makeGetDataTypes()); + dispatch(getGenomes()); }, [dispatch]); if (isAutoAuthenticating || projectsStatus === RequestStatus.Pending) { diff --git a/src/js/constants/configConstants.ts b/src/js/constants/configConstants.ts index 541f7ff2..e3afd21b 100644 --- a/src/js/constants/configConstants.ts +++ b/src/js/constants/configConstants.ts @@ -1,4 +1,4 @@ -import { PORTAL_URL } from '@/config'; +import { PUBLIC_URL_NO_TRAILING_SLASH, PORTAL_URL } from '@/config'; export const MAX_CHARTS = 3; @@ -10,6 +10,8 @@ export const projectsUrl = `${PORTAL_URL}/api/metadata/api/projects`; export const katsuLastIngestionsUrl = `${PORTAL_URL}/api/metadata/data-types`; export const gohanLastIngestionsUrl = `${PORTAL_URL}/api/gohan/data-types`; +export const referenceGenomesUrl = `${PUBLIC_URL_NO_TRAILING_SLASH}/api/reference/genomes`; + export const DEFAULT_TRANSLATION = 'default_translation'; export const CUSTOMIZABLE_TRANSLATION = 'translation'; diff --git a/src/js/features/reference/hooks.ts b/src/js/features/reference/hooks.ts new file mode 100644 index 00000000..15485104 --- /dev/null +++ b/src/js/features/reference/hooks.ts @@ -0,0 +1,45 @@ +import { useEffect, useState } from 'react'; +import { useAuthorizationHeader } from 'bento-auth-js'; +import { referenceGenomesUrl } from '@/constants/configConstants'; +import { RequestStatus } from '@/types/requests'; +import type { GenomeFeature } from './types'; + +export const useGeneNameSearch = (referenceGenomeID: string | undefined, nameQuery: string | null | undefined) => { + const authHeader = useAuthorizationHeader(); + + const [status, setStatus] = useState(RequestStatus.Idle); + const [data, setData] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + if (!referenceGenomeID || !nameQuery) return; + + const params = new URLSearchParams({ name: nameQuery, name_fzy: 'true', limit: '10' }); + const searchUrl = `${referenceGenomesUrl}/${referenceGenomeID}/features?${params.toString()}`; + + setError(null); + + (async () => { + setStatus(RequestStatus.Pending); + + try { + const res = await fetch(searchUrl, { headers: { Accept: 'application/json', ...authHeader } }); + const resData = await res.json(); + if (res.ok) { + console.debug('Genome feature search - got results:', resData.results); + setData(resData.results); + setStatus(RequestStatus.Fulfilled); + } else { + setError(`Genome feature search failed with message: ${resData.message}`); + setStatus(RequestStatus.Rejected); + } + } catch (e) { + console.error(e); + setError(`Genome feature search failed: ${(e as Error).toString()}`); + setStatus(RequestStatus.Rejected); + } + })(); + }, [referenceGenomeID, nameQuery, authHeader]); + + return { status, data, error }; +}; diff --git a/src/js/features/reference/reference.store.ts b/src/js/features/reference/reference.store.ts new file mode 100644 index 00000000..71e84cf2 --- /dev/null +++ b/src/js/features/reference/reference.store.ts @@ -0,0 +1,60 @@ +import axios from 'axios'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; + +import { referenceGenomesUrl } from '@/constants/configConstants'; +import type { RootState } from '@/store'; +import { RequestStatus } from '@/types/requests'; +import { printAPIError } from '@/utils/error.util'; + +import type { Genome } from './types'; + +const storeName = 'reference'; + +export type ReferenceState = { + genomesStatus: RequestStatus; + genomes: Genome[]; + genomesByID: Record; +}; + +const initialState: ReferenceState = { + genomesStatus: RequestStatus.Idle, + genomes: [], + genomesByID: {}, +}; + +export const getGenomes = createAsyncThunk( + `${storeName}/getGenomes`, + (_, { rejectWithValue }) => { + return axios + .get(referenceGenomesUrl) + .then((res) => res.data) + .catch(printAPIError(rejectWithValue)); + }, + { + condition(_, { getState }) { + const { genomesStatus } = getState().reference; + return genomesStatus === RequestStatus.Idle; + }, + } +); + +const reference = createSlice({ + name: storeName, + initialState, + reducers: {}, + extraReducers(builder) { + builder.addCase(getGenomes.pending, (state) => { + state.genomesStatus = RequestStatus.Pending; + }); + builder.addCase(getGenomes.fulfilled, (state, { payload }) => { + state.genomes = payload; + state.genomesByID = Object.fromEntries(payload.map((g) => [g.id, g])); + state.genomesStatus = RequestStatus.Fulfilled; + }); + builder.addCase(getGenomes.rejected, (state) => { + state.genomesStatus = RequestStatus.Rejected; + }); + }, +}); + +export default reference.reducer; diff --git a/src/js/features/reference/types.ts b/src/js/features/reference/types.ts new file mode 100644 index 00000000..656957a2 --- /dev/null +++ b/src/js/features/reference/types.ts @@ -0,0 +1,41 @@ +import type { OntologyTerm } from '@/types/ontology'; + +// See also: https://github.com/bento-platform/bento_reference_service/blob/main/bento_reference_service/models.py + +export type Contig = { + name: string; + aliases: string[]; + md5: string; + ga4gh: string; + length: number; + circular: boolean; + refget_uris: string[]; +}; + +export type Genome = { + id: string; + aliases: string[]; + md5: string; + ga4gh: string; + fasta: string; + fai: string; + gff3_gz: string; + gff3_gz_tbi: string; + taxon: OntologyTerm; + contigs: Contig[]; + uri: string; +}; + +export type GenomeFeature = { + genome_id: string; + contig_name: string; + + strand: '-' | '+' | '?' | '.'; + + feature_id: string; + feature_name: string; + feature_type: string; + + source: string; + entries: { start_pos: number; end_pos: number; score: number | null; phase: number | null }[]; +}; diff --git a/src/js/store.ts b/src/js/store.ts index d7bd59cb..bded9dd1 100644 --- a/src/js/store.ts +++ b/src/js/store.ts @@ -13,6 +13,7 @@ import lastIngestionDataReducer from '@/features/ingestion/lastIngestion.store'; import beaconReducer from './features/beacon/beacon.store'; import beaconNetworkReducer from './features/beacon/network.store'; import metadataReducer from '@/features/metadata/metadata.store'; +import reference from '@/features/reference/reference.store'; import { getValue, saveValue } from './utils/localStorage'; interface PersistedState { @@ -39,6 +40,7 @@ export const store = configureStore({ beacon: beaconReducer, beaconNetwork: beaconNetworkReducer, metadata: metadataReducer, + reference, }, preloadedState: persistedState, }); diff --git a/src/js/types/ontology.ts b/src/js/types/ontology.ts new file mode 100644 index 00000000..46001b6a --- /dev/null +++ b/src/js/types/ontology.ts @@ -0,0 +1,4 @@ +export type OntologyTerm = { + id: string; + label: string; +};