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:
+2
-1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user