mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-06 13:10:40 +01:00
* fix(tanstack-table): reset prev column id ref when changing storageKey Since all tables we have some far unmounts enterely we didn't notice this bug until I refactor the infrastructure monitoring to be shared and dynamic via config Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=090fcc6bcd198251bf3c883f842d8798&p=3a5fcc6bcd19800e92a3dd70bb3a08fd&pm=s * refactor(infrastructure-monitoring): remove cellvalue tooltip component Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd19803581aff083c0a65717&pm=s * feat(k8s-base-details): add copy link Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd19802aa8e4cc69b88d1fce&pm=s * refactor(hosts): move disk usage closer to cpu/memory usage Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd1980b292facdfb92b95fa0&pm=s * refactor(k8s-base-details): change wording to go to explorer, and move explorer to right Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd1980f19e61fedee47a5d5b&pm=s * fix(k8s-base): use text-wrap instead of forcing line break To be able to collapse line to one line on bigger screens Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd19808a8a30ed9ddbfa029a&pm=s * feat(tanstack-table): add support to reset scroll Without this prop, infra monitoring will keep the same scroll on horizontal when changing category * fix(infrastructure-monitoring): reset horizontal scroll on change category To simulate, just move horizontal scroll to right and then change category that can have horizontal scroll as well * refactor(k8s-list): reorganize filters Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd19809a9a7fca0138558551&pm=s * refactor(infra-monitoring): reorganize table options Add support to specify font size, max lines per row, and the columns Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd1980cfadcdeb2f590c3dc3&pm=s * fix(infra-monitoring): add delay to show hover & do not close on click to copy Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd19801c8ae8c6283357021b&pm=s * refactor(infra-monitoring): handle no data in a consistent way With opacity: 0.6 as recommended by design team * refactor(infra-monitoring): move details drawer to signozhq/ui Few problems with integration but I'm planning to fix it later on our component lib Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd1980babfc2f29686e86d91&pm=s * fix(infra-monitoring-details-logs): disable group by/replace filter actions This is so deep in the prop that is better to disable this than trying to support part of this (replace filter) Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd1980d59ba1dd7d1658e9b3&pm=s * fix(infra-monitoring-details-logs): explorer action inherit filters On logs, if you add custom logs and then click in a specific log to see the details, and you click in the go to explorer, it will inherit filters you had added earlier Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd1980a68bedfd8bbc17e137&pm=s * fix(infra-monitoring-logs): scroll not working & search not allowing type This issue is caused because radix lock the scroll for any component not under the drawer. This also fixed the issue with search, not sure why this was related Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd1980648061f29a990712dd&pm=s Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd19805d9404d3d0f78f2c75&pm=s * fix(infra-monitoring-logs): issue with double scroll Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd198099b764f8e03bc20ac1&pm=s * refactor(infra-monitoring): remove more usages of antd components * refactor(infra-monitoring): use toggle to hide columns This is a similar pattern we have on dashboards Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd198000b535ca511e4e8c97&pm=s * fix(infra-monitoring): do not refetch when click in the row To prevent doing two requests when data in memory is already enough * fix(infra-monitoring): docs path Ensure all the docs path goes to correct column/chart docs Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd19809581f3d96dd6ecdeab&pm=s Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd19809d9c2bf2ec41a4c92b&pm=s * chore(volumes): fix docs path for charts * feat(infra-monitoring-v2-events): add event for page visited * feat(infra-monitoring-v2-events): add event for filter customized * feat(infra-monitoring-v2-events): add event for time range customized * feat(infra-monitoring-v2-events): add event for column customized * feat(infra-monitoring-v2-events): add event for column sorted * feat(infra-monitoring-v2-events): add event for drawer time range customized * feat(infra-monitoring-v2-events): add event for group by change * feat(infra-monitoring-v2-events): add event for filter customized * feat(infra-monitoring-v2-events): add event for tab viewed * feat(infra-monitoring-v2-events): add event for explorer visited * fix(infrastructure-monitoring-v2): migrate tooltip to be from tanstack * fix(k8s-expanded-row): add missing pre-cache when row is expanded * fix(infra-monitoring-v2): copy ID via react-use after hook removal frontend/src/hooks/useCopyToClipboard.ts was deleted on main (#12333) in favour of react-use's execCommand-based copy, leaving this import dangling and breaking tsc and the frontend build. --------- Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
699 lines
18 KiB
TypeScript
699 lines
18 KiB
TypeScript
import type { ComponentProps, CSSProperties } from 'react';
|
|
import {
|
|
forwardRef,
|
|
memo,
|
|
useCallback,
|
|
useEffect,
|
|
useImperativeHandle,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useRef,
|
|
} from 'react';
|
|
import type { TableComponents } from 'react-virtuoso';
|
|
import { TableVirtuoso, TableVirtuosoHandle } from 'react-virtuoso';
|
|
import { Loader } from '@signozhq/icons';
|
|
import { DndContext, pointerWithin } from '@dnd-kit/core';
|
|
import {
|
|
horizontalListSortingStrategy,
|
|
SortableContext,
|
|
} from '@dnd-kit/sortable';
|
|
import { ComboboxSimple } from '@signozhq/ui/combobox';
|
|
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
|
import { Pagination } from '@signozhq/ui/pagination';
|
|
import type { Row } from '@tanstack/react-table';
|
|
import {
|
|
ColumnDef,
|
|
ColumnPinningState,
|
|
getCoreRowModel,
|
|
useReactTable,
|
|
} from '@tanstack/react-table';
|
|
import { Spin } from 'antd';
|
|
import cx from 'classnames';
|
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
|
|
|
import TanStackCustomTableRow from './TanStackCustomTableRow';
|
|
import TanStackHeaderRow from './TanStackHeaderRow';
|
|
import {
|
|
ColumnVisibilitySync,
|
|
TableLoadingSync,
|
|
TanStackTableStateProvider,
|
|
} from './TanStackTableStateContext';
|
|
import {
|
|
FlatItem,
|
|
SortState,
|
|
TableRowContext,
|
|
TanStackTableHandle,
|
|
TanStackTableProps,
|
|
} from './types';
|
|
import { chromePerformanceMeasureTanstackTable } from './perfDevtools';
|
|
import { useColumnDnd } from './useColumnDnd';
|
|
import { useColumnHandlers } from './useColumnHandlers';
|
|
import { useColumnState } from './useColumnState';
|
|
import { useEffectiveData } from './useEffectiveData';
|
|
import { useFlatItems } from './useFlatItems';
|
|
import { useResetScroll } from './useResetScroll';
|
|
import { useRowKeyData } from './useRowKeyData';
|
|
import { useTableParams } from './useTableParams';
|
|
import { buildPageSizeItems, buildTanstackColumnDef } from './utils';
|
|
import { VirtuosoTableColGroup } from './VirtuosoTableColGroup';
|
|
|
|
import tableStyles from './TanStackTable.module.scss';
|
|
import viewStyles from './TanStackTableView.module.scss';
|
|
import { chromePerformanceNow } from 'lib/chromePerformanceDevTools';
|
|
|
|
const COLUMN_DND_AUTO_SCROLL = {
|
|
layoutShiftCompensation: false as const,
|
|
threshold: { x: 0.2, y: 0 },
|
|
};
|
|
|
|
const INCREASE_VIEWPORT_BY = { top: 500, bottom: 500 };
|
|
|
|
const noopColumnVisibility = (): void => {};
|
|
|
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
function TanStackTableInner<TData, TItemKey = string>(
|
|
{
|
|
data,
|
|
columns,
|
|
columnStorageKey,
|
|
respectColumnOrder = true,
|
|
columnSizing: columnSizingProp,
|
|
onColumnSizingChange,
|
|
onColumnOrderChange,
|
|
onColumnRemove,
|
|
isLoading = false,
|
|
skeletonRowCount = 10,
|
|
enableQueryParams,
|
|
pagination,
|
|
paginationClassname,
|
|
onEndReached,
|
|
getRowKey,
|
|
getItemKey,
|
|
groupBy,
|
|
getGroupKey,
|
|
getRowStyle,
|
|
getRowClassName,
|
|
getRowTestId,
|
|
isRowActive,
|
|
renderRowActions,
|
|
onRowClick,
|
|
onRowClickNewTab,
|
|
onSort,
|
|
activeRowIndex,
|
|
renderExpandedRow,
|
|
getRowCanExpand,
|
|
tableScrollerProps,
|
|
plainTextCellLineClamp,
|
|
cellTypographySize,
|
|
className,
|
|
testId,
|
|
prefixPaginationContent,
|
|
suffixPaginationContent,
|
|
enableAlternatingRowColors,
|
|
disableVirtualScroll,
|
|
resetScrollKey,
|
|
}: TanStackTableProps<TData, TItemKey>,
|
|
forwardedRef: React.ForwardedRef<TanStackTableHandle>,
|
|
): JSX.Element {
|
|
if (disableVirtualScroll && onEndReached) {
|
|
throw new Error(
|
|
'TanStackTable: Cannot use onEndReached with disableVirtualScroll. Infinite scroll requires virtualization.',
|
|
);
|
|
}
|
|
|
|
const renderStart = chromePerformanceNow();
|
|
|
|
const virtuosoRef = useRef<TableVirtuosoHandle | null>(null);
|
|
const isDarkMode = useIsDarkMode();
|
|
|
|
const {
|
|
page,
|
|
limit,
|
|
setPage: internalSetPage,
|
|
setLimit: internalSetLimit,
|
|
orderBy,
|
|
setOrderBy: internalSetOrderBy,
|
|
expanded,
|
|
setExpanded,
|
|
} = useTableParams(enableQueryParams, {
|
|
page: pagination?.defaultPage,
|
|
limit: pagination?.defaultLimit ?? pagination?.calculatedPageSize ?? 10,
|
|
});
|
|
|
|
const pageSizeItems = useMemo(
|
|
() => buildPageSizeItems(pagination?.calculatedPageSize),
|
|
[pagination?.calculatedPageSize],
|
|
);
|
|
|
|
const setOrderBy = useCallback(
|
|
(sort: SortState | null) => {
|
|
internalSetOrderBy(sort);
|
|
onSort?.(sort);
|
|
},
|
|
[internalSetOrderBy, onSort],
|
|
);
|
|
|
|
const setPage = useCallback(
|
|
(p: number) => {
|
|
internalSetPage(p);
|
|
pagination?.onPageChange?.(p);
|
|
},
|
|
[internalSetPage, pagination],
|
|
);
|
|
|
|
const setLimit = useCallback(
|
|
(l: number) => {
|
|
internalSetLimit(l);
|
|
internalSetPage(1);
|
|
pagination?.onLimitChange?.(l);
|
|
},
|
|
[internalSetLimit, internalSetPage, pagination],
|
|
);
|
|
|
|
const isGrouped = (groupBy?.length ?? 0) > 0;
|
|
|
|
const {
|
|
columnVisibility: storeVisibility,
|
|
columnSizing: storeSizing,
|
|
sortedColumns,
|
|
hideColumn,
|
|
setColumnSizing: storeSetSizing,
|
|
setColumnOrder: storeSetOrder,
|
|
} = useColumnState({
|
|
storageKey: columnStorageKey,
|
|
columns,
|
|
isGrouped,
|
|
respectColumnOrder,
|
|
});
|
|
|
|
// Use store values when columnStorageKey is provided, otherwise fall back to props/defaults
|
|
const effectiveColumns = columnStorageKey ? sortedColumns : columns;
|
|
const effectiveVisibility = columnStorageKey ? storeVisibility : {};
|
|
const effectiveSizing = columnStorageKey
|
|
? storeSizing
|
|
: (columnSizingProp ?? {});
|
|
|
|
const effectiveData = useEffectiveData<TData>({
|
|
data,
|
|
isLoading,
|
|
limit,
|
|
skeletonRowCount,
|
|
});
|
|
|
|
const { rowKeyData, getRowKeyData } = useRowKeyData<TData, TItemKey>({
|
|
data: effectiveData,
|
|
isLoading,
|
|
getRowKey,
|
|
getItemKey,
|
|
groupBy,
|
|
getGroupKey,
|
|
});
|
|
|
|
const {
|
|
handleColumnSizingChange,
|
|
handleColumnOrderChange,
|
|
handleRemoveColumn,
|
|
} = useColumnHandlers({
|
|
columnStorageKey,
|
|
respectColumnOrder,
|
|
effectiveSizing,
|
|
storeSetSizing,
|
|
storeSetOrder,
|
|
hideColumn,
|
|
onColumnSizingChange,
|
|
onColumnOrderChange,
|
|
onColumnRemove,
|
|
});
|
|
|
|
const columnPinning = useMemo<ColumnPinningState>(
|
|
() => ({
|
|
left: effectiveColumns.filter((c) => c.pin === 'left').map((c) => c.id),
|
|
right: effectiveColumns.filter((c) => c.pin === 'right').map((c) => c.id),
|
|
}),
|
|
[effectiveColumns],
|
|
);
|
|
|
|
const tanstackColumns = useMemo<ColumnDef<TData>[]>(
|
|
() =>
|
|
effectiveColumns.map((colDef) =>
|
|
buildTanstackColumnDef<TData, TItemKey>(colDef, isRowActive, getRowKeyData),
|
|
),
|
|
[effectiveColumns, isRowActive, getRowKeyData],
|
|
);
|
|
|
|
const getRowId = useCallback(
|
|
(row: TData, index: number): string => {
|
|
if (rowKeyData) {
|
|
return rowKeyData[index]?.finalKey ?? String(index);
|
|
}
|
|
const r = row as Record<string, unknown>;
|
|
if (r != null && typeof r.id !== 'undefined') {
|
|
return String(r.id);
|
|
}
|
|
return String(index);
|
|
},
|
|
[rowKeyData],
|
|
);
|
|
|
|
const tableGetRowCanExpand = useCallback(
|
|
(row: Row<TData>): boolean =>
|
|
getRowCanExpand ? getRowCanExpand(row.original) : true,
|
|
[getRowCanExpand],
|
|
);
|
|
|
|
const isExpandEnabled = Boolean(renderExpandedRow);
|
|
useEffect(() => {
|
|
const hasExpanded =
|
|
typeof expanded === 'boolean' ? expanded : Object.keys(expanded).length > 0;
|
|
if (!isExpandEnabled && hasExpanded) {
|
|
setExpanded({});
|
|
}
|
|
}, [isExpandEnabled, expanded, setExpanded]);
|
|
|
|
const table = useReactTable({
|
|
data: effectiveData,
|
|
columns: tanstackColumns,
|
|
enableColumnResizing: true,
|
|
enableColumnPinning: true,
|
|
columnResizeMode: 'onChange',
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getRowId,
|
|
enableExpanding: isExpandEnabled,
|
|
getRowCanExpand: renderExpandedRow ? tableGetRowCanExpand : undefined,
|
|
onColumnSizingChange: handleColumnSizingChange,
|
|
onColumnVisibilityChange: noopColumnVisibility,
|
|
onExpandedChange: setExpanded,
|
|
state: {
|
|
columnSizing: effectiveSizing,
|
|
columnVisibility: effectiveVisibility,
|
|
columnPinning,
|
|
expanded,
|
|
},
|
|
});
|
|
|
|
// Keep refs to avoid recreating virtuosoComponents on every resize/render
|
|
const tableRef = useRef(table);
|
|
tableRef.current = table;
|
|
const columnsRef = useRef(effectiveColumns);
|
|
columnsRef.current = effectiveColumns;
|
|
|
|
const tableRows = table.getRowModel().rows;
|
|
|
|
const { flatItems, flatIndexForActiveRow } = useFlatItems({
|
|
tableRows,
|
|
renderExpandedRow,
|
|
expanded,
|
|
activeRowIndex,
|
|
});
|
|
|
|
// keep previous count just to avoid flashing the pagination component
|
|
const prevTotalCountRef = useRef(pagination?.total || 0);
|
|
if (pagination?.total && pagination?.total > 0) {
|
|
prevTotalCountRef.current = pagination?.total;
|
|
}
|
|
const effectiveTotalCount = !isLoading
|
|
? pagination?.total || 0
|
|
: prevTotalCountRef.current;
|
|
|
|
useEffect(() => {
|
|
if (flatIndexForActiveRow < 0) {
|
|
return;
|
|
}
|
|
virtuosoRef.current?.scrollToIndex({
|
|
index: flatIndexForActiveRow,
|
|
align: 'center',
|
|
behavior: 'auto',
|
|
});
|
|
}, [flatIndexForActiveRow]);
|
|
|
|
useResetScroll(virtuosoRef, resetScrollKey);
|
|
|
|
const { sensors, columnIds, handleDragEnd } = useColumnDnd({
|
|
columns: effectiveColumns,
|
|
onColumnOrderChange: handleColumnOrderChange,
|
|
});
|
|
|
|
const hasSingleColumn = useMemo(
|
|
() => effectiveColumns.filter((c) => !c.pin).length <= 1,
|
|
[effectiveColumns],
|
|
);
|
|
|
|
const canRemoveColumn = !hasSingleColumn;
|
|
|
|
const flatHeaders = useMemo(
|
|
() => table.getFlatHeaders().filter((header) => !header.isPlaceholder),
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[tanstackColumns, columnPinning, effectiveVisibility],
|
|
);
|
|
|
|
const columnsById = useMemo(
|
|
() => new Map(effectiveColumns.map((c) => [c.id, c] as const)),
|
|
[effectiveColumns],
|
|
);
|
|
|
|
const visibleColumnsCount = table.getVisibleFlatColumns().length;
|
|
|
|
useLayoutEffect(() => {
|
|
chromePerformanceMeasureTanstackTable('Table render', renderStart, {
|
|
track: 'Table render',
|
|
color: 'primary',
|
|
tooltipText: 'TanStackTable render + commit',
|
|
properties: [
|
|
['rows', String(flatItems.length)],
|
|
['columns', String(visibleColumnsCount)],
|
|
['loading', String(isLoading)],
|
|
],
|
|
});
|
|
});
|
|
|
|
const columnOrderKey = useMemo(() => columnIds.join(','), [columnIds]);
|
|
const columnVisibilityKey = useMemo(
|
|
() =>
|
|
table
|
|
.getVisibleFlatColumns()
|
|
.map((c) => c.id)
|
|
.join(','),
|
|
// we want to explicitly have table out of this deps
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[effectiveVisibility, columnIds],
|
|
);
|
|
|
|
const virtuosoContext = useMemo<TableRowContext<TData, TItemKey>>(
|
|
() => ({
|
|
getRowStyle,
|
|
getRowClassName,
|
|
getRowTestId,
|
|
isRowActive,
|
|
renderRowActions,
|
|
onRowClick,
|
|
onRowClickNewTab,
|
|
renderExpandedRow,
|
|
getRowKeyData,
|
|
colCount: visibleColumnsCount,
|
|
isDarkMode,
|
|
plainTextCellLineClamp,
|
|
hasSingleColumn,
|
|
columnOrderKey,
|
|
columnVisibilityKey,
|
|
enableAlternatingRowColors,
|
|
}),
|
|
[
|
|
getRowStyle,
|
|
getRowClassName,
|
|
getRowTestId,
|
|
isRowActive,
|
|
renderRowActions,
|
|
onRowClick,
|
|
onRowClickNewTab,
|
|
renderExpandedRow,
|
|
getRowKeyData,
|
|
visibleColumnsCount,
|
|
isDarkMode,
|
|
plainTextCellLineClamp,
|
|
hasSingleColumn,
|
|
columnOrderKey,
|
|
columnVisibilityKey,
|
|
enableAlternatingRowColors,
|
|
],
|
|
);
|
|
|
|
const tableHeader = useCallback(() => {
|
|
return (
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={pointerWithin}
|
|
onDragEnd={handleDragEnd}
|
|
autoScroll={COLUMN_DND_AUTO_SCROLL}
|
|
>
|
|
<SortableContext items={columnIds} strategy={horizontalListSortingStrategy}>
|
|
<tr>
|
|
{flatHeaders.map((header, index) => {
|
|
const column = columnsById.get(header.id);
|
|
if (!column) {
|
|
return null;
|
|
}
|
|
return (
|
|
<TanStackHeaderRow
|
|
key={header.id}
|
|
column={column}
|
|
header={header}
|
|
isDarkMode={isDarkMode}
|
|
hasSingleColumn={hasSingleColumn}
|
|
onRemoveColumn={handleRemoveColumn}
|
|
canRemoveColumn={canRemoveColumn}
|
|
orderBy={orderBy}
|
|
onSort={setOrderBy}
|
|
isLastColumn={index === flatHeaders.length - 1}
|
|
/>
|
|
);
|
|
})}
|
|
</tr>
|
|
</SortableContext>
|
|
</DndContext>
|
|
);
|
|
}, [
|
|
sensors,
|
|
handleDragEnd,
|
|
columnIds,
|
|
flatHeaders,
|
|
columnsById,
|
|
isDarkMode,
|
|
hasSingleColumn,
|
|
handleRemoveColumn,
|
|
canRemoveColumn,
|
|
orderBy,
|
|
setOrderBy,
|
|
]);
|
|
|
|
const handleEndReached = useCallback(
|
|
(index: number): void => {
|
|
onEndReached?.(index);
|
|
},
|
|
[onEndReached],
|
|
);
|
|
|
|
const isInfiniteScrollMode = Boolean(onEndReached);
|
|
const showInfiniteScrollLoader = isInfiniteScrollMode && isLoading;
|
|
|
|
useImperativeHandle(
|
|
forwardedRef,
|
|
(): TanStackTableHandle =>
|
|
new Proxy(
|
|
{
|
|
goToPage: (p: number): void => {
|
|
setPage(p);
|
|
virtuosoRef.current?.scrollToIndex({
|
|
index: 0,
|
|
align: 'start',
|
|
});
|
|
},
|
|
} as TanStackTableHandle,
|
|
{
|
|
get(target, prop): unknown {
|
|
if (prop in target) {
|
|
return Reflect.get(target, prop);
|
|
}
|
|
const v = virtuosoRef.current as unknown as Record<string, unknown>;
|
|
const value = v?.[prop as string];
|
|
if (typeof value === 'function') {
|
|
return (value as (...a: unknown[]) => unknown).bind(virtuosoRef.current);
|
|
}
|
|
return value;
|
|
},
|
|
},
|
|
),
|
|
[setPage],
|
|
);
|
|
|
|
const showPagination = Boolean(pagination && !onEndReached);
|
|
|
|
const { className: tableScrollerClassName, ...restTableScrollerProps } =
|
|
tableScrollerProps ?? {};
|
|
|
|
const cellTypographyClass = useMemo((): string | undefined => {
|
|
if (cellTypographySize === 'small') {
|
|
return viewStyles.cellTypographySmall;
|
|
}
|
|
if (cellTypographySize === 'medium') {
|
|
return viewStyles.cellTypographyMedium;
|
|
}
|
|
if (cellTypographySize === 'large') {
|
|
return viewStyles.cellTypographyLarge;
|
|
}
|
|
return undefined;
|
|
}, [cellTypographySize]);
|
|
|
|
const virtuosoClassName = useMemo(
|
|
() =>
|
|
cx(
|
|
viewStyles.tanstackTableVirtuosoScroll,
|
|
cellTypographyClass,
|
|
tableScrollerClassName,
|
|
),
|
|
[cellTypographyClass, tableScrollerClassName],
|
|
);
|
|
|
|
const virtuosoTableStyle = useMemo(
|
|
() =>
|
|
({
|
|
'--tanstack-plain-body-line-clamp': plainTextCellLineClamp,
|
|
}) as CSSProperties,
|
|
[plainTextCellLineClamp],
|
|
);
|
|
|
|
type VirtuosoTableComponentProps = ComponentProps<
|
|
NonNullable<
|
|
TableComponents<FlatItem<TData>, TableRowContext<TData, TItemKey>>['Table']
|
|
>
|
|
>;
|
|
|
|
// Use refs in virtuosoComponents to keep the component reference stable during resize
|
|
// This prevents Virtuoso from re-rendering all rows when columns are resized
|
|
const virtuosoComponents = useMemo(
|
|
(): TableComponents<FlatItem<TData>, TableRowContext<TData, TItemKey>> => ({
|
|
Table: ({ style, children }: VirtuosoTableComponentProps): JSX.Element => (
|
|
<table className={tableStyles.tanStackTable} style={style}>
|
|
<VirtuosoTableColGroup
|
|
columns={columnsRef.current}
|
|
table={tableRef.current}
|
|
/>
|
|
{children}
|
|
</table>
|
|
),
|
|
TableRow: TanStackCustomTableRow,
|
|
}),
|
|
[],
|
|
);
|
|
|
|
return (
|
|
<div
|
|
className={cx(viewStyles.tanstackTableViewWrapper, className)}
|
|
data-has-group-by={(groupBy?.length || 0) > 0}
|
|
>
|
|
<TanStackTableStateProvider>
|
|
<TableLoadingSync
|
|
isLoading={isLoading}
|
|
isInfiniteScrollMode={isInfiniteScrollMode}
|
|
/>
|
|
<ColumnVisibilitySync visibility={effectiveVisibility} />
|
|
<TooltipProvider>
|
|
{disableVirtualScroll ? (
|
|
<div
|
|
className={virtuosoClassName}
|
|
{...restTableScrollerProps}
|
|
data-testid={testId}
|
|
>
|
|
<table className={tableStyles.tanStackTable} style={virtuosoTableStyle}>
|
|
<VirtuosoTableColGroup columns={effectiveColumns} table={table} />
|
|
<thead>{tableHeader()}</thead>
|
|
<tbody>
|
|
{(isLoading && data.length === 0
|
|
? flatItems.slice(0, skeletonRowCount)
|
|
: flatItems
|
|
).map((item, index) => (
|
|
<TanStackCustomTableRow
|
|
key={
|
|
item.kind === 'expansion' ? `${item.row.id}-expansion` : item.row.id
|
|
}
|
|
item={item}
|
|
context={virtuosoContext}
|
|
data-index={index}
|
|
data-item-index={index}
|
|
data-known-size={0}
|
|
/>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
) : (
|
|
<TableVirtuoso<FlatItem<TData>, TableRowContext<TData, TItemKey>>
|
|
className={virtuosoClassName}
|
|
ref={virtuosoRef}
|
|
{...restTableScrollerProps}
|
|
data={flatItems}
|
|
totalCount={flatItems.length}
|
|
context={virtuosoContext}
|
|
increaseViewportBy={INCREASE_VIEWPORT_BY}
|
|
initialTopMostItemIndex={
|
|
flatIndexForActiveRow >= 0 ? flatIndexForActiveRow : 0
|
|
}
|
|
fixedHeaderContent={tableHeader}
|
|
style={virtuosoTableStyle}
|
|
components={virtuosoComponents}
|
|
endReached={onEndReached ? handleEndReached : undefined}
|
|
data-testid={testId}
|
|
/>
|
|
)}
|
|
{showInfiniteScrollLoader && (
|
|
<div
|
|
className={viewStyles.tanstackLoadingOverlay}
|
|
data-testid="tanstack-infinite-loader"
|
|
>
|
|
<Spin
|
|
indicator={<Loader className="animate-spin" />}
|
|
tip="Loading more..."
|
|
/>
|
|
</div>
|
|
)}
|
|
{showPagination && pagination && (
|
|
<div className={cx(viewStyles.paginationContainer, paginationClassname)}>
|
|
{prefixPaginationContent}
|
|
{pagination.showTotalCount && effectiveTotalCount > 0 && (
|
|
<span
|
|
className={viewStyles.paginationTotalCount}
|
|
data-testid="pagination-total-count"
|
|
>
|
|
Showing {(page - 1) * limit + 1} -{' '}
|
|
{Math.min(page * limit, effectiveTotalCount)} of {effectiveTotalCount}
|
|
{pagination.totalCountLabel ? ` ${pagination.totalCountLabel}` : ''}
|
|
</span>
|
|
)}
|
|
<Pagination
|
|
current={page}
|
|
pageSize={limit}
|
|
total={effectiveTotalCount}
|
|
onPageChange={(p): void => {
|
|
setPage(p);
|
|
pagination.onPageChange?.(p);
|
|
}}
|
|
/>
|
|
{pagination.showPageSize !== false && (
|
|
<div className={viewStyles.paginationPageSize}>
|
|
<ComboboxSimple
|
|
testId="pagination-page-size"
|
|
value={limit?.toString()}
|
|
defaultValue="10"
|
|
onChange={(value): void => {
|
|
setLimit(+value);
|
|
pagination.onLimitChange?.(+value);
|
|
if (page !== 1) {
|
|
setPage(1);
|
|
pagination.onPageChange?.(1);
|
|
}
|
|
}}
|
|
items={pageSizeItems}
|
|
/>
|
|
</div>
|
|
)}
|
|
{suffixPaginationContent}
|
|
</div>
|
|
)}
|
|
</TooltipProvider>
|
|
</TanStackTableStateProvider>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const TanStackTableForward = forwardRef(TanStackTableInner) as <
|
|
TData,
|
|
TItemKey = string,
|
|
>(
|
|
props: TanStackTableProps<TData, TItemKey> & {
|
|
ref?: React.Ref<TanStackTableHandle>;
|
|
},
|
|
) => JSX.Element;
|
|
|
|
export const TanStackTableBase = memo(
|
|
TanStackTableForward,
|
|
) as typeof TanStackTableForward;
|