From 7cb2e03352da77a4427988320288daf946189e4b Mon Sep 17 00:00:00 2001 From: Gregor Eichelberger Date: Fri, 20 Mar 2026 09:29:34 +0100 Subject: [PATCH 1/2] Block auto refresh on filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filter-driven reloads are now centralized to prevent multiple data fetches at once. Further, added a feature to cancel ongoing requests and ensure that older responses don’t replace newer data. After applying filters, there’s a temporary pause on auto-refresh to prevent interruptions while users are still filtering. --- src/components/shared/Table.tsx | 39 +------- src/components/shared/TableFilterProfiles.tsx | 14 +-- src/components/shared/TableFilters.tsx | 21 ++-- src/components/shared/TablePage.tsx | 96 +++++++++++++++++-- 4 files changed, 102 insertions(+), 68 deletions(-) diff --git a/src/components/shared/Table.tsx b/src/components/shared/Table.tsx index 8a3c7e88c3..681b950469 100644 --- a/src/components/shared/Table.tsx +++ b/src/components/shared/Table.tsx @@ -23,7 +23,6 @@ import { selectRowIds, selectRowById, rowsSelectors, - resetTableProperties, } from "../../slices/tableSlice"; import { changeAllSelected, @@ -36,16 +35,14 @@ import cn from "classnames"; import EditTableViewModal from "../shared/EditTableViewModal"; import Notifications from "./Notifications"; -import { AppThunk, useAppDispatch, useAppSelector } from "../../store"; +import { useAppDispatch, useAppSelector } from "../../store"; import { TableColumn } from "../../configs/tableConfigs/aclsTableConfig"; import ButtonLikeAnchor from "./ButtonLikeAnchor"; import { ModalHandle } from "./modals/Modal"; import { ParseKeys } from "i18next"; import { LuChevronDown, LuChevronLeft, LuChevronRight, LuChevronUp } from "react-icons/lu"; -import { useLocation } from "react-router"; import Select, { components, DropdownIndicatorProps } from "react-select"; import { pageSizeStyles } from "../../utils/componentStyles"; -import { GenericAsyncThunk } from "../../utils/utils"; export type TemplateMap = { [key: string]: ({ row }: { row: T }) => JSX.Element | JSX.Element[] @@ -56,49 +53,15 @@ export type TemplateMap = { */ const Table = ({ templateMap, - fetchResource, - loadResourceIntoTable, }: { templateMap: TemplateMap - fetchResource: GenericAsyncThunk, - loadResourceIntoTable: () => AppThunk, }) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const location = useLocation(); const editTableViewModalRef = useRef(null); const selectAllCheckboxRef = useRef(null); - useEffect(() => { - // State variable for interrupting the load function - let allowLoadIntoTable = true; - - // Clear table of previous data - dispatch(resetTableProperties()); - - // Load resource on mount - const loadResource = async () => { - // Fetching resources from server - await dispatch(fetchResource()); - - // Load resources into table - if (allowLoadIntoTable) { - dispatch(loadResourceIntoTable()); - } - }; - loadResource(); - - // Fetch resources every minute - const fetchResourceInterval = setInterval(() => { loadResource(); }, 5000); - - return () => { - allowLoadIntoTable = false; - clearInterval(fetchResourceInterval); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [location.hash]); - const forceDeselectAll = () => { dispatch(changeAllSelected(false)); if (selectAllCheckboxRef.current?.checked) { diff --git a/src/components/shared/TableFilterProfiles.tsx b/src/components/shared/TableFilterProfiles.tsx index 298904a002..0eba6b5343 100644 --- a/src/components/shared/TableFilterProfiles.tsx +++ b/src/components/shared/TableFilterProfiles.tsx @@ -12,14 +12,13 @@ import { } from "../../thunks/tableThunks"; import { getFilters } from "../../selectors/tableFilterSelectors"; import { FilterData, loadFilterProfile } from "../../slices/tableFilterSlice"; -import { AppThunk, useAppDispatch, useAppSelector } from "../../store"; +import { useAppDispatch, useAppSelector } from "../../store"; import { useHotkeys } from "react-hotkeys-hook"; import { availableHotkeys } from "../../configs/hotkeysConfig"; import ButtonLikeAnchor from "./ButtonLikeAnchor"; import { ParseKeys } from "i18next"; import { Resource } from "../../slices/tableSlice"; import { LuSettings, LuX } from "react-icons/lu"; -import { GenericAsyncThunk } from "../../utils/utils"; /** * This component renders the table filter profiles in the upper right corner when clicked on settings icon of the @@ -29,13 +28,11 @@ const TableFiltersProfiles = ({ showFilterSettings, setFilterSettings, loadResource, - loadResourceIntoTable, resource, }: { showFilterSettings: boolean, setFilterSettings: (_: boolean) => void, - loadResource: GenericAsyncThunk, - loadResourceIntoTable: () => AppThunk, + loadResource: () => Promise, resource: Resource, }) => { const dispatch = useAppDispatch(); @@ -132,14 +129,13 @@ const TableFiltersProfiles = ({ } }; - const chooseFilterProfile = (filterMap: FilterData[]) => { + const chooseFilterProfile = async (filterMap: FilterData[]) => { dispatch(loadFilterProfile(filterMap)); // No matter what, we go to page one. dispatch(goToPage(0)); // Reload resources when filters are removed - dispatch(loadResource()); - dispatch(loadResourceIntoTable()); + await loadResource(); }; return ( @@ -169,7 +165,7 @@ const TableFiltersProfiles = ({ currentProfiles.map((profile, key) => (
  • chooseFilterProfile(profile.filterMap)} + onClick={() => { void chooseFilterProfile(profile.filterMap); }} tooltipText={profile.description as ParseKeys} > {profile.name.substr(0, 70)} diff --git a/src/components/shared/TableFilters.tsx b/src/components/shared/TableFilters.tsx index 9dd7251fcb..50b5c45be3 100644 --- a/src/components/shared/TableFilters.tsx +++ b/src/components/shared/TableFilters.tsx @@ -19,9 +19,9 @@ import { import TableFilterProfiles from "./TableFilterProfiles"; import { availableHotkeys } from "../../configs/hotkeysConfig"; import { useHotkeys } from "react-hotkeys-hook"; -import { AppThunk, useAppDispatch, useAppSelector } from "../../store"; +import { useAppDispatch, useAppSelector } from "../../store"; import { renderValidDate } from "../../utils/dateUtils"; -import { GenericAsyncThunk, getCurrentLanguageInformation } from "../../utils/utils"; +import { getCurrentLanguageInformation } from "../../utils/utils"; import DropDown from "./DropDown"; import ButtonLikeAnchor from "./ButtonLikeAnchor"; import { ParseKeys } from "i18next"; @@ -38,11 +38,9 @@ import i18n from "../../i18n/i18n"; */ const TableFilters = ({ loadResource, - loadResourceIntoTable, resource, }: { - loadResource: GenericAsyncThunk, - loadResourceIntoTable: () => AppThunk, + loadResource: () => Promise, resource: Resource, }) => { const { t } = useTranslation(); @@ -85,8 +83,7 @@ const TableFilters = ({ dispatch(resetFilterValues()); // Reload resources when filters are removed - await dispatch(loadResource()); - dispatch(loadResourceIntoTable()); + await loadResource(); }; // Remove a certain filter @@ -100,8 +97,7 @@ const TableFilters = ({ dispatch(editFilterValue({ filterName: filter.name, value: "", resource })); // Reload resources when filter is removed - await dispatch(loadResource()); - dispatch(loadResourceIntoTable()); + await loadResource(); }; const handleSearchChange = (value: string) => { @@ -152,8 +148,7 @@ const TableFilters = ({ // No matter what, we go to page one. dispatch(goToPage(0)); // Reload of resource - await dispatch(loadResource()); - dispatch(loadResourceIntoTable()); + await loadResource(); }; useEffect(() => { @@ -217,8 +212,7 @@ const TableFilters = ({ setSelectedFilter(""); // Reload of resource after going to very first page. dispatch(goToPage(0)); - await dispatch(loadResource()); - dispatch(loadResourceIntoTable()); + await loadResource(); } } }; @@ -383,7 +377,6 @@ const TableFilters = ({ setFilterSettings={setFilterSettings} resource={resource} loadResource={loadResource} - loadResourceIntoTable={loadResourceIntoTable} /> )} diff --git a/src/components/shared/TablePage.tsx b/src/components/shared/TablePage.tsx index 57ad2c5afa..f71d8390a9 100644 --- a/src/components/shared/TablePage.tsx +++ b/src/components/shared/TablePage.tsx @@ -1,12 +1,13 @@ -import { ReactNode } from "react"; +import { ReactNode, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import TableFilters from "../shared/TableFilters"; import Table, { TemplateMap } from "../shared/Table"; import Notifications from "../shared/Notifications"; import { CreateType, NavBarLink } from "../NavBar"; -import { AppThunk, RootState, useAppSelector } from "../../store"; -import { Resource, Row } from "../../slices/tableSlice"; +import { AppThunk, RootState, useAppDispatch, useAppSelector } from "../../store"; +import { resetTableProperties, Resource, Row } from "../../slices/tableSlice"; import { ParseKeys } from "i18next"; +import { useLocation } from "react-router"; import MainPage from "./MainPage"; import { GenericAsyncThunk } from "../../utils/utils"; @@ -37,9 +38,93 @@ const TablePage = ({ children?: ReactNode }) => { const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const currentLoadRequest = useRef<{ abort:() => void } | null>(null); + const latestLoadRequestId = useRef(0); + const allowLoadIntoTable = useRef(true); + const currentLoadSource = useRef<"auto" | "filters" | null>(null); + const autoRefreshPaused = useRef(false); + const autoRefreshPauseTimeout = useRef | null>(null); + + const location = useLocation(); const numberOfRows = useAppSelector(state => getTotalResources(state)); + const pauseAutoRefresh = () => { + autoRefreshPaused.current = true; + + if (autoRefreshPauseTimeout.current) { + clearTimeout(autoRefreshPauseTimeout.current); + } + + autoRefreshPauseTimeout.current = setTimeout(() => { + autoRefreshPaused.current = false; + autoRefreshPauseTimeout.current = null; + }, 2000); + }; + + const loadResource = async (source: "auto" | "filters" = "auto") => { + if (source === "filters") { + pauseAutoRefresh(); + } + + if (source === "auto" && autoRefreshPaused.current) { + return; + } + + if (source === "auto" && currentLoadSource.current === "filters") { + return; + } + + const requestId = ++latestLoadRequestId.current; + currentLoadSource.current = source; + + currentLoadRequest.current?.abort?.(); + + const fetchRequest = dispatch(fetchResource()); + currentLoadRequest.current = fetchRequest; + + const fetchResult = await fetchRequest; + + if (requestId === latestLoadRequestId.current) { + currentLoadSource.current = null; + } + + if ( + allowLoadIntoTable.current + && requestId === latestLoadRequestId.current + && fetchResult?.meta?.requestStatus === "fulfilled" + ) { + dispatch(loadResourceIntoTable()); + } + }; + + const loadResourceFromFilters = () => loadResource("filters"); + + useEffect(() => { + allowLoadIntoTable.current = true; + + // Clear table of previous data + dispatch(resetTableProperties()); + + // Load resource on mount + loadResource("auto"); + + // Fetch resources every minute + const fetchResourceInterval = setInterval(() => { void loadResource("auto"); }, 5000); + + return () => { + allowLoadIntoTable.current = false; + currentLoadRequest.current?.abort?.(); + if (autoRefreshPauseTimeout.current) { + clearTimeout(autoRefreshPauseTimeout.current); + autoRefreshPauseTimeout.current = null; + } + clearInterval(fetchResourceInterval); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location.hash]); + return ( ({ {/* Include filters component */} @@ -66,8 +150,6 @@ const TablePage = ({ {/* Include table component */} ); From c873623b73794d0131b1d2dafa972a56f49c8110 Mon Sep 17 00:00:00 2001 From: Gregor Eichelberger Date: Fri, 20 Mar 2026 09:36:02 +0100 Subject: [PATCH 2/2] Fix duplicate requests on filtering Removed duplicate fetch behavior in search/filter apply logic by avoiding page-navigation fetch plus manual reload in the same interaction. --- src/components/shared/TableFilters.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/shared/TableFilters.tsx b/src/components/shared/TableFilters.tsx index 50b5c45be3..284f1d8887 100644 --- a/src/components/shared/TableFilters.tsx +++ b/src/components/shared/TableFilters.tsx @@ -14,8 +14,10 @@ import { resetFilterValues, } from "../../slices/tableFilterSlice"; import { - goToPage, -} from "../../thunks/tableThunks"; + deselectAll, + setOffset, + setPageActive, +} from "../../slices/tableSlice"; import TableFilterProfiles from "./TableFilterProfiles"; import { availableHotkeys } from "../../configs/hotkeysConfig"; import { useHotkeys } from "react-hotkeys-hook"; @@ -146,7 +148,9 @@ const TableFilters = ({ const applyFilterChangesDebounced = async () => { console.log("Applying filter changes with value: " + itemValue); // No matter what, we go to page one. - dispatch(goToPage(0)); + dispatch(deselectAll()); + dispatch(setOffset(0)); + dispatch(setPageActive(0)); // Reload of resource await loadResource(); }; @@ -211,7 +215,9 @@ const TableFilters = ({ setFilterSelector(false); setSelectedFilter(""); // Reload of resource after going to very first page. - dispatch(goToPage(0)); + dispatch(deselectAll()); + dispatch(setOffset(0)); + dispatch(setPageActive(0)); await loadResource(); } }