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
This commit is contained in:
Jev Forsberg
2024-02-23 10:00:24 -07:00
committed by GitHub
parent 92fa868a77
commit a97562906c
4 changed files with 110 additions and 59 deletions
@@ -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<Props> = {}) {
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({
@@ -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<VariableSizeList>(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<FooterItem[] | undefined>(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) => {
</div>
) : (
<div style={{ height: height - headerHeight, width }} className={tableStyles.noData}>
No data
{noValuesDisplayText}
</div>
)}
{footerItems && (
@@ -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;
}
/**
@@ -63,6 +63,7 @@ export function TablePanel(props: Props) {
cellHeight={options.cellHeight}
timeRange={timeRange}
enableSharedCrosshair={config.featureToggles.tableSharedCrosshair && enableSharedCrosshair}
fieldConfig={fieldConfig}
/>
);