Table: Add custom cell rendering option (#70999)

This commit is contained in:
Andrej Ocenas
2023-07-17 11:20:33 +02:00
committed by GitHub
parent 44b55a1ca6
commit bca9fc5293
11 changed files with 82 additions and 30 deletions

View File

@@ -656,6 +656,7 @@ export enum TableCellDisplayMode {
ColorBackground = 'color-background', ColorBackground = 'color-background',
ColorBackgroundSolid = 'color-background-solid', ColorBackgroundSolid = 'color-background-solid',
ColorText = 'color-text', ColorText = 'color-text',
Custom = 'custom',
Gauge = 'gauge', Gauge = 'gauge',
GradientGauge = 'gradient-gauge', GradientGauge = 'gradient-gauge',
Image = 'image', Image = 'image',
@@ -883,6 +884,10 @@ export interface TableFieldOptions {
displayMode?: TableCellDisplayMode; displayMode?: TableCellDisplayMode;
filterable?: boolean; filterable?: boolean;
hidden?: boolean; // ?? default is missing or false ?? hidden?: boolean; // ?? default is missing or false ??
/**
* Hides any header for a column, usefull for columns that show some static content or buttons.
*/
hideHeader?: boolean;
inspect: boolean; inspect: boolean;
minWidth?: number; minWidth?: number;
width?: number; width?: number;

View File

@@ -4,7 +4,7 @@ package common
// in the table such as colored text, JSON, gauge, etc. // in the table such as colored text, JSON, gauge, etc.
// The color-background-solid, gradient-gauge, and lcd-gauge // The color-background-solid, gradient-gauge, and lcd-gauge
// modes are deprecated in favor of new cell subOptions // modes are deprecated in favor of new cell subOptions
TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" | "sparkline" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline") TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" | "sparkline"| "custom" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline|Custom")
// Display mode to the "Colored Background" display // Display mode to the "Colored Background" display
// mode for table cells. Either displays a solid color (basic mode) // mode for table cells. Either displays a solid color (basic mode)
@@ -86,5 +86,7 @@ TableFieldOptions: {
hidden?: bool // ?? default is missing or false ?? hidden?: bool // ?? default is missing or false ??
inspect: bool | *false inspect: bool | *false
filterable?: bool filterable?: bool
// Hides any header for a column, usefull for columns that show some static content or buttons.
hideHeader?: bool
} @cuetsy(kind="interface") } @cuetsy(kind="interface")

View File

@@ -10,12 +10,12 @@ import {
Field, Field,
DisplayValue, DisplayValue,
} from '@grafana/data'; } from '@grafana/data';
import { BarGaugeDisplayMode, BarGaugeValueMode } from '@grafana/schema'; import { BarGaugeDisplayMode, BarGaugeValueMode, TableCellDisplayMode } from '@grafana/schema';
import { BarGauge } from '../BarGauge/BarGauge'; import { BarGauge } from '../BarGauge/BarGauge';
import { DataLinksContextMenu, DataLinksContextMenuApi } from '../DataLinks/DataLinksContextMenu'; import { DataLinksContextMenu, DataLinksContextMenuApi } from '../DataLinks/DataLinksContextMenu';
import { TableCellProps, TableCellDisplayMode } from './types'; import { TableCellProps } from './types';
import { getCellOptions } from './utils'; import { getCellOptions } from './utils';
const defaultScale: ThresholdsConfig = { const defaultScale: ThresholdsConfig = {

View File

@@ -3,7 +3,7 @@ import React, { ReactElement } from 'react';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { DisplayValue, formattedValueToString } from '@grafana/data'; import { DisplayValue, formattedValueToString } from '@grafana/data';
import { TableCellBackgroundDisplayMode, TableCellOptions } from '@grafana/schema'; import { TableCellBackgroundDisplayMode, TableCellDisplayMode } from '@grafana/schema';
import { useStyles2 } from '../../themes'; import { useStyles2 } from '../../themes';
import { getCellLinks, getTextColorForAlphaBackground } from '../../utils'; import { getCellLinks, getTextColorForAlphaBackground } from '../../utils';
@@ -12,28 +12,33 @@ import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu';
import { CellActions } from './CellActions'; import { CellActions } from './CellActions';
import { TableStyles } from './styles'; import { TableStyles } from './styles';
import { TableCellDisplayMode, TableCellProps, TableFieldOptions } from './types'; import { TableCellProps, TableFieldOptions, CustomCellRendererProps, TableCellOptions } from './types';
import { getCellOptions } from './utils'; import { getCellOptions } from './utils';
export const DefaultCell = (props: TableCellProps) => { export const DefaultCell = (props: TableCellProps) => {
const { field, cell, tableStyles, row, cellProps } = props; const { field, cell, tableStyles, row, cellProps, frame } = props;
const inspectEnabled = Boolean((field.config.custom as TableFieldOptions)?.inspect); const inspectEnabled = Boolean((field.config.custom as TableFieldOptions)?.inspect);
const displayValue = field.display!(cell.value); const displayValue = field.display!(cell.value);
let value: string | ReactElement;
if (React.isValidElement(cell.value)) {
value = cell.value;
} else {
value = formattedValueToString(displayValue);
}
const showFilters = props.onCellFilterAdded && field.config.filterable; const showFilters = props.onCellFilterAdded && field.config.filterable;
const showActions = (showFilters && cell.value !== undefined) || inspectEnabled; const showActions = (showFilters && cell.value !== undefined) || inspectEnabled;
const cellOptions = getCellOptions(field); const cellOptions = getCellOptions(field);
const cellStyle = getCellStyle(tableStyles, cellOptions, displayValue, inspectEnabled); const cellStyle = getCellStyle(tableStyles, cellOptions, displayValue, inspectEnabled);
const hasLinks = Boolean(getCellLinks(field, row)?.length); const hasLinks = Boolean(getCellLinks(field, row)?.length);
const clearButtonStyle = useStyles2(clearLinkButtonStyles); const clearButtonStyle = useStyles2(clearLinkButtonStyles);
let value: string | ReactElement;
if (cellOptions.type === TableCellDisplayMode.Custom) {
const CustomCellComponent: React.ComponentType<CustomCellRendererProps> = cellOptions.cellComponent;
value = <CustomCellComponent field={field} value={cell.value} rowIndex={row.index} frame={frame} />;
} else {
if (React.isValidElement(cell.value)) {
value = cell.value;
} else {
value = formattedValueToString(displayValue);
}
}
return ( return (
<div {...cellProps} className={cellStyle}> <div {...cellProps} className={cellStyle}>

View File

@@ -260,12 +260,13 @@ export const Table = memo((props: Props) => {
columnIndex={index} columnIndex={index}
columnCount={row.cells.length} columnCount={row.cells.length}
timeRange={timeRange} timeRange={timeRange}
frame={data}
/> />
))} ))}
</div> </div>
); );
}, },
[onCellFilterAdded, page, enablePagination, prepareRow, rows, tableStyles, renderSubTable, timeRange] [onCellFilterAdded, page, enablePagination, prepareRow, rows, tableStyles, renderSubTable, timeRange, data]
); );
const onNavigate = useCallback( const onNavigate = useCallback(

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Cell } from 'react-table'; import { Cell } from 'react-table';
import { TimeRange } from '@grafana/data'; import { TimeRange, DataFrame } from '@grafana/data';
import { TableStyles } from './styles'; import { TableStyles } from './styles';
import { GrafanaTableColumn, TableFilterActionCallback } from './types'; import { GrafanaTableColumn, TableFilterActionCallback } from './types';
@@ -14,9 +14,10 @@ export interface Props {
columnCount: number; columnCount: number;
timeRange?: TimeRange; timeRange?: TimeRange;
userProps?: object; userProps?: object;
frame: DataFrame;
} }
export const TableCell = ({ cell, tableStyles, onCellFilterAdded, timeRange, userProps }: Props) => { export const TableCell = ({ cell, tableStyles, onCellFilterAdded, timeRange, userProps, frame }: Props) => {
const cellProps = cell.getCellProps(); const cellProps = cell.getCellProps();
const field = (cell.column as unknown as GrafanaTableColumn).field; const field = (cell.column as unknown as GrafanaTableColumn).field;
@@ -39,5 +40,6 @@ export const TableCell = ({ cell, tableStyles, onCellFilterAdded, timeRange, use
innerWidth, innerWidth,
timeRange, timeRange,
userProps, userProps,
frame,
}) as React.ReactElement; }) as React.ReactElement;
}; };

View File

@@ -3,16 +3,11 @@ import { FC } from 'react';
import { CellProps, Column, Row, TableState, UseExpandedRowProps } from 'react-table'; import { CellProps, Column, Row, TableState, UseExpandedRowProps } from 'react-table';
import { DataFrame, Field, KeyValue, SelectableValue, TimeRange } from '@grafana/data'; import { DataFrame, Field, KeyValue, SelectableValue, TimeRange } from '@grafana/data';
import { TableCellHeight } from '@grafana/schema'; import * as schema from '@grafana/schema';
import { TableStyles } from './styles'; import { TableStyles } from './styles';
export { export { type FieldTextAlignment, TableCellBackgroundDisplayMode, TableCellDisplayMode } from '@grafana/schema';
type TableFieldOptions,
TableCellDisplayMode,
type FieldTextAlignment,
TableCellBackgroundDisplayMode,
} from '@grafana/schema';
export interface TableRow { export interface TableRow {
[x: string]: any; [x: string]: any;
@@ -37,6 +32,7 @@ export interface TableCellProps extends CellProps<any> {
field: Field; field: Field;
onCellFilterAdded?: TableFilterActionCallback; onCellFilterAdded?: TableFilterActionCallback;
innerWidth: number; innerWidth: number;
frame: DataFrame;
} }
export type CellComponent = FC<TableCellProps>; export type CellComponent = FC<TableCellProps>;
@@ -84,9 +80,38 @@ export interface Props {
footerOptions?: TableFooterCalc; footerOptions?: TableFooterCalc;
footerValues?: FooterItem[]; footerValues?: FooterItem[];
enablePagination?: boolean; enablePagination?: boolean;
cellHeight?: TableCellHeight; cellHeight?: schema.TableCellHeight;
/** @alpha */ /** @alpha */
subData?: DataFrame[]; subData?: DataFrame[];
/** @alpha Used by SparklineCell when provided */ /** @alpha Used by SparklineCell when provided */
timeRange?: TimeRange; timeRange?: TimeRange;
} }
/**
* @alpha
* Props that will be passed to the TableCustomCellOptions.cellComponent when rendered.
*/
export interface CustomCellRendererProps {
field: Field;
rowIndex: number;
frame: DataFrame;
// Would be great to have generic type for this but that would need having a generic DataFrame type where the field
// types could be propagated here.
value: unknown;
}
/**
* @alpha
* Can be used to define completely custom cell contents by providing a custom cellComponent.
*/
export interface TableCustomCellOptions {
cellComponent: FC<CustomCellRendererProps>;
type: schema.TableCellDisplayMode.Custom;
}
// As cue/schema cannot define function types (as main point of schema is to be serializable) we have to extend the
// types here with the dynamic API. This means right now this is not usable as a table panel option for example.
export type TableCellOptions = schema.TableCellOptions | TableCustomCellOptions;
export type TableFieldOptions = Omit<schema.TableFieldOptions, 'cellOptions'> & {
cellOptions: TableCellOptions;
};

View File

@@ -21,7 +21,7 @@ import {
BarGaugeDisplayMode, BarGaugeDisplayMode,
TableAutoCellOptions, TableAutoCellOptions,
TableCellBackgroundDisplayMode, TableCellBackgroundDisplayMode,
TableCellOptions, TableCellDisplayMode,
} from '@grafana/schema'; } from '@grafana/schema';
import { BarGaugeCell } from './BarGaugeCell'; import { BarGaugeCell } from './BarGaugeCell';
@@ -34,7 +34,7 @@ import { RowExpander } from './RowExpander';
import { SparklineCell } from './SparklineCell'; import { SparklineCell } from './SparklineCell';
import { import {
CellComponent, CellComponent,
TableCellDisplayMode, TableCellOptions,
TableFieldOptions, TableFieldOptions,
FooterItem, FooterItem,
GrafanaTableColumn, GrafanaTableColumn,
@@ -130,7 +130,7 @@ export function getColumns(
Cell, Cell,
id: fieldIndex.toString(), id: fieldIndex.toString(),
field: field, field: field,
Header: getFieldDisplayName(field, data), Header: fieldTableOptions.hideHeader ? '' : getFieldDisplayName(field, data),
accessor: (_row: any, i: number) => { accessor: (_row: any, i: number) => {
return field.values[i]; return field.values[i];
}, },
@@ -169,6 +169,7 @@ export function getColumns(
export function getCellComponent(displayMode: TableCellDisplayMode, field: Field): CellComponent { export function getCellComponent(displayMode: TableCellDisplayMode, field: Field): CellComponent {
switch (displayMode) { switch (displayMode) {
case TableCellDisplayMode.Custom:
case TableCellDisplayMode.ColorText: case TableCellDisplayMode.ColorText:
case TableCellDisplayMode.ColorBackground: case TableCellDisplayMode.ColorBackground:
return DefaultCell; return DefaultCell;

View File

@@ -83,7 +83,9 @@ export { SetInterval } from './SetInterval/SetInterval';
export { Table } from './Table/Table'; export { Table } from './Table/Table';
export { export {
TableCellDisplayMode, type TableCustomCellOptions,
type CustomCellRendererProps,
type TableFieldOptions,
type TableSortByFieldState, type TableSortByFieldState,
type TableFooterCalc, type TableFooterCalc,
type AdHocFilterItem, type AdHocFilterItem,

View File

@@ -34,7 +34,6 @@ export {
LegendDisplayMode, LegendDisplayMode,
type VizLegendOptions, type VizLegendOptions,
type OptionsWithLegend, type OptionsWithLegend,
type TableFieldOptions,
TableCellDisplayMode, TableCellDisplayMode,
type FieldTextAlignment, type FieldTextAlignment,
type VizTextDisplayOptions, type VizTextDisplayOptions,

View File

@@ -147,13 +147,23 @@ export const SearchResultsTable = React.memo(
columnIndex={index} columnIndex={index}
columnCount={row.cells.length} columnCount={row.cells.length}
userProps={{ href: url, onClick: onClickItem }} userProps={{ href: url, onClick: onClickItem }}
frame={response.view.dataFrame}
/> />
); );
})} })}
</div> </div>
); );
}, },
[rows, prepareRow, response.view.fields.url?.values, highlightIndex, styles, tableStyles, onClickItem] [
rows,
prepareRow,
response.view.fields.url?.values,
highlightIndex,
styles,
tableStyles,
onClickItem,
response.view.dataFrame,
]
); );
if (!rows.length) { if (!rows.length) {