Skip to content

refact: rewrite fetching based on checks+hooks #216

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
6 changes: 3 additions & 3 deletions src/js/components/Beacon/BeaconCommon/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { Button, Form, Space, Switch, Tooltip } from 'antd';
import type { FormInstance } from 'antd/es/form';

import { useBeaconNetwork } from '@/features/beacon/hooks';
import { useConfig } from '@/features/config/hooks';
import { toggleQuerySectionsUnionOrIntersection } from '@/features/beacon/network.store';
import { useAppSelector, useAppDispatch, useTranslationFn } from '@/hooks';
import { useAppDispatch, useTranslationFn } from '@/hooks';
import type { FormFilter } from '@/types/beacon';
import type { SearchFieldResponse } from '@/types/search';

Expand Down Expand Up @@ -39,8 +40,7 @@ const BUTTON_STYLE = { margin: '10px 0' };
const Filters = ({ filters, setFilters, form, querySections, isNetworkQuery }: FiltersProps) => {
const t = useTranslationFn();

const maxFilters = useAppSelector((state) => state.config.maxQueryParameters);
const maxQueryParametersRequired = useAppSelector((state) => state.config.maxQueryParametersRequired);
const { maxQueryParameters: maxFilters, maxQueryParametersRequired } = useConfig();
const activeFilters = filters.filter((f) => f.active);
const hasMaxFilters = maxQueryParametersRequired && activeFilters.length >= maxFilters;

Expand Down
4 changes: 2 additions & 2 deletions src/js/components/Beacon/BeaconQueryUi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import Loader from '@/components/Loader';
import { WRAPPER_STYLE } from '@/constants/beaconConstants';
import { makeBeaconQuery } from '@/features/beacon/beacon.store';
import { useBeacon } from '@/features/beacon/hooks';
import { useAppSelector } from '@/hooks';
import { useSearchQuery } from '@/features/search/hooks';

import BeaconSearchResults from './BeaconSearchResults';
import BeaconQueryFormUi from './BeaconCommon/BeaconQueryFormUi';

const BeaconQueryUi = () => {
const { isFetchingBeaconConfig, beaconAssemblyIds, isFetchingQueryResponse, apiErrorMessage } = useBeacon();
const { querySections } = useAppSelector((state) => state.query);
const { querySections } = useSearchQuery();

return isFetchingBeaconConfig ? (
<Loader />
Expand Down
66 changes: 24 additions & 42 deletions src/js/components/BentoAppRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { Routes, Route, useNavigate, useParams, Outlet } from 'react-router-dom';
import { useAutoAuthenticate, useIsAuthenticated } from 'bento-auth-js';
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 { makeGetKatsuPublic, makeGetSearchFields } from '@/features/search/query.store';
import { getBeaconConfig } from '@/features/beacon/beacon.store';
import { getBeaconNetworkConfig } from '@/features/beacon/network.store';
import { fetchGohanData, fetchKatsuData } from '@/features/ingestion/lastIngestion.store';
import { makeGetDataTypes } from '@/features/dataTypes/dataTypes.store';
import { useMetadata } from '@/features/metadata/hooks';
import { getProjects, markScopeSet, selectScope } from '@/features/metadata/metadata.store';
import { invalidateConfig } from '@/features/config/config.store';
import { invalidateData } from '@/features/data/data.store';
import { useMetadata, useSelectedScope } from '@/features/metadata/hooks';
import { type DiscoveryScope, markScopeSet, selectScope } from '@/features/metadata/metadata.store';

import Loader from '@/components/Loader';
import DefaultLayout from '@/components/Util/DefaultLayout';
import { BEACON_NETWORK_ENABLED } from '@/config';
import { BentoRoute } from '@/types/routes';
import { scopeEqual, validProjectDataset } from '@/utils/router';

Expand All @@ -25,22 +18,25 @@ import Search from './Search/Search';
import ProvenanceTab from './Provenance/ProvenanceTab';
import BeaconQueryUi from './Beacon/BeaconQueryUi';
import NetworkUi from './Beacon/BeaconNetwork/NetworkUi';
import { invalidateQuerySections, invalidateResults } from '@/features/search/query.store';

const ScopedRoute = () => {
const { projectId, datasetId } = useParams();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { selectedScope, projects } = useMetadata();
const { selectedScope, projects, hasAttempted: hasAttemptedProjects } = useMetadata();

useEffect(() => {
if (!hasAttemptedProjects) return; // Wait for projects to load first

// Update selectedScope based on URL parameters
const valid = validProjectDataset(projects, { project: projectId, dataset: datasetId });

// Don't change the scope object if the scope value is the same, otherwise it'll trigger needless re-renders.
if (scopeEqual(selectedScope.scope, valid.scope)) {
// Make sure scope is marked as set to trigger the first load.
// This can happen when the true URL scope is the whole instance, which is also the initial scope value.
dispatch(markScopeSet());
if (!selectedScope.scopeSet) dispatch(markScopeSet());
return;
}

Expand All @@ -67,46 +63,32 @@ const ScopedRoute = () => {
}
const newPathString = '/' + newPath.join('/');
navigate(newPathString, { replace: true });
}, [projects, projectId, datasetId, dispatch, navigate, selectedScope]);
}, [hasAttemptedProjects, projects, projectId, datasetId, dispatch, navigate, selectedScope]);

return <Outlet />;
};

const BentoAppRouter = () => {
const dispatch = useAppDispatch();

const { isAutoAuthenticating } = useAutoAuthenticate();
const isAuthenticated = useIsAuthenticated();
const { selectedScope, isFetching: isFetchingProjects } = useMetadata();

useEffect(() => {
if (!selectedScope.scopeSet) return;
dispatch(makeGetConfigRequest()).then(() => dispatch(getBeaconConfig()));

if (BEACON_NETWORK_ENABLED) {
dispatch(getBeaconNetworkConfig());
}
const { isFetching: isFetchingProjects } = useMetadata();

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(makeGetKatsuPublic());
dispatch(fetchKatsuData());
}, [dispatch, isAuthenticated, selectedScope]);
const { scopeSet, scope } = useSelectedScope();
const lastAuthz = useRef<boolean>(!!isAuthenticated);
const lastScope = useRef<DiscoveryScope | null>(null);

useEffect(() => {
dispatch(getProjects());
dispatch(makeGetAboutRequest());
dispatch(fetchGohanData());
dispatch(makeGetServiceInfoRequest());
if (isAuthenticated) {
dispatch(makeGetDataTypes());
// If the scope or isAuthenticated changes, we need to invalidate current state data
if ((scopeSet && !scopeEqual(scope, lastScope.current)) || isAuthenticated !== lastAuthz.current) {
dispatch(invalidateConfig());
dispatch(invalidateData());
dispatch(invalidateQuerySections());
dispatch(invalidateResults());
lastScope.current = scope;
lastAuthz.current = !!isAuthenticated;
}
}, [dispatch, isAuthenticated]);
}, [dispatch, isAuthenticated, scopeSet, scope]);

if (isAutoAuthenticating || isFetchingProjects) {
return <Loader />;
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;
5 changes: 3 additions & 2 deletions src/js/components/Overview/Counts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { BiDna } from 'react-icons/bi';

import ExpSvg from '../Util/ExpSvg';
import { BOX_SHADOW, COUNTS_FILL } from '@/constants/overviewConstants';
import { useAppSelector, useTranslationFn } from '@/hooks';
import { useTranslationFn } from '@/hooks';
import { useData } from '@/features/data/hooks';

const styles: Record<string, CSSProperties> = {
countCard: {
Expand All @@ -20,7 +21,7 @@ const CountsHelp = ({ children }: { children: ReactNode }) => <div style={{ maxW
const Counts = () => {
const t = useTranslationFn();

const { counts, isFetchingData } = useAppSelector((state) => state.data);
const { counts, isFetchingData } = useData();

// Break down help into multiple sentences inside an array to make translation a bit easier.
const data = [
Expand Down
5 changes: 3 additions & 2 deletions src/js/components/Overview/Drawer/ManageChartsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ const { Title } = Typography;
import ChartTree from './ChartTree';

import type { ChartDataField } from '@/types/data';
import { useAppSelector, useAppDispatch, useTranslationFn } from '@/hooks';
import { useAppDispatch, useTranslationFn } from '@/hooks';
import { hideAllSectionCharts, setAllDisplayedCharts, resetLayout } from '@/features/data/data.store';
import { useData } from '@/features/data/hooks';

const ManageChartsDrawer = ({ onManageDrawerClose, manageDrawerVisible }: ManageChartsDrawerProps) => {
const t = useTranslationFn();

const dispatch = useAppDispatch();

const sections = useAppSelector((state) => state.data.sections);
const { sections } = useData();

return (
<Drawer
Expand Down
7 changes: 3 additions & 4 deletions src/js/components/Overview/LastIngestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ import { Card, Empty, Space, Typography } from 'antd';
import { CalendarOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';

import { useAppSelector } from '@/hooks';
import { BOX_SHADOW } from '@/constants/overviewConstants';
import { useLastIngestionData } from '@/features/ingestion/hooks';
import { getDataTypeLabel } from '@/types/dataTypes';

import type { LastIngestionDataTypeResponse } from '@/types/lastIngestionDataTypeResponse';
import { BOX_SHADOW } from '@/constants/overviewConstants';

const LastIngestionInfo: React.FC = () => {
const { t, i18n } = useTranslation();

const dataTypesObject = useAppSelector((state) => state.lastIngestionData?.dataTypes) || {};
const { dataTypes: dataTypesObject } = useLastIngestionData();

const sortedDataTypes = Object.values(dataTypesObject).sort((a, b) => a.label.localeCompare(b.label));

Expand Down
13 changes: 12 additions & 1 deletion src/js/components/Overview/OverviewDisplayData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react';
import { Space } from 'antd';

import { disableChart } from '@/features/data/data.store';
import { useClickableCharts } from '@/features/data/hooks';
import { useAppDispatch } from '@/hooks';
import { useSmallScreen } from '@/hooks/useResponsiveContext';

Expand Down Expand Up @@ -30,8 +31,18 @@ const OverviewDisplayData = ({ section, allCharts }: OverviewDisplayDataProps) =
[dispatch]
);

const clickableCharts = useClickableCharts();

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={clickableCharts.has(chart.id)}
/>
);
};

if (isSmallScreen) {
Expand Down
13 changes: 5 additions & 8 deletions src/js/components/Overview/PublicOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import LastIngestionInfo from './LastIngestion';
import Loader from '@/components/Loader';
import Dataset from '@/components/Provenance/Dataset';

import { useAppSelector } from '@/hooks';
import { useSelectedProject, useSelectedScope } from '@/features/metadata/hooks';
import { useTranslation } from 'react-i18next';
import { useContent } from '@/features/content/hooks';
import { useData } from '@/features/data/hooks';

const ABOUT_CARD_STYLE = { width: '100%', maxWidth: '1390px', borderRadius: '11pX', ...BOX_SHADOW };
const MANAGE_CHARTS_BUTTON_STYLE = { right: '5em', bottom: '1.5em', transform: 'scale(125%)' };
Expand All @@ -27,12 +28,8 @@ const PublicOverview = () => {
const [drawerVisible, setDrawerVisible] = useState(false);
const [aboutContent, setAboutContent] = useState('');

const {
isFetchingData: isFetchingOverviewData,
isContentPopulated,
sections,
} = useAppSelector((state) => state.data);
const { isFetchingAbout, about } = useAppSelector((state) => state.content);
const { isFetchingAbout, about } = useContent();
const { isFetchingData: isFetchingOverviewData, sections } = useData();

const selectedProject = useSelectedProject();
const { scope } = useSelectedScope();
Expand All @@ -58,7 +55,7 @@ const PublicOverview = () => {
saveToLocalStorage(sections);
}, [sections]);

return !isContentPopulated || isFetchingOverviewData ? (
return isFetchingOverviewData ? (
<Loader />
) : (
<>
Expand Down
10 changes: 3 additions & 7 deletions src/js/components/Search/MakeQueryOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { Row, Col, Checkbox } from 'antd';
import OptionDescription from './OptionDescription';
import SelectOption from './SelectOption';

import { useConfig } from '@/features/config/hooks';
import { useSearchQuery } from '@/features/search/hooks';
import { useAppSelector, useTranslationFn } from '@/hooks';
import { useTranslationFn } from '@/hooks';
import type { Field } from '@/types/search';
import { buildQueryParamsUrl, queryParamsWithoutKey } from '@/utils/search';

Expand All @@ -18,7 +19,7 @@ const MakeQueryOption = ({ queryField }: MakeQueryOptionProps) => {

const { title, id, description, config, options } = queryField;

const { maxQueryParameters } = useAppSelector((state) => state.config);
const { maxQueryParameters } = useConfig();
const { queryParamCount, queryParams } = useSearchQuery();

const isChecked = id in queryParams;
Expand All @@ -38,11 +39,6 @@ const MakeQueryOption = ({ queryField }: MakeQueryOptionProps) => {
// Don't need to dispatch - the code handling the URL change will dispatch the fetch for us instead.
}, [id, isChecked, navigate, options, pathname, queryParams]);

// TODO: allow disabling max query parameters for authenticated and authorized users when Katsu has AuthZ
// useQueryWithAuthIfAllowed()
// const maxQueryParametersRequired = useAppSelector((state) => state.config.maxQueryParametersRequired);
// const hasMaxFilters = maxQueryParametersRequired && queryParamCount >= maxQueryParameters;
// const disabled = isChecked ? false : hasMaxFilters;
const disabled = isChecked ? false : queryParamCount >= maxQueryParameters;

return (
Expand Down
Loading