Skip to content

Commit

Permalink
Merge branch 'main' into entity-page-title
Browse files Browse the repository at this point in the history
  • Loading branch information
Raubzeug authored Aug 16, 2024
2 parents 15d0879 + 2519c80 commit a4f5941
Show file tree
Hide file tree
Showing 21 changed files with 749 additions and 486 deletions.
6 changes: 5 additions & 1 deletion src/components/EntitiesCount/EntitiesCount.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import {Label} from '@gravity-ui/uikit';

import {cn} from '../../utils/cn';

import i18n from './i18n';

const b = cn('ydb-entities-count');

interface EntitiesCountProps {
current: number | string;
total?: number | string;
Expand All @@ -28,7 +32,7 @@ export const EntitiesCount = ({total, current, label, loading, className}: Entit
}

return (
<Label theme="info" size="m" className={className}>
<Label theme="info" size="m" className={b(null, className)}>
{content}
</Label>
);
Expand Down
247 changes: 56 additions & 191 deletions src/components/PaginatedTable/PaginatedTable.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,19 @@
import React from 'react';

import type {IResponseError} from '../../types/api/error';
import {getArray} from '../../utils';
import {ResponseError} from '../Errors/ResponseError';
import {TableWithControlsLayout} from '../TableWithControlsLayout/TableWithControlsLayout';

import {TableChunk} from './TableChunk';
import {TableHead} from './TableHead';
import {EmptyTableRow} from './TableRow';
import {DEFAULT_REQUEST_TIMEOUT, DEFAULT_TABLE_ROW_HEIGHT} from './constants';
import {DEFAULT_TABLE_ROW_HEIGHT} from './constants';
import i18n from './i18n';
import {
createPaginatedTableReducer,
initChunk,
removeChunk,
resetChunks,
setChunkData,
setChunkError,
setChunkLoading,
} from './reducer';
import {b} from './shared';
import type {
Column,
FetchData,
GetRowClassName,
HandleTableColumnsResize,
OnEntry,
OnLeave,
OnSort,
RenderControls,
RenderEmptyDataMessage,
RenderErrorMessage,
Expand All @@ -37,200 +23,81 @@ import {useIntersectionObserver} from './useIntersectionObserver';

import './PaginatedTable.scss';

export interface PaginatedTableProps<T> {
export interface PaginatedTableProps<T, F> {
limit: number;
fetchData: FetchData<T>;
fetchData: FetchData<T, F>;
filters?: F;
tableName: string;
columns: Column<T>[];
getRowClassName?: GetRowClassName<T>;
rowHeight?: number;
parentContainer?: Element | null;
initialSortParams?: SortParams;
onColumnsResize?: HandleTableColumnsResize;

renderControls?: RenderControls;
renderEmptyDataMessage?: RenderEmptyDataMessage;
renderErrorMessage?: RenderErrorMessage;

dependencyArray?: unknown[]; // Fully reload table on params change
}

export const PaginatedTable = <T,>({
export const PaginatedTable = <T, F>({
limit,
fetchData,
filters,
tableName,
columns,
getRowClassName,
rowHeight = DEFAULT_TABLE_ROW_HEIGHT,
parentContainer,
initialSortParams,
onColumnsResize,
renderControls,
renderEmptyDataMessage,
renderErrorMessage,
dependencyArray,
}: PaginatedTableProps<T>) => {
const inited = React.useRef(false);
const tableContainer = React.useRef<HTMLDivElement>(null);

const [state, dispatch] = React.useReducer(createPaginatedTableReducer<T>(), {});

renderEmptyDataMessage,
}: PaginatedTableProps<T, F>) => {
const [sortParams, setSortParams] = React.useState<SortParams | undefined>(initialSortParams);

const [totalEntities, setTotalEntities] = React.useState(limit);
const [foundEntities, setFoundEntities] = React.useState(0);
const [activeChunks, setActiveChunks] = React.useState<number[]>([]);
const [isInitialLoad, setIsInitialLoad] = React.useState(true);

const [error, setError] = React.useState<IResponseError>();

const pendingRequests = React.useRef<Record<string, ReturnType<typeof setTimeout>>>({});

const fetchChunkData = React.useCallback(
async (id: string) => {
dispatch(setChunkLoading(id));

const timer = setTimeout(async () => {
const offset = Number(id) * limit;

try {
const response = await fetchData(limit, offset, sortParams);
const {data, total, found} = response;

setTotalEntities(total);
setFoundEntities(found);
inited.current = true;

dispatch(setChunkData(id, data));
} catch (err) {
// Do not set error on cancelled requests
if ((err as IResponseError)?.isCancelled) {
return;
}

dispatch(setChunkError(id, err as IResponseError));
setError(err as IResponseError);
}
}, DEFAULT_REQUEST_TIMEOUT);

// Chunk data load could be triggered by different events
// Cancel previous chunk request, while it is pending (instead of concurrentId)
if (pendingRequests.current[id]) {
const oldTimer = pendingRequests.current[id];
window.clearTimeout(oldTimer);
}
pendingRequests.current[id] = timer;
},
[fetchData, limit, sortParams],
);
const tableContainer = React.useRef<HTMLDivElement>(null);

const onEntry = React.useCallback<OnEntry>((id) => {
dispatch(initChunk(id));
const handleDataFetched = React.useCallback((total: number, found: number) => {
setTotalEntities(total);
setFoundEntities(found);
setIsInitialLoad(false);
}, []);

const onLeave = React.useCallback<OnLeave>((id) => {
dispatch(removeChunk(id));

// If there is a pending request for the removed chunk, cancel it
// It made to prevent excessive requests on fast scroll
if (pendingRequests.current[id]) {
const timer = pendingRequests.current[id];
window.clearTimeout(timer);
delete pendingRequests.current[id];
}
const onEntry = React.useCallback((id: string) => {
setActiveChunks((prev) => [...new Set([...prev, Number(id)])]);
}, []);

// Cancel all pending requests on component unmount
React.useEffect(() => {
return () => {
Object.values(pendingRequests.current).forEach((timer) => {
window.clearTimeout(timer);
});
pendingRequests.current = {};
};
const onLeave = React.useCallback((id: string) => {
setActiveChunks((prev) => prev.filter((chunk) => chunk !== Number(id)));
}, []);

// Load chunks if they become active
// This mecanism helps to set chunk active state from different sources, but load data only once
// Only currently active chunks should be in state so iteration by the whole state shouldn't be a problem
React.useEffect(() => {
for (const id of Object.keys(state)) {
const chunk = state[Number(id)];

if (chunk?.active && !chunk?.loading && !chunk?.wasLoaded) {
fetchChunkData(id);
}
}
}, [fetchChunkData, state]);
const observer = useIntersectionObserver({onEntry, onLeave, parentContainer});

// Reset table on filters change
React.useEffect(() => {
// Reset counts, so table unmount unneeded chunks
// reset table on filters change
React.useLayoutEffect(() => {
setTotalEntities(limit);
setFoundEntities(0);
setError(undefined);

// Remove all chunks from state
dispatch(resetChunks());

// Reset table state for the controls
inited.current = false;

// If there is a parent, scroll to parent container ref
// Else scroll to table top
// It helps to prevent layout shifts, when chunks quantity is changed
setIsInitialLoad(true);
if (parentContainer) {
parentContainer.scrollTo(0, 0);
} else {
tableContainer.current?.scrollTo(0, 0);
}

// Make table start to load data
dispatch(initChunk('0'));
}, [dependencyArray, limit, parentContainer]);

// Reload currently active chunks
// Use case - sort params change, so data should be updated, but without chunks unmount
const reloadCurrentViewport = () => {
for (const id of Object.keys(state)) {
if (state[Number(id)]?.active) {
dispatch(initChunk(id));
}
}
};

const handleSort: OnSort = (params) => {
setSortParams(params);
reloadCurrentViewport();
};

const observer = useIntersectionObserver({onEntry, onLeave, parentContainer});

// Render at least 1 chunk
const totalLength = foundEntities || limit;
const chunksCount = Math.ceil(totalLength / limit);
setActiveChunks([0]);
}, [filters, limit, parentContainer]);

const renderChunks = () => {
if (!observer) {
return null;
}

return getArray(chunksCount).map((value) => {
const chunkData = state[value];

return (
<TableChunk
observer={observer}
key={value}
id={value}
chunkSize={limit}
rowHeight={rowHeight}
columns={columns}
chunkData={chunkData}
getRowClassName={getRowClassName}
/>
);
});
};

const renderData = () => {
if (inited.current && foundEntities === 0) {
if (!isInitialLoad && foundEntities === 0) {
return (
<tbody>
<EmptyTableRow columns={columns}>
Expand All @@ -240,44 +107,42 @@ export const PaginatedTable = <T,>({
);
}

// If first chunk is loaded with the error, display error
// In case of other chunks table will be inited
if (!inited.current && error) {
return (
<tbody>
<EmptyTableRow columns={columns}>
{renderErrorMessage ? (
renderErrorMessage(error)
) : (
<ResponseError error={error} />
)}
</EmptyTableRow>
</tbody>
);
}

return renderChunks();
const totalLength = foundEntities || limit;
const chunksCount = Math.ceil(totalLength / limit);

return getArray(chunksCount).map((value) => (
<TableChunk<T, F>
key={value}
id={value}
limit={limit}
rowHeight={rowHeight}
columns={columns}
fetchData={fetchData}
filters={filters}
tableName={tableName}
sortParams={sortParams}
getRowClassName={getRowClassName}
renderErrorMessage={renderErrorMessage}
onDataFetched={handleDataFetched}
isActive={activeChunks.includes(value)}
observer={observer}
/>
));
};

const renderTable = () => {
return (
<table className={b('table')}>
<TableHead
columns={columns}
onSort={handleSort}
onColumnsResize={onColumnsResize}
/>
{renderData()}
</table>
);
};
const renderTable = () => (
<table className={b('table')}>
<TableHead columns={columns} onSort={setSortParams} onColumnsResize={onColumnsResize} />
{renderChunks()}
</table>
);

const renderContent = () => {
if (renderControls) {
return (
<TableWithControlsLayout>
<TableWithControlsLayout.Controls>
{renderControls({inited: inited.current, totalEntities, foundEntities})}
{renderControls({inited: !isInitialLoad, totalEntities, foundEntities})}
</TableWithControlsLayout.Controls>
<TableWithControlsLayout.Table>{renderTable()}</TableWithControlsLayout.Table>
</TableWithControlsLayout>
Expand Down
7 changes: 4 additions & 3 deletions src/components/PaginatedTable/ResizeablePaginatedTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ function updateColumnsWidth<T>(columns: Column<T>[], columnsWidthSetup: ColumnWi
});
}

interface ResizeablePaginatedTableProps<T> extends Omit<PaginatedTableProps<T>, 'onColumnsResize'> {
interface ResizeablePaginatedTableProps<T, F>
extends Omit<PaginatedTableProps<T, F>, 'onColumnsResize'> {
columnsWidthLSKey: string;
}

export function ResizeablePaginatedTable<T>({
export function ResizeablePaginatedTable<T, F>({
columnsWidthLSKey,
columns,
...props
}: ResizeablePaginatedTableProps<T>) {
}: ResizeablePaginatedTableProps<T, F>) {
const [tableColumnsWidth, setTableColumnsWidth] = useTableResize(columnsWidthLSKey);

const updatedColumns = updateColumnsWidth(columns, tableColumnsWidth);
Expand Down
Loading

0 comments on commit a4f5941

Please sign in to comment.