From a97562906c350e434ca07019ab1f5d2a7e2d646d Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Fri, 23 Feb 2024 10:00:24 -0700 Subject: [PATCH] Table: Add ability for Table to render Standard Options "No value" value when DataFrames or field values are empty (#82948) * baldm0mma/no_value_message/ add fieldConfig to Table props * baldm0mma/no_value_message/ add noValuesDisplayText to table props * baldm0mma/no_value_message/ add fieldConfig to tablePanel * baldm0mma/no_value_message/ add tests * baldm0mma/no_value_message/ update test values * baldm0mma/no_value_message/ update args in tests * baldm0mma/no_value_message/ update with NO_DATA_TEXT const --- .../src/components/Table/Table.test.tsx | 160 +++++++++++------- .../grafana-ui/src/components/Table/Table.tsx | 5 +- .../grafana-ui/src/components/Table/types.ts | 3 +- public/app/plugins/panel/table/TablePanel.tsx | 1 + 4 files changed, 110 insertions(+), 59 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/Table.test.tsx b/packages/grafana-ui/src/components/Table/Table.test.tsx index e6ed28d2a8b..1f8c73c1420 100644 --- a/packages/grafana-ui/src/components/Table/Table.test.tsx +++ b/packages/grafana-ui/src/components/Table/Table.test.tsx @@ -15,57 +15,66 @@ jest.mock('@floating-ui/react', () => ({ }), })); -function getDefaultDataFrame(): DataFrame { - const dataFrame = toDataFrame({ - name: 'A', - fields: [ - { - name: 'time', - type: FieldType.time, - values: [1609459200000, 1609470000000, 1609462800000, 1609466400000], - config: { - custom: { - filterable: false, - }, +const dataFrameData = { + name: 'A', + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1609459200000, 1609470000000, 1609462800000, 1609466400000], + config: { + custom: { + filterable: false, }, }, - { - name: 'temperature', - type: FieldType.number, - values: [10, NaN, 11, 12], - config: { - custom: { - filterable: false, - }, - links: [ - { - targetBlank: true, - title: 'Value link', - url: '${__value.text}', - }, - ], + }, + { + name: 'temperature', + type: FieldType.number, + values: [10, NaN, 11, 12], + config: { + custom: { + filterable: false, }, - }, - { - name: 'img', - type: FieldType.string, - values: ['data:image/png;base64,1', 'data:image/png;base64,2', 'data:image/png;base64,3'], - config: { - custom: { - filterable: false, - displayMode: 'image', + links: [ + { + targetBlank: true, + title: 'Value link', + url: '${__value.text}', }, - links: [ - { - targetBlank: true, - title: 'Image link', - url: '${__value.text}', - }, - ], - }, + ], }, - ], - }); + }, + { + name: 'img', + type: FieldType.string, + values: ['data:image/png;base64,1', 'data:image/png;base64,2', 'data:image/png;base64,3'], + config: { + custom: { + filterable: false, + displayMode: 'image', + }, + links: [ + { + targetBlank: true, + title: 'Image link', + url: '${__value.text}', + }, + ], + }, + }, + ], +}; + +const fullDataFrame = toDataFrame(dataFrameData); + +const emptyValuesDataFrame = toDataFrame({ + ...dataFrameData, + // Remove all values + fields: dataFrameData.fields.map((field) => ({ ...field, values: [] })), +}); + +function getDataFrame(dataFrame: DataFrame): DataFrame { return applyOverrides(dataFrame); } @@ -76,7 +85,7 @@ function applyOverrides(dataFrame: DataFrame) { defaults: {}, overrides: [], }, - replaceVariables: (value, vars, format) => { + replaceVariables: (value, vars, _format) => { return vars && value === '${__value.text}' ? '${__value.text} interpolation' : value; }, timeZone: 'utc', @@ -91,7 +100,7 @@ function getTestContext(propOverrides: Partial = {}) { const onColumnResize = jest.fn(); const props: Props = { ariaLabel: 'aria-label', - data: getDefaultDataFrame(), + data: getDataFrame(fullDataFrame), height: 600, width: 800, onSortByChange, @@ -136,16 +145,53 @@ function getRowsData(rows: HTMLElement[]): Object[] { } describe('Table', () => { - describe('when mounted without data', () => { - it('then no data to show should be displayed', () => { - getTestContext({ data: toDataFrame([]) }); - expect(getTable()).toBeInTheDocument(); - expect(screen.queryByRole('row')).not.toBeInTheDocument(); - expect(screen.getByText(/No data/i)).toBeInTheDocument(); + describe('when mounted with EMPTY data', () => { + describe('and Standard Options `No value` value is NOT set', () => { + it('the default `no data` message should be displayed', () => { + getTestContext({ data: toDataFrame([]) }); + expect(getTable()).toBeInTheDocument(); + expect(screen.queryByRole('row')).not.toBeInTheDocument(); + expect(screen.getByText(/No data/i)).toBeInTheDocument(); + }); + }); + + describe('and Standard Options `No value` value IS set', () => { + it('the `No value` Standard Options message should be displayed', () => { + const noValuesDisplayText = 'All healthy'; + getTestContext({ + data: toDataFrame([]), + fieldConfig: { defaults: { noValue: noValuesDisplayText }, overrides: [] }, + }); + expect(getTable()).toBeInTheDocument(); + expect(screen.queryByRole('row')).not.toBeInTheDocument(); + expect(screen.getByText(noValuesDisplayText)).toBeInTheDocument(); + }); }); }); describe('when mounted with data', () => { + describe('but empty values', () => { + describe('and Standard Options `No value` value is NOT set', () => { + it('the default `no data` message should be displayed', () => { + getTestContext({ data: getDataFrame(emptyValuesDataFrame) }); + expect(getTable()).toBeInTheDocument(); + expect(screen.getByText(/No data/i)).toBeInTheDocument(); + }); + }); + + describe('and Standard Options `No value` value IS set', () => { + it('the `No value` Standard Options message should be displayed', () => { + const noValuesDisplayText = 'All healthy'; + getTestContext({ + data: getDataFrame(emptyValuesDataFrame), + fieldConfig: { defaults: { noValue: noValuesDisplayText }, overrides: [] }, + }); + expect(getTable()).toBeInTheDocument(); + expect(screen.getByText(noValuesDisplayText)).toBeInTheDocument(); + }); + }); + }); + it('then correct rows should be rendered', () => { getTestContext(); expect(getTable()).toBeInTheDocument(); @@ -351,7 +397,7 @@ describe('Table', () => { const onColumnResize = jest.fn(); const props: Props = { ariaLabel: 'aria-label', - data: getDefaultDataFrame(), + data: getDataFrame(fullDataFrame), height: 600, width: 800, onSortByChange, @@ -493,7 +539,7 @@ describe('Table', () => { const onColumnResize = jest.fn(); const props: Props = { ariaLabel: 'aria-label', - data: getDefaultDataFrame(), + data: getDataFrame(fullDataFrame), height: 600, width: 800, onSortByChange, @@ -549,7 +595,7 @@ describe('Table', () => { }) ); - const defaultFrame = getDefaultDataFrame(); + const defaultFrame = getDataFrame(fullDataFrame); getTestContext({ data: applyOverrides({ diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 774856991cd..b418401bc94 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -29,6 +29,7 @@ import { getColumns, sortCaseInsensitive, sortNumber, getFooterItems, createFoot const COLUMN_MIN_WIDTH = 150; const FOOTER_ROW_HEIGHT = 36; +const NO_DATA_TEXT = 'No data'; export const Table = memo((props: Props) => { const { @@ -49,6 +50,7 @@ export const Table = memo((props: Props) => { timeRange, enableSharedCrosshair = false, initialRowIndex = undefined, + fieldConfig, } = props; const listRef = useRef(null); @@ -58,6 +60,7 @@ export const Table = memo((props: Props) => { const tableStyles = useTableStyles(theme, cellHeight); const headerHeight = noHeader ? 0 : tableStyles.rowHeight; const [footerItems, setFooterItems] = useState(footerValues); + const noValuesDisplayText = fieldConfig?.defaults?.noValue ?? NO_DATA_TEXT; const footerHeight = useMemo(() => { const EXTENDED_ROW_HEIGHT = FOOTER_ROW_HEIGHT; @@ -324,7 +327,7 @@ export const Table = memo((props: Props) => { ) : (
- No data + {noValuesDisplayText}
)} {footerItems && ( diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index 62a6d1471ee..b3d1f9f3be7 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -2,7 +2,7 @@ import { Property } from 'csstype'; import { FC } from 'react'; import { CellProps, Column, Row, TableState, UseExpandedRowProps } from 'react-table'; -import { DataFrame, Field, KeyValue, SelectableValue, TimeRange } from '@grafana/data'; +import { DataFrame, Field, KeyValue, SelectableValue, TimeRange, FieldConfigSource } from '@grafana/data'; import * as schema from '@grafana/schema'; import { TableStyles } from './styles'; @@ -99,6 +99,7 @@ export interface Props { enableSharedCrosshair?: boolean; // The index of the field value that the table will initialize scrolled to initialRowIndex?: number; + fieldConfig?: FieldConfigSource; } /** diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index 15df95afde4..e3d9d9f774d 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -63,6 +63,7 @@ export function TablePanel(props: Props) { cellHeight={options.cellHeight} timeRange={timeRange} enableSharedCrosshair={config.featureToggles.tableSharedCrosshair && enableSharedCrosshair} + fieldConfig={fieldConfig} /> );