From 3fcffec88946f7b57aeb8b518e976a59289ef452 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 25 Nov 2024 17:06:51 -0500 Subject: [PATCH 01/17] feat: search count help text https://redmine.c3g-app.sd4h.ca/issues/2412 --- src/js/components/Overview/Counts.tsx | 50 +++++++------------ .../components/Search/SearchResultsCounts.tsx | 8 +-- .../components/Util/CountsTitleWithHelp.tsx | 35 +++++++++++++ src/js/types/entities.ts | 1 + 4 files changed, 58 insertions(+), 36 deletions(-) create mode 100644 src/js/components/Util/CountsTitleWithHelp.tsx create mode 100644 src/js/types/entities.ts diff --git a/src/js/components/Overview/Counts.tsx b/src/js/components/Overview/Counts.tsx index 16c29b1c..3ac2789b 100644 --- a/src/js/components/Overview/Counts.tsx +++ b/src/js/components/Overview/Counts.tsx @@ -1,13 +1,14 @@ -import { type CSSProperties, type ReactNode } from 'react'; -import { Card, Popover, Space, Statistic, Typography } from 'antd'; -import { ExperimentOutlined, InfoCircleOutlined, TeamOutlined } from '@ant-design/icons'; +import type { CSSProperties, ReactNode } from 'react'; +import { Card, Space, Statistic, Typography } from 'antd'; +import { ExperimentOutlined, TeamOutlined } from '@ant-design/icons'; import { BiDna } from 'react-icons/bi'; -import { T_PLURAL_COUNT } from '@/constants/i18n'; +import CountsTitleWithHelp from '@/components/Util/CountsTitleWithHelp'; import { BOX_SHADOW, COUNTS_FILL } from '@/constants/overviewConstants'; import { NO_RESULTS_DASHES } from '@/constants/searchConstants'; import { useAppSelector, useTranslationFn } from '@/hooks'; import { useCanSeeUncensoredCounts } from '@/hooks/censorship'; +import type { BentoEntity } from '@/types/entities'; const styles: Record = { countCard: { @@ -17,7 +18,7 @@ const styles: Record = { }, }; -const CountsHelp = ({ children }: { children: ReactNode }) =>
{children}
; +type CountEntry = { entity: BentoEntity; icon: ReactNode; count: number }; const Counts = () => { const t = useTranslationFn(); @@ -27,7 +28,7 @@ const Counts = () => { const uncensoredCounts = useCanSeeUncensoredCounts(); // Break down help into multiple sentences inside an array to make translation a bit easier. - const data = [ + const data: CountEntry[] = [ { entity: 'individual', icon: , @@ -49,32 +50,17 @@ const Counts = () => { <> {t('Counts')} - {data.map(({ entity, icon, count }, i) => { - const title = t(`entities.${entity}`, T_PLURAL_COUNT); - return ( - - - {title} - { - {t(`entities.${entity}_help`, { joinArrays: ' ' })}} - > - - - } - - } - value={count || (uncensoredCounts ? count : NO_RESULTS_DASHES)} - valueStyle={{ color: COUNTS_FILL }} - prefix={icon} - loading={isFetchingData} - /> - - ); - })} + {data.map(({ entity, icon, count }, i) => ( + + } + value={count || (uncensoredCounts ? count : NO_RESULTS_DASHES)} + valueStyle={{ color: COUNTS_FILL }} + prefix={icon} + loading={isFetchingData} + /> + + ))} ); diff --git a/src/js/components/Search/SearchResultsCounts.tsx b/src/js/components/Search/SearchResultsCounts.tsx index f3547673..78279b32 100644 --- a/src/js/components/Search/SearchResultsCounts.tsx +++ b/src/js/components/Search/SearchResultsCounts.tsx @@ -3,7 +3,7 @@ import { Skeleton, Space, Statistic } from 'antd'; import { ExperimentOutlined, TeamOutlined } from '@ant-design/icons'; import { BiDna } from 'react-icons/bi'; -import { T_PLURAL_COUNT } from '@/constants/i18n'; +import CountsTitleWithHelp from '@/components/Util/CountsTitleWithHelp'; import { COUNTS_FILL } from '@/constants/overviewConstants'; import { NO_RESULTS_DASHES } from '@/constants/searchConstants'; import { useTranslationFn } from '@/hooks'; @@ -59,7 +59,7 @@ const SearchResultsCounts = ({ ].join(' ')} > } value={ hasInsufficientData ? t(message ?? '') @@ -72,14 +72,14 @@ const SearchResultsCounts = ({ /> } value={hasInsufficientData || (!uncensoredCounts && !biosampleCount) ? NO_RESULTS_DASHES : biosampleCount} valueStyle={STAT_STYLE} // Slight fixup for alignment of non-Antd icon: prefix={} /> } value={hasInsufficientData || (!uncensoredCounts && !experimentCount) ? NO_RESULTS_DASHES : experimentCount} valueStyle={STAT_STYLE} prefix={} diff --git a/src/js/components/Util/CountsTitleWithHelp.tsx b/src/js/components/Util/CountsTitleWithHelp.tsx new file mode 100644 index 00000000..54f1a341 --- /dev/null +++ b/src/js/components/Util/CountsTitleWithHelp.tsx @@ -0,0 +1,35 @@ +import type { ReactNode } from 'react'; +import { Popover, Space } from 'antd'; +import { InfoCircleOutlined } from '@ant-design/icons'; + +import { T_PLURAL_COUNT } from '@/constants/i18n'; +import { useTranslationFn } from '@/hooks'; +import type { BentoEntity } from '@/types/entities'; + +const CountsHelpPopoverText = ({ children }: { children: ReactNode }) => ( +
{children}
+); + +const CountsTitleWithHelp = ({ entity }: CountsHelpProps) => { + const t = useTranslationFn(); + + const title = t(`entities.${entity}`, T_PLURAL_COUNT); + + return ( + + {title} + {t(`entities.${entity}_help`, { joinArrays: ' ' })}} + > + + + + ); +}; + +type CountsHelpProps = { + entity: BentoEntity; +}; + +export default CountsTitleWithHelp; diff --git a/src/js/types/entities.ts b/src/js/types/entities.ts new file mode 100644 index 00000000..3404ab22 --- /dev/null +++ b/src/js/types/entities.ts @@ -0,0 +1 @@ +export type BentoEntity = 'phenopacket' | 'individual' | 'biosample' | 'experiment' | 'variant'; From ce383732099682c39420e2cdf14cd179e7e65538 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 26 Nov 2024 10:56:20 -0500 Subject: [PATCH 02/17] fix: missing beacon network configuration + docs for env vars --- README.md | 15 ++++++++++++++- create_config_prod.js | 1 + src/public/config.js | 1 + 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dcd0c252..d18aef91 100755 --- a/README.md +++ b/README.md @@ -8,6 +8,19 @@ it directly. ## Prerequisites: - Node Package Manager -## Translations in dev mode +## Development + +### Adding a new environment configuration variable + +Any new environment configuration variable must be registered in several places: + +* [`./create_config_prod.js`](./create_config_prod.js): mapping the environment variable to a config object entry for + production +* [`./webpack.config.js`](./webpack.config.js): setting a default value for Webpack building +* [`./src/public/config.js`](./src/public/config.js): creating the shape of the global config object +* [`./src/js/config.ts`](./src/js/config.ts): loading from the global config object (production) or from the environment + via Webpack (development) + +### Translations in dev mode Add your English to French translations in `dist/public/locales/fr/translation_fr.json` for them to appear on the website. diff --git a/create_config_prod.js b/create_config_prod.js index 57fdf8e8..c24b95f1 100644 --- a/create_config_prod.js +++ b/create_config_prod.js @@ -6,6 +6,7 @@ const siteConfig = { TRANSLATED: parseBoolean(process.env.BENTO_PUBLIC_TRANSLATED), BEACON_URL: process.env.BEACON_URL || null, BEACON_UI_ENABLED: parseBoolean(process.env.BENTO_BEACON_UI_ENABLED), + BEACON_NETWORK_ENABLED: parseBoolean(process.env.BENTO_BEACON_NETWORK_ENABLED), // Authentication PUBLIC_URL: process.env.BENTO_PUBLIC_URL || null, diff --git a/src/public/config.js b/src/public/config.js index 8e98a7ef..779177de 100644 --- a/src/public/config.js +++ b/src/public/config.js @@ -7,6 +7,7 @@ BENTO_PUBLIC_CONFIG = { TRANSLATED: null, BEACON_URL: null, BEACON_UI_ENABLED: null, + BEACON_NETWORK_ENABLED: null, // Authentication PUBLIC_URL: null, CLIENT_ID: null, From 6244248990960c6ae55dd47745e55ceb08545dc1 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 26 Nov 2024 10:59:12 -0500 Subject: [PATCH 03/17] docs: improve env var notes --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d18aef91..fee95e8d 100755 --- a/README.md +++ b/README.md @@ -12,14 +12,16 @@ it directly. ### Adding a new environment configuration variable -Any new environment configuration variable must be registered in several places: +Any new environment / configuration variable must be registered in several places: -* [`./create_config_prod.js`](./create_config_prod.js): mapping the environment variable to a config object entry for - production -* [`./webpack.config.js`](./webpack.config.js): setting a default value for Webpack building -* [`./src/public/config.js`](./src/public/config.js): creating the shape of the global config object -* [`./src/js/config.ts`](./src/js/config.ts): loading from the global config object (production) or from the environment - via Webpack (development) +1. [`./create_config_prod.js`](./create_config_prod.js): mapping the environment variable to a config object entry for + production. +2. [`./webpack.config.js`](./webpack.config.js): setting a default value for the environment variable; used in Webpack development + builds. +3. [`./src/public/config.js`](./src/public/config.js): creating the shape of the global config object (using the config object entry key, + mapped to in 1.) +4. [`./src/js/config.ts`](./src/js/config.ts): loading from the global config object (production) via key or from the + environment variable directly, through Webpack replacement (development). ### Translations in dev mode Add your English to French translations in `dist/public/locales/fr/translation_fr.json` for them to appear on the From 8340358180908f989d2881bca247dee44333fe4d Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 26 Nov 2024 11:15:08 -0500 Subject: [PATCH 04/17] chore: bump version to 0.22.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 41e12cfc..9f8d24b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bento_public", - "version": "0.21.0", + "version": "0.22.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bento_public", - "version": "0.21.0", + "version": "0.22.0", "license": "LGPL-3.0-only", "dependencies": { "@ant-design/icons": "^5.5.1", diff --git a/package.json b/package.json index 22abb4a3..f2b75108 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bento_public", - "version": "0.21.0", + "version": "0.22.0", "description": "A publicly accessible portal for clinical datasets, where users are able to see high-level statistics of the data available through predefined variables of interest and search the data using limited variables at a time. This portal allows users to gain a generic understanding of the data available (secure and firewalled) without the need to access it directly. Initially, this portal facilitates the search in English language only, but the French language will be added at a later time.", "main": "index.js", "scripts": { From 08b182b1526e7fde989c7c5390f144d40fb814c9 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 26 Nov 2024 14:54:30 -0500 Subject: [PATCH 05/17] chore: address confusing UX for network chromosome/assembly --- src/js/components/Beacon/BeaconCommon/VariantsForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/components/Beacon/BeaconCommon/VariantsForm.tsx b/src/js/components/Beacon/BeaconCommon/VariantsForm.tsx index 4a05a14d..3184d81d 100644 --- a/src/js/components/Beacon/BeaconCommon/VariantsForm.tsx +++ b/src/js/components/Beacon/BeaconCommon/VariantsForm.tsx @@ -92,7 +92,7 @@ const VariantsForm = ({ isNetworkQuery, beaconAssemblyIds }: VariantsFormProps) const formFields = { referenceName: { name: 'Chromosome', - placeholder: !currentAssemblyID ? t('beacon.select_asm') : '', + placeholder: !isNetworkQuery && !currentAssemblyID ? t('beacon.select_asm') : '', initialValue: '', }, start: { @@ -130,7 +130,7 @@ const VariantsForm = ({ isNetworkQuery, beaconAssemblyIds }: VariantsFormProps) From bed806a282dc39cfb095c4217c1541ebabff5f90 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 26 Nov 2024 14:59:14 -0500 Subject: [PATCH 06/17] chore(network): don't re-show help indicator for every node --- .../components/Search/SearchResultsCounts.tsx | 6 +++--- .../components/Util/CountsTitleWithHelp.tsx | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/js/components/Search/SearchResultsCounts.tsx b/src/js/components/Search/SearchResultsCounts.tsx index 78279b32..a5e55e2a 100644 --- a/src/js/components/Search/SearchResultsCounts.tsx +++ b/src/js/components/Search/SearchResultsCounts.tsx @@ -59,7 +59,7 @@ const SearchResultsCounts = ({ ].join(' ')} > } + title={} value={ hasInsufficientData ? t(message ?? '') @@ -72,14 +72,14 @@ const SearchResultsCounts = ({ /> } + title={} value={hasInsufficientData || (!uncensoredCounts && !biosampleCount) ? NO_RESULTS_DASHES : biosampleCount} valueStyle={STAT_STYLE} // Slight fixup for alignment of non-Antd icon: prefix={} /> } + title={} value={hasInsufficientData || (!uncensoredCounts && !experimentCount) ? NO_RESULTS_DASHES : experimentCount} valueStyle={STAT_STYLE} prefix={} diff --git a/src/js/components/Util/CountsTitleWithHelp.tsx b/src/js/components/Util/CountsTitleWithHelp.tsx index 54f1a341..e617be9c 100644 --- a/src/js/components/Util/CountsTitleWithHelp.tsx +++ b/src/js/components/Util/CountsTitleWithHelp.tsx @@ -10,26 +10,31 @@ const CountsHelpPopoverText = ({ children }: { children: ReactNode }) => (
{children}
); -const CountsTitleWithHelp = ({ entity }: CountsHelpProps) => { +const CountsTitleWithHelp = ({ entity, showHelp }: CountsHelpProps) => { const t = useTranslationFn(); + showHelp = showHelp ?? true; // If undefined, we should show help by default. + const title = t(`entities.${entity}`, T_PLURAL_COUNT); return ( {title} - {t(`entities.${entity}_help`, { joinArrays: ' ' })}} - > - - + {showHelp && ( + {t(`entities.${entity}_help`, { joinArrays: ' ' })}} + > + + + )} ); }; type CountsHelpProps = { entity: BentoEntity; + showHelp?: boolean; }; export default CountsTitleWithHelp; From 688e7496fc1892942f8b7783b0e353a718f4a0f3 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 26 Nov 2024 15:14:56 -0500 Subject: [PATCH 07/17] chore(beacon): don't clear chrom if availableContigs stays blank --- .../Beacon/BeaconCommon/VariantsForm.tsx | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/js/components/Beacon/BeaconCommon/VariantsForm.tsx b/src/js/components/Beacon/BeaconCommon/VariantsForm.tsx index 3184d81d..fad72f65 100644 --- a/src/js/components/Beacon/BeaconCommon/VariantsForm.tsx +++ b/src/js/components/Beacon/BeaconCommon/VariantsForm.tsx @@ -1,4 +1,4 @@ -import { type CSSProperties, useEffect, useMemo } from 'react'; +import { type CSSProperties, useEffect, useState } from 'react'; import { Col, Form, Row } from 'antd'; import type { DefaultOptionType } from 'antd/es/select/index'; @@ -70,17 +70,24 @@ const VariantsForm = ({ isNetworkQuery, beaconAssemblyIds }: VariantsFormProps) const form = Form.useFormInstance(); const currentAssemblyID = Form.useWatch('Assembly ID', form); - // Right now, we cannot figure out the contig options for the network, so we fall back to a normal input box. - const availableContigs = useMemo( - () => - !isNetworkQuery && currentAssemblyID && genomesByID[currentAssemblyID] - ? genomesByID[currentAssemblyID].contigs - .map(contigToOption) - .sort(contigOptionSort) - .filter(filterOutHumanLikeExtraContigs) - : [], - [isNetworkQuery, currentAssemblyID, genomesByID] - ); + const [availableContigs, setAvailableContigs] = useState([]); + + useEffect(() => { + // Right now, we cannot figure out the contig options for the network, so we fall back to a normal input box. + if (!isNetworkQuery && currentAssemblyID && genomesByID[currentAssemblyID]) { + setAvailableContigs( + genomesByID[currentAssemblyID].contigs + .map(contigToOption) + .sort(contigOptionSort) + .filter(filterOutHumanLikeExtraContigs) + ); + } else { + // Keep existing memory address for existing empty array if availableContigs was already empty, to avoid + // re-render/clearing effect. + setAvailableContigs((ac) => (ac.length ? [] : ac)); + } + }, [isNetworkQuery, currentAssemblyID, genomesByID]); + const assemblySelect = !!availableContigs.length; useEffect(() => { From b3f9eceafc4ad1dd3b753bd56226a457464c0f2b Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 27 Nov 2024 17:01:08 -0500 Subject: [PATCH 08/17] refact(overview): use hook for calculating chart clickability --- src/js/components/Overview/ChartCard.tsx | 6 ++--- .../Overview/OverviewDisplayData.tsx | 13 +++++++++-- .../components/Overview/OverviewSection.tsx | 12 ++++++++-- src/js/components/Overview/PublicOverview.tsx | 13 +++++------ src/js/features/data/data.store.ts | 22 +------------------ src/js/features/data/hooks.ts | 14 ++++++++++++ .../features/data/makeGetDataRequest.thunk.ts | 1 - src/js/types/data.ts | 1 - 8 files changed, 45 insertions(+), 37 deletions(-) create mode 100644 src/js/features/data/hooks.ts diff --git a/src/js/components/Overview/ChartCard.tsx b/src/js/components/Overview/ChartCard.tsx index d2c3a4c3..939bd521 100644 --- a/src/js/components/Overview/ChartCard.tsx +++ b/src/js/components/Overview/ChartCard.tsx @@ -12,7 +12,7 @@ import SmallChartCardTitle from '@/components/Util/SmallChartCardTitle'; const CARD_STYLE: CSSProperties = { height: '415px', borderRadius: '11px', ...BOX_SHADOW }; const ROW_EMPTY_STYLE: CSSProperties = { height: `${CHART_HEIGHT}px` }; -const ChartCard = memo(({ section, chart, onRemoveChart }: ChartCardProps) => { +const ChartCard = memo(({ section, chart, onRemoveChart, searchable }: ChartCardProps) => { const t = useTranslationFn(); const containerRef = useRef(null); const width = useElementWidth(containerRef, chart.width); @@ -21,7 +21,6 @@ const ChartCard = memo(({ section, chart, onRemoveChart }: ChartCardProps) => { data, field: { id, description, title, config }, chartConfig, - isSearchable, } = chart; const extraOptionsData = [ @@ -59,7 +58,7 @@ const ChartCard = memo(({ section, chart, onRemoveChart }: ChartCardProps) => { units={config?.units || ''} id={id} key={id} - isClickable={isSearchable} + isClickable={!!searchable} /> ) : ( @@ -77,6 +76,7 @@ export interface ChartCardProps { section: string; chart: ChartDataField; onRemoveChart: (arg: { section: string; id: string }) => void; + searchable?: boolean; } export default ChartCard; diff --git a/src/js/components/Overview/OverviewDisplayData.tsx b/src/js/components/Overview/OverviewDisplayData.tsx index c0f735ac..f9c72370 100644 --- a/src/js/components/Overview/OverviewDisplayData.tsx +++ b/src/js/components/Overview/OverviewDisplayData.tsx @@ -11,7 +11,7 @@ import ChartCard from './ChartCard'; import type { ChartDataField } from '@/types/data'; -const OverviewDisplayData = ({ section, allCharts }: OverviewDisplayDataProps) => { +const OverviewDisplayData = ({ section, allCharts, searchableFields }: OverviewDisplayDataProps) => { const dispatch = useAppDispatch(); const isSmallScreen = useSmallScreen(); @@ -31,7 +31,15 @@ const OverviewDisplayData = ({ section, allCharts }: OverviewDisplayDataProps) = ); const renderItem = (chart: ChartDataField) => { - return ; + return ( + + ); }; if (isSmallScreen) { @@ -48,6 +56,7 @@ const OverviewDisplayData = ({ section, allCharts }: OverviewDisplayDataProps) = export interface OverviewDisplayDataProps { section: string; allCharts: ChartDataField[]; + searchableFields: Set; } export default OverviewDisplayData; diff --git a/src/js/components/Overview/OverviewSection.tsx b/src/js/components/Overview/OverviewSection.tsx index b930c3d4..ee3d72dc 100644 --- a/src/js/components/Overview/OverviewSection.tsx +++ b/src/js/components/Overview/OverviewSection.tsx @@ -4,13 +4,21 @@ import OverviewDisplayData from './OverviewDisplayData'; import { useTranslationFn } from '@/hooks'; import type { ChartDataField } from '@/types/data'; -const OverviewSection = ({ title, chartData }: { title: string; chartData: ChartDataField[] }) => { +const OverviewSection = ({ + title, + chartData, + searchableFields, +}: { + title: string; + chartData: ChartDataField[]; + searchableFields: Set; +}) => { const t = useTranslationFn(); return ( {t(title)} - + ); }; diff --git a/src/js/components/Overview/PublicOverview.tsx b/src/js/components/Overview/PublicOverview.tsx index 1c6f71e2..30611cf5 100644 --- a/src/js/components/Overview/PublicOverview.tsx +++ b/src/js/components/Overview/PublicOverview.tsx @@ -15,6 +15,7 @@ import Loader from '@/components/Loader'; import Dataset from '@/components/Provenance/Dataset'; import { useAppSelector } from '@/hooks'; +import { useSearchableFields } from '@/features/data/hooks'; import { useSelectedProject, useSelectedScope } from '@/features/metadata/hooks'; import { useTranslation } from 'react-i18next'; import { RequestStatus } from '@/types/requests'; @@ -28,11 +29,7 @@ const PublicOverview = () => { const [drawerVisible, setDrawerVisible] = useState(false); const [aboutContent, setAboutContent] = useState(''); - const { - isFetchingData: isFetchingOverviewData, - isContentPopulated, - sections, - } = useAppSelector((state) => state.data); + const { isFetchingData: isFetchingOverviewData, sections } = useAppSelector((state) => state.data); const { status: aboutStatus, about } = useAppSelector((state) => state.content); const selectedProject = useSelectedProject(); @@ -59,7 +56,9 @@ const PublicOverview = () => { saveToLocalStorage(sections); }, [sections]); - return !isContentPopulated || isFetchingOverviewData ? ( + const searchableFields = useSearchableFields(); + + return isFetchingOverviewData ? ( ) : ( <> @@ -94,7 +93,7 @@ const PublicOverview = () => { {displayedSections.map(({ sectionTitle, charts }, i) => (
- +
))} diff --git a/src/js/features/data/data.store.ts b/src/js/features/data/data.store.ts index 6c3dcc82..a7c1fec1 100644 --- a/src/js/features/data/data.store.ts +++ b/src/js/features/data/data.store.ts @@ -1,23 +1,12 @@ import type { PayloadAction } from '@reduxjs/toolkit'; -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; import { makeGetDataRequestThunk } from './makeGetDataRequest.thunk'; import type { Sections } from '@/types/data'; import type { Counts } from '@/types/overviewResponse'; -import type { QueryState } from '@/features/search/query.store'; - -export const populateClickable = createAsyncThunk( - 'data/populateClickable', - async (_, { getState }) => { - return getState() - .query.querySections.flatMap((section) => section.fields) - .map((field) => field.id); - } -); interface DataState { isFetchingData: boolean; - isContentPopulated: boolean; defaultLayout: Sections; sections: Sections; counts: Counts; @@ -25,7 +14,6 @@ interface DataState { const initialState: DataState = { isFetchingData: true, - isContentPopulated: false, defaultLayout: [], sections: [], counts: { @@ -101,14 +89,6 @@ const data = createSlice({ }) .addCase(makeGetDataRequestThunk.rejected, (state) => { state.isFetchingData = false; - }) - .addCase(populateClickable.fulfilled, (state, { payload }) => { - state.sections.forEach((section) => { - section.charts.forEach((chart) => { - chart.isSearchable = payload.includes(chart.id); - }); - }); - state.isContentPopulated = true; }); }, }); diff --git a/src/js/features/data/hooks.ts b/src/js/features/data/hooks.ts new file mode 100644 index 00000000..e35346dc --- /dev/null +++ b/src/js/features/data/hooks.ts @@ -0,0 +1,14 @@ +import { useMemo } from 'react'; +import { useSearchQuery } from '@/features/search/hooks'; + +export const useSearchableFields = () => { + /** + * Hook which calculates a set of searchable fields (which share IDs with charts), which can be used, for example, to + * choose whether to add a click event to a chart for the field. + */ + const { querySections } = useSearchQuery(); + return useMemo( + () => new Set(querySections.flatMap((section) => section.fields).map((field) => field.id)), + [querySections] + ); +}; diff --git a/src/js/features/data/makeGetDataRequest.thunk.ts b/src/js/features/data/makeGetDataRequest.thunk.ts index 583e78a8..6c5d7374 100644 --- a/src/js/features/data/makeGetDataRequest.thunk.ts +++ b/src/js/features/data/makeGetDataRequest.thunk.ts @@ -40,7 +40,6 @@ export const makeGetDataRequestThunk = createAsyncThunk< // Initial display state isDisplayed: i < MAX_CHARTS, width: chart.width ?? DEFAULT_CHART_WIDTH, // initial configured width; users can change it from here - isSearchable: false, }; }; diff --git a/src/js/types/data.ts b/src/js/types/data.ts index 4099a2ef..b41f53eb 100644 --- a/src/js/types/data.ts +++ b/src/js/types/data.ts @@ -23,7 +23,6 @@ export interface ChartDataField { // display options: isDisplayed: boolean; // whether the chart is currently displayed (state data) width: number; // current width (state data); initial data taken from chart config - isSearchable: boolean; // whether the field is searchable } export interface ChartData { From 5d0d598e76ad8e89aa012d146c0f0407e23f253f Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 28 Nov 2024 10:39:07 -0500 Subject: [PATCH 09/17] rm old references to populateClickable action --- src/js/components/BentoAppRouter.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/js/components/BentoAppRouter.tsx b/src/js/components/BentoAppRouter.tsx index c2545bb5..6665a077 100644 --- a/src/js/components/BentoAppRouter.tsx +++ b/src/js/components/BentoAppRouter.tsx @@ -5,7 +5,7 @@ import { useAppDispatch } from '@/hooks'; import { makeGetConfigRequest, makeGetServiceInfoRequest } from '@/features/config/config.store'; import { makeGetAboutRequest } from '@/features/content/content.store'; -import { makeGetDataRequestThunk, populateClickable } from '@/features/data/data.store'; +import { makeGetDataRequestThunk } from '@/features/data/data.store'; import { makeGetKatsuPublic, makeGetSearchFields } from '@/features/search/query.store'; import { getBeaconConfig } from '@/features/beacon/beacon.store'; import { getBeaconNetworkConfig } from '@/features/beacon/network.store'; @@ -92,12 +92,8 @@ const BentoAppRouter = () => { } dispatch(makeGetAboutRequest()); - // The "Populate clickable" action needs both chart sections and search fields to be available. - // TODO: this is not a very good pattern. It would be better to have a memoized way of determining click-ability at - // render time. - Promise.all([dispatch(makeGetDataRequestThunk()), dispatch(makeGetSearchFields())]).then(() => - dispatch(populateClickable()) - ); + dispatch(makeGetDataRequestThunk()); + dispatch(makeGetSearchFields()); dispatch(makeGetKatsuPublic()); dispatch(fetchKatsuData()); }, [dispatch, isAuthenticated, selectedScope]); From 3b0c087a2c67160f5fbd8ebce389f952e79d4fc1 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 28 Nov 2024 10:48:13 -0500 Subject: [PATCH 10/17] refact: move to RequestStatus for overview data --- src/js/components/BentoAppRouter.tsx | 3 ++- src/js/components/Overview/Counts.tsx | 9 ++++++--- .../components/Overview/Drawer/ManageChartsDrawer.tsx | 2 +- src/js/components/Overview/PublicOverview.tsx | 9 +++++---- src/js/constants/requests.ts | 3 +++ src/js/features/data/data.store.ts | 11 ++++++----- 6 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 src/js/constants/requests.ts diff --git a/src/js/components/BentoAppRouter.tsx b/src/js/components/BentoAppRouter.tsx index 6665a077..8a2e313d 100644 --- a/src/js/components/BentoAppRouter.tsx +++ b/src/js/components/BentoAppRouter.tsx @@ -18,6 +18,7 @@ import { getGenomes } from '@/features/reference/reference.store'; import Loader from '@/components/Loader'; import DefaultLayout from '@/components/Util/DefaultLayout'; import { BEACON_NETWORK_ENABLED } from '@/config'; +import { WAITING_STATES } from '@/constants/requests'; import { RequestStatus } from '@/types/requests'; import { BentoRoute } from '@/types/routes'; import { scopeEqual, validProjectDataset } from '@/utils/router'; @@ -35,7 +36,7 @@ const ScopedRoute = () => { const { selectedScope, projects, projectsStatus } = useMetadata(); useEffect(() => { - if ([RequestStatus.Idle, RequestStatus.Pending].includes(projectsStatus)) return; // Wait for projects to load first + if (WAITING_STATES.includes(projectsStatus)) return; // Wait for projects to load first // Update selectedScope based on URL parameters const valid = validProjectDataset(projects, { project: projectId, dataset: datasetId }); diff --git a/src/js/components/Overview/Counts.tsx b/src/js/components/Overview/Counts.tsx index 3ac2789b..425da8bf 100644 --- a/src/js/components/Overview/Counts.tsx +++ b/src/js/components/Overview/Counts.tsx @@ -5,6 +5,7 @@ import { BiDna } from 'react-icons/bi'; import CountsTitleWithHelp from '@/components/Util/CountsTitleWithHelp'; import { BOX_SHADOW, COUNTS_FILL } from '@/constants/overviewConstants'; +import { WAITING_STATES } from '@/constants/requests'; import { NO_RESULTS_DASHES } from '@/constants/searchConstants'; import { useAppSelector, useTranslationFn } from '@/hooks'; import { useCanSeeUncensoredCounts } from '@/hooks/censorship'; @@ -23,7 +24,7 @@ type CountEntry = { entity: BentoEntity; icon: ReactNode; count: number }; const Counts = () => { const t = useTranslationFn(); - const { counts, isFetchingData } = useAppSelector((state) => state.data); + const { counts, status } = useAppSelector((state) => state.data); const uncensoredCounts = useCanSeeUncensoredCounts(); @@ -46,18 +47,20 @@ const Counts = () => { }, ]; + const waitingForData = WAITING_STATES.includes(status); + return ( <> {t('Counts')} {data.map(({ entity, icon, count }, i) => ( - + } value={count || (uncensoredCounts ? count : NO_RESULTS_DASHES)} valueStyle={{ color: COUNTS_FILL }} prefix={icon} - loading={isFetchingData} + loading={waitingForData} /> ))} diff --git a/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx b/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx index 3711aef4..f50f2854 100644 --- a/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx +++ b/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx @@ -13,7 +13,7 @@ const ManageChartsDrawer = ({ onManageDrawerClose, manageDrawerVisible }: Manage const dispatch = useAppDispatch(); - const sections = useAppSelector((state) => state.data.sections); + const { sections } = useAppSelector((state) => state.data); return ( { const [drawerVisible, setDrawerVisible] = useState(false); const [aboutContent, setAboutContent] = useState(''); - const { isFetchingData: isFetchingOverviewData, sections } = useAppSelector((state) => state.data); + const { status: overviewDataStatus, sections } = useAppSelector((state) => state.data); const { status: aboutStatus, about } = useAppSelector((state) => state.content); const selectedProject = useSelectedProject(); @@ -37,9 +38,9 @@ const PublicOverview = () => { useEffect(() => { // Save sections to localStorage when they change - if (isFetchingOverviewData) return; + if (overviewDataStatus != RequestStatus.Fulfilled) return; saveToLocalStorage(sections); - }, [isFetchingOverviewData, sections]); + }, [overviewDataStatus, sections]); useEffect(() => { const activeLanguage = i18n.language; @@ -58,7 +59,7 @@ const PublicOverview = () => { const searchableFields = useSearchableFields(); - return isFetchingOverviewData ? ( + return WAITING_STATES.includes(overviewDataStatus) ? ( ) : ( <> diff --git a/src/js/constants/requests.ts b/src/js/constants/requests.ts new file mode 100644 index 00000000..aedca9b5 --- /dev/null +++ b/src/js/constants/requests.ts @@ -0,0 +1,3 @@ +import { RequestStatus } from '@/types/requests'; + +export const WAITING_STATES = [RequestStatus.Idle, RequestStatus.Pending]; diff --git a/src/js/features/data/data.store.ts b/src/js/features/data/data.store.ts index a7c1fec1..4b285949 100644 --- a/src/js/features/data/data.store.ts +++ b/src/js/features/data/data.store.ts @@ -4,16 +4,17 @@ import { createSlice } from '@reduxjs/toolkit'; import { makeGetDataRequestThunk } from './makeGetDataRequest.thunk'; import type { Sections } from '@/types/data'; import type { Counts } from '@/types/overviewResponse'; +import { RequestStatus } from '@/types/requests'; interface DataState { - isFetchingData: boolean; + status: RequestStatus; defaultLayout: Sections; sections: Sections; counts: Counts; } const initialState: DataState = { - isFetchingData: true, + status: RequestStatus.Idle, defaultLayout: [], sections: [], counts: { @@ -79,16 +80,16 @@ const data = createSlice({ extraReducers: (builder) => { builder .addCase(makeGetDataRequestThunk.pending, (state) => { - state.isFetchingData = true; + state.status = RequestStatus.Pending; }) .addCase(makeGetDataRequestThunk.fulfilled, (state, { payload }) => { state.sections = payload.sectionData; state.defaultLayout = payload.defaultData; state.counts = payload.counts; - state.isFetchingData = false; + state.status = RequestStatus.Fulfilled; }) .addCase(makeGetDataRequestThunk.rejected, (state) => { - state.isFetchingData = false; + state.status = RequestStatus.Rejected; }); }, }); From 41dfda5f54fa368e82a3186b22b4a47f7b59091c Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 6 Dec 2024 13:31:06 -0500 Subject: [PATCH 11/17] i18n: missing translations for chart management --- .../Overview/Drawer/ManageChartsDrawer.tsx | 12 ++++++++---- src/js/components/Overview/PublicOverview.tsx | 4 ++-- src/public/locales/en/default_translation_en.json | 3 +++ src/public/locales/fr/default_translation_fr.json | 3 +++ 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx b/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx index f50f2854..370b0772 100644 --- a/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx +++ b/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx @@ -6,6 +6,7 @@ import ChartTree from './ChartTree'; import type { ChartDataField } from '@/types/data'; import { useAppSelector, useAppDispatch, useTranslationFn } from '@/hooks'; +import { useSmallScreen } from '@/hooks/useResponsiveContext'; import { hideAllSectionCharts, setAllDisplayedCharts, resetLayout } from '@/features/data/data.store'; const ManageChartsDrawer = ({ onManageDrawerClose, manageDrawerVisible }: ManageChartsDrawerProps) => { @@ -13,6 +14,8 @@ const ManageChartsDrawer = ({ onManageDrawerClose, manageDrawerVisible }: Manage const dispatch = useAppDispatch(); + const isSmallScreen = useSmallScreen(); + const { sections } = useAppSelector((state) => state.data); return ( @@ -21,6 +24,7 @@ const ManageChartsDrawer = ({ onManageDrawerClose, manageDrawerVisible }: Manage placement="right" onClose={onManageDrawerClose} open={manageDrawerVisible} + width={isSmallScreen ? '100vw' : 400} extra={ } @@ -55,7 +59,7 @@ const ManageChartsDrawer = ({ onManageDrawerClose, manageDrawerVisible }: Manage dispatch(setAllDisplayedCharts({ section: sectionTitle })); }} > - Show All + {t('Show All')} diff --git a/src/js/components/Overview/PublicOverview.tsx b/src/js/components/Overview/PublicOverview.tsx index 2c2bfcf2..1add9750 100644 --- a/src/js/components/Overview/PublicOverview.tsx +++ b/src/js/components/Overview/PublicOverview.tsx @@ -25,7 +25,7 @@ const ABOUT_CARD_STYLE = { width: '100%', maxWidth: '1390px', borderRadius: '11p const MANAGE_CHARTS_BUTTON_STYLE = { right: '5em', bottom: '1.5em', transform: 'scale(125%)' }; const PublicOverview = () => { - const { i18n } = useTranslation(); + const { i18n, t } = useTranslation(); const [drawerVisible, setDrawerVisible] = useState(false); const [aboutContent, setAboutContent] = useState(''); @@ -105,7 +105,7 @@ const PublicOverview = () => { } - tooltip="Manage Charts" + tooltip={t('Manage Charts')} style={MANAGE_CHARTS_BUTTON_STYLE} onClick={onManageChartsOpen} /> diff --git a/src/public/locales/en/default_translation_en.json b/src/public/locales/en/default_translation_en.json index 9582a7d1..21d14d27 100644 --- a/src/public/locales/en/default_translation_en.json +++ b/src/public/locales/en/default_translation_en.json @@ -64,6 +64,9 @@ "Download DATS File": "Download DATS File", "Charts": "Charts", "Manage Charts": "Manage Charts", + "Show All": "Show All", + "Reset": "Reset", + "Hide All": "Hide All", "Remove this chart": "Remove this chart", "Width": "Width", "Insufficient data available.": "Insufficient data available.", diff --git a/src/public/locales/fr/default_translation_fr.json b/src/public/locales/fr/default_translation_fr.json index a82f046a..8a280036 100644 --- a/src/public/locales/fr/default_translation_fr.json +++ b/src/public/locales/fr/default_translation_fr.json @@ -64,6 +64,9 @@ "Download DATS File": "Télécharcher le fichier DATS", "Charts": "Tableaux", "Manage Charts": "Gestion des tableaux", + "Show All": "Aff. tous", + "Reset": "Réinit.", + "Hide All": "Cacher tous", "Remove this chart": "Supprimer ce tableau", "Width": "Largeur", "Insufficient data available.": "Pas suffisamment de données.", From ac57386702705011cd305811f43dd2b6a0e8ded4 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 6 Dec 2024 13:36:24 -0500 Subject: [PATCH 12/17] lint: comment charts drawer width logic --- src/js/components/Overview/Drawer/ManageChartsDrawer.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx b/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx index 370b0772..c2720c3b 100644 --- a/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx +++ b/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx @@ -24,6 +24,8 @@ const ManageChartsDrawer = ({ onManageDrawerClose, manageDrawerVisible }: Manage placement="right" onClose={onManageDrawerClose} open={manageDrawerVisible} + // If we're on a small device, make the drawer full-screen width instead of a fixed width. + // The default value for Ant Design is 372. width={isSmallScreen ? '100vw' : 400} extra={ From 2b1c42a82f7babe1c7d102ae77573e382586cf71 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 6 Dec 2024 13:38:50 -0500 Subject: [PATCH 13/17] style: wider chart drawer + alt show transl --- src/js/components/Overview/Drawer/ManageChartsDrawer.tsx | 2 +- src/public/locales/fr/default_translation_fr.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx b/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx index c2720c3b..f1034704 100644 --- a/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx +++ b/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx @@ -26,7 +26,7 @@ const ManageChartsDrawer = ({ onManageDrawerClose, manageDrawerVisible }: Manage open={manageDrawerVisible} // If we're on a small device, make the drawer full-screen width instead of a fixed width. // The default value for Ant Design is 372. - width={isSmallScreen ? '100vw' : 400} + width={isSmallScreen ? '100vw' : 420} extra={