Skip to content

Commit

Permalink
Merge pull request #235 from bento-platform/refact/chart-clickable
Browse files Browse the repository at this point in the history
refact(overview): use hook for calculating chart clickability
  • Loading branch information
davidlougheed authored Dec 3, 2024
2 parents 78c4b2f + 3b0c087 commit dc825bd
Show file tree
Hide file tree
Showing 12 changed files with 69 additions and 56 deletions.
13 changes: 5 additions & 8 deletions src/js/components/BentoAppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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 });
Expand Down Expand Up @@ -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]);
Expand Down
6 changes: 3 additions & 3 deletions src/js/components/Overview/ChartCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>(null);
const width = useElementWidth(containerRef, chart.width);
Expand All @@ -21,7 +21,6 @@ const ChartCard = memo(({ section, chart, onRemoveChart }: ChartCardProps) => {
data,
field: { id, description, title, config },
chartConfig,
isSearchable,
} = chart;

const extraOptionsData = [
Expand Down Expand Up @@ -59,7 +58,7 @@ const ChartCard = memo(({ section, chart, onRemoveChart }: ChartCardProps) => {
units={config?.units || ''}
id={id}
key={id}
isClickable={isSearchable}
isClickable={!!searchable}
/>
) : (
<Row style={ROW_EMPTY_STYLE} justify="center" align="middle">
Expand All @@ -77,6 +76,7 @@ export interface ChartCardProps {
section: string;
chart: ChartDataField;
onRemoveChart: (arg: { section: string; id: string }) => void;
searchable?: boolean;
}

export default ChartCard;
9 changes: 6 additions & 3 deletions src/js/components/Overview/Counts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();

Expand All @@ -46,18 +47,20 @@ const Counts = () => {
},
];

const waitingForData = WAITING_STATES.includes(status);

return (
<>
<Typography.Title level={3}>{t('Counts')}</Typography.Title>
<Space wrap>
{data.map(({ entity, icon, count }, i) => (
<Card key={i} style={{ ...styles.countCard, height: isFetchingData ? 138 : 114 }}>
<Card key={i} style={{ ...styles.countCard, height: waitingForData ? 138 : 114 }}>
<Statistic
title={<CountsTitleWithHelp entity={entity} />}
value={count || (uncensoredCounts ? count : NO_RESULTS_DASHES)}
valueStyle={{ color: COUNTS_FILL }}
prefix={icon}
loading={isFetchingData}
loading={waitingForData}
/>
</Card>
))}
Expand Down
2 changes: 1 addition & 1 deletion src/js/components/Overview/Drawer/ManageChartsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Drawer
Expand Down
13 changes: 11 additions & 2 deletions src/js/components/Overview/OverviewDisplayData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -31,7 +31,15 @@ const OverviewDisplayData = ({ section, allCharts }: OverviewDisplayDataProps) =
);

const renderItem = (chart: ChartDataField) => {
return <ChartCard key={chart.id} chart={chart} section={section} onRemoveChart={onRemoveChart} />;
return (
<ChartCard
key={chart.id}
chart={chart}
section={section}
onRemoveChart={onRemoveChart}
searchable={searchableFields.has(chart.id)}
/>
);
};

if (isSmallScreen) {
Expand All @@ -48,6 +56,7 @@ const OverviewDisplayData = ({ section, allCharts }: OverviewDisplayDataProps) =
export interface OverviewDisplayDataProps {
section: string;
allCharts: ChartDataField[];
searchableFields: Set<string>;
}

export default OverviewDisplayData;
12 changes: 10 additions & 2 deletions src/js/components/Overview/OverviewSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
}) => {
const t = useTranslationFn();

return (
<Space direction="vertical" size={0} style={{ width: '100%' }}>
<Typography.Title level={3}>{t(title)}</Typography.Title>
<OverviewDisplayData section={title} allCharts={chartData} />
<OverviewDisplayData section={title} allCharts={chartData} searchableFields={searchableFields} />
</Space>
);
};
Expand Down
18 changes: 9 additions & 9 deletions src/js/components/Overview/PublicOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -15,6 +16,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';
Expand All @@ -28,21 +30,17 @@ const PublicOverview = () => {
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();
const { scope } = useSelectedScope();

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;
Expand All @@ -59,7 +57,9 @@ const PublicOverview = () => {
saveToLocalStorage(sections);
}, [sections]);

return !isContentPopulated || isFetchingOverviewData ? (
const searchableFields = useSearchableFields();

return WAITING_STATES.includes(overviewDataStatus) ? (
<Loader />
) : (
<>
Expand Down Expand Up @@ -94,7 +94,7 @@ const PublicOverview = () => {
<Col flex={1}>
{displayedSections.map(({ sectionTitle, charts }, i) => (
<div key={i} className="overview">
<OverviewSection title={sectionTitle} chartData={charts} />
<OverviewSection title={sectionTitle} chartData={charts} searchableFields={searchableFields} />
</div>
))}
<LastIngestionInfo />
Expand Down
3 changes: 3 additions & 0 deletions src/js/constants/requests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { RequestStatus } from '@/types/requests';

export const WAITING_STATES = [RequestStatus.Idle, RequestStatus.Pending];
33 changes: 7 additions & 26 deletions src/js/features/data/data.store.ts
Original file line number Diff line number Diff line change
@@ -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<string[], void, { state: { query: QueryState } }>(
'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: {
Expand Down Expand Up @@ -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;
});
},
});
Expand Down
14 changes: 14 additions & 0 deletions src/js/features/data/hooks.ts
Original file line number Diff line number Diff line change
@@ -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]
);
};
1 change: 0 additions & 1 deletion src/js/features/data/makeGetDataRequest.thunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};

Expand Down
1 change: 0 additions & 1 deletion src/js/types/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit dc825bd

Please sign in to comment.