2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-02 03:30:54 +00:00

[UI] Stored table state (#9902)

* Add useStoredTableState zustand

* Store table page size in the zustand state

* Store tablesorting data in zustand state

* Actually provide records...

* Transfer table names state

* Store hidden columns too
This commit is contained in:
Oliver
2025-06-30 08:13:04 +10:00
committed by GitHub
parent c6d53b8c57
commit f3072b804e
6 changed files with 180 additions and 218 deletions

View File

@ -52,19 +52,16 @@ export type TableState = {
hasSelectedRecords: boolean; hasSelectedRecords: boolean;
setSelectedRecords: (records: any[]) => void; setSelectedRecords: (records: any[]) => void;
clearSelectedRecords: () => void; clearSelectedRecords: () => void;
hiddenColumns: string[] | null;
setHiddenColumns: (columns: string[]) => void;
searchTerm: string; searchTerm: string;
setSearchTerm: (term: string) => void; setSearchTerm: (term: string) => void;
recordCount: number; recordCount: number;
setRecordCount: (count: number) => void; setRecordCount: (count: number) => void;
page: number; page: number;
setPage: (page: number) => void; setPage: (page: number) => void;
pageSize: number;
setPageSize: (pageSize: number) => void;
storedDataLoaded: boolean;
records: any[]; records: any[];
setRecords: (records: any[]) => void; setRecords: (records: any[]) => void;
updateRecord: (record: any) => void; updateRecord: (record: any) => void;
hiddenColumns: string[];
setHiddenColumns: (columns: string[]) => void;
idAccessor?: string; idAccessor?: string;
}; };

View File

@ -8,6 +8,7 @@ import { useShallow } from 'zustand/react/shallow';
import { api } from '../App'; import { api } from '../App';
import { useLocalState } from '../states/LocalState'; import { useLocalState } from '../states/LocalState';
import { useServerApiState } from '../states/ServerApiState'; import { useServerApiState } from '../states/ServerApiState';
import { useStoredTableState } from '../states/StoredTableState';
import { fetchGlobalStates } from '../states/states'; import { fetchGlobalStates } from '../states/states';
export const defaultLocale = 'en'; export const defaultLocale = 'en';
@ -117,7 +118,7 @@ export function LanguageContext({
fetchGlobalStates(); fetchGlobalStates();
// Clear out cached table column names // Clear out cached table column names
useLocalState.getState().clearTableColumnNames(); useStoredTableState.getState().clearTableColumnNames();
}) })
/* istanbul ignore next */ /* istanbul ignore next */
.catch((err) => { .catch((err) => {

View File

@ -1,4 +1,4 @@
import { randomId, useLocalStorage } from '@mantine/hooks'; import { randomId } from '@mantine/hooks';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
@ -6,12 +6,6 @@ import type { FilterSetState } from '@lib/types/Filters';
import type { TableState } from '@lib/types/Tables'; import type { TableState } from '@lib/types/Tables';
import { useFilterSet } from './UseFilterSet'; import { useFilterSet } from './UseFilterSet';
// Interface for the stored table data in local storage
interface StoredTableData {
pageSize: number;
hiddenColumns: string[] | null;
}
/** /**
* A custom hook for managing the state of an <InvenTreeTable> component. * A custom hook for managing the state of an <InvenTreeTable> component.
* *
@ -51,6 +45,9 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
[expandedRecords] [expandedRecords]
); );
// Array of columns which are hidden
const [hiddenColumns, setHiddenColumns] = useState<string[]>([]);
// Array of selected records // Array of selected records
const [selectedRecords, setSelectedRecords] = useState<any[]>([]); const [selectedRecords, setSelectedRecords] = useState<any[]>([]);
@ -73,70 +70,6 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
const [page, setPage] = useState<number>(1); const [page, setPage] = useState<number>(1);
const [storedDataLoaded, setStoredDataLoaded] = useState<boolean>(false);
const [tableData, setTableData] = useState<StoredTableData>({
pageSize: 25,
hiddenColumns: null
});
const [storedTableData, setStoredTableData] =
useLocalStorage<StoredTableData>({
key: `inventree-table-data-${tableName}`,
getInitialValueInEffect: true,
// sync: false, // Do not use this option - see below
defaultValue: {
pageSize: 25,
hiddenColumns: null
},
deserialize: (value: any) => {
const tableData =
value === undefined
? {
pageSize: 25,
hiddenColumns: null
}
: JSON.parse(value);
if (!storedDataLoaded) {
setStoredDataLoaded((wasLoaded: boolean) => {
if (!wasLoaded) {
// First load of stored table data - copy to local state
// We only do this on first load, to avoid live syncing between tabs
// Note: The 'sync: false' option is not used, it does not perform as expected
setTableData(tableData);
}
return true;
});
}
return tableData;
}
});
const setPageSize = useCallback((size: number) => {
setStoredTableData((prev) => ({
...prev,
pageSize: size
}));
setTableData((prev) => ({
...prev,
pageSize: size
}));
}, []);
const setHiddenColumns = useCallback((columns: string[] | null) => {
setStoredTableData((prev) => {
return {
...prev,
hiddenColumns: columns
};
});
setTableData((prev) => ({
...prev,
hiddenColumns: columns
}));
}, []);
// Search term // Search term
const [searchTerm, setSearchTerm] = useState<string>(''); const [searchTerm, setSearchTerm] = useState<string>('');
@ -186,17 +119,14 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
setSelectedRecords, setSelectedRecords,
clearSelectedRecords, clearSelectedRecords,
hasSelectedRecords, hasSelectedRecords,
pageSize: tableData.pageSize,
hiddenColumns: tableData.hiddenColumns,
setHiddenColumns,
searchTerm, searchTerm,
setSearchTerm, setSearchTerm,
recordCount, recordCount,
setRecordCount, setRecordCount,
hiddenColumns,
setHiddenColumns,
page, page,
setPage, setPage,
setPageSize,
storedDataLoaded,
records, records,
setRecords, setRecords,
updateRecord, updateRecord,

View File

@ -1,4 +1,3 @@
import type { DataTableSortStatus } from 'mantine-datatable';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
@ -35,17 +34,6 @@ interface LocalStateProps {
// panels // panels
lastUsedPanels: Record<string, string>; lastUsedPanels: Record<string, string>;
setLastUsedPanel: (panelKey: string) => (value: string) => void; setLastUsedPanel: (panelKey: string) => (value: string) => void;
tableColumnNames: Record<string, Record<string, string>>;
getTableColumnNames: (tableKey: string) => Record<string, string>;
setTableColumnNames: (
tableKey: string
) => (names: Record<string, string>) => void;
tableSorting: Record<string, any>;
getTableSorting: (tableKey: string) => DataTableSortStatus;
setTableSorting: (
tableKey: string
) => (sorting: DataTableSortStatus<any>) => void;
clearTableColumnNames: () => void;
detailDrawerStack: number; detailDrawerStack: number;
addDetailDrawer: (value: number | false) => void; addDetailDrawer: (value: number | false) => void;
navigationOpen: boolean; navigationOpen: boolean;
@ -140,36 +128,7 @@ export const useLocalState = create<LocalStateProps>()(
}); });
} }
}, },
// tables
tableColumnNames: {},
getTableColumnNames: (tableKey) => {
return get().tableColumnNames[tableKey] || null;
},
setTableColumnNames: (tableKey) => (names) => {
// Update the table column names for the given table
set({
tableColumnNames: {
...get().tableColumnNames,
[tableKey]: names
}
});
},
clearTableColumnNames: () => {
set({ tableColumnNames: {} });
},
tableSorting: {},
getTableSorting: (tableKey) => {
return get().tableSorting[tableKey] || {};
},
setTableSorting: (tableKey) => (sorting) => {
// Update the table sorting for the given table
set({
tableSorting: {
...get().tableSorting,
[tableKey]: sorting
}
});
},
// detail drawers // detail drawers
detailDrawerStack: 0, detailDrawerStack: 0,
addDetailDrawer: (value) => { addDetailDrawer: (value) => {

View File

@ -0,0 +1,91 @@
import type { DataTableSortStatus } from 'mantine-datatable';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const DEFAULT_PAGE_SIZE: number = 25;
/**
* Interfacing for storing persistent table state in the browser.
*
* The following properties are stored:
* - pageSize: The number of rows to display per page in the table.
* - hiddenColumns: An array of column names that are hidden in a given table.
* - columnNames: An object mapping table keys to arrays of column names.
* - sorting: An object mapping table keys to sorting configurations.
*/
interface StoredTableStateProps {
pageSize: number;
setPageSize: (size: number) => void;
tableSorting: Record<string, any>;
getTableSorting: (tableKey: string) => DataTableSortStatus;
setTableSorting: (
tableKey: string
) => (sorting: DataTableSortStatus<any>) => void;
tableColumnNames: Record<string, Record<string, string>>;
getTableColumnNames: (tableKey: string) => Record<string, string>;
setTableColumnNames: (
tableKey: string
) => (names: Record<string, string>) => void;
clearTableColumnNames: () => void;
hiddenColumns: Record<string, string[]>;
getHiddenColumns: (tableKey: string) => string[] | null;
setHiddenColumns: (tableKey: string) => (columns: string[]) => void;
}
export const useStoredTableState = create<StoredTableStateProps>()(
persist(
(set, get) => ({
pageSize: DEFAULT_PAGE_SIZE,
setPageSize: (size: number) => {
set((state) => ({
pageSize: size
}));
},
tableSorting: {},
getTableSorting: (tableKey) => {
return get().tableSorting[tableKey] || {};
},
setTableSorting: (tableKey) => (sorting) => {
// Update the table sorting for the given table
set({
tableSorting: {
...get().tableSorting,
[tableKey]: sorting
}
});
},
tableColumnNames: {},
getTableColumnNames: (tableKey) => {
return get().tableColumnNames[tableKey] || null;
},
setTableColumnNames: (tableKey) => (names) => {
// Update the table column names for the given table
set({
tableColumnNames: {
...get().tableColumnNames,
[tableKey]: names
}
});
},
clearTableColumnNames: () => {
set({ tableColumnNames: {} });
},
hiddenColumns: {},
getHiddenColumns: (tableKey) => {
return get().hiddenColumns?.[tableKey] ?? null;
},
setHiddenColumns: (tableKey) => (columns) => {
// Update the hidden columns for the given table
set({
hiddenColumns: {
...get().hiddenColumns,
[tableKey]: columns
}
});
}
}),
{
name: 'inventree-table-state'
}
)
);

View File

@ -1,10 +1,5 @@
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { import { Box, type MantineStyleProp, Stack } from '@mantine/core';
Box,
LoadingOverlay,
type MantineStyleProp,
Stack
} from '@mantine/core';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { import {
type ContextMenuItemOptions, type ContextMenuItemOptions,
@ -35,12 +30,12 @@ import { resolveItem } from '../functions/conversion';
import { extractAvailableFields, mapFields } from '../functions/forms'; import { extractAvailableFields, mapFields } from '../functions/forms';
import { showApiErrorMessage } from '../functions/notifications'; import { showApiErrorMessage } from '../functions/notifications';
import { useLocalState } from '../states/LocalState'; import { useLocalState } from '../states/LocalState';
import { useStoredTableState } from '../states/StoredTableState';
import type { TableColumn } from './Column'; import type { TableColumn } from './Column';
import InvenTreeTableHeader from './InvenTreeTableHeader'; import InvenTreeTableHeader from './InvenTreeTableHeader';
import { type RowAction, RowActions } from './RowActions'; import { type RowAction, RowActions } from './RowActions';
const ACTIONS_COLUMN_ACCESSOR: string = '--actions--'; const ACTIONS_COLUMN_ACCESSOR: string = '--actions--';
const defaultPageSize: number = 25;
const PAGE_SIZES = [10, 15, 20, 25, 50, 100, 500]; const PAGE_SIZES = [10, 15, 20, 25, 50, 100, 500];
/** /**
@ -140,13 +135,18 @@ export function InvenTreeTable<T extends Record<string, any>>({
columns: TableColumn<T>[]; columns: TableColumn<T>[];
props: InvenTreeTableProps<T>; props: InvenTreeTableProps<T>;
}>) { }>) {
const { userTheme } = useLocalState();
const { const {
pageSize,
setPageSize,
getHiddenColumns,
setHiddenColumns,
getTableColumnNames, getTableColumnNames,
setTableColumnNames, setTableColumnNames,
getTableSorting, getTableSorting,
setTableSorting, setTableSorting
userTheme } = useStoredTableState();
} = useLocalState();
const [fieldNames, setFieldNames] = useState<Record<string, string>>({}); const [fieldNames, setFieldNames] = useState<Record<string, string>>({});
@ -191,7 +191,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
// Request OPTIONS data from the API, before we load the table // Request OPTIONS data from the API, before we load the table
const tableOptionQuery = useQuery({ const tableOptionQuery = useQuery({
enabled: !!url && !tableData && tableState.storedDataLoaded, enabled: !!url && !tableData,
queryKey: [ queryKey: [
'options', 'options',
url, url,
@ -274,30 +274,6 @@ export function InvenTreeTable<T extends Record<string, any>>({
return tableProps.enableSelection || tableProps.enableBulkDelete || false; return tableProps.enableSelection || tableProps.enableBulkDelete || false;
}, [tableProps]); }, [tableProps]);
useEffect(() => {
// On first table render, "hide" any default hidden columns
if (tableProps.enableColumnSwitching == false) {
return;
}
// A "null" value indicates that the initial "hidden" columns have not been set
if (tableState.storedDataLoaded && tableState.hiddenColumns == null) {
const columnNames: string[] = columns
.filter((col) => {
// Find any switchable columns which are hidden by default
return col.switchable != false && col.defaultVisible == false;
})
.map((col) => col.accessor);
tableState.setHiddenColumns(columnNames);
}
}, [
columns,
tableProps.enableColumnSwitching,
tableState.hiddenColumns,
tableState.storedDataLoaded
]);
// Check if any columns are switchable (can be hidden) // Check if any columns are switchable (can be hidden)
const hasSwitchableColumns: boolean = useMemo(() => { const hasSwitchableColumns: boolean = useMemo(() => {
if (props.enableColumnSwitching == false) { if (props.enableColumnSwitching == false) {
@ -327,10 +303,6 @@ export function InvenTreeTable<T extends Record<string, any>>({
const dataColumns: any = useMemo(() => { const dataColumns: any = useMemo(() => {
let cols: TableColumn[] = columns.filter((col) => col?.hidden != true); let cols: TableColumn[] = columns.filter((col) => col?.hidden != true);
if (!tableState.storedDataLoaded) {
cols = cols.filter((col) => col?.defaultVisible != false);
}
cols = cols.map((col) => { cols = cols.map((col) => {
// If the column is *not* switchable, it is always visible // If the column is *not* switchable, it is always visible
// Otherwise, check if it is "default hidden" // Otherwise, check if it is "default hidden"
@ -373,24 +345,29 @@ export function InvenTreeTable<T extends Record<string, any>>({
fieldNames, fieldNames,
tableProps.rowActions, tableProps.rowActions,
tableState.hiddenColumns, tableState.hiddenColumns,
tableState.selectedRecords, tableState.selectedRecords
tableState.storedDataLoaded
]); ]);
// Callback when column visibility is toggled // Callback when column visibility is toggled
function toggleColumn(columnName: string) { const toggleColumn = useCallback(
const newColumns = [...dataColumns]; (columnName: string) => {
const newColumns = [...dataColumns];
const colIdx = newColumns.findIndex((col) => col.accessor == columnName); const colIdx = newColumns.findIndex((col) => col.accessor == columnName);
if (colIdx >= 0 && colIdx < newColumns.length) { if (colIdx >= 0 && colIdx < newColumns.length) {
newColumns[colIdx].hidden = !newColumns[colIdx].hidden; newColumns[colIdx].hidden = !newColumns[colIdx].hidden;
} }
tableState.setHiddenColumns( const hiddenColumns = newColumns
newColumns.filter((col) => col.hidden).map((col) => col.accessor) .filter((col) => col.hidden)
); .map((col) => col.accessor);
}
tableState.setHiddenColumns(hiddenColumns);
setHiddenColumns(cacheKey)(hiddenColumns);
},
[cacheKey, dataColumns]
);
// Final state of the table columns // Final state of the table columns
const tableColumns = useDataTableColumns({ const tableColumns = useDataTableColumns({
@ -456,8 +433,6 @@ export function InvenTreeTable<T extends Record<string, any>>({
// Pagination // Pagination
if (tableProps.enablePagination && paginate) { if (tableProps.enablePagination && paginate) {
const pageSize = tableState.pageSize ?? defaultPageSize;
if (pageSize != tableState.pageSize) tableState.setPageSize(pageSize);
queryParams.limit = pageSize; queryParams.limit = pageSize;
queryParams.offset = (tableState.page - 1) * pageSize; queryParams.offset = (tableState.page - 1) * pageSize;
} }
@ -476,30 +451,44 @@ export function InvenTreeTable<T extends Record<string, any>>({
return queryParams; return queryParams;
}, },
[ [
sortStatus,
tableProps.params, tableProps.params,
tableProps.enablePagination, tableProps.enablePagination,
tableState.filterSet.activeFilters, tableState.filterSet.activeFilters,
tableState.queryFilters, tableState.queryFilters,
tableState.searchTerm, tableState.searchTerm,
tableState.pageSize,
tableState.setPageSize,
sortStatus,
getOrderingTerm getOrderingTerm
] ]
); );
const [sortingLoaded, setSortingLoaded] = useState<boolean>(false); const [cacheLoaded, setCacheLoaded] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
const tableKey: string = tableState.tableKey.split('-')[0]; const sorting: DataTableSortStatus = getTableSorting(cacheKey);
const sorting: DataTableSortStatus = getTableSorting(tableKey);
if (sorting && !!sorting.columnAccessor && !!sorting.direction) { if (sorting && !!sorting.columnAccessor && !!sorting.direction) {
setSortStatus(sorting); setSortStatus(sorting);
} }
setSortingLoaded(true); const hiddenColumns = getHiddenColumns(cacheKey);
}, []);
if (hiddenColumns == null) {
// A "null" value indicates that the initial "hidden" columns have not been set
const columnNames: string[] = columns
.filter((col) => {
// Find any switchable columns which are hidden by default
return col.switchable != false && col.defaultVisible == false;
})
.map((col) => col.accessor);
setHiddenColumns(cacheKey)(columnNames);
tableState.setHiddenColumns(columnNames);
} else {
tableState.setHiddenColumns(hiddenColumns);
}
setCacheLoaded(true);
}, [cacheKey]);
// Return the ordering parameter // Return the ordering parameter
function getOrderingTerm() { function getOrderingTerm() {
@ -521,13 +510,15 @@ export function InvenTreeTable<T extends Record<string, any>>({
tableProps.noRecordsText ?? t`No records found` tableProps.noRecordsText ?? t`No records found`
); );
const handleSortStatusChange = (status: DataTableSortStatus<T>) => { const handleSortStatusChange = useCallback(
tableState.setPage(1); (status: DataTableSortStatus<T>) => {
setSortStatus(status); tableState.setPage(1);
setSortStatus(status);
const tableKey = tableState.tableKey.split('-')[0]; setTableSorting(cacheKey)(status);
setTableSorting(tableKey)(status); },
}; [cacheKey]
);
// Function to perform API query to fetch required data // Function to perform API query to fetch required data
const fetchTableData = async () => { const fetchTableData = async () => {
@ -538,16 +529,11 @@ export function InvenTreeTable<T extends Record<string, any>>({
return []; return [];
} }
if (!sortingLoaded) { if (!cacheLoaded) {
// Sorting not yet loaded - do not load! // Sorting not yet loaded - do not load!
return []; return [];
} }
if (!tableState.storedDataLoaded) {
// Table data not yet loaded - do not load!
return [];
}
return api return api
.get(url, { .get(url, {
params: queryParams, params: queryParams,
@ -581,15 +567,13 @@ export function InvenTreeTable<T extends Record<string, any>>({
queryKey: [ queryKey: [
'tabledata', 'tabledata',
url, url,
tableState.tableKey,
tableState.page, tableState.page,
props.params, props.params,
sortingLoaded, cacheLoaded,
sortStatus.columnAccessor, sortStatus.columnAccessor,
sortStatus.direction, sortStatus.direction,
tableState.tableKey, tableState.tableKey,
tableState.filterSet.activeFilters, tableState.filterSet.activeFilters,
tableState.storedDataLoaded,
tableState.searchTerm tableState.searchTerm
], ],
retry: 5, retry: 5,
@ -602,7 +586,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
return true; return true;
}, },
enabled: !!url && !tableData && tableState.storedDataLoaded, enabled: !!url && !tableData,
queryFn: fetchTableData queryFn: fetchTableData
}); });
@ -629,16 +613,17 @@ export function InvenTreeTable<T extends Record<string, any>>({
tableState.isLoading tableState.isLoading
]); ]);
const tablePageSize = useMemo(() => {
if (tableProps.enablePagination != false) {
return pageSize;
} else {
return tableState.recordCount;
}
}, [tableProps.enablePagination, pageSize, tableState.recordCount]);
// Update tableState.records when new data received // Update tableState.records when new data received
useEffect(() => { useEffect(() => {
const data = tableData ?? apiData ?? []; tableState.setRecords(tableData ?? apiData ?? []);
tableState.setRecords(data);
// set pagesize to length if pagination is disabled
if (!tableProps.enablePagination) {
tableState.setPageSize(data?.length ?? defaultPageSize);
}
}, [tableData, apiData]); }, [tableData, apiData]);
// Callback when a cell is clicked // Callback when a cell is clicked
@ -735,12 +720,12 @@ export function InvenTreeTable<T extends Record<string, any>>({
return showContextMenu(items)(event); return showContextMenu(items)(event);
}; };
// pagination refresh table if pageSize changes // Pagination refresh table if pageSize changes
function updatePageSize(newData: number) { const updatePageSize = useCallback((size: number) => {
tableState.setPageSize(newData); setPageSize(size);
tableState.setPage(1); tableState.setPage(1);
tableState.refreshTable(); tableState.refreshTable();
} }, []);
/** /**
* Memoize row expansion options: * Memoize row expansion options:
@ -776,7 +761,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
_params = { _params = {
..._params, ..._params,
totalRecords: tableState.recordCount, totalRecords: tableState.recordCount,
recordsPerPage: tableState.pageSize, recordsPerPage: tablePageSize,
page: tableState.page, page: tableState.page,
onPageChange: tableState.setPage, onPageChange: tableState.setPage,
recordsPerPageOptions: PAGE_SIZES, recordsPerPageOptions: PAGE_SIZES,
@ -786,9 +771,9 @@ export function InvenTreeTable<T extends Record<string, any>>({
return _params; return _params;
}, [ }, [
tablePageSize,
tableProps.enablePagination, tableProps.enablePagination,
tableState.recordCount, tableState.recordCount,
tableState.pageSize,
tableState.page, tableState.page,
tableState.setPage, tableState.setPage,
updatePageSize updatePageSize
@ -806,7 +791,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
<> <>
<Stack gap='xs'> <Stack gap='xs'>
{!tableProps.noHeader && ( {!tableProps.noHeader && (
<Boundary label={`InvenTreeTableHeader-${tableState.tableKey}`}> <Boundary label={`InvenTreeTableHeader-${cacheKey}`}>
<InvenTreeTableHeader <InvenTreeTableHeader
tableUrl={url} tableUrl={url}
tableState={tableState} tableState={tableState}
@ -818,9 +803,8 @@ export function InvenTreeTable<T extends Record<string, any>>({
/> />
</Boundary> </Boundary>
)} )}
<Boundary label={`InvenTreeTable-${tableState.tableKey}`}> <Boundary label={`InvenTreeTable-${cacheKey}`}>
<Box pos='relative'> <Box pos='relative'>
<LoadingOverlay visible={!tableState.storedDataLoaded} />
<DataTable <DataTable
withTableBorder={!tableProps.noHeader} withTableBorder={!tableProps.noHeader}
withColumnBorders withColumnBorders