[release-12.0.1] TableNG: Fix sparkline sorting + column key config (#104955)

TableNG: Fix sparkline sorting + column key config (#104008)

* fix: sorting sparkline cells

* fix: prevent same cell name issues

* chore: clean

* chore: bring back nuked sparkline fix

* chore: mob feedback

* chore: use getDisplayName function

* chore: missed two!!

* chore: improved betterer results

---------

Co-authored-by: Adela Almasan <adela.almasan@grafana.com>
(cherry picked from commit 72932e10b2)
This commit is contained in:
Alex Spencer
2025-05-05 11:22:29 -06:00
committed by GitHub
parent 2e4020a93b
commit 6db7f10bbc
6 changed files with 54 additions and 22 deletions
@@ -10,6 +10,7 @@ import { getFieldTypeIcon } from '../../../../types';
import { Icon } from '../../../Icon/Icon';
import { Filter } from '../Filter/Filter';
import { TableColumnResizeActionCallback, FilterType, TableRow, TableSummaryRow } from '../types';
import { getDisplayName } from '../utils';
interface HeaderCellProps {
column: Column<TableRow, TableSummaryRow>;
@@ -46,16 +47,17 @@ const HeaderCell: React.FC<HeaderCellProps> = ({
const headerRef = useRef<HTMLDivElement>(null);
const filterable = field.config?.custom?.filterable ?? false;
const displayName = getDisplayName(field);
let isColumnFilterable = filterable;
if (field.config.custom?.filterable !== filterable) {
isColumnFilterable = field.config.custom?.filterable || false;
}
// we have to remove/reset the filter if the column is not filterable
if (!isColumnFilterable && filter[field.name]) {
if (!isColumnFilterable && filter[displayName]) {
setFilter((filter: FilterType) => {
const newFilter = { ...filter };
delete newFilter[field.name];
delete newFilter[displayName];
return newFilter;
});
}
@@ -18,7 +18,7 @@ import {
FILTER_OUT_OPERATOR,
TableCellNGProps,
} from '../types';
import { getCellColors, getTextAlign } from '../utils';
import { getCellColors, getDisplayName, getTextAlign } from '../utils';
import { ActionsCell } from './ActionsCell';
import AutoCell from './AutoCell';
@@ -49,6 +49,7 @@ export function TableCellNG(props: TableCellNGProps) {
} = props;
const cellInspect = field.config?.custom?.inspect ?? false;
const displayName = getDisplayName(field);
const { config: fieldConfig } = field;
const defaultCellOptions: TableAutoCellOptions = { type: TableCellDisplayMode.Auto };
@@ -171,15 +172,23 @@ export function TableCellNG(props: TableCellNGProps) {
const onFilterFor = useCallback(() => {
if (onCellFilterAdded) {
onCellFilterAdded({ key: field.name, operator: FILTER_FOR_OPERATOR, value: String(value ?? '') });
onCellFilterAdded({
key: displayName,
operator: FILTER_FOR_OPERATOR,
value: String(value ?? ''),
});
}
}, [field.name, onCellFilterAdded, value]);
}, [displayName, onCellFilterAdded, value]);
const onFilterOut = useCallback(() => {
if (onCellFilterAdded) {
onCellFilterAdded({ key: field.name, operator: FILTER_OUT_OPERATOR, value: String(value ?? '') });
onCellFilterAdded({
key: displayName,
operator: FILTER_OUT_OPERATOR,
value: String(value ?? ''),
});
}
}, [field.name, onCellFilterAdded, value]);
}, [displayName, onCellFilterAdded, value]);
return (
<div ref={divWidthRef} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} className={styles.cell}>
@@ -1,5 +1,7 @@
import { Field, formattedValueToString, SelectableValue } from '@grafana/data';
import { getDisplayName } from '../utils';
export function calculateUniqueFieldValues(rows: any[], field?: Field) {
if (!field || rows.length === 0) {
return {};
@@ -9,7 +11,7 @@ export function calculateUniqueFieldValues(rows: any[], field?: Field) {
for (let index = 0; index < rows.length; index++) {
const row = rows[index];
const fieldValue = row[field.name];
const fieldValue = row[getDisplayName(field)];
const displayValue = field.display ? field.display(fieldValue) : fieldValue;
const value = field.display ? formattedValueToString(displayValue) : displayValue;
@@ -48,6 +48,7 @@ import {
getCellHeightCalculator,
getComparator,
getDefaultRowHeight,
getDisplayName,
getFooterItemNG,
getFooterStyles,
getIsNestedTable,
@@ -196,7 +197,7 @@ export function TableNG(props: TableNGProps) {
// Create a map of column key to column type
const columnTypes = useMemo(
() => props.data.fields.reduce<ColumnTypes>((acc, { name, type }) => ({ ...acc, [name]: type }), {}),
() => props.data.fields.reduce<ColumnTypes>((acc, field) => ({ ...acc, [getDisplayName(field)]: field.type }), {}),
[props.data.fields]
);
@@ -204,7 +205,10 @@ export function TableNG(props: TableNGProps) {
const textWraps = useMemo(
() =>
props.data.fields.reduce<{ [key: string]: boolean }>(
(acc, { name, config }) => ({ ...acc, [name]: config?.custom?.cellOptions?.wrapText ?? false }),
(acc, field) => ({
...acc,
[getDisplayName(field)]: field.config?.custom?.cellOptions?.wrapText ?? false,
}),
{}
),
[props.data.fields]
@@ -218,12 +222,13 @@ export function TableNG(props: TableNGProps) {
const widths: Record<string, number> = {};
// Set default widths from field config if they exist
props.data.fields.forEach(({ name, config }) => {
const configWidth = config?.custom?.width;
props.data.fields.forEach((field) => {
const displayName = getDisplayName(field);
const configWidth = field.config?.custom?.width;
const totalWidth = typeof configWidth === 'number' ? configWidth : COLUMN.DEFAULT_WIDTH;
// subtract out padding and 1px right border
const contentWidth = totalWidth - 2 * TABLE.CELL_PADDING - 1;
widths[name] = contentWidth;
widths[displayName] = contentWidth;
});
// Measure actual widths if available
@@ -243,9 +248,9 @@ export function TableNG(props: TableNGProps) {
}, [props.data.fields]);
const fieldDisplayType = useMemo(() => {
return props.data.fields.reduce<Record<string, TableCellDisplayMode>>((acc, { config, name }) => {
if (config?.custom?.cellOptions?.type) {
acc[name] = config.custom.cellOptions.type;
return props.data.fields.reduce<Record<string, TableCellDisplayMode>>((acc, field) => {
if (field.config?.custom?.cellOptions?.type) {
acc[getDisplayName(field)] = field.config.custom.cellOptions.type;
}
return acc;
}, {});
@@ -264,7 +269,9 @@ export function TableNG(props: TableNGProps) {
);
const getDisplayedValue = (row: TableRow, key: string) => {
const field = props.data.fields.find((field) => field.name === key)!;
const field = props.data.fields.find((field) => {
return getDisplayName(field) === key;
})!;
const displayedValue = formattedValueToString(field.display!(row[key]));
return displayedValue;
};
@@ -763,7 +770,7 @@ export function mapFrameToDataGrid({
return;
}
const fieldTableOptions: TableFieldOptionsType = field.config.custom || {};
const key = field.name;
const key = getDisplayName(field);
const justifyColumnContent = getTextAlign(field);
const footerStyles = getFooterStyles(justifyColumnContent);
@@ -779,7 +786,7 @@ export function mapFrameToDataGrid({
key,
name: field.name,
field,
cellClass: textWraps[field.name] ? styles.cellWrapped : styles.cell,
cellClass: textWraps[getDisplayName(field)] ? styles.cellWrapped : styles.cell,
renderCell: (props: RenderCellProps<TableRow, TableSummaryRow>): JSX.Element => {
const { row, rowIdx } = props;
const cellType = field.config?.custom?.cellOptions?.type ?? TableCellDisplayMode.Auto;
@@ -806,7 +813,7 @@ export function mapFrameToDataGrid({
defaultLineHeight,
defaultRowHeight,
TABLE.CELL_PADDING,
textWraps[field.name],
textWraps[getDisplayName(field)],
field,
cellType
)
@@ -11,6 +11,7 @@ import {
ActionModel,
InterpolateFunction,
FieldType,
DataFrameWithValue,
} from '@grafana/data';
import { TableCellOptions, TableCellHeight, TableFieldOptions } from '@grafana/schema';
@@ -66,6 +67,7 @@ export type TableCellValue =
| Date // FieldType.time
| DataFrame // For nested data
| DataFrame[] // For nested frames
| DataFrameWithValue // For sparklines
| undefined; // For undefined values
export interface TableRow {
@@ -313,7 +313,7 @@ export function getFooterItemNG(rows: TableRow[], field: Field, options: TableFo
const value = reduceField({
field: {
...field,
values: rows.map((row) => row[field.name]),
values: rows.map((row) => row[getDisplayName(field)]),
},
reducers: options.reducer,
})[calc];
@@ -470,7 +470,7 @@ export const frameToRecords = (frame: DataFrame): TableRow[] => {
rows[rowCount] = {
__depth: 0,
__index: i,
${frame.fields.map((field, fieldIdx) => `${JSON.stringify(field.name)}: values[${fieldIdx}][i]`).join(',')}
${frame.fields.map((field, fieldIdx) => `${JSON.stringify(getDisplayName(field))}: values[${fieldIdx}][i]`).join(',')}
};
rowCount += 1;
if (rows[rowCount-1]['Nested frames']){
@@ -520,6 +520,12 @@ export interface MapFrameToGridOptions extends TableNGProps {
const compare = new Intl.Collator('en', { sensitivity: 'base', numeric: true }).compare;
export function getComparator(sortColumnType: FieldType): Comparator {
switch (sortColumnType) {
// Handle sorting for frame type fields (sparklines)
case FieldType.frame:
return (a, b) => {
// @ts-ignore The values are DataFrameWithValue
return (a?.value ?? 0) - (b?.value ?? 0);
};
case FieldType.time:
case FieldType.number:
case FieldType.boolean:
@@ -594,3 +600,7 @@ export function migrateTableDisplayModeToCellOptions(displayMode: TableCellDispl
/** Returns true if the DataFrame contains nested frames */
export const getIsNestedTable = (dataFrame: DataFrame): boolean =>
dataFrame.fields.some(({ type }) => type === FieldType.nestedFrames);
export const getDisplayName = (field: Field): string => {
return field.state?.displayName ?? field.name;
};