diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index edcb619702e..c23f3a7384b 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -208,7 +208,7 @@ export function TableNG(props: TableNGProps) { }); // Create a map of column key to text wrap - const footerCalcs = useFooterCalcs(sortedRows, data.fields, { enabled: hasFooter, footerOptions, isCountRowsSet }); + const footerCalcs = useFooterCalcs(sortedRows, data, { enabled: hasFooter, footerOptions, isCountRowsSet }); const applyToRowBgFn = useMemo(() => getApplyToRowBgFn(data.fields, theme) ?? undefined, [data.fields, theme]); const renderRow = useMemo( diff --git a/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts b/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts index 094804a3a28..afbc4fff691 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts @@ -1,7 +1,7 @@ import { act, renderHook } from '@testing-library/react'; import { varPreLine } from 'uwrap'; -import { Field, FieldType } from '@grafana/data'; +import { cacheFieldDisplayNames, createDataFrame, Field, FieldType } from '@grafana/data'; import { useFilteredRows, @@ -226,11 +226,11 @@ describe('TableNG hooks', () => { describe('useFooterCalcs', () => { const rows = [ - { Field1: 1, Text: 'a', __depth: 0, __index: 0 }, - { Field1: 2, Text: 'b', __depth: 0, __index: 1 }, - { Field1: 3, Text: 'c', __depth: 0, __index: 2 }, - { Field2: 3, Text: 'd', __depth: 0, __index: 3 }, - { Field2: 10, Text: 'e', __depth: 0, __index: 4 }, + { 'Field 1': 1, Text: 'a', __depth: 0, __index: 0 }, + { 'Field 1': 2, Text: 'b', __depth: 0, __index: 1 }, + { 'Field 1': 3, Text: 'c', __depth: 0, __index: 2 }, + { 'Field 2': 3, Text: 'd', __depth: 0, __index: 3 }, + { 'Field 2': 10, Text: 'e', __depth: 0, __index: 4 }, ]; const numericField: Field = { @@ -239,6 +239,7 @@ describe('TableNG hooks', () => { values: [1, 2, 3], config: { custom: {}, + displayName: 'Field 1', }, display: (value: unknown) => ({ text: String(value), @@ -247,7 +248,6 @@ describe('TableNG hooks', () => { prefix: undefined, suffix: undefined, }), - state: {}, getLinks: undefined, }; @@ -255,7 +255,10 @@ describe('TableNG hooks', () => { name: 'Field2', type: FieldType.number, values: [3, 10], - config: { custom: {} }, + config: { + custom: {}, + displayName: 'Field 2', + }, display: (value: unknown) => ({ text: String(value), numeric: Number(value), @@ -263,7 +266,6 @@ describe('TableNG hooks', () => { prefix: undefined, suffix: undefined, }), - state: {}, getLinks: undefined, }; @@ -279,105 +281,135 @@ describe('TableNG hooks', () => { prefix: undefined, suffix: undefined, }), - state: {}, getLinks: undefined, }; it('should calculate sum for numeric fields', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, numericField], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: { show: true, reducer: ['sum'] }, - }) - ); + }); + }); expect(result.current).toEqual(['Total', '6']); // 1 + 2 + 3 }); it('should calculate mean for numeric fields', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, numericField], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: { show: true, reducer: ['mean'] }, - }) - ); + }); + }); expect(result.current).toEqual(['Mean', '2']); // (1 + 2 + 3) / 3 }); it('should return an empty string for non-numeric fields', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, textField], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, textField] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: { show: true, reducer: ['sum'] }, - }) - ); + }); + }); expect(result.current).toEqual(['Total', '']); }); it('should return empty array if no footerOptions are provided', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, textField], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField, numericField2] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: undefined, - }) - ); + }); + }); expect(result.current).toEqual([]); }); it('should return empty array when footer is disabled', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, textField], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField, numericField2] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: false, footerOptions: { show: true, reducer: ['sum'] }, - }) - ); + }); + }); expect(result.current).toEqual([]); }); it('should return empty array when reducer is undefined', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, textField], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, textField] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: { show: true, reducer: undefined }, - }) - ); + }); + }); expect(result.current).toEqual([]); }); it('should return empty array when reducer is empty', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, textField], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField, numericField2] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: { show: true, reducer: [] }, - }) - ); + }); + }); expect(result.current).toEqual([]); }); it('should return empty string if fields array doesnt include this field', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, numericField, numericField2], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField, numericField2] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: { show: true, reducer: ['sum'], fields: ['Field2', 'Field3'] }, - }) - ); + }); + }); expect(result.current).toEqual(['Total', '', '13']); }); it('should return the calculation if fields array includes this field', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, numericField, numericField2], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField, numericField2] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: { show: true, reducer: ['sum'], fields: ['Field1', 'Field2', 'Field3'] }, - }) - ); + }); + }); + + expect(result.current).toEqual(['Total', '6', '13']); + }); + + it('should return the calculation if fields array includes this field by either name or display name', () => { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField, numericField2] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { + enabled: true, + footerOptions: { show: true, reducer: ['sum'], fields: ['Field1', 'Field 2'] }, + }); + }); expect(result.current).toEqual(['Total', '6', '13']); }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts index 7e0229903fb..0533c3cf3d0 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts @@ -2,7 +2,16 @@ import { useState, useMemo, useEffect, useCallback, useRef, useLayoutEffect, Ref import { Column, DataGridHandle, DataGridProps, SortColumn } from 'react-data-grid'; import { varPreLine } from 'uwrap'; -import { Field, fieldReducers, FieldType, formattedValueToString, reduceField } from '@grafana/data'; +import { + DataFrame, + Field, + FieldMatcherID, + fieldReducers, + FieldType, + formattedValueToString, + getFieldMatcher, + reduceField, +} from '@grafana/data'; import { useTheme2 } from '../../../themes/ThemeContext'; import { TableCellDisplayMode, TableColumnResizeActionCallback } from '../types'; @@ -255,7 +264,7 @@ export interface FooterCalcsOptions { export function useFooterCalcs( rows: TableRow[], - fields: Field[], + data: DataFrame, { enabled, footerOptions, isCountRowsSet }: FooterCalcsOptions ): string[] { return useMemo(() => { @@ -265,7 +274,11 @@ export function useFooterCalcs( return []; } - return fields.map((field, index) => { + const fieldNameMatcher = footerOptions.fields + ? getFieldMatcher({ id: FieldMatcherID.byNames, options: { names: footerOptions.fields } }) + : undefined; + + return data.fields.map((field, index) => { if (field.state?.calcs) { delete field.state?.calcs; } @@ -289,8 +302,9 @@ export function useFooterCalcs( return ''; } - // If fields array is specified, only show footer for fields included in that array - if (footerOptions.fields?.length && !footerOptions.fields?.includes(getDisplayName(field))) { + // If fields array is specified, only show footer for fields included in that array. + // the array can include either the display name or the field name. + if (fieldNameMatcher && !fieldNameMatcher(field, data, [data])) { return ''; } @@ -305,7 +319,7 @@ export function useFooterCalcs( return formattedValueToString(displayFn(value)); }); - }, [fields, enabled, footerOptions, isCountRowsSet, rows]); + }, [data, enabled, footerOptions, isCountRowsSet, rows]); } interface TypographyCtx { diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts index 191e9fa76e4..e0b83527470 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -537,7 +537,10 @@ export const processNestedTableRows = ( /** * @internal - * returns the display name of a field + * returns the display name of a field. + * We intentionally do not want to use @grafana/data's getFieldDisplayName here, + * instead we have a call to cacheFieldDisplayNames up in TablePanel to handle this + * before we begin. */ export const getDisplayName = (field: Field): string => { return field.state?.displayName ?? field.name;