From cdfce1e731cdebbcef5acb57233cdcaa68e2a0e6 Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Wed, 15 May 2024 11:37:35 -0400 Subject: [PATCH 01/94] factor out beacon tooltips --- .../ToolTips/MetadataInstructions.tsx | 10 ++++++ .../BeaconCommon/ToolTips/SearchToolTip.tsx | 13 +++++++ .../BeaconCommon/ToolTips/ToolTipText.tsx | 5 +++ .../ToolTips/VariantsInstructions.tsx | 36 +++++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 src/js/components/BeaconCommon/ToolTips/MetadataInstructions.tsx create mode 100644 src/js/components/BeaconCommon/ToolTips/SearchToolTip.tsx create mode 100644 src/js/components/BeaconCommon/ToolTips/ToolTipText.tsx create mode 100644 src/js/components/BeaconCommon/ToolTips/VariantsInstructions.tsx diff --git a/src/js/components/BeaconCommon/ToolTips/MetadataInstructions.tsx b/src/js/components/BeaconCommon/ToolTips/MetadataInstructions.tsx new file mode 100644 index 00000000..9fd626c0 --- /dev/null +++ b/src/js/components/BeaconCommon/ToolTips/MetadataInstructions.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { ToolTipText } from './ToolTipText'; +import { useTranslationDefault } from '@/hooks'; + +const METADATA_INSTRUCTIONS = 'Search over clinical or phenotypic properties.'; + +export const metadataInstructions = () => { + const td = useTranslationDefault(); + return {td(METADATA_INSTRUCTIONS)}; +}; diff --git a/src/js/components/BeaconCommon/ToolTips/SearchToolTip.tsx b/src/js/components/BeaconCommon/ToolTips/SearchToolTip.tsx new file mode 100644 index 00000000..0f2cd723 --- /dev/null +++ b/src/js/components/BeaconCommon/ToolTips/SearchToolTip.tsx @@ -0,0 +1,13 @@ +import React, { ReactNode } from 'react'; +import { Tooltip } from 'antd'; +import { InfoCircleOutlined } from '@ant-design/icons'; + +const SearchToolTip = ({ children }: { children: ReactNode }) => { + return ( + + + + ); + }; + + export default SearchToolTip; \ No newline at end of file diff --git a/src/js/components/BeaconCommon/ToolTips/ToolTipText.tsx b/src/js/components/BeaconCommon/ToolTips/ToolTipText.tsx new file mode 100644 index 00000000..ac31a302 --- /dev/null +++ b/src/js/components/BeaconCommon/ToolTips/ToolTipText.tsx @@ -0,0 +1,5 @@ +import React from 'react'; +import { Typography } from 'antd'; +const { Text } = Typography; + +export const ToolTipText = ({ children }: { children: string }) => {children}; diff --git a/src/js/components/BeaconCommon/ToolTips/VariantsInstructions.tsx b/src/js/components/BeaconCommon/ToolTips/VariantsInstructions.tsx new file mode 100644 index 00000000..cbbab760 --- /dev/null +++ b/src/js/components/BeaconCommon/ToolTips/VariantsInstructions.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { ToolTipText } from './ToolTipText'; +import { useTranslationDefault } from '@/hooks'; +import { Space, Typography } from 'antd'; +const { Title } = Typography; + +// complexity of instructions suggests the form isn't intuitive enough +const VARIANTS_INSTRUCTIONS_TITLE = 'Variant search'; +const VARIANTS_INSTRUCTIONS_LINE1a = + 'To search for all variants inside a range: fill both "Variant start" and "Variant end",'; +const VARIANTS_INSTRUCTIONS_LINE1b = + 'all variants inside the range will be returned. You can optionally filter by reference or alternate bases.'; + +const VARIANTS_INSTRUCTIONS_LINE2a = + 'To search for a variant at a particular position, either set "Variant end" to the same value in "Variant start",'; +const VARIANTS_INSTRUCTIONS_LINE2b = 'or fill in values for both reference and alternate bases.'; +const VARIANTS_INSTRUCTIONS_LINE3 = '"Chromosome", "Variant start" and "Assembly ID" are always required.'; +const VARIANTS_INSTRUCTIONS_LINE4a = 'Coordinates are one-based.'; +const VARIANTS_INSTRUCTIONS_LINE4b = 'Leave this form blank to search by metadata only.'; + +const VariantsInstructions = () => { + const td = useTranslationDefault(); + return ( + + + {VARIANTS_INSTRUCTIONS_TITLE} + + {td(VARIANTS_INSTRUCTIONS_LINE1a) + ' ' + td(VARIANTS_INSTRUCTIONS_LINE1b)} + {td(VARIANTS_INSTRUCTIONS_LINE2a) + ' ' + td(VARIANTS_INSTRUCTIONS_LINE2b)} + {td(VARIANTS_INSTRUCTIONS_LINE3)} + {td(VARIANTS_INSTRUCTIONS_LINE4a) + ' ' + td(VARIANTS_INSTRUCTIONS_LINE4b)} + + ); +}; + +export default VariantsInstructions; From 893925af26f001d70d44f2ab6b2aecfc6b9083a6 Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Wed, 15 May 2024 13:50:57 -0400 Subject: [PATCH 02/94] add beacon network tab --- src/js/components/TabbedDashboard.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/js/components/TabbedDashboard.tsx b/src/js/components/TabbedDashboard.tsx index c7811625..2baba5fd 100644 --- a/src/js/components/TabbedDashboard.tsx +++ b/src/js/components/TabbedDashboard.tsx @@ -18,10 +18,12 @@ import PublicOverview from './Overview/PublicOverview'; import Search from './Search/Search'; import ProvenanceTab from './Provenance/ProvenanceTab'; import BeaconQueryUi from './Beacon/BeaconQueryUi'; +import NetworkUi from './BeaconNetwork/NetworkUi'; import { useAppDispatch, useAppSelector, useTranslationDefault } from '@/hooks'; import { buildQueryParamsUrl } from '@/utils/search'; import { makeGetDataTypes } from '@/features/dataTypes/dataTypes.store'; import { BEACON_UI_ENABLED } from '@/config'; +import { BEACON_NETWORK_UI_ENABLED } from '@/config'; import SitePageLoading from './SitePageLoading'; const TabbedDashboard = () => { @@ -96,6 +98,13 @@ const TabbedDashboard = () => { active: BEACON_UI_ENABLED, key: 'beacon', }, + { + title: 'Beacon Network', + content: , + loading: false, + active: BEACON_NETWORK_UI_ENABLED, + key: 'beacon_network', + }, { title: 'Provenance', content: , From e5978065e13fc7ac1227aa7031262910fb74fc42 Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Thu, 16 May 2024 15:48:26 -0400 Subject: [PATCH 03/94] temp network types --- src/js/types/beaconNetwork.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/js/types/beaconNetwork.ts diff --git a/src/js/types/beaconNetwork.ts b/src/js/types/beaconNetwork.ts new file mode 100644 index 00000000..23548059 --- /dev/null +++ b/src/js/types/beaconNetwork.ts @@ -0,0 +1,27 @@ +import { BeaconQueryResponse } from "./beacon"; +import { ChartData } from "./data"; + +export interface BeaconOrgDetails { + logoUrl: string; + name: string; + id: string; + welcomeUrl: string; + contactUrl: string; +} + +// TODO, should probably standardize with standard response +export interface BeaconNetworkResponse { + individualCount: number; + biosampleCount: number; + experimentCount: number; + biosampleChartData: ChartData[], + experimentChartData: ChartData[], +} + +export interface RespondingBeacon { + organization: BeaconOrgDetails; + response: BeaconNetworkResponse; //or something else + bentoUrl: string; + description: string; +} + From a18382bc61e237bcfef238fb767dbcdfe9355db8 Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Tue, 28 May 2024 15:32:39 -0400 Subject: [PATCH 04/94] redux for beacon network config --- src/js/features/beacon/networkConfig.store.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/js/features/beacon/networkConfig.store.ts diff --git a/src/js/features/beacon/networkConfig.store.ts b/src/js/features/beacon/networkConfig.store.ts new file mode 100644 index 00000000..0a6c0fe3 --- /dev/null +++ b/src/js/features/beacon/networkConfig.store.ts @@ -0,0 +1,62 @@ +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { makeAuthorizationHeader } from 'bento-auth-js'; +import { RootState } from '@/store'; +import { serializeChartData } from '@/utils/chart'; +import { beaconApiError } from '@/utils/beaconApiError'; +import { BeaconQueryPayload } from '@/types/beacon'; +import { BeaconNetworkAggregatedResponse, BeaconNetworkConfig } from '@/types/beaconNetwork'; +import { ChartData } from '@/types/data'; +import { BEACON_URL } from '@/config'; + +// temp, should be passed in from somewhere else +const BEACON_NETWORK_ROOT = 'https://bentov2.local/api/beacon/network/'; + +// network config currently just a list of beacons in the network with info about each one + +export const getBeaconNetworkConfig = createAsyncThunk< + BeaconNetworkConfig, + void, + { state: RootState; rejectValue: string } +>('beaconConfig/getBeaconNetworkConfig', (_, { rejectWithValue }) => { + return axios + .get(BEACON_NETWORK_ROOT) + .then((res) => res.data) + .catch(beaconApiError(rejectWithValue)); +}); + +type beaconNetworkIntitalStateType = { + isFetchingBeaconNetworkConfig: boolean; + hasBeaconNetworkError: boolean; + networkBeacons: BeaconNetworkConfig; +}; + +const initialState: beaconNetworkIntitalStateType = { + isFetchingBeaconNetworkConfig: false, + hasBeaconNetworkError: false, + networkBeacons: {}, +}; + +const beaconNetwork = createSlice({ + name: 'beaconNetwork', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(getBeaconNetworkConfig.pending, (state) => { + console.log('getBeaconNetworkConfig.pending'); + state.isFetchingBeaconNetworkConfig = true; + }); + builder.addCase(getBeaconNetworkConfig.fulfilled, (state, { payload }) => { + console.log('getBeaconNetworkConfig.fulfilled'); + state.isFetchingBeaconNetworkConfig = false; + state.networkBeacons = payload; + }); + builder.addCase(getBeaconNetworkConfig.rejected, (state, { payload }) => { + state.isFetchingBeaconNetworkConfig = false; + state.hasBeaconNetworkError = true; + console.log('getBeaconNetworkConfig.rejected'); + }); + }, +}); + +export default beaconNetwork.reducer; From 331fcc77f8fde96aa092f47983778d4ebefb5f48 Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Wed, 29 May 2024 15:23:21 -0400 Subject: [PATCH 05/94] updates to beacon types --- src/js/types/beacon.ts | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/js/types/beacon.ts b/src/js/types/beacon.ts index 976ea11d..640d8706 100644 --- a/src/js/types/beacon.ts +++ b/src/js/types/beacon.ts @@ -1,5 +1,6 @@ import { Rule } from 'antd/es/form'; import { Datum } from '@/types/overviewResponse'; +import { ChartData } from './data'; // ---------------------------- // form handling @@ -7,6 +8,10 @@ import { Datum } from '@/types/overviewResponse'; export type BeaconAssemblyIds = string[]; +// generic "info" response field +// only requirement in beacon spec is that it's an object +type GenericInfoField = Record + export interface FormField { name: string; rules?: Rule[]; @@ -105,6 +110,9 @@ export interface BeaconQueryResponse { responseSummary?: { numTotalResults: number; }; + meta?: { + beaconId: string; + } } export interface BeaconErrorData { @@ -113,3 +121,43 @@ export interface BeaconErrorData { errorMessage: string; }; } + +export interface BeaconOrganization { + id: string; + name: string; + description?: string + address?: string; + contactUrl?: string; + logoUrl?: string; + welcomeUrl?: string; + info?: GenericInfoField; +} + +export interface BeaconInfo { + id: string; + name: string; + apiVersion: string; + environment: string; + organization: BeaconOrganization; + version?: string; + welcomeUrl?: string; + alternativeUrl?: string; + createDateTime?: string; + updateDateTime?: string; + info?: GenericInfoField; +} + +// ---------------------------- +// response packaging +// ---------------------------- + +export interface FlattenedBeaconState { + isFetchingQueryResponse: boolean; + hasApiError: boolean; + apiErrorMessage: string; + individualCount?: number; + biosampleCount?: number; + biosampleChartData?: ChartData[]; + experimentCount?: number; + experimentChartData?: ChartData[]; +}; \ No newline at end of file From 6010861631d3fb567188cf4305c54bf136180d15 Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Wed, 29 May 2024 15:25:51 -0400 Subject: [PATCH 06/94] dead code --- src/js/features/beacon/beaconQuery.store.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/js/features/beacon/beaconQuery.store.ts b/src/js/features/beacon/beaconQuery.store.ts index 9d8b4822..9ba88d0b 100644 --- a/src/js/features/beacon/beaconQuery.store.ts +++ b/src/js/features/beacon/beaconQuery.store.ts @@ -25,7 +25,6 @@ export const makeBeaconQuery = createAsyncThunk< type BeaconQueryInitialStateType = { isFetchingQueryResponse: boolean; - response: BeaconQueryResponse; individualCount: number; biosampleCount: number; biosampleChartData: ChartData[]; @@ -37,7 +36,6 @@ type BeaconQueryInitialStateType = { const initialState: BeaconQueryInitialStateType = { isFetchingQueryResponse: false, - response: {}, individualCount: 0, biosampleCount: 0, biosampleChartData: [], From 929dabc2e1e134a5ef9d31fe510877ca7acfb79a Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Wed, 29 May 2024 15:28:23 -0400 Subject: [PATCH 07/94] second pass at network config redux --- src/js/features/beacon/networkConfig.store.ts | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/js/features/beacon/networkConfig.store.ts b/src/js/features/beacon/networkConfig.store.ts index 0a6c0fe3..bcf446bc 100644 --- a/src/js/features/beacon/networkConfig.store.ts +++ b/src/js/features/beacon/networkConfig.store.ts @@ -1,18 +1,13 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import axios from 'axios'; -import { makeAuthorizationHeader } from 'bento-auth-js'; +// import { makeAuthorizationHeader } from 'bento-auth-js'; import { RootState } from '@/store'; -import { serializeChartData } from '@/utils/chart'; import { beaconApiError } from '@/utils/beaconApiError'; -import { BeaconQueryPayload } from '@/types/beacon'; -import { BeaconNetworkAggregatedResponse, BeaconNetworkConfig } from '@/types/beaconNetwork'; -import { ChartData } from '@/types/data'; -import { BEACON_URL } from '@/config'; - -// temp, should be passed in from somewhere else -const BEACON_NETWORK_ROOT = 'https://bentov2.local/api/beacon/network/'; +import { ConfigForNetworkBeacon, BeaconNetworkConfig } from '@/types/beaconNetwork'; +import { BEACON_NETWORK_ROOT } from '@/constants/beaconConstants'; // network config currently just a list of beacons in the network with info about each one +// should probably add more details (eg version for whatever beacon is hosting the network) export const getBeaconNetworkConfig = createAsyncThunk< BeaconNetworkConfig, @@ -28,13 +23,13 @@ export const getBeaconNetworkConfig = createAsyncThunk< type beaconNetworkIntitalStateType = { isFetchingBeaconNetworkConfig: boolean; hasBeaconNetworkError: boolean; - networkBeacons: BeaconNetworkConfig; + networkBeacons: ConfigForNetworkBeacon[]; }; const initialState: beaconNetworkIntitalStateType = { isFetchingBeaconNetworkConfig: false, hasBeaconNetworkError: false, - networkBeacons: {}, + networkBeacons: [], }; const beaconNetwork = createSlice({ @@ -42,19 +37,16 @@ const beaconNetwork = createSlice({ initialState, reducers: {}, extraReducers: (builder) => { - builder.addCase(getBeaconNetworkConfig.pending, (state) => { - console.log('getBeaconNetworkConfig.pending'); + builder.addCase(getBeaconNetworkConfig.pending, (state, action) => { state.isFetchingBeaconNetworkConfig = true; }); builder.addCase(getBeaconNetworkConfig.fulfilled, (state, { payload }) => { - console.log('getBeaconNetworkConfig.fulfilled'); state.isFetchingBeaconNetworkConfig = false; state.networkBeacons = payload; }); builder.addCase(getBeaconNetworkConfig.rejected, (state, { payload }) => { state.isFetchingBeaconNetworkConfig = false; state.hasBeaconNetworkError = true; - console.log('getBeaconNetworkConfig.rejected'); }); }, }); From 3a0f373da1d67a07616c5b5a473eb4c3cc120b0e Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Wed, 29 May 2024 15:30:41 -0400 Subject: [PATCH 08/94] redux for single network beacon --- .../beacon/singleBeaconQuery.store.ts | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/js/features/beacon/singleBeaconQuery.store.ts diff --git a/src/js/features/beacon/singleBeaconQuery.store.ts b/src/js/features/beacon/singleBeaconQuery.store.ts new file mode 100644 index 00000000..160275d3 --- /dev/null +++ b/src/js/features/beacon/singleBeaconQuery.store.ts @@ -0,0 +1,93 @@ +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import axios from 'axios'; +// import { makeAuthorizationHeader } from 'bento-auth-js'; +import { RootState } from '@/store'; +import { serializeChartData } from '@/utils/chart'; +import { beaconApiError } from '@/utils/beaconApiError'; +import { BeaconQueryPayload, BeaconQueryResponse, FlattenedBeaconState } from '@/types/beacon'; +import { QueryToNetworkBeacon } from '@/types/beaconNetwork'; +import { ChartData } from '@/types/data'; + +// TODO (eventually): deduplicate with beaconQuery.store.ts + +export const singleBeaconQuery = createAsyncThunk< + BeaconQueryResponse, + QueryToNetworkBeacon, + { state: RootState; rejectValue: string } +>('singleBeaconQuery', async ({ beaconId, url, payload }, { rejectWithValue }) => { + // currently no auth in beacon network + // these would only make sense if we start creating tokens that apply to more than one bento instance + // const token = getState().auth.accessToken; + // const headers = makeAuthorizationHeader(token); + + console.log('singleBeaconQuery()'); + + return axios + .post(url, payload) + .then((res) => { + const data = res.data; + data.beaconid = beaconId; + return data; + }) + .catch(beaconApiError(rejectWithValue)); +}); + +interface beaconNetworkStateType { + respondingBeacons: { + [beaconId: string]: FlattenedBeaconState; + }; +} + +const initialState: beaconNetworkStateType = { + respondingBeacons: {}, +}; + +const singleBeaconQuerySlice = createSlice({ + name: 'singleBeaconQuery', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(singleBeaconQuery.pending, (state, action) => { + console.log('singleBeaconQuery.pending'); + const beaconId = action.meta.arg.beaconId; + const beaconState = { + hasApiError: false, + apiErrorMessage: '', + isFetchingQueryResponse: true, + }; + state.respondingBeacons[beaconId] = beaconState; + }); + builder.addCase(singleBeaconQuery.fulfilled, (state, action) => { + console.log('singleBeaconQuery.fulfilled'); + const beaconId = action.meta.arg.beaconId; + const { payload } = action; + const beaconState: FlattenedBeaconState = { + hasApiError: false, + apiErrorMessage: '', + isFetchingQueryResponse: false, + }; + if (payload.info?.bento) { + beaconState.biosampleCount = payload.info.bento?.biosamples?.count; + beaconState.biosampleChartData = serializeChartData(payload.info.bento?.biosamples?.sampled_tissue); + beaconState.experimentCount = payload.info.bento?.experiments?.count; + beaconState.experimentChartData = serializeChartData(payload.info.bento?.experiments?.experiment_type); + } + if (payload.responseSummary) { + beaconState.individualCount = payload.responseSummary.numTotalResults; + } + state.respondingBeacons[beaconId] = beaconState; + }); + builder.addCase(singleBeaconQuery.rejected, (state, action) => { + console.log('singleBeaconQuery.rejected'); + const beaconId = action.meta.arg.beaconId; + const beaconState: FlattenedBeaconState = { + hasApiError: true, + apiErrorMessage: action.payload as string, //passed from rejectWithValue + isFetchingQueryResponse: false, + }; + state.respondingBeacons[beaconId] = beaconState; + }); + }, +}); + +export default singleBeaconQuerySlice.reducer; From e329bfd155e38ca15c89b3d39b797d9d73e9e04c Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Wed, 29 May 2024 15:36:55 -0400 Subject: [PATCH 09/94] first pass at beacon network query redux --- src/js/features/beacon/networkQuery.store.ts | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/js/features/beacon/networkQuery.store.ts diff --git a/src/js/features/beacon/networkQuery.store.ts b/src/js/features/beacon/networkQuery.store.ts new file mode 100644 index 00000000..e3ab4662 --- /dev/null +++ b/src/js/features/beacon/networkQuery.store.ts @@ -0,0 +1,38 @@ +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +// import { makeAuthorizationHeader } from 'bento-auth-js'; +import { RootState } from '@/store'; +import { beaconApiError } from '@/utils/beaconApiError'; +import { BeaconQueryPayload } from '@/types/beacon'; +import { BeaconNetworkAggregatedResponse } from '@/types/beaconNetwork'; +import { singleBeaconQuery } from './singleBeaconQuery.store'; +import { BEACON_NETWORK_ROOT } from '@/constants/beaconConstants'; + +// probably more biolerplate here than needed +// we only really need to dispatch singleBeaconQuery() once for each beacon in the network + +// can parameterize at some point in the future +const DEFAULT_QUERY_ENDPOINT = '/individuals'; + +const queryUrl = (beaconId: string, endpoint: string): string => { + return BEACON_NETWORK_ROOT + 'beacons/' + beaconId + endpoint; +}; + +export const beaconNetworkQuery = createAsyncThunk( + 'beaconNetwork/beaconNetworkQuery', + async (payload, { getState, rejectWithValue, dispatch }) => { + // no auth in network prototype + // const token = getState().auth.accessToken; + // const headers = makeAuthorizationHeader(token); + + console.log('beaconNetworkQuery()'); + + const beacons = getState().beaconNetworkConfig.networkBeacons; + return await Promise.all( + beacons.map((b) => { + const url = queryUrl(b.id, DEFAULT_QUERY_ENDPOINT); + return dispatch(singleBeaconQuery({ beaconId: b.id, url: url, payload: payload })); + }) + ).catch(beaconApiError(rejectWithValue)); + } +); + From 81de8e4ad13f57e7b44b022692b0212339516406 Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Fri, 31 May 2024 16:38:21 -0400 Subject: [PATCH 10/94] beacon network redux --- .../beacon/networkBeaconQuery.store.ts | 93 +++++++++++++++++++ src/js/features/beacon/networkConfig.store.ts | 8 +- src/js/features/beacon/networkQuery.store.ts | 8 +- 3 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 src/js/features/beacon/networkBeaconQuery.store.ts diff --git a/src/js/features/beacon/networkBeaconQuery.store.ts b/src/js/features/beacon/networkBeaconQuery.store.ts new file mode 100644 index 00000000..848773e9 --- /dev/null +++ b/src/js/features/beacon/networkBeaconQuery.store.ts @@ -0,0 +1,93 @@ +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import axios from 'axios'; +// import { makeAuthorizationHeader } from 'bento-auth-js'; +import { RootState } from '@/store'; +import { serializeChartData } from '@/utils/chart'; +import { beaconApiError } from '@/utils/beaconApiError'; +import { BeaconQueryPayload, BeaconQueryResponse, FlattenedBeaconResponse } from '@/types/beacon'; +import { QueryToNetworkBeacon } from '@/types/beaconNetwork'; +import { ChartData } from '@/types/data'; + +// TODO (eventually): deduplicate with beaconQuery.store.ts + +export const networkBeaconQuery = createAsyncThunk< + BeaconQueryResponse, + QueryToNetworkBeacon, + { state: RootState; rejectValue: string } +>('networkBeaconQuery', async ({ beaconId, url, payload }, { rejectWithValue }) => { + // currently no auth in beacon network + // these would only make sense if we start creating tokens that apply to more than one bento instance + // const token = getState().auth.accessToken; + // const headers = makeAuthorizationHeader(token); + + console.log('networkBeaconQuery()'); + + return axios + .post(url, payload) + .then((res) => { + const data = res.data; + data.beaconid = beaconId; + return data; + }) + .catch(beaconApiError(rejectWithValue)); +}); + +interface beaconNetworkStateType { + beacons: { + [beaconId: string]: FlattenedBeaconResponse; + }; +} + +const initialState: beaconNetworkStateType = { + beacons: {}, +}; + +const networkBeaconQuerySlice = createSlice({ + name: 'networkBeaconQuery', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(networkBeaconQuery.pending, (state, action) => { + console.log('networkBeaconQuery.pending'); + const beaconId = action.meta.arg.beaconId; + const beaconState = { + hasApiError: false, + apiErrorMessage: '', + isFetchingQueryResponse: true, + }; + state.beacons[beaconId] = beaconState; + }); + builder.addCase(networkBeaconQuery.fulfilled, (state, action) => { + console.log('networkBeaconQuery.fulfilled'); + const beaconId = action.meta.arg.beaconId; + const { payload } = action; + const beaconState: FlattenedBeaconResponse = { + hasApiError: false, + apiErrorMessage: '', + isFetchingQueryResponse: false, + }; + if (payload.info?.bento) { + beaconState.biosampleCount = payload.info.bento?.biosamples?.count; + beaconState.biosampleChartData = serializeChartData(payload.info.bento?.biosamples?.sampled_tissue); + beaconState.experimentCount = payload.info.bento?.experiments?.count; + beaconState.experimentChartData = serializeChartData(payload.info.bento?.experiments?.experiment_type); + } + if (payload.responseSummary) { + beaconState.individualCount = payload.responseSummary.numTotalResults; + } + state.beacons[beaconId] = beaconState; + }); + builder.addCase(networkBeaconQuery.rejected, (state, action) => { + console.log('networkBeaconQuery.rejected'); + const beaconId = action.meta.arg.beaconId; + const beaconState: FlattenedBeaconResponse = { + hasApiError: true, + apiErrorMessage: action.payload as string, //passed from rejectWithValue + isFetchingQueryResponse: false, + }; + state.beacons[beaconId] = beaconState; + }); + }, +}); + +export default networkBeaconQuerySlice.reducer; diff --git a/src/js/features/beacon/networkConfig.store.ts b/src/js/features/beacon/networkConfig.store.ts index bcf446bc..0ca548a9 100644 --- a/src/js/features/beacon/networkConfig.store.ts +++ b/src/js/features/beacon/networkConfig.store.ts @@ -3,7 +3,7 @@ import axios from 'axios'; // import { makeAuthorizationHeader } from 'bento-auth-js'; import { RootState } from '@/store'; import { beaconApiError } from '@/utils/beaconApiError'; -import { ConfigForNetworkBeacon, BeaconNetworkConfig } from '@/types/beaconNetwork'; +import { NetworkBeacon, BeaconNetworkConfig } from '@/types/beaconNetwork'; import { BEACON_NETWORK_ROOT } from '@/constants/beaconConstants'; // network config currently just a list of beacons in the network with info about each one @@ -23,13 +23,13 @@ export const getBeaconNetworkConfig = createAsyncThunk< type beaconNetworkIntitalStateType = { isFetchingBeaconNetworkConfig: boolean; hasBeaconNetworkError: boolean; - networkBeacons: ConfigForNetworkBeacon[]; + beacons: NetworkBeacon[]; }; const initialState: beaconNetworkIntitalStateType = { isFetchingBeaconNetworkConfig: false, hasBeaconNetworkError: false, - networkBeacons: [], + beacons: [], }; const beaconNetwork = createSlice({ @@ -42,7 +42,7 @@ const beaconNetwork = createSlice({ }); builder.addCase(getBeaconNetworkConfig.fulfilled, (state, { payload }) => { state.isFetchingBeaconNetworkConfig = false; - state.networkBeacons = payload; + state.beacons = payload; }); builder.addCase(getBeaconNetworkConfig.rejected, (state, { payload }) => { state.isFetchingBeaconNetworkConfig = false; diff --git a/src/js/features/beacon/networkQuery.store.ts b/src/js/features/beacon/networkQuery.store.ts index e3ab4662..a01018c9 100644 --- a/src/js/features/beacon/networkQuery.store.ts +++ b/src/js/features/beacon/networkQuery.store.ts @@ -4,11 +4,11 @@ import { RootState } from '@/store'; import { beaconApiError } from '@/utils/beaconApiError'; import { BeaconQueryPayload } from '@/types/beacon'; import { BeaconNetworkAggregatedResponse } from '@/types/beaconNetwork'; -import { singleBeaconQuery } from './singleBeaconQuery.store'; +import { networkBeaconQuery } from './networkBeaconQuery.store'; import { BEACON_NETWORK_ROOT } from '@/constants/beaconConstants'; // probably more biolerplate here than needed -// we only really need to dispatch singleBeaconQuery() once for each beacon in the network +// we only really need to dispatch networkBeaconQuery() once for each beacon in the network // can parameterize at some point in the future const DEFAULT_QUERY_ENDPOINT = '/individuals'; @@ -26,11 +26,11 @@ export const beaconNetworkQuery = createAsyncThunk { const url = queryUrl(b.id, DEFAULT_QUERY_ENDPOINT); - return dispatch(singleBeaconQuery({ beaconId: b.id, url: url, payload: payload })); + return dispatch(networkBeaconQuery({ beaconId: b.id, url: url, payload: payload })); }) ).catch(beaconApiError(rejectWithValue)); } From d95ee5f87bdfb62ddf37a67a172f780649d99a8d Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Fri, 31 May 2024 16:39:05 -0400 Subject: [PATCH 11/94] beacon typing additions --- src/js/types/beacon.ts | 19 +++++++++---------- src/js/types/beaconNetwork.ts | 34 ++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/js/types/beacon.ts b/src/js/types/beacon.ts index 640d8706..f7e7548f 100644 --- a/src/js/types/beacon.ts +++ b/src/js/types/beacon.ts @@ -10,7 +10,7 @@ export type BeaconAssemblyIds = string[]; // generic "info" response field // only requirement in beacon spec is that it's an object -type GenericInfoField = Record +type GenericInfoField = Record; export interface FormField { name: string; @@ -112,7 +112,7 @@ export interface BeaconQueryResponse { }; meta?: { beaconId: string; - } + }; } export interface BeaconErrorData { @@ -122,10 +122,10 @@ export interface BeaconErrorData { }; } -export interface BeaconOrganization { +export interface BeaconOrganizationType { id: string; name: string; - description?: string + description?: string; address?: string; contactUrl?: string; logoUrl?: string; @@ -133,17 +133,16 @@ export interface BeaconOrganization { info?: GenericInfoField; } -export interface BeaconInfo { +export interface BeaconServiceInfo { id: string; name: string; apiVersion: string; environment: string; - organization: BeaconOrganization; + organization: BeaconOrganizationType; + description?: string; version?: string; welcomeUrl?: string; alternativeUrl?: string; - createDateTime?: string; - updateDateTime?: string; info?: GenericInfoField; } @@ -151,7 +150,7 @@ export interface BeaconInfo { // response packaging // ---------------------------- -export interface FlattenedBeaconState { +export interface FlattenedBeaconResponse { isFetchingQueryResponse: boolean; hasApiError: boolean; apiErrorMessage: string; @@ -160,4 +159,4 @@ export interface FlattenedBeaconState { biosampleChartData?: ChartData[]; experimentCount?: number; experimentChartData?: ChartData[]; -}; \ No newline at end of file +} diff --git a/src/js/types/beaconNetwork.ts b/src/js/types/beaconNetwork.ts index 23548059..54144071 100644 --- a/src/js/types/beaconNetwork.ts +++ b/src/js/types/beaconNetwork.ts @@ -1,5 +1,20 @@ -import { BeaconQueryResponse } from "./beacon"; -import { ChartData } from "./data"; +import { + BeaconConfigResponse, + BeaconServiceInfo, + BeaconQueryResponse, + BeaconQueryPayload, + FlattenedBeaconResponse, +} from './beacon'; +import { ChartData } from './data'; + +export interface NetworkBeacon extends BeaconServiceInfo { + apiUrl: string; + overview?: any; // to update as design settles + // queryResponse?: FlattenedBeaconResponse +} + +// more to come +export type BeaconNetworkConfig = NetworkBeacon[]; export interface BeaconOrgDetails { logoUrl: string; @@ -10,18 +25,25 @@ export interface BeaconOrgDetails { } // TODO, should probably standardize with standard response -export interface BeaconNetworkResponse { +export interface BeaconFlattenedAggregateResponse { individualCount: number; biosampleCount: number; experimentCount: number; - biosampleChartData: ChartData[], - experimentChartData: ChartData[], + biosampleChartData: ChartData[]; + experimentChartData: ChartData[]; } export interface RespondingBeacon { organization: BeaconOrgDetails; - response: BeaconNetworkResponse; //or something else + response: FlattenedBeaconResponse; //or something else bentoUrl: string; description: string; } +export type BeaconNetworkAggregatedResponse = RespondingBeacon[]; + +export interface QueryToNetworkBeacon { + beaconId: string; + url: string; + payload: BeaconQueryPayload; +} From f0a7a9223d34c8a4b73aea8e7934708393f77555 Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Fri, 31 May 2024 16:39:56 -0400 Subject: [PATCH 12/94] first pass of card for network beacon --- .../BeaconNetwork/BeaconDetails.tsx | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/js/components/BeaconNetwork/BeaconDetails.tsx diff --git a/src/js/components/BeaconNetwork/BeaconDetails.tsx b/src/js/components/BeaconNetwork/BeaconDetails.tsx new file mode 100644 index 00000000..9f4aeb2c --- /dev/null +++ b/src/js/components/BeaconNetwork/BeaconDetails.tsx @@ -0,0 +1,133 @@ +import React, { useState } from 'react'; +import { useAppSelector } from '@/hooks'; +import { Button, Card, Col, Row, Space, Statistic, Tag, Typography } from 'antd'; +import { TeamOutlined } from '@ant-design/icons'; +import { BiDna } from 'react-icons/bi'; +import ExpSvg from '../Util/ExpSvg'; +import { BOX_SHADOW, COUNTS_FILL } from '@/constants/overviewConstants'; +import SearchResultsPane from '../Search/SearchResultsPane'; +import BeaconOrganization from './BeaconOrganization'; +import { NetworkBeacon, BeaconFlattenedAggregateResponse } from '@/types/beaconNetwork'; + +import { useTranslationDefault } from '@/hooks'; +import { FlattenedBeaconResponse } from '@/types/beacon'; +const { Title } = Typography; +const { Meta } = Card; +// get name, logo, and overview details from /overview for each instance + +//get results for each beacon from redux and pass as props + +// add a tag for each assembly + +// link for bento_public beacon for an instance is at top-level "welcomeUrl" +// this ONLY exists for instances with a beacon UI +// there is no general "bento_public" link for instances +// note that the top-level "welcomeUrl" is separate from organization.welcomeUrl + +const BeaconDetails = ({ beacon, response }: BeaconDetailsProps) => { + console.log({ beacon }); + + const t = useTranslationDefault(); + const { id, organization, welcomeUrl, description, overview } = beacon; + const { variants } = overview; + const assemblies = Object.keys(variants); + + console.log({ assemblies }); + + const { individualCount, biosampleCount, experimentCount, biosampleChartData, experimentChartData } = response; + const [showFullCard, setShowFullCard] = useState(false); + + // some fields may be missing from response, eg when there's an error + const individualCountValue = individualCount ?? 0; + const biosampleCountValue = biosampleCount ?? 0; + const biosampleChartDataValue = biosampleChartData ?? []; + const experimentCountValue = experimentCount ?? 0; + const experimentChartDataValue = experimentChartData ?? []; + + console.log('BeaconDetails()'); + + const toggleFullCard = () => { + setShowFullCard(!showFullCard); + }; + + return ( + + + {organization.name} + + + } + style={{ + margin: '5px', + borderRadius: '10px', + padding: '5px', + maxWidth: showFullCard ? '1200px' : '680px', + width: '100%', + ...BOX_SHADOW, + }} + styles={{body: {paddingBottom: "10px"}}} + extra={} + > + + + + + + + {assemblies.map((a) => ( + {a} + ))} + } + /> + } + /> + } + /> + + + + {showFullCard && ( + + + + + )} + + ); +}; + +export interface BeaconDetailsProps { + beacon: NetworkBeacon; + response: FlattenedBeaconResponse; +} + +export default BeaconDetails; From 1a7a63bfb0f4f47f3441db18db2510c98a32a9e6 Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Fri, 31 May 2024 16:40:39 -0400 Subject: [PATCH 13/94] beacon org --- .../BeaconNetwork/BeaconOrganization.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/js/components/BeaconNetwork/BeaconOrganization.tsx diff --git a/src/js/components/BeaconNetwork/BeaconOrganization.tsx b/src/js/components/BeaconNetwork/BeaconOrganization.tsx new file mode 100644 index 00000000..93b49d8f --- /dev/null +++ b/src/js/components/BeaconNetwork/BeaconOrganization.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Card, Typography } from 'antd'; +import { BeaconOrganizationType } from '@/types/beacon'; +const { Title, Text, Link } = Typography; + +const LINK_STYLE = { padding: '10px' }; +const ORG_CARD_STYLE = { background: 'hotpink' }; +const CARD_STYLES = { + body: { background: '#f5f5f5', width: '100%' }, +}; + +const BeaconOrganization = ({ organization, bentoUrl, description }: BeaconOrganizationProps) => { + return ( + + {description} +
+ + Home Page + + + Bento + +
+
+ ); +}; + +export interface BeaconOrganizationProps { + organization: BeaconOrganizationType; + bentoUrl: string | undefined; + description: string | undefined; +} + +export default BeaconOrganization; From 7f45677a96aa31bcb8fa9625b3546e535077ca1a Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Fri, 31 May 2024 16:41:04 -0400 Subject: [PATCH 14/94] add network to tabbed dashboard --- src/js/components/TabbedDashboard.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/js/components/TabbedDashboard.tsx b/src/js/components/TabbedDashboard.tsx index 2baba5fd..714e00ea 100644 --- a/src/js/components/TabbedDashboard.tsx +++ b/src/js/components/TabbedDashboard.tsx @@ -11,6 +11,7 @@ import { makeGetDataRequestThunk } from '@/features/data/data.store'; import { makeGetSearchFields } from '@/features/search/query.store'; import { makeGetProvenanceRequest } from '@/features/provenance/provenance.store'; import { getBeaconConfig } from '@/features/beacon/beaconConfig.store'; +import { getBeaconNetworkConfig } from '@/features/beacon/networkConfig.store'; import { fetchGohanData, fetchKatsuData } from '@/features/ingestion/lastIngestion.store'; import Loader from './Loader'; @@ -19,6 +20,7 @@ import Search from './Search/Search'; import ProvenanceTab from './Provenance/ProvenanceTab'; import BeaconQueryUi from './Beacon/BeaconQueryUi'; import NetworkUi from './BeaconNetwork/NetworkUi'; +import DumbNetworkUi from './BeaconNetwork/DumbNetworkUi'; import { useAppDispatch, useAppSelector, useTranslationDefault } from '@/hooks'; import { buildQueryParamsUrl } from '@/utils/search'; import { makeGetDataTypes } from '@/features/dataTypes/dataTypes.store'; @@ -37,7 +39,10 @@ const TabbedDashboard = () => { const isAuthenticated = useIsAuthenticated(); useEffect(() => { - dispatch(makeGetConfigRequest()).then(() => dispatch(getBeaconConfig())); + dispatch(makeGetConfigRequest()).then(() => { + dispatch(getBeaconConfig()); + dispatch(getBeaconNetworkConfig()); + }); dispatch(makeGetAboutRequest()); dispatch(makeGetDataRequestThunk()); dispatch(makeGetSearchFields()); @@ -55,6 +60,7 @@ const TabbedDashboard = () => { const isFetchingSearchFields = useAppSelector((state) => state.query.isFetchingFields); const queryParams = useAppSelector((state) => state.query.queryParams); const isFetchingBeaconConfig = useAppSelector((state) => state.beaconConfig?.isFetchingBeaconConfig); + const isFetchingNetworkConfig = useAppSelector((state) => state.beaconNetwork.isFetchingBeaconNetworkConfig); const onChange = useCallback( (key: string) => { @@ -101,7 +107,7 @@ const TabbedDashboard = () => { { title: 'Beacon Network', content: , - loading: false, + loading: isFetchingNetworkConfig, active: BEACON_NETWORK_UI_ENABLED, key: 'beacon_network', }, From abafc28e5e2cf78d0b950da23ab990cbd65ed088 Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Fri, 31 May 2024 16:41:45 -0400 Subject: [PATCH 15/94] add beacon network redux to store --- src/js/store.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/js/store.ts b/src/js/store.ts index a92d517b..957d4a5c 100644 --- a/src/js/store.ts +++ b/src/js/store.ts @@ -16,6 +16,8 @@ import lastIngestionDataReducer from '@/features/ingestion/lastIngestion.store'; import provenanceReducer from '@/features/provenance/provenance.store'; import beaconConfigReducer from './features/beacon/beaconConfig.store'; import beaconQueryReducer from './features/beacon/beaconQuery.store'; +import beaconNetworkConfigReducer from './features/beacon/networkConfig.store'; +import networkBeaconQueryReducer from './features/beacon/networkBeaconQuery.store'; import { getValue, saveValue } from './utils/localStorage'; interface PersistedState { @@ -41,6 +43,8 @@ export const store = configureStore({ lastIngestionData: lastIngestionDataReducer, beaconConfig: beaconConfigReducer, beaconQuery: beaconQueryReducer, + beaconNetwork: beaconNetworkConfigReducer, + beaconNetworkResponse: networkBeaconQueryReducer }, preloadedState: persistedState, }); From 69d9c27f1794a9a58612c848bc79c95d53ffdaa7 Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Wed, 5 Jun 2024 09:29:37 -0400 Subject: [PATCH 16/94] first pass at common beacon / beacon network form UI --- .../BeaconCommon/BeaconQueryFormUi.tsx | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 src/js/components/BeaconCommon/BeaconQueryFormUi.tsx diff --git a/src/js/components/BeaconCommon/BeaconQueryFormUi.tsx b/src/js/components/BeaconCommon/BeaconQueryFormUi.tsx new file mode 100644 index 00000000..0424ccdb --- /dev/null +++ b/src/js/components/BeaconCommon/BeaconQueryFormUi.tsx @@ -0,0 +1,313 @@ +import React, { useEffect, useState } from 'react'; +import { useAppSelector, useAppDispatch, useTranslationDefault } from '@/hooks'; + +import { Button, Card, Col, Form, Row, Space, Switch, Tooltip, Typography } from 'antd'; +import VariantsForm from '../Beacon/VariantsForm'; +import Filters from './Filters'; +import SearchToolTip from './ToolTips/SearchToolTip'; +import VariantsInstructions from './ToolTips/VariantsInstructions'; +import BeaconErrorMessage from '../Beacon/BeaconErrorMessage'; +import { MetadataInstructions } from './ToolTips/MetadataInstructions'; +import { + BeaconAssemblyIds, + BeaconQueryPayload, + BeaconQueryThunk, + FormFilter, + FormValues, + PayloadFilter, + PayloadVariantsQuery, +} from '@/types/beacon'; +import { Section } from '@/types/search'; +import { useIsAuthenticated } from 'bento-auth-js'; + +const VARIANTS_FORM_ERROR_MESSAGE = + 'Variants form should include either an end position or both reference and alternate bases'; + +import { BOX_SHADOW } from '@/constants/overviewConstants'; +import { + WRAPPER_STYLE, + FORM_ROW_GUTTERS, + CARD_STYLE, + BUTTON_AREA_STYLE, + BUTTON_STYLE, + CARD_STYLES, +} from '@/constants/beaconConstants'; +import { SwitcherTwoTone } from '@ant-design/icons'; + +const STARTER_FILTER = { index: 1, active: true }; + +const BeaconQueryFormUi = ({ + isFetchingConfig, //to remove?, don't render this component until config is known (?) + isFetchingQueryResponse, //network query is "fire-and-forget" so this doesn't apply here + isNetworkQuery, + beaconAssemblyIds, + querySections, + launchQuery, +}: BeaconQueryFormUiProps) => { + const td = useTranslationDefault(); + const [form] = Form.useForm(); + + const [filters, setFilters] = useState([STARTER_FILTER]); + + const [hasFormError, setHasFormError] = useState(false); + const [formErrorMessage, setFormErrorMessage] = useState(''); + // remember if user closed alert, so we can force re-render of a new one later + const [errorAlertClosed, setErrorAlertClosed] = useState(false); + const [unionNetworkFilters, setUnionNetworkFilters] = useState(false); + + const hasVariants = beaconAssemblyIds.length > 0; + const formInitialValues = { 'Assembly ID': beaconAssemblyIds.length === 1 && beaconAssemblyIds[0] }; + const uiInstructions = hasVariants ? 'Search by genomic variants, clinical metadata or both.' : ''; + + const hasApiError = isNetworkQuery ? false : useAppSelector((state) => state.beaconQuery.hasApiError); + const apiErrorMessage = isNetworkQuery ? false : useAppSelector((state) => state.beaconQuery.apiErrorMessage); + + const hasError = hasFormError || hasApiError; + const showError = hasError && !errorAlertClosed; + + // should not be possible to have both errors simultaneously + // and api error is more important + const errorMessage = apiErrorMessage || formErrorMessage; + + const clearFormError = () => { + setHasFormError(false); + setFormErrorMessage(''); + }; + + const handleNetworkFilterToggle = () => { + setUnionNetworkFilters(!unionNetworkFilters); + }; + + const dispatch = useAppDispatch(); + const isAuthenticated = useIsAuthenticated(); + const launchEmptyQuery = () => dispatch(launchQuery(requestPayload({}, []))); + + useEffect(() => { + // only for local query?? + launchEmptyQuery(); + + // set assembly id options matching what's in gohan + form.setFieldsValue(formInitialValues); + }, [isFetchingConfig, isAuthenticated]); + + // following GA4GH recommendations, UI is one-based, but API is zero-based, "half-open" + // so to convert to zero-based, we only modify the start value + // see eg https://genome-blog.soe.ucsc.edu/blog/2016/12/12/the-ucsc-genome-browser-coordinate-counting-systems/ + const convertToZeroBased = (start: string) => Number(start) - 1; + + const handleFinish = (formValues: FormValues) => { + // if bad form, block submit and show user error + if (!variantsFormValid(formValues)) { + setHasFormError(true); + setErrorAlertClosed(false); + setFormErrorMessage(td(VARIANTS_FORM_ERROR_MESSAGE)); + return; + } + + clearFormError(); + setErrorAlertClosed(false); + const jsonPayload = packageBeaconJSON(formValues); + console.log('dispatching beacon query'); + + dispatch(launchQuery(jsonPayload)); + }; + + const handleClearForm = () => { + setFilters([STARTER_FILTER]); + form.resetFields(); + form.setFieldsValue(formInitialValues); + clearFormError(); + setErrorAlertClosed(false); + launchEmptyQuery(); + }; + + const handleValuesChange = (_: Partial, allValues: FormValues) => { + form.validateFields(['Chromosome', 'Variant start', 'Variant end', 'Reference base(s)', 'Alternate base(s)']); + + // clear any existing errors if form now valid + if (variantsFormValid(allValues)) { + clearFormError(); + } + // can also check filter values here (to e.g. avoid offering duplicate options) + }; + + const packageBeaconJSON = (values: FormValues) => { + let query = {} as PayloadVariantsQuery; + const payloadFilters = packageFilters(values); + const hasVariantsQuery = values?.['Chromosome'] || values?.['Variant start'] || values?.['Reference base(s)']; + if (hasVariantsQuery) { + query = { + referenceName: values['Chromosome'], + start: [convertToZeroBased(values['Variant start'])], + assemblyId: values['Assembly ID'], + }; + if (values['Variant end']) { + query.end = [Number(values['Variant end'])]; + } + if (values['Reference base(s)']) { + query.referenceBases = values['Reference base(s)']; + } + if (values['Alternate base(s)']) { + query.alternateBases = values['Alternate base(s)']; + } + } + + return requestPayload(query, payloadFilters); + }; + + const requestPayload = (query: PayloadVariantsQuery, payloadFilters: PayloadFilter[]): BeaconQueryPayload => ({ + meta: { apiVersion: '2.0.0' }, + query: { requestParameters: { g_variant: query }, filters: payloadFilters }, + bento: { showSummaryStatistics: true }, + }); + + const packageFilters = (values: FormValues): PayloadFilter[] => { + // ignore optional first filter when left blank + if (filters.length === 1 && !values.filterId1) { + return []; + } + + return filters + .filter((f) => f.active) + .map((f) => ({ + id: values[`filterId${f.index}`], + operator: '=', + value: values[`filterValue${f.index}`], + })); + }; + + const variantsFormValid = (formValues: FormValues) => { + // valid variant form states: + // empty except possibly autofilled assemblyID (no variant query) + // chrom, start, assemblyID, end (range query) + // chrom, start, assemblyID, alt, ref (sequence query) + // https://docs.genomebeacons.org/variant-queries/ + + // as an alternative, we could require "end" always, then form logic would be less convoluted + // just set start=end to search for SNPs + + const empty = !( + formValues['Chromosome'] || + formValues['Variant start'] || + formValues['Variant end'] || + formValues['Reference base(s)'] || + formValues['Alternate base(s)'] + ); + const rangeQuery = + formValues['Chromosome'] && formValues['Variant start'] && formValues['Variant end'] && formValues['Assembly ID']; + + const sequenceQuery = + formValues['Chromosome'] && + formValues['Variant start'] && + formValues['Assembly ID'] && + formValues['Reference base(s)'] && + formValues['Alternate base(s)']; + return empty || rangeQuery || sequenceQuery; + }; + + const searchButtonText = isNetworkQuery ? 'Search Network' : 'Search Beacon'; + + const NetworkFilterToggle = () => { + return ( + +
+

{unionNetworkFilters ? 'all filters' : 'common filters only'}

+ +
+
+ ); + }; + + return ( + +

{td(uiInstructions)}

+
+ + {hasVariants && ( + + + + + } + > + + + + )} + + + <>{td('Metadata')} + + } + style={CARD_STYLE} + styles={CARD_STYLES} + extra={ +
+ {isNetworkQuery ? : null} + + + +
+ } + > + +
+ +
+ + + {showError && ( + setErrorAlertClosed(true)} + /> + )} +
+ + +
+ +
+
+
+ ); +}; + +export interface BeaconQueryFormUiProps { + isFetchingConfig: boolean; + isFetchingQueryResponse: boolean; + isNetworkQuery: boolean; + beaconAssemblyIds: BeaconAssemblyIds; + querySections: Section[]; + launchQuery: BeaconQueryThunk; +} + +export default BeaconQueryFormUi; From c8d64f0dda2dc816deb07d9528c3a3bb5ddaefd9 Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Wed, 5 Jun 2024 15:39:53 -0400 Subject: [PATCH 17/94] network beacon redux --- .../beacon/networkBeaconQuery.store.ts | 6 +- .../beacon/singleBeaconQuery.store.ts | 93 ------------------- 2 files changed, 4 insertions(+), 95 deletions(-) delete mode 100644 src/js/features/beacon/singleBeaconQuery.store.ts diff --git a/src/js/features/beacon/networkBeaconQuery.store.ts b/src/js/features/beacon/networkBeaconQuery.store.ts index 848773e9..18104ba4 100644 --- a/src/js/features/beacon/networkBeaconQuery.store.ts +++ b/src/js/features/beacon/networkBeaconQuery.store.ts @@ -61,9 +61,11 @@ const networkBeaconQuerySlice = createSlice({ console.log('networkBeaconQuery.fulfilled'); const beaconId = action.meta.arg.beaconId; const { payload } = action; + const hasErrorResponse = payload.hasOwnProperty("error") + const beaconState: FlattenedBeaconResponse = { - hasApiError: false, - apiErrorMessage: '', + hasApiError: hasErrorResponse, + apiErrorMessage: hasErrorResponse ? payload.error?.errorMessage ?? "error" : "", isFetchingQueryResponse: false, }; if (payload.info?.bento) { diff --git a/src/js/features/beacon/singleBeaconQuery.store.ts b/src/js/features/beacon/singleBeaconQuery.store.ts deleted file mode 100644 index 160275d3..00000000 --- a/src/js/features/beacon/singleBeaconQuery.store.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; -import axios from 'axios'; -// import { makeAuthorizationHeader } from 'bento-auth-js'; -import { RootState } from '@/store'; -import { serializeChartData } from '@/utils/chart'; -import { beaconApiError } from '@/utils/beaconApiError'; -import { BeaconQueryPayload, BeaconQueryResponse, FlattenedBeaconState } from '@/types/beacon'; -import { QueryToNetworkBeacon } from '@/types/beaconNetwork'; -import { ChartData } from '@/types/data'; - -// TODO (eventually): deduplicate with beaconQuery.store.ts - -export const singleBeaconQuery = createAsyncThunk< - BeaconQueryResponse, - QueryToNetworkBeacon, - { state: RootState; rejectValue: string } ->('singleBeaconQuery', async ({ beaconId, url, payload }, { rejectWithValue }) => { - // currently no auth in beacon network - // these would only make sense if we start creating tokens that apply to more than one bento instance - // const token = getState().auth.accessToken; - // const headers = makeAuthorizationHeader(token); - - console.log('singleBeaconQuery()'); - - return axios - .post(url, payload) - .then((res) => { - const data = res.data; - data.beaconid = beaconId; - return data; - }) - .catch(beaconApiError(rejectWithValue)); -}); - -interface beaconNetworkStateType { - respondingBeacons: { - [beaconId: string]: FlattenedBeaconState; - }; -} - -const initialState: beaconNetworkStateType = { - respondingBeacons: {}, -}; - -const singleBeaconQuerySlice = createSlice({ - name: 'singleBeaconQuery', - initialState, - reducers: {}, - extraReducers: (builder) => { - builder.addCase(singleBeaconQuery.pending, (state, action) => { - console.log('singleBeaconQuery.pending'); - const beaconId = action.meta.arg.beaconId; - const beaconState = { - hasApiError: false, - apiErrorMessage: '', - isFetchingQueryResponse: true, - }; - state.respondingBeacons[beaconId] = beaconState; - }); - builder.addCase(singleBeaconQuery.fulfilled, (state, action) => { - console.log('singleBeaconQuery.fulfilled'); - const beaconId = action.meta.arg.beaconId; - const { payload } = action; - const beaconState: FlattenedBeaconState = { - hasApiError: false, - apiErrorMessage: '', - isFetchingQueryResponse: false, - }; - if (payload.info?.bento) { - beaconState.biosampleCount = payload.info.bento?.biosamples?.count; - beaconState.biosampleChartData = serializeChartData(payload.info.bento?.biosamples?.sampled_tissue); - beaconState.experimentCount = payload.info.bento?.experiments?.count; - beaconState.experimentChartData = serializeChartData(payload.info.bento?.experiments?.experiment_type); - } - if (payload.responseSummary) { - beaconState.individualCount = payload.responseSummary.numTotalResults; - } - state.respondingBeacons[beaconId] = beaconState; - }); - builder.addCase(singleBeaconQuery.rejected, (state, action) => { - console.log('singleBeaconQuery.rejected'); - const beaconId = action.meta.arg.beaconId; - const beaconState: FlattenedBeaconState = { - hasApiError: true, - apiErrorMessage: action.payload as string, //passed from rejectWithValue - isFetchingQueryResponse: false, - }; - state.respondingBeacons[beaconId] = beaconState; - }); - }, -}); - -export default singleBeaconQuerySlice.reducer; From c729ec969fdc644c5c7cfb39252b91a0d5bc4f4f Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Fri, 7 Jun 2024 15:20:16 -0400 Subject: [PATCH 18/94] reducer for network overview --- .../beacon/networkBeaconQuery.store.ts | 60 ++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/src/js/features/beacon/networkBeaconQuery.store.ts b/src/js/features/beacon/networkBeaconQuery.store.ts index 18104ba4..1a6e465f 100644 --- a/src/js/features/beacon/networkBeaconQuery.store.ts +++ b/src/js/features/beacon/networkBeaconQuery.store.ts @@ -4,9 +4,9 @@ import axios from 'axios'; import { RootState } from '@/store'; import { serializeChartData } from '@/utils/chart'; import { beaconApiError } from '@/utils/beaconApiError'; -import { BeaconQueryPayload, BeaconQueryResponse, FlattenedBeaconResponse } from '@/types/beacon'; -import { QueryToNetworkBeacon } from '@/types/beaconNetwork'; import { ChartData } from '@/types/data'; +import { BeaconQueryPayload, BeaconQueryResponse, FlattenedBeaconResponse } from '@/types/beacon'; +import { BeaconFlattenedAggregateResponse, QueryToNetworkBeacon } from '@/types/beaconNetwork'; // TODO (eventually): deduplicate with beaconQuery.store.ts @@ -20,8 +20,6 @@ export const networkBeaconQuery = createAsyncThunk< // const token = getState().auth.accessToken; // const headers = makeAuthorizationHeader(token); - console.log('networkBeaconQuery()'); - return axios .post(url, payload) .then((res) => { @@ -33,19 +31,66 @@ export const networkBeaconQuery = createAsyncThunk< }); interface beaconNetworkStateType { + networkOverview: BeaconFlattenedAggregateResponse; beacons: { [beaconId: string]: FlattenedBeaconResponse; }; } +type TempChartObject = Record; + const initialState: beaconNetworkStateType = { + networkOverview: { + individualCount: 0, + biosampleCount: 0, + experimentCount: 0, + biosampleChartData: [], + experimentChartData: [], + }, beacons: {}, }; +const chartArrayToChartObj = (cArr: ChartData[]): TempChartObject => { + const obj: TempChartObject = {}; + cArr.forEach((c) => { + obj[c.x] = c.y; + }); + return obj; +}; + +const chartObjToChartArr = (cObj: TempChartObject): ChartData[] => { + const arr = []; + for (const key in cObj) { + arr.push({ x: key, y: cObj[key] }); + } + return arr; +}; + +const mergeCharts = (c1: ChartData[], c2: ChartData[]): ChartData[] => { + const merged = chartArrayToChartObj(c1); + c2.forEach((c) => { + merged[c.x] = (merged[c.x] ?? 0) + c.y; + }); + return chartObjToChartArr(merged); +}; + const networkBeaconQuerySlice = createSlice({ name: 'networkBeaconQuery', initialState, - reducers: {}, + reducers: { + computeNetworkOverview(state) { + const overview = initialState.networkOverview; + for (const b in state.beacons) { + const beacon = state.beacons[b]; + overview.individualCount += beacon.individualCount ?? 0; + overview.biosampleCount += beacon.biosampleCount ?? 0; + overview.experimentCount += beacon.experimentCount ?? 0; + overview.biosampleChartData = mergeCharts(overview.biosampleChartData, beacon.biosampleChartData ?? []); + overview.experimentChartData = mergeCharts(overview.experimentChartData, beacon.experimentChartData ?? []); + } + state.networkOverview = overview; + }, + }, extraReducers: (builder) => { builder.addCase(networkBeaconQuery.pending, (state, action) => { console.log('networkBeaconQuery.pending'); @@ -61,11 +106,11 @@ const networkBeaconQuerySlice = createSlice({ console.log('networkBeaconQuery.fulfilled'); const beaconId = action.meta.arg.beaconId; const { payload } = action; - const hasErrorResponse = payload.hasOwnProperty("error") + const hasErrorResponse = payload.hasOwnProperty('error'); const beaconState: FlattenedBeaconResponse = { hasApiError: hasErrorResponse, - apiErrorMessage: hasErrorResponse ? payload.error?.errorMessage ?? "error" : "", + apiErrorMessage: hasErrorResponse ? payload.error?.errorMessage ?? 'error' : '', isFetchingQueryResponse: false, }; if (payload.info?.bento) { @@ -92,4 +137,5 @@ const networkBeaconQuerySlice = createSlice({ }, }); +export const { computeNetworkOverview } = networkBeaconQuerySlice.actions; export default networkBeaconQuerySlice.reducer; From 9e13ecbdc194a3a117ed107c6efc0e0300d54d6d Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Fri, 7 Jun 2024 15:21:46 -0400 Subject: [PATCH 19/94] toggle for network query filters --- src/js/features/beacon/networkConfig.store.ts | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/js/features/beacon/networkConfig.store.ts b/src/js/features/beacon/networkConfig.store.ts index 0ca548a9..6c1c4f6e 100644 --- a/src/js/features/beacon/networkConfig.store.ts +++ b/src/js/features/beacon/networkConfig.store.ts @@ -3,6 +3,7 @@ import axios from 'axios'; // import { makeAuthorizationHeader } from 'bento-auth-js'; import { RootState } from '@/store'; import { beaconApiError } from '@/utils/beaconApiError'; +import { BeaconAssemblyIds } from '@/types/beacon'; import { NetworkBeacon, BeaconNetworkConfig } from '@/types/beaconNetwork'; import { BEACON_NETWORK_ROOT } from '@/constants/beaconConstants'; @@ -23,26 +24,58 @@ export const getBeaconNetworkConfig = createAsyncThunk< type beaconNetworkIntitalStateType = { isFetchingBeaconNetworkConfig: boolean; hasBeaconNetworkError: boolean; + assemblyIds: BeaconAssemblyIds; + querySectionsUnion: any; + querySectionsIntersection: any; + isQuerySectionsUnion: boolean; // horrible English + currentQuerySections: any; beacons: NetworkBeacon[]; }; const initialState: beaconNetworkIntitalStateType = { isFetchingBeaconNetworkConfig: false, hasBeaconNetworkError: false, + querySectionsUnion: [], + querySectionsIntersection: [], + isQuerySectionsUnion: true, + currentQuerySections: [], + assemblyIds: [], beacons: [], }; +const networkAssemblyIds = (beacons: NetworkBeacon[]) => { + // reduce to list of assemblies + const assemblyIds = beacons.reduce( + (assemblies: BeaconAssemblyIds, b: NetworkBeacon) => [...assemblies, ...Object.keys(b.overview?.variants ?? {})], + [] + ); + // return unique values only + return [...new Set(assemblyIds)]; +}; + const beaconNetwork = createSlice({ name: 'beaconNetwork', initialState, - reducers: {}, + reducers: { + toggleQuerySectionsUnionOrIntersection(state) { + // update, then set boolean + state.currentQuerySections = state.isQuerySectionsUnion + ? state.querySectionsIntersection + : state.querySectionsUnion; + state.isQuerySectionsUnion = !state.isQuerySectionsUnion; + }, + }, extraReducers: (builder) => { builder.addCase(getBeaconNetworkConfig.pending, (state, action) => { state.isFetchingBeaconNetworkConfig = true; }); builder.addCase(getBeaconNetworkConfig.fulfilled, (state, { payload }) => { state.isFetchingBeaconNetworkConfig = false; - state.beacons = payload; + state.beacons = payload.beacons; + state.querySectionsUnion = payload.filtersUnion; + state.querySectionsIntersection = payload.filtersIntersection; + state.currentQuerySections = payload.filtersUnion; + state.assemblyIds = networkAssemblyIds(payload.beacons); }); builder.addCase(getBeaconNetworkConfig.rejected, (state, { payload }) => { state.isFetchingBeaconNetworkConfig = false; @@ -51,4 +84,5 @@ const beaconNetwork = createSlice({ }, }); +export const { toggleQuerySectionsUnionOrIntersection } = beaconNetwork.actions; export default beaconNetwork.reducer; From dd622640d44b56792fba4365d37c219c1fe63995 Mon Sep 17 00:00:00 2001 From: Gordon Krieger Date: Fri, 7 Jun 2024 15:23:23 -0400 Subject: [PATCH 20/94] first pass at beacon common filters components --- src/js/components/BeaconCommon/Filter.tsx | 99 +++++++++++++++++++ src/js/components/BeaconCommon/Filters.tsx | 108 +++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 src/js/components/BeaconCommon/Filter.tsx create mode 100644 src/js/components/BeaconCommon/Filters.tsx diff --git a/src/js/components/BeaconCommon/Filter.tsx b/src/js/components/BeaconCommon/Filter.tsx new file mode 100644 index 00000000..beffbd52 --- /dev/null +++ b/src/js/components/BeaconCommon/Filter.tsx @@ -0,0 +1,99 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslationCustom, useTranslationDefault } from '@/hooks'; +import { Button, Form, Select, Space } from 'antd'; +import { CloseOutlined } from '@ant-design/icons'; +import { FormInstance } from 'antd/es/form'; +import { FormFilter, FilterOption, FilterPullDownKey, FilterPullDownValue, GenericOptionType } from '@/types/beacon'; +import { Section, Field } from '@/types/search'; + +// TODOs: +// any search key (eg "sex") selected in one filter should not available in other +// for clarity they should probably appear, but be greyed out +// this requires rendering select options as