TableNG: Bugfixes (#102905)

* fixes: sort persistence, sort trigger panel dirty state, use field display names

* fix: for nested tables, use column.name for header text

* chore: fix location of cache display names
This commit is contained in:
Alex Spencer
2025-03-27 14:02:57 -06:00
committed by GitHub
parent 51825cfffe
commit 98a1dfbad4
5 changed files with 64 additions and 21 deletions
+2 -1
View File
@@ -694,7 +694,8 @@ exports[`better eslint`] = {
"packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"]
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"]
],
"packages/grafana-ui/src/components/Table/TableNG/utils.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@@ -44,7 +44,7 @@ const HeaderCell: React.FC<HeaderCellProps> = ({
crossFilterRows,
showTypeIcons,
}) => {
const styles = useStyles2(getStyles);
const styles = useStyles2(getStyles, justifyContent);
const headerRef = useRef<HTMLDivElement>(null);
let isColumnFilterable = filterable;
@@ -99,7 +99,7 @@ const HeaderCell: React.FC<HeaderCellProps> = ({
return (
<div
ref={headerRef}
style={{ display: 'flex', justifyContent }}
className={styles.headerCell}
// TODO find a better solution to this issue, see: https://github.com/adazzle/react-data-grid/issues/3535
// Unblock spacebar event
onKeyDown={(event) => {
@@ -110,13 +110,9 @@ const HeaderCell: React.FC<HeaderCellProps> = ({
>
<button className={styles.headerCellLabel} onClick={handleSort}>
{showTypeIcons && <Icon name={getFieldTypeIcon(field)} title={field?.type} size="sm" />}
<div>{column.name}</div>
{direction &&
(direction === 'ASC' ? (
<Icon name="arrow-up" size="lg" className={styles.sortIcon} />
) : (
<Icon name="arrow-down" size="lg" className={styles.sortIcon} />
))}
{/* Used cached displayName if available, otherwise use the column name (nested tables) */}
<div>{field.state?.displayName ?? column.name}</div>
{direction && (direction === 'ASC' ? <Icon name="arrow-up" size="lg" /> : <Icon name="arrow-down" size="lg" />)}
</button>
{isColumnFilterable && (
@@ -134,7 +130,12 @@ const HeaderCell: React.FC<HeaderCellProps> = ({
);
};
const getStyles = (theme: GrafanaTheme2) => ({
const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => ({
headerCell: css({
display: 'flex',
gap: theme.spacing(0.5),
justifyContent,
}),
headerCellLabel: css({
border: 'none',
padding: 0,
@@ -146,7 +147,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
fontWeight: theme.typography.fontWeightMedium,
display: 'flex',
alignItems: 'center',
marginRight: theme.spacing(0.5),
color: theme.colors.text.secondary,
gap: theme.spacing(1),
@@ -155,9 +155,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
color: theme.colors.text.link,
},
}),
sortIcon: css({
marginLeft: theme.spacing(0.5),
}),
});
export { HeaderCell };
@@ -1,7 +1,14 @@
import 'react-data-grid/lib/styles.css';
import { css } from '@emotion/css';
import { useMemo, useState, useLayoutEffect, useCallback, useRef, useEffect } from 'react';
import DataGrid, { RenderCellProps, RenderRowProps, Row, SortColumn, DataGridHandle } from 'react-data-grid';
import DataGrid, {
RenderCellProps,
RenderRowProps,
Row,
SortColumn,
DataGridHandle,
SortDirection,
} from 'react-data-grid';
import { useMeasure } from 'react-use';
import {
@@ -65,14 +72,29 @@ export function TableNG(props: TableNGProps) {
fieldConfig,
footerOptions,
height,
initialSortBy,
noHeader,
onColumnResize,
onSortByChange,
width,
data,
enableSharedCrosshair,
showTypeIcons,
} = props;
const initialSortColumns = useMemo<SortColumn[]>(() => {
const initialSort = initialSortBy?.map(({ displayName, desc }) => {
const matchingField = data.fields.find(({ state }) => state?.displayName === displayName);
const columnKey = matchingField?.name || displayName;
return {
columnKey,
direction: (desc ? 'DESC' : 'ASC') as SortDirection,
};
});
return initialSort ?? [];
}, []); // eslint-disable-line react-hooks/exhaustive-deps
/* ------------------------------- Local state ------------------------------ */
const [revId, setRevId] = useState(0);
const [contextMenuProps, setContextMenuProps] = useState<{
@@ -89,7 +111,7 @@ export function TableNG(props: TableNGProps) {
// This state will trigger re-render for recalculating row heights
const [, setResizeTrigger] = useState(0);
const [, setReadyForRowHeightCalc] = useState(false);
const [sortColumns, setSortColumns] = useState<readonly SortColumn[]>([]);
const [sortColumns, setSortColumns] = useState<readonly SortColumn[]>(initialSortColumns);
const [expandedRows, setExpandedRows] = useState<number[]>([]);
const [isNestedTable, setIsNestedTable] = useState(false);
const scrollPositionRef = useRef<ScrollPosition>({ x: 0, y: 0 });
@@ -100,7 +122,7 @@ export function TableNG(props: TableNGProps) {
const crossFilterRows = useRef<Record<string, TableRow[]>>({});
const headerCellRefs = useRef<Record<string, HTMLDivElement>>({});
// TODO: This ref persists sortColumns between renders. setSortColumns is still used to trigger re-render
const sortColumnsRef = useRef(sortColumns);
const sortColumnsRef = useRef<SortColumn[]>(initialSortColumns);
const prevProps = useRef(props);
const calcsRef = useRef<string[]>([]);
const [paginationWrapperRef, { height: paginationHeight }] = useMeasure<HTMLDivElement>();
@@ -364,6 +386,7 @@ export function TableNG(props: TableNGProps) {
filter,
headerCellRefs,
isCountRowsSet,
onSortByChange,
osContext,
rows,
// INFO: sortedRows is for correct row indexing for cell background coloring
@@ -438,6 +461,13 @@ export function TableNG(props: TableNGProps) {
};
};
// Reset sortColumns when initialSortBy changes
useEffect(() => {
if (initialSortColumns.length > 0) {
setSortColumns(initialSortColumns);
}
}, [initialSortColumns]);
// Restore scroll position after re-renders
useEffect(() => {
if (tableRef.current?.element) {
@@ -566,6 +596,7 @@ export function mapFrameToDataGrid({
filter,
headerCellRefs,
isCountRowsSet,
onSortByChange,
osContext,
rows,
sortedRows,
@@ -752,9 +783,18 @@ export function mapFrameToDataGrid({
column={column}
rows={rows}
field={field}
onSort={(columnKey, direction, isMultiSort) =>
handleSort(columnKey, direction, isMultiSort, setSortColumns, sortColumnsRef)
}
onSort={(columnKey, direction, isMultiSort) => {
handleSort(columnKey, direction, isMultiSort, setSortColumns, sortColumnsRef);
// Update panel context with the new sort order
if (onSortByChange) {
const sortByFields = sortColumnsRef.current.map(({ columnKey, direction }) => ({
displayName: columnKey,
desc: direction === 'DESC',
}));
onSortByChange(sortByFields);
}
}}
direction={sortDirection}
justifyContent={justifyColumnContent}
filter={filter}
@@ -22,6 +22,7 @@ import {
TableCellDisplayMode,
TableCellHeight,
TableCellOptions,
TableSortByFieldState,
} from '@grafana/schema';
import { TableCellInspectorMode } from '../..';
@@ -477,6 +478,7 @@ export interface MapFrameToGridOptions extends TableNGProps {
filter: FilterType;
headerCellRefs: React.MutableRefObject<Record<string, HTMLDivElement>>;
isCountRowsSet: boolean;
onSortByChange?: (sortBy: TableSortByFieldState[]) => void;
osContext: OffscreenCanvasRenderingContext2D | null;
rows: TableRow[];
sortedRows: TableRow[];
@@ -10,6 +10,7 @@ import {
PanelProps,
SelectableValue,
Field,
cacheFieldDisplayNames,
} from '@grafana/data';
import { config, PanelDataErrorView } from '@grafana/runtime';
import { Select, usePanelContext, useTheme2 } from '@grafana/ui';
@@ -26,6 +27,8 @@ interface Props extends PanelProps<Options> {}
export function TablePanel(props: Props) {
const { data, height, width, options, fieldConfig, id, timeRange, replaceVariables } = props;
cacheFieldDisplayNames(data.series);
const theme = useTheme2();
const panelContext = usePanelContext();
const frames = hasDeprecatedParentRowIndex(data.series)