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:
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user