diff --git a/README.md b/README.md index dcd0c252..fee95e8d 100755 --- a/README.md +++ b/README.md @@ -8,6 +8,21 @@ 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: + +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 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/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": { diff --git a/src/js/components/Beacon/BeaconCommon/Filters.tsx b/src/js/components/Beacon/BeaconCommon/Filters.tsx index 0b83fd29..60435834 100644 --- a/src/js/components/Beacon/BeaconCommon/Filters.tsx +++ b/src/js/components/Beacon/BeaconCommon/Filters.tsx @@ -16,7 +16,7 @@ const NetworkFilterToggle = () => { const { isQuerySectionsUnion } = useBeaconNetwork(); return ( - +
dispatch(toggleQuerySectionsUnionOrIntersection())} diff --git a/src/js/components/Beacon/BeaconCommon/VariantsForm.tsx b/src/js/components/Beacon/BeaconCommon/VariantsForm.tsx index 4a05a14d..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(() => { @@ -92,7 +99,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 +137,7 @@ const VariantsForm = ({ isNetworkQuery, beaconAssemblyIds }: VariantsFormProps) diff --git a/src/js/components/Beacon/BeaconNetwork/NetworkSearchResults.tsx b/src/js/components/Beacon/BeaconNetwork/NetworkSearchResults.tsx index 5fe994ca..a8a01cdd 100644 --- a/src/js/components/Beacon/BeaconNetwork/NetworkSearchResults.tsx +++ b/src/js/components/Beacon/BeaconNetwork/NetworkSearchResults.tsx @@ -43,7 +43,7 @@ const NetworkSearchResults = () => { // currently not possible to have both a network error and results, but this may change in the future const resultsExtra = ( - {hasBeaconNetworkError && Network Error} + {hasBeaconNetworkError && {t('beacon.network_error')}} {!noResponsesYet && isFetchingAtLeastOneResponse && ( } /> )} diff --git a/src/js/components/BentoAppRouter.tsx b/src/js/components/BentoAppRouter.tsx index c2545bb5..8a2e313d 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'; @@ -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 }); @@ -92,12 +93,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]); diff --git a/src/js/components/Overview/Chart.tsx b/src/js/components/Overview/Chart.tsx index 91b471a9..172300ae 100644 --- a/src/js/components/Overview/Chart.tsx +++ b/src/js/components/Overview/Chart.tsx @@ -39,6 +39,7 @@ const Chart = memo(({ chartConfig, data, units, id, isClickable }: ChartProps) = }; const { chart_type: type } = chartConfig; + units = t(units); // Units can be a word, like "years". Make sure this word gets translated. switch (type) { case CHART_TYPE_BAR: @@ -101,7 +102,7 @@ const Chart = memo(({ chartConfig, data, units, id, isClickable }: ChartProps) = }} renderPopupBody={(_f, d) => ( <> - Count: {(d ?? 0).toString()} {units} + {t('Count') + ':'} {(d ?? 0).toString()} {units} )} /> 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/Counts.tsx b/src/js/components/Overview/Counts.tsx index 16c29b1c..425da8bf 100644 --- a/src/js/components/Overview/Counts.tsx +++ b/src/js/components/Overview/Counts.tsx @@ -1,13 +1,15 @@ -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 { WAITING_STATES } from '@/constants/requests'; 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,17 +19,17 @@ const styles: Record = { }, }; -const CountsHelp = ({ children }: { children: ReactNode }) =>
{children}
; +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(); // Break down help into multiple sentences inside an array to make translation a bit easier. - const data = [ + const data: CountEntry[] = [ { entity: 'individual', icon: , @@ -45,36 +47,23 @@ const Counts = () => { }, ]; + const waitingForData = WAITING_STATES.includes(status); + return ( <> {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={waitingForData} + /> + + ))}
); diff --git a/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx b/src/js/components/Overview/Drawer/ManageChartsDrawer.tsx index 3711aef4..f1034704 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,7 +14,9 @@ const ManageChartsDrawer = ({ onManageDrawerClose, manageDrawerVisible }: Manage const dispatch = useAppDispatch(); - const sections = useAppSelector((state) => state.data.sections); + const isSmallScreen = useSmallScreen(); + + const { sections } = useAppSelector((state) => state.data); return ( } @@ -55,7 +61,7 @@ const ManageChartsDrawer = ({ onManageDrawerClose, manageDrawerVisible }: Manage dispatch(setAllDisplayedCharts({ section: sectionTitle })); }} > - Show All + {t('Show All')} 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 1ea142e1..c3e540da 100644 --- a/src/js/components/Overview/PublicOverview.tsx +++ b/src/js/components/Overview/PublicOverview.tsx @@ -6,6 +6,7 @@ import { convertSequenceAndDisplayData, saveValue } from '@/utils/localStorage'; import type { Sections } from '@/types/data'; import { BOX_SHADOW, LOCALSTORAGE_CHARTS_KEY } from '@/constants/overviewConstants'; +import { WAITING_STATES } from '@/constants/requests'; import OverviewSection from './OverviewSection'; import ManageChartsDrawer from './Drawer/ManageChartsDrawer'; @@ -16,6 +17,7 @@ import Dataset from '@/components/Provenance/Catalogue/Dataset'; import Catalogue from '@/components/Provenance/Catalogue/Catalogue'; import { useAppSelector } from '@/hooks'; +import { useSearchableFields } from '@/features/data/hooks'; import { useMetadata, useSelectedProject, useSelectedScope } from '@/features/metadata/hooks'; import { useTranslation } from 'react-i18next'; import { RequestStatus } from '@/types/requests'; @@ -24,16 +26,12 @@ 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(''); - const { - isFetchingData: isFetchingOverviewData, - isContentPopulated, - sections, - } = useAppSelector((state) => state.data); + const { status: overviewDataStatus, sections } = useAppSelector((state) => state.data); const { status: aboutStatus, about } = useAppSelector((state) => state.content); const selectedProject = useSelectedProject(); @@ -42,9 +40,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; @@ -61,9 +59,11 @@ const PublicOverview = () => { saveToLocalStorage(sections); }, [sections]); + const searchableFields = useSearchableFields(); + if (!selectedProject && projects.length > 1) return ; - return !isContentPopulated || isFetchingOverviewData ? ( + return WAITING_STATES.includes(overviewDataStatus) ? ( ) : ( <> @@ -98,7 +98,7 @@ const PublicOverview = () => { {displayedSections.map(({ sectionTitle, charts }, i) => (
- +
))} @@ -109,7 +109,7 @@ const PublicOverview = () => { } - tooltip="Manage Charts" + tooltip={t('Manage Charts')} style={MANAGE_CHARTS_BUTTON_STYLE} onClick={onManageChartsOpen} /> diff --git a/src/js/components/Search/SearchResultsCounts.tsx b/src/js/components/Search/SearchResultsCounts.tsx index f3547673..a5e55e2a 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/SiteFooter.tsx b/src/js/components/SiteFooter.tsx index 791ba6a5..b80e3424 100644 --- a/src/js/components/SiteFooter.tsx +++ b/src/js/components/SiteFooter.tsx @@ -14,7 +14,7 @@ const SiteFooter = () => {
- {t('Powered by')} + {t('footer.powered_by')} Bento @@ -22,19 +22,19 @@ const SiteFooter = () => {
- Copyright © 2019-2024 the{' '} + {t('footer.copyright')} 2019-2024{' '} - Canadian Centre for Computational Genomics + {t('footer.c3g')} .
- Bento is licensed under the{' '} + {t('footer.licensed_under') + ' '} LGPLv3 - . The source code is available on{' '} + . {t('footer.source_available') + ' '} Github @@ -43,7 +43,7 @@ const SiteFooter = () => {
- {t('Terms of Use')} + {t('footer.terms_of_use')}
diff --git a/src/js/components/Util/CountsTitleWithHelp.tsx b/src/js/components/Util/CountsTitleWithHelp.tsx new file mode 100644 index 00000000..e617be9c --- /dev/null +++ b/src/js/components/Util/CountsTitleWithHelp.tsx @@ -0,0 +1,40 @@ +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, 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} + {showHelp && ( + {t(`entities.${entity}_help`, { joinArrays: ' ' })}} + > + + + )} + + ); +}; + +type CountsHelpProps = { + entity: BentoEntity; + showHelp?: boolean; +}; + +export default CountsTitleWithHelp; 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 6c3dcc82..4b285949 100644 --- a/src/js/features/data/data.store.ts +++ b/src/js/features/data/data.store.ts @@ -1,31 +1,20 @@ 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); - } -); +import { RequestStatus } from '@/types/requests'; interface DataState { - isFetchingData: boolean; - isContentPopulated: boolean; + status: RequestStatus; defaultLayout: Sections; sections: Sections; counts: Counts; } const initialState: DataState = { - isFetchingData: true, - isContentPopulated: false, + status: RequestStatus.Idle, defaultLayout: [], sections: [], counts: { @@ -91,24 +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; - }) - .addCase(populateClickable.fulfilled, (state, { payload }) => { - state.sections.forEach((section) => { - section.charts.forEach((chart) => { - chart.isSearchable = payload.includes(chart.id); - }); - }); - state.isContentPopulated = true; + state.status = RequestStatus.Rejected; }); }, }); 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 { 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'; diff --git a/src/public/assets/branding_color.png b/src/public/assets/branding.lightbg.png similarity index 100% rename from src/public/assets/branding_color.png rename to src/public/assets/branding.lightbg.png 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, diff --git a/src/public/locales/en/default_translation_en.json b/src/public/locales/en/default_translation_en.json index 8c7447d4..9df703db 100644 --- a/src/public/locales/en/default_translation_en.json +++ b/src/public/locales/en/default_translation_en.json @@ -64,11 +64,12 @@ "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.", - "Powered by": "Powered by", - "Terms of Use": "Terms of Use", "No Data": "No Data", "Counts": "Counts", "Search by genomic variants, clinical metadata or both.": "Search by genomic variants, clinical metadata or both.", @@ -104,14 +105,18 @@ "Datasets": "Datasets", "Clear": "Clear", "Select": "Select", + "Created": "Created", + "Updated": "Updated", "beacon": { "search_beacon": "Search Beacon", "search_network": "Search Network", "beacon_error": "Beacon Error", "results_from": "Results from", "network_search_results": "Network Search Results", + "network_error": "Network Error", "show_all_filters": "show all filters", "common_filters_only": "common filters only", + "network_filter_toggle_help": "Choose all search filters across the network, or only those common to all beacons.", "home_page": "Home Page", "select_asm": "Select an assembly", "variants_form_error": "Variants form should include a chromosome and either an end position or both reference and alternate bases", @@ -130,6 +135,12 @@ "Leave this form blank to search by metadata only." ] }, - "Created": "Created", - "Updated": "Updated" + "footer": { + "powered_by": "Powered by", + "copyright": "Copyright ©", + "c3g": "the Canadian Centre for Computational Genomics", + "licensed_under": "Bento is licensed under the", + "source_available": "The source code is available on", + "terms_of_use": "Terms of Use" + } } diff --git a/src/public/locales/fr/default_translation_fr.json b/src/public/locales/fr/default_translation_fr.json index 03c96133..11dddde7 100644 --- a/src/public/locales/fr/default_translation_fr.json +++ b/src/public/locales/fr/default_translation_fr.json @@ -64,11 +64,12 @@ "Download DATS File": "Télécharcher le fichier DATS", "Charts": "Tableaux", "Manage Charts": "Gestion des tableaux", + "Show All": "Montrer tous", + "Reset": "Réinit.", + "Hide All": "Cacher tous", "Remove this chart": "Supprimer ce tableau", "Width": "Largeur", "Insufficient data available.": "Pas suffisamment de données.", - "Powered by": "Propulsé par", - "Terms of Use": "Conditions d'utilisation (en anglais)", "No Data": "Pas de données", "Counts": "Totaux", "Search by genomic variants, clinical metadata or both.": "Recherche par variant génomique, métadonnées cliniques ou les deux.", @@ -104,14 +105,18 @@ "Datasets": "Jeux de données", "Clear": "Effacer", "Select": "Sélectionner", + "Created": "Créé", + "Updated": "Mis à jour", "beacon": { "search_beacon": "Recherche sur le Beacon", "search_network": "Recherche sur le réseau", "beacon_error": "Erreur de Beacon", "results_from": "Résultats de", "network_search_results": "Résultats de la recherche sur le réseau", + "network_error": "Erreur de réseau", "show_all_filters": "afficher tous les filtres", "common_filters_only": "filtres communs uniquement", + "network_filter_toggle_help": "Choisissez tous les filtres de recherche du réseau ou seulement ceux qui sont communs à tous les Beacons.", "home_page": "Page d'accueil", "select_asm": "Sélectionner un assemblage génomique", "variants_form_error": "Le formulaire de recherche de variants doit inclure un chromosome et soit la position de fin, ou les bases de référence et les bases alternatives.", @@ -130,6 +135,12 @@ "Laissez ce formulaire vide pour effectuer une recherche sur les métadonnées seulement." ] }, - "Created": "Créé", - "Updated": "Mis à jour" + "footer": { + "powered_by": "Propulsé par", + "copyright": "©", + "c3g": "le Centre canadien de génomique computationnelle", + "licensed_under": "Bento est sous licence", + "source_available": "Le code source est disponible sur", + "terms_of_use": "Conditions d'utilisation (en anglais)" + } }