TableRT sorta working

This commit is contained in:
Ashley Harrison
2025-12-03 12:33:16 +00:00
parent c32da02543
commit 50150f36fc
5 changed files with 83 additions and 197 deletions
-3
View File
@@ -843,9 +843,6 @@
}
},
"packages/grafana-ui/src/components/Table/TableRT/Table.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
},
"@typescript-eslint/no-explicit-any": {
"count": 2
}
@@ -1,8 +1,8 @@
import { css, cx } from '@emotion/css';
import { CSSProperties, UIEventHandler, useCallback, useEffect, useMemo, useState } from 'react';
import { CSSProperties, useCallback, useEffect, useMemo, useState } from 'react';
import * as React from 'react';
import { Cell, Row, TableState, HeaderGroup } from 'react-table';
import { VariableSizeList } from 'react-window';
import { List, useListRef } from 'react-window';
import { Subscription, debounceTime } from 'rxjs';
import {
@@ -18,7 +18,6 @@ import {
import { TableCellDisplayMode, TableCellHeight } from '@grafana/schema';
import { useTheme2 } from '../../../themes/ThemeContext';
import CustomScrollbar from '../../CustomScrollbar/CustomScrollbar';
import { usePanelContext } from '../../PanelChrome';
import { TableCell } from '../Cells/TableCell';
import {
@@ -42,14 +41,10 @@ interface RowsListProps {
data: DataFrame;
rows: Row[];
enableSharedCrosshair: boolean;
headerHeight: number;
rowHeight: number;
itemCount: number;
pageIndex: number;
listHeight: number;
width: number;
cellHeight?: TableCellHeight;
listRef: React.RefObject<VariableSizeList>;
tableState: TableState;
tableStyles: TableStyles;
nestedDataField?: Field;
@@ -70,11 +65,8 @@ export const RowsList = (props: RowsListProps) => {
const {
data,
rows,
headerHeight,
footerPaginationEnabled,
rowHeight,
itemCount,
pageIndex,
tableState,
prepareRow,
onCellFilterAdded,
@@ -84,7 +76,6 @@ export const RowsList = (props: RowsListProps) => {
tableStyles,
nestedDataField,
listHeight,
listRef,
enableSharedCrosshair = false,
initialRowIndex = undefined,
headerGroups,
@@ -95,11 +86,25 @@ export const RowsList = (props: RowsListProps) => {
setInspectCell,
} = props;
const listRef = useListRef(null);
const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex);
if (initialRowIndex === undefined && rowHighlightIndex !== undefined) {
setRowHighlightIndex(undefined);
}
useEffect(() => {
if (rowHighlightIndex !== undefined) {
// TODO can we do this without a setTimeout?
setTimeout(() => {
listRef.current?.scrollToRow({
index: rowHighlightIndex,
align: 'center',
behavior: 'instant',
});
});
}
}, [rowHighlightIndex, listRef]);
const theme = useTheme2();
const panelContext = usePanelContext();
@@ -234,15 +239,6 @@ export const RowsList = (props: RowsListProps) => {
};
}, [data, enableSharedCrosshair, footerPaginationEnabled, onDataHoverEvent, panelContext]);
let scrollTop: number | undefined = undefined;
if (rowHighlightIndex !== undefined) {
const firstMatchedRowIndex = rows.findIndex((row) => row.index === rowHighlightIndex);
if (firstMatchedRowIndex !== -1) {
scrollTop = headerHeight + (firstMatchedRowIndex - 1) * rowHeight;
}
}
const rowIndexForPagination = useCallback(
(index: number) => {
return tableState.pageIndex * tableState.pageSize + index;
@@ -316,6 +312,17 @@ export const RowsList = (props: RowsListProps) => {
);
style.height = bbox.height;
}
// some disgusting code to mutate the style object to convert transform to top
// this is all so that hover behaviour is maintained
// using transform creates new stacking contexts which means hover states don't overlay correctly
const yPos = style.transform?.match(/translateY\((.*)\)/)?.[1];
style = {
...style,
top: yPos,
transform: undefined,
};
const { key, ...rowProps } = row.getRowProps({ style, ...additionalProps });
return (
@@ -414,36 +421,17 @@ export const RowsList = (props: RowsListProps) => {
return tableStyles.rowHeight;
};
const handleScroll: UIEventHandler = (event) => {
const { scrollTop } = event.currentTarget;
if (listRef.current !== null) {
listRef.current.scrollTo(scrollTop);
}
};
// It's a hack for text wrapping.
// VariableSizeList component didn't know that we manually set row height.
// So we need to reset the list when the rows high changes.
useEffect(() => {
if (listRef.current) {
listRef.current.resetAfterIndex(0);
}
}, [rows, listRef]);
return (
<CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true} scrollTop={scrollTop}>
<VariableSizeList
key={`${rowHeight}${pageIndex}`}
height={listHeight}
itemCount={itemCount}
itemSize={getItemSize}
width={'100%'}
ref={listRef}
style={{ overflow: undefined }}
>
{({ index, style }) => RenderRow({ index, style, rowHighlightIndex })}
</VariableSizeList>
</CustomScrollbar>
<List
rowProps={{}}
rowHeight={getItemSize}
rowCount={itemCount}
listRef={listRef}
style={{
height: listHeight,
width,
}}
rowComponent={({ index, style }) => RenderRow({ index, style, rowHighlightIndex })}
/>
);
};
@@ -8,7 +8,6 @@ import {
useSortBy,
useTable,
} from 'react-table';
import { VariableSizeList } from 'react-window';
import { FieldType, ReducerID, getRowUniqueId, getFieldMatcher } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@@ -16,12 +15,10 @@ import { Trans } from '@grafana/i18n';
import { TableCellHeight } from '@grafana/schema';
import { useTheme2 } from '../../../themes/ThemeContext';
import { CustomScrollbar } from '../../CustomScrollbar/CustomScrollbar';
import { Pagination } from '../../Pagination/Pagination';
import { TableCellInspector } from '../TableCellInspector';
import { useFixScrollbarContainer, useResetVariableListSizeCache } from '../hooks';
import { getInitialState, useTableStateReducer } from '../reducer';
import { FooterItem, GrafanaTableState, InspectCell, TableRTProps as Props } from '../types';
import { FooterItem, InspectCell, TableRTProps as Props } from '../types';
import {
getColumns,
sortCaseInsensitive,
@@ -70,7 +67,6 @@ export const Table = memo((props: Props) => {
replaceVariables,
} = props;
const listRef = useRef<VariableSizeList>(null);
const tableDivRef = useRef<HTMLDivElement>(null);
const variableSizeListScrollbarRef = useRef<HTMLDivElement>(null);
const theme = useTheme2();
@@ -200,7 +196,6 @@ export const Table = memo((props: Props) => {
toggleAllRowsExpanded,
} = useTable(options, useFilters, useSortBy, useAbsoluteLayout, useResizeColumns, useExpanded, usePagination);
const extendedState = state as GrafanaTableState;
toggleAllRowsExpandedRef.current = toggleAllRowsExpanded;
/*
@@ -270,9 +265,6 @@ export const Table = memo((props: Props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
useResetVariableListSizeCache(extendedState, listRef, data, hasUniqueId);
useFixScrollbarContainer(variableSizeListScrollbarRef, tableDivRef);
const onNavigate = useCallback(
(toPage: number) => {
gotoPage(toPage - 1);
@@ -340,60 +332,51 @@ export const Table = memo((props: Props) => {
ref={tableDivRef}
style={{ width, height }}
>
<CustomScrollbar hideVerticalTrack={true}>
<div className={tableStyles.tableContentWrapper(totalColumnsWidth)}>
{!noHeader && (
<HeaderRow headerGroups={headerGroups} showTypeIcons={showTypeIcons} tableStyles={tableStyles} />
)}
{itemCount > 0 ? (
<div
data-testid={selectors.components.Panels.Visualization.Table.body}
ref={variableSizeListScrollbarRef}
>
<RowsList
headerGroups={headerGroups}
data={data}
rows={rows}
width={width}
cellHeight={cellHeight}
headerHeight={headerHeight}
rowHeight={tableStyles.rowHeight}
itemCount={itemCount}
pageIndex={state.pageIndex}
listHeight={listHeight}
listRef={listRef}
tableState={state}
prepareRow={prepareRow}
timeRange={timeRange}
onCellFilterAdded={onCellFilterAdded}
nestedDataField={nestedDataField}
tableStyles={tableStyles}
footerPaginationEnabled={Boolean(enablePagination)}
enableSharedCrosshair={enableSharedCrosshair}
initialRowIndex={initialRowIndex}
longestField={longestField}
textWrapField={textWrapField}
getActions={getActions}
replaceVariables={replaceVariables}
setInspectCell={setInspectCell}
/>
</div>
) : (
<div style={{ height: height - headerHeight, width }} className={tableStyles.noData}>
{noValuesDisplayText}
</div>
)}
{footerItems && (
<FooterRow
isPaginationVisible={Boolean(enablePagination)}
footerValues={footerItems}
footerGroups={footerGroups}
totalColumnsWidth={totalColumnsWidth}
<div className={tableStyles.tableContentWrapper(totalColumnsWidth)}>
{!noHeader && (
<HeaderRow headerGroups={headerGroups} showTypeIcons={showTypeIcons} tableStyles={tableStyles} />
)}
{itemCount > 0 ? (
<div data-testid={selectors.components.Panels.Visualization.Table.body} ref={variableSizeListScrollbarRef}>
<RowsList
headerGroups={headerGroups}
data={data}
rows={rows}
width={width}
cellHeight={cellHeight}
itemCount={itemCount}
listHeight={listHeight}
tableState={state}
prepareRow={prepareRow}
timeRange={timeRange}
onCellFilterAdded={onCellFilterAdded}
nestedDataField={nestedDataField}
tableStyles={tableStyles}
footerPaginationEnabled={Boolean(enablePagination)}
enableSharedCrosshair={enableSharedCrosshair}
initialRowIndex={initialRowIndex}
longestField={longestField}
textWrapField={textWrapField}
getActions={getActions}
replaceVariables={replaceVariables}
setInspectCell={setInspectCell}
/>
)}
</div>
</CustomScrollbar>
</div>
) : (
<div style={{ height: height - headerHeight, width }} className={tableStyles.noData}>
{noValuesDisplayText}
</div>
)}
{footerItems && (
<FooterRow
isPaginationVisible={Boolean(enablePagination)}
footerValues={footerItems}
footerGroups={footerGroups}
totalColumnsWidth={totalColumnsWidth}
tableStyles={tableStyles}
/>
)}
</div>
{paginationEl}
</div>
@@ -111,6 +111,7 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
width: '100%',
overflow: 'auto',
display: 'flex',
position: 'relative',
flexDirection: 'column',
}),
thead: css({
@@ -1,83 +0,0 @@
import { useEffect } from 'react';
import * as React from 'react';
import { VariableSizeList } from 'react-window';
import { DataFrame } from '@grafana/data';
import { GrafanaTableState } from './types';
/**
To have the custom vertical scrollbar always visible (https://github.com/grafana/grafana/issues/52136),
we need to bring the element from the VariableSizeList scope to the outer Table container scope,
because the VariableSizeList scope has overflow. By moving scrollbar to container scope we will have
it always visible since the entire width is in view.
Select the scrollbar element from the VariableSizeList scope
*/
export function useFixScrollbarContainer(
variableSizeListScrollbarRef: React.RefObject<HTMLDivElement>,
tableDivRef: React.RefObject<HTMLDivElement>
) {
useEffect(() => {
if (variableSizeListScrollbarRef.current && tableDivRef.current) {
const listVerticalScrollbarHTML = variableSizeListScrollbarRef.current.querySelector('.track-vertical');
// Select Table custom scrollbars
const tableScrollbarView = tableDivRef.current.firstChild;
//If they exist, move the scrollbar element to the Table container scope
if (tableScrollbarView && listVerticalScrollbarHTML) {
listVerticalScrollbarHTML.remove();
if (tableScrollbarView instanceof HTMLElement) {
tableScrollbarView.querySelector(':scope > .track-vertical')?.remove();
tableScrollbarView.append(listVerticalScrollbarHTML);
}
}
}
});
}
/**
react-table caches the height of cells, so we need to reset them when expanding/collapsing rows.
We use `lastExpandedOrCollapsedIndex` since collapsed rows disappear from `expandedIndexes` but still keep their expanded
height.
*/
export function useResetVariableListSizeCache(
extendedState: GrafanaTableState,
listRef: React.RefObject<VariableSizeList>,
data: DataFrame,
hasUniqueId: boolean
) {
// Make sure we trigger the reset when keys change in any way
const expandedRowsRepr = JSON.stringify(Object.keys(extendedState.expanded));
useEffect(() => {
// By default, reset all rows
let resetIndex = 0;
// If we have unique field, extendedState.expanded keys are not row indexes but IDs so instead of trying to search
// for correct index we just reset the whole table.
if (!hasUniqueId) {
// If we don't have we reset from the last changed index.
if (Number.isFinite(extendedState.lastExpandedOrCollapsedIndex)) {
resetIndex = extendedState.lastExpandedOrCollapsedIndex!;
}
// Account for paging.
resetIndex =
extendedState.pageIndex === 0
? resetIndex - 1
: resetIndex - extendedState.pageIndex - extendedState.pageIndex * extendedState.pageSize;
}
listRef.current?.resetAfterIndex(Math.max(resetIndex, 0));
return;
}, [
extendedState.lastExpandedOrCollapsedIndex,
extendedState.pageSize,
extendedState.pageIndex,
listRef,
data,
expandedRowsRepr,
hasUniqueId,
]);
}