From c23b1394b1bf31f67e0b8eb4426bd974ca7958d6 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Nov 2024 15:00:27 -0500 Subject: [PATCH 01/22] refact: use i18n for data type / entity labels --- .../Beacon/BeaconCommon/BeaconQueryFormUi.tsx | 2 +- src/js/components/Overview/Counts.tsx | 16 +++++++++------- src/js/components/Overview/LastIngestion.tsx | 5 +++-- src/js/components/Search/SearchResultsCounts.tsx | 6 +++--- src/js/components/Search/SearchResultsPane.tsx | 10 +++++++--- .../locales/en/default_translation_en.json | 15 ++++++++++----- .../locales/fr/default_translation_fr.json | 11 ++++++++++- 7 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx b/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx index 5c15d909..7dd3c2e3 100644 --- a/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx +++ b/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx @@ -233,7 +233,7 @@ const BeaconQueryFormUi = ({ {hasVariants && ( { // Break down help into multiple sentences inside an array to make translation a bit easier. const data = [ { - title: 'Individuals', + entity: 'individual', help: ['individual_help_1'], icon: , count: counts.individuals, }, { - title: 'Biosamples', + entity: 'biosample', help: ['biosample_help_1'], icon: , count: counts.biosamples, }, { - title: 'Experiments', + entity: 'experiment', help: ['experiment_help_1', 'experiment_help_2'], icon: , count: counts.experiments, @@ -48,17 +48,19 @@ const Counts = () => { <> {t('Counts')} - {data.map(({ title, help, icon, count }, i) => { - const titleTransl = t(`entities.${title}`); + {data.map(({ entity, help, icon, count }, i) => { + // false count – just need the highest form of plural + // - see https://www.i18next.com/translation-function/plurals + const title = t(`entities.${entity}`, { count: 100 }); return ( - {titleTransl} + {title} {help && ( {help.map((h, i) => ( diff --git a/src/js/components/Overview/LastIngestion.tsx b/src/js/components/Overview/LastIngestion.tsx index f00b19c0..f21bd043 100644 --- a/src/js/components/Overview/LastIngestion.tsx +++ b/src/js/components/Overview/LastIngestion.tsx @@ -5,7 +5,6 @@ import { CalendarOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; import { useAppSelector } from '@/hooks'; -import { getDataTypeLabel } from '@/types/dataTypes'; import type { LastIngestionDataTypeResponse } from '@/types/lastIngestionDataTypeResponse'; import { BOX_SHADOW } from '@/constants/overviewConstants'; @@ -48,7 +47,9 @@ const LastIngestionInfo: React.FC = () => { - {t(getDataTypeLabel(dataType.id))} + {/* false count – just need the highest form of plural + - see https://www.i18next.com/translation-function/plurals */} + {t(`entities.${dataType.id}`, { count: 100 })} {' '} diff --git a/src/js/components/Search/SearchResultsCounts.tsx b/src/js/components/Search/SearchResultsCounts.tsx index 9d778aad..559599b2 100644 --- a/src/js/components/Search/SearchResultsCounts.tsx +++ b/src/js/components/Search/SearchResultsCounts.tsx @@ -58,7 +58,7 @@ const SearchResultsCounts = ({ ].join(' ')} > } /> } diff --git a/src/js/components/Search/SearchResultsPane.tsx b/src/js/components/Search/SearchResultsPane.tsx index d1f971fc..2b804cb7 100644 --- a/src/js/components/Search/SearchResultsPane.tsx +++ b/src/js/components/Search/SearchResultsPane.tsx @@ -33,7 +33,7 @@ const SearchResultsPane = ({ () => [ { dataIndex: 'id', - title: t('entities.Individual'), + title: t('entities.individual', { count: 1 }), render: (id: string) => ( {id} @@ -85,7 +85,9 @@ const SearchResultsPane = ({ <> - {t('entities.Biosamples')} + {/* false count – just need the highest form of plural + - see https://www.i18next.com/translation-function/plurals */} + {t('entities.biosample', { count: 100 })} {!hasInsufficientData && biosampleChartData.length ? ( @@ -95,7 +97,9 @@ const SearchResultsPane = ({ - {t('entities.Experiments')} + {/* false count – just need the highest form of plural + - see https://www.i18next.com/translation-function/plurals */} + {t('entities.experiment', { count: 100 })} {!hasInsufficientData && experimentChartData.length ? ( diff --git a/src/public/locales/en/default_translation_en.json b/src/public/locales/en/default_translation_en.json index 92a1f624..2f892ab6 100644 --- a/src/public/locales/en/default_translation_en.json +++ b/src/public/locales/en/default_translation_en.json @@ -6,16 +6,21 @@ "Portal": "Portal", "missing": "missing", "entities": { - "Phenopackets": "Clinical Data", + "phenopacket_one": "Phenopacket", + "phenopacket_other": "Clinical Data", + "individual_one": "Individual", + "individual_other": "Individuals", "Individual": "Individual", - "Individuals": "Individuals", "individual_help_1": "Individuals represent a specific person / organism, and may have one or multiple associated biosamples, each with associated experiments.", - "Biosamples": "Biosamples", + "biosample_one": "Biosample", + "biosample_other": "Biosamples", "biosample_help_1": "Biosamples are usually biological material extracted from a specific individual.", - "Experiments": "Experiments", + "experiment_one": "Experiment", + "experiment_other": "Experiments", "experiment_help_1": "Experiments are a process done to a specific sample, e.g., whole-genome sequencing.", "experiment_help_2": "One lab experiment may result in multiple experiment records inside the portal, such as with multiplexed samples.", - "Variants": "Variants" + "variant_one": "Variant", + "variant_other": "Variants" }, "in": "in", "Get Data": "Get Data", diff --git a/src/public/locales/fr/default_translation_fr.json b/src/public/locales/fr/default_translation_fr.json index 3a78a7cf..1e0794f6 100644 --- a/src/public/locales/fr/default_translation_fr.json +++ b/src/public/locales/fr/default_translation_fr.json @@ -6,16 +6,25 @@ "Portal": "Portail", "missing": "manquant(s)", "entities": { + "phenopacket_one": "Phenopacket", + "phenopacket_other": "Données cliniques", "Phenopackets": "Données cliniques", + "individual_one": "Participant", + "individual_other": "Participants", "Individual": "Participant", "Individuals": "Participants", "individual_help_1": "Les participants représentent une personne/un organisme spécifique et peuvent avoir un ou plusieurs échantillons biologiques associés, chacun avec des expériences associées.", + "biosample_one": "Échantillon biologique", + "biosample_other": "Échantillons biologiques", "Biosamples": "Échantillons biologiques", "biosample_help_1": "Les échantillons biologiques sont généralement du matériel biologique extrait d’un participant spécifique.", + "experiment_one": "Expérience", + "experiment_other": "Expériences", "Experiments": "Expériences", "experiment_help_1": "Les expériences sont un processus effectué sur un échantillon spécifique, par exemple le séquençage du génome entier.", "experiment_help_2": "Une expérience par un laboratoire peut donner lieu à plusieurs enregistrements d'expériences dans le portail, par exemple avec des échantillons multiplexés.", - "Variants": "Variants" + "variant_one": "Variant", + "variant_other": "Variants" }, "in": "en", "Get Data": "Obtenir", From 688c573c873b9190cf45218d4de98259a413b9c7 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Nov 2024 15:06:10 -0500 Subject: [PATCH 02/22] chore: rm unused data type stuff --- src/js/types/dataTypes.ts | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/js/types/dataTypes.ts b/src/js/types/dataTypes.ts index 07fbd03d..e69de29b 100644 --- a/src/js/types/dataTypes.ts +++ b/src/js/types/dataTypes.ts @@ -1,22 +0,0 @@ -export enum DataTypes { - phenopacket = 'phenopacket', - experiment = 'experiment', - experiment_result = 'experiment_result', - variant = 'variant', -} - -export const DataTypesLabels = { - [DataTypes.phenopacket]: 'entities.Phenopackets', - [DataTypes.experiment]: 'entities.Experiments', - [DataTypes.experiment_result]: 'entities.Experiment Results', - [DataTypes.variant]: 'entities.Variants', -}; - -export const getDataTypeLabel = (dataTypeString: string): string => { - const dataTypesValues = Object.values(DataTypes) as string[]; - if (dataTypesValues.includes(dataTypeString)) { - const dataType: DataTypes = DataTypes[dataTypeString as keyof typeof DataTypes]; - return DataTypesLabels[dataType]; - } - return 'Unknown Data Type'; -}; From 292081aad31de00b5943f3bec8ced78d6c501d86 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Nov 2024 15:07:13 -0500 Subject: [PATCH 03/22] chore: clean up default french transl --- src/public/locales/fr/default_translation_fr.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/public/locales/fr/default_translation_fr.json b/src/public/locales/fr/default_translation_fr.json index 1e0794f6..340f26ab 100644 --- a/src/public/locales/fr/default_translation_fr.json +++ b/src/public/locales/fr/default_translation_fr.json @@ -8,19 +8,14 @@ "entities": { "phenopacket_one": "Phenopacket", "phenopacket_other": "Données cliniques", - "Phenopackets": "Données cliniques", "individual_one": "Participant", "individual_other": "Participants", - "Individual": "Participant", - "Individuals": "Participants", "individual_help_1": "Les participants représentent une personne/un organisme spécifique et peuvent avoir un ou plusieurs échantillons biologiques associés, chacun avec des expériences associées.", "biosample_one": "Échantillon biologique", "biosample_other": "Échantillons biologiques", - "Biosamples": "Échantillons biologiques", "biosample_help_1": "Les échantillons biologiques sont généralement du matériel biologique extrait d’un participant spécifique.", "experiment_one": "Expérience", "experiment_other": "Expériences", - "Experiments": "Expériences", "experiment_help_1": "Les expériences sont un processus effectué sur un échantillon spécifique, par exemple le séquençage du génome entier.", "experiment_help_2": "Une expérience par un laboratoire peut donner lieu à plusieurs enregistrements d'expériences dans le portail, par exemple avec des échantillons multiplexés.", "variant_one": "Variant", From ade4daceb9326317b2c3f33cb0cf505eee5ba178 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Nov 2024 15:27:22 -0500 Subject: [PATCH 04/22] refact: rewrite data types reducer + fetch even if not auth'd --- package-lock.json | 1 + package.json | 1 + src/js/components/BentoAppRouter.tsx | 6 +-- src/js/features/config/config.store.ts | 6 ++- src/js/features/dataTypes/dataTypes.store.ts | 49 ++++++++++++-------- src/js/store.ts | 2 + src/js/types/dataTypes.ts | 12 +++++ 7 files changed, 53 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index b76eb83c..394d64de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "redux-thunk": "^2.4.1" }, "devDependencies": { + "@types/json-schema": "^7.0.15", "@types/react-dom": "^18.3.1", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.16.1", diff --git a/package.json b/package.json index 5f4b2b63..22def57c 100755 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "redux-thunk": "^2.4.1" }, "devDependencies": { + "@types/json-schema": "^7.0.15", "@types/react-dom": "^18.3.1", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.16.1", diff --git a/src/js/components/BentoAppRouter.tsx b/src/js/components/BentoAppRouter.tsx index 38644cbb..c68f8278 100644 --- a/src/js/components/BentoAppRouter.tsx +++ b/src/js/components/BentoAppRouter.tsx @@ -106,10 +106,8 @@ const BentoAppRouter = () => { dispatch(makeGetAboutRequest()); dispatch(fetchGohanData()); dispatch(makeGetServiceInfoRequest()); - if (isAuthenticated) { - dispatch(makeGetDataTypes()); - } - }, [dispatch, isAuthenticated]); + dispatch(makeGetDataTypes()); + }, [dispatch]); if (isAutoAuthenticating || projectsStatus === RequestStatus.Pending) { return ; diff --git a/src/js/features/config/config.store.ts b/src/js/features/config/config.store.ts index 4be6d1ac..13f89502 100644 --- a/src/js/features/config/config.store.ts +++ b/src/js/features/config/config.store.ts @@ -52,7 +52,11 @@ export const makeGetServiceInfoRequest = createAsyncThunk< condition(_, { getState }) { const { serviceInfoStatus } = getState().config; const cond = serviceInfoStatus === RequestStatus.Idle; - if (!cond) console.debug(`makeGetServiceInfoRequest(), but a prior attempt gave status: ${serviceInfoStatus}`); + if (!cond) { + console.debug( + `makeGetServiceInfoRequest() was attempted, but a prior attempt gave status: ${serviceInfoStatus}` + ); + } return cond; }, } diff --git a/src/js/features/dataTypes/dataTypes.store.ts b/src/js/features/dataTypes/dataTypes.store.ts index a57f59ba..e148af8e 100644 --- a/src/js/features/dataTypes/dataTypes.store.ts +++ b/src/js/features/dataTypes/dataTypes.store.ts @@ -1,34 +1,45 @@ import axios from 'axios'; -import type { PayloadAction } from '@reduxjs/toolkit'; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import type { RootState } from '@/store'; +import type { BentoServiceDataType } from '@/types/dataTypes'; +import { RequestStatus } from '@/types/requests'; import { authorizedRequestConfig } from '@/utils/requests'; -// TODO: find a way to allow this without an auth token export const makeGetDataTypes = createAsyncThunk< - object, + BentoServiceDataType[], void, { rejectValue: string; state: RootState; } ->('dataTypes/makeGetDataTypes', async (_, { getState }) => { - // Not scoped currently - this is a way to get all data types in an instance from service registry, but it may make - // sense in the future to forward query params to nested calls depending on scope value especially in the case of - // count handling. TBD - TODO: figure this out - const res = await axios.get('/api/service-registry/data-types', authorizedRequestConfig(getState())); - return res.data; -}); +>( + 'dataTypes/makeGetDataTypes', + async (_, { getState }) => { + // Not scoped currently - this is a way to get all data types in an instance from service registry, but it may make + // sense in the future to forward query params to nested calls depending on scope value especially in the case of + // count handling. TBD - TODO: figure this out + const res = await axios.get('/api/service-registry/data-types', authorizedRequestConfig(getState())); + return res.data; + }, + { + condition(_, { getState }) { + const { status } = getState().dataTypes; + const cond = status === RequestStatus.Idle; + if (!cond) console.debug(`makeGetDataTypes() was attempted, but a prior attempt gave status: ${status}`); + return cond; + }, + } +); export type DataTypesState = { - isFetching: boolean; - dataTypes: object; + status: RequestStatus; + dataTypesById: Record; }; const initialState: DataTypesState = { - isFetching: false, - dataTypes: {}, + status: RequestStatus.Idle, + dataTypesById: {}, }; const dataTypes = createSlice({ @@ -37,14 +48,14 @@ const dataTypes = createSlice({ reducers: {}, extraReducers(builder) { builder.addCase(makeGetDataTypes.pending, (state) => { - state.isFetching = true; + state.status = RequestStatus.Pending; }); - builder.addCase(makeGetDataTypes.fulfilled, (state, { payload }: PayloadAction) => { - state.isFetching = false; - state.dataTypes = { ...payload }; + builder.addCase(makeGetDataTypes.fulfilled, (state, { payload }) => { + state.status = RequestStatus.Fulfilled; + state.dataTypesById = Object.fromEntries(payload.map((dt) => [dt.id, dt])); }); builder.addCase(makeGetDataTypes.rejected, (state) => { - state.isFetching = false; + state.status = RequestStatus.Rejected; }); }, }); diff --git a/src/js/store.ts b/src/js/store.ts index b6f954b3..d7bd59cb 100644 --- a/src/js/store.ts +++ b/src/js/store.ts @@ -7,6 +7,7 @@ import { LS_OPENID_CONFIG_KEY, AuthReducer as auth, OIDCReducer as openIdConfigu import configReducer from '@/features/config/config.store'; import contentReducer from '@/features/content/content.store'; import dataReducer from '@/features/data/data.store'; +import dataTypesReducer from '@/features/dataTypes/dataTypes.store'; import queryReducer from '@/features/search/query.store'; import lastIngestionDataReducer from '@/features/ingestion/lastIngestion.store'; import beaconReducer from './features/beacon/beacon.store'; @@ -32,6 +33,7 @@ export const store = configureStore({ config: configReducer, content: contentReducer, data: dataReducer, + dataTypes: dataTypesReducer, query: queryReducer, lastIngestionData: lastIngestionDataReducer, beacon: beaconReducer, diff --git a/src/js/types/dataTypes.ts b/src/js/types/dataTypes.ts index e69de29b..36ef5493 100644 --- a/src/js/types/dataTypes.ts +++ b/src/js/types/dataTypes.ts @@ -0,0 +1,12 @@ +import type { JSONSchema7 } from 'json-schema'; + +export type BentoDataType = { + id: string; + label: string; + queryable: boolean; + schema: JSONSchema7; + metadata_schema: JSONSchema7; + count?: number; +}; + +export type BentoServiceDataType = BentoDataType & { service_base_url: string }; From de6269b79371e61e686bc8aa889ee96d86725c57 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 14 Nov 2024 12:52:19 -0500 Subject: [PATCH 05/22] chore: clean up stray old en transl --- src/public/locales/en/default_translation_en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/public/locales/en/default_translation_en.json b/src/public/locales/en/default_translation_en.json index 2f892ab6..21bd29ea 100644 --- a/src/public/locales/en/default_translation_en.json +++ b/src/public/locales/en/default_translation_en.json @@ -10,7 +10,6 @@ "phenopacket_other": "Clinical Data", "individual_one": "Individual", "individual_other": "Individuals", - "Individual": "Individual", "individual_help_1": "Individuals represent a specific person / organism, and may have one or multiple associated biosamples, each with associated experiments.", "biosample_one": "Biosample", "biosample_other": "Biosamples", From e10976b85d56e8fa25e85559a5d1aa013ca9ae52 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 14 Nov 2024 12:54:14 -0500 Subject: [PATCH 06/22] refact: i18n count constants --- .../Beacon/BeaconCommon/BeaconQueryFormUi.tsx | 3 ++- src/js/components/Overview/Counts.tsx | 5 ++--- src/js/components/Overview/LastIngestion.tsx | 5 ++--- src/js/components/Search/SearchResultsCounts.tsx | 7 ++++--- src/js/components/Search/SearchResultsPane.tsx | 11 ++++------- src/js/constants/i18n.ts | 4 ++++ 6 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 src/js/constants/i18n.ts diff --git a/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx b/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx index 7dd3c2e3..1452b66a 100644 --- a/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx +++ b/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx @@ -26,6 +26,7 @@ import { BUTTON_STYLE, CARD_STYLES, } from '@/constants/beaconConstants'; +import { T_PLURAL_COUNT } from '@/constants/i18n'; const STARTER_FILTER = { index: 1, active: true }; const VARIANTS_FORM_ERROR_MESSAGE = @@ -233,7 +234,7 @@ const BeaconQueryFormUi = ({ {hasVariants && ( { {t('Counts')} {data.map(({ entity, help, icon, count }, i) => { - // false count – just need the highest form of plural - // - see https://www.i18next.com/translation-function/plurals - const title = t(`entities.${entity}`, { count: 100 }); + const title = t(`entities.${entity}`, T_PLURAL_COUNT); return ( { @@ -47,9 +48,7 @@ const LastIngestionInfo: React.FC = () => { - {/* false count – just need the highest form of plural - - see https://www.i18next.com/translation-function/plurals */} - {t(`entities.${dataType.id}`, { count: 100 })} + {t(`entities.${dataType.id}`, T_PLURAL_COUNT)} {' '} diff --git a/src/js/components/Search/SearchResultsCounts.tsx b/src/js/components/Search/SearchResultsCounts.tsx index 559599b2..35ff9a0d 100644 --- a/src/js/components/Search/SearchResultsCounts.tsx +++ b/src/js/components/Search/SearchResultsCounts.tsx @@ -3,6 +3,7 @@ import { Skeleton, Space, Statistic } from 'antd'; import { TeamOutlined } from '@ant-design/icons'; import { BiDna } from 'react-icons/bi'; +import { T_PLURAL_COUNT } from '@/constants/i18n'; import { COUNTS_FILL } from '@/constants/overviewConstants'; import { NO_RESULTS_DASHES } from '@/constants/searchConstants'; import ExpSvg from '@/components/Util/ExpSvg'; @@ -58,7 +59,7 @@ const SearchResultsCounts = ({ ].join(' ')} > } /> } diff --git a/src/js/components/Search/SearchResultsPane.tsx b/src/js/components/Search/SearchResultsPane.tsx index 2b804cb7..db003cd6 100644 --- a/src/js/components/Search/SearchResultsPane.tsx +++ b/src/js/components/Search/SearchResultsPane.tsx @@ -4,6 +4,7 @@ import { LeftOutlined } from '@ant-design/icons'; import { PieChart } from 'bento-charts'; import { PORTAL_URL } from '@/config'; +import { T_PLURAL_COUNT, T_SINGULAR_COUNT } from '@/constants/i18n'; import { BOX_SHADOW, PIE_CHART_HEIGHT } from '@/constants/overviewConstants'; import { useTranslationFn } from '@/hooks'; import type { DiscoveryResults } from '@/types/data'; @@ -33,7 +34,7 @@ const SearchResultsPane = ({ () => [ { dataIndex: 'id', - title: t('entities.individual', { count: 1 }), + title: t('entities.individual', T_SINGULAR_COUNT), render: (id: string) => ( {id} @@ -85,9 +86,7 @@ const SearchResultsPane = ({ <> - {/* false count – just need the highest form of plural - - see https://www.i18next.com/translation-function/plurals */} - {t('entities.biosample', { count: 100 })} + {t('entities.biosample', T_PLURAL_COUNT)} {!hasInsufficientData && biosampleChartData.length ? ( @@ -97,9 +96,7 @@ const SearchResultsPane = ({ - {/* false count – just need the highest form of plural - - see https://www.i18next.com/translation-function/plurals */} - {t('entities.experiment', { count: 100 })} + {t('entities.experiment', T_PLURAL_COUNT)} {!hasInsufficientData && experimentChartData.length ? ( diff --git a/src/js/constants/i18n.ts b/src/js/constants/i18n.ts new file mode 100644 index 00000000..f7d27e20 --- /dev/null +++ b/src/js/constants/i18n.ts @@ -0,0 +1,4 @@ +// false count – just need the highest form of plural +// - see https://www.i18next.com/translation-function/plurals +export const T_PLURAL_COUNT = { count: 100 }; +export const T_SINGULAR_COUNT = { count: 1 }; From 52f0bcf6fe08cf0c805cd670dc2843aae97e7027 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 15 Nov 2024 13:48:33 -0500 Subject: [PATCH 07/22] chore: configure webpack to clean up dist/ my dist folder was bloating gradually because of webpack dev server, this should fix that issue. --- package.json | 4 ++-- webpack.config.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 22def57c..88e92fc6 100755 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "main": "index.js", "scripts": { "build": "npx webpack --mode=production", - "build-dev": "rm -rf dist/* && npx webpack --mode=development", - "watch": "rm -rf dist/* && npx webpack --mode=development --watch", + "build-dev": "npx webpack --mode=development", + "watch": "npx webpack --mode=development --watch", "start": "webpack serve --mode development --no-web-socket-server", "lint": "npx eslint src/js", "lint-fix": "npx eslint --fix src/js", diff --git a/webpack.config.js b/webpack.config.js index 3f45cbe0..269c9ed0 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,6 +16,7 @@ const config = { publicPath: '/', // filename: "js/bundle.js", filename: 'js/[name][chunkhash].js', + clean: true, }, module: { rules: [ From a7662f4683cab59b86d4b7a8f0a2b522e1e7ac5c Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 21 Nov 2024 11:46:08 -0500 Subject: [PATCH 08/22] 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; +}; From d8e693bcbcc6451d787da4f863c3a5277727c388 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 21 Nov 2024 11:46:19 -0500 Subject: [PATCH 09/22] chore: rm needless
--- src/js/components/Beacon/BeaconCommon/VariantInput.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/components/Beacon/BeaconCommon/VariantInput.tsx b/src/js/components/Beacon/BeaconCommon/VariantInput.tsx index 3251713a..397774d9 100644 --- a/src/js/components/Beacon/BeaconCommon/VariantInput.tsx +++ b/src/js/components/Beacon/BeaconCommon/VariantInput.tsx @@ -5,11 +5,11 @@ import type { FormField } from '@/types/beacon'; const VariantInput = ({ field, disabled }: VariantInputProps) => { const t = useTranslationFn(); return ( -
+ <> -
+ ); }; From 4155e3498b184e4f5deca5ed9ce97085d19926f2 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 21 Nov 2024 14:21:27 -0500 Subject: [PATCH 10/22] feat(beacon): add assembly-wise chromosome selector TODO: translation, testing, network --- .../Beacon/BeaconCommon/VariantInput.tsx | 20 ++++- .../Beacon/BeaconCommon/VariantsForm.tsx | 90 ++++++++++++++++--- src/js/features/reference/hooks.ts | 5 ++ 3 files changed, 99 insertions(+), 16 deletions(-) diff --git a/src/js/components/Beacon/BeaconCommon/VariantInput.tsx b/src/js/components/Beacon/BeaconCommon/VariantInput.tsx index 397774d9..5d727dd8 100644 --- a/src/js/components/Beacon/BeaconCommon/VariantInput.tsx +++ b/src/js/components/Beacon/BeaconCommon/VariantInput.tsx @@ -1,13 +1,26 @@ -import { Form, Input } from 'antd'; +import { Form, Input, Select } from 'antd'; +import type { DefaultOptionType } from 'antd/es/select/index'; import { useTranslationFn } from '@/hooks'; import type { FormField } from '@/types/beacon'; -const VariantInput = ({ field, disabled }: VariantInputProps) => { +type InputMode = { type: 'input' } | { type: 'select'; options?: DefaultOptionType[] }; + +const VariantInput = ({ field, disabled, mode }: VariantInputProps) => { const t = useTranslationFn(); return ( <> - + {!mode || mode.type === 'input' ? ( + + ) : ( + - {assemblyIdOptions} - +