From bca9fc5293429920ac1948194f62f4de69a70153 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Mon, 17 Jul 2023 11:20:33 +0200 Subject: [PATCH] Table: Add custom cell rendering option (#70999) --- .../grafana-schema/src/common/common.gen.ts | 5 +++ packages/grafana-schema/src/common/table.cue | 4 +- .../src/components/Table/BarGaugeCell.tsx | 4 +- .../src/components/Table/DefaultCell.tsx | 25 ++++++----- .../grafana-ui/src/components/Table/Table.tsx | 3 +- .../src/components/Table/TableCell.tsx | 6 ++- .../grafana-ui/src/components/Table/types.ts | 41 +++++++++++++++---- .../grafana-ui/src/components/Table/utils.ts | 7 ++-- packages/grafana-ui/src/components/index.ts | 4 +- packages/grafana-ui/src/schema.ts | 1 - .../page/components/SearchResultsTable.tsx | 12 +++++- 11 files changed, 82 insertions(+), 30 deletions(-) diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index d574469dd30..b51eae11884 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -656,6 +656,7 @@ export enum TableCellDisplayMode { ColorBackground = 'color-background', ColorBackgroundSolid = 'color-background-solid', ColorText = 'color-text', + Custom = 'custom', Gauge = 'gauge', GradientGauge = 'gradient-gauge', Image = 'image', @@ -883,6 +884,10 @@ export interface TableFieldOptions { displayMode?: TableCellDisplayMode; filterable?: boolean; 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; minWidth?: number; width?: number; diff --git a/packages/grafana-schema/src/common/table.cue b/packages/grafana-schema/src/common/table.cue index b5045dae96f..a5e06ee9113 100644 --- a/packages/grafana-schema/src/common/table.cue +++ b/packages/grafana-schema/src/common/table.cue @@ -4,7 +4,7 @@ package common // in the table such as colored text, JSON, gauge, etc. // The color-background-solid, gradient-gauge, and lcd-gauge // 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 // mode for table cells. Either displays a solid color (basic mode) @@ -86,5 +86,7 @@ TableFieldOptions: { hidden?: bool // ?? default is missing or false ?? inspect: bool | *false filterable?: bool + // Hides any header for a column, usefull for columns that show some static content or buttons. + hideHeader?: bool } @cuetsy(kind="interface") diff --git a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx index e43c776c3f0..0fd4ae84589 100644 --- a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx @@ -10,12 +10,12 @@ import { Field, DisplayValue, } from '@grafana/data'; -import { BarGaugeDisplayMode, BarGaugeValueMode } from '@grafana/schema'; +import { BarGaugeDisplayMode, BarGaugeValueMode, TableCellDisplayMode } from '@grafana/schema'; import { BarGauge } from '../BarGauge/BarGauge'; import { DataLinksContextMenu, DataLinksContextMenuApi } from '../DataLinks/DataLinksContextMenu'; -import { TableCellProps, TableCellDisplayMode } from './types'; +import { TableCellProps } from './types'; import { getCellOptions } from './utils'; const defaultScale: ThresholdsConfig = { diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index 1f55e3cc588..050dbdf1e02 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -3,7 +3,7 @@ import React, { ReactElement } from 'react'; import tinycolor from 'tinycolor2'; import { DisplayValue, formattedValueToString } from '@grafana/data'; -import { TableCellBackgroundDisplayMode, TableCellOptions } from '@grafana/schema'; +import { TableCellBackgroundDisplayMode, TableCellDisplayMode } from '@grafana/schema'; import { useStyles2 } from '../../themes'; import { getCellLinks, getTextColorForAlphaBackground } from '../../utils'; @@ -12,28 +12,33 @@ import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; import { CellActions } from './CellActions'; import { TableStyles } from './styles'; -import { TableCellDisplayMode, TableCellProps, TableFieldOptions } from './types'; +import { TableCellProps, TableFieldOptions, CustomCellRendererProps, TableCellOptions } from './types'; import { getCellOptions } from './utils'; 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 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 showActions = (showFilters && cell.value !== undefined) || inspectEnabled; const cellOptions = getCellOptions(field); const cellStyle = getCellStyle(tableStyles, cellOptions, displayValue, inspectEnabled); const hasLinks = Boolean(getCellLinks(field, row)?.length); const clearButtonStyle = useStyles2(clearLinkButtonStyles); + let value: string | ReactElement; + + if (cellOptions.type === TableCellDisplayMode.Custom) { + const CustomCellComponent: React.ComponentType = cellOptions.cellComponent; + value = ; + } else { + if (React.isValidElement(cell.value)) { + value = cell.value; + } else { + value = formattedValueToString(displayValue); + } + } return (
diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index d3cd40f6b71..6042f3df5e1 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -260,12 +260,13 @@ export const Table = memo((props: Props) => { columnIndex={index} columnCount={row.cells.length} timeRange={timeRange} + frame={data} /> ))}
); }, - [onCellFilterAdded, page, enablePagination, prepareRow, rows, tableStyles, renderSubTable, timeRange] + [onCellFilterAdded, page, enablePagination, prepareRow, rows, tableStyles, renderSubTable, timeRange, data] ); const onNavigate = useCallback( diff --git a/packages/grafana-ui/src/components/Table/TableCell.tsx b/packages/grafana-ui/src/components/Table/TableCell.tsx index ef8f18f18af..329ae6568f3 100644 --- a/packages/grafana-ui/src/components/Table/TableCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableCell.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Cell } from 'react-table'; -import { TimeRange } from '@grafana/data'; +import { TimeRange, DataFrame } from '@grafana/data'; import { TableStyles } from './styles'; import { GrafanaTableColumn, TableFilterActionCallback } from './types'; @@ -14,9 +14,10 @@ export interface Props { columnCount: number; timeRange?: TimeRange; 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 field = (cell.column as unknown as GrafanaTableColumn).field; @@ -39,5 +40,6 @@ export const TableCell = ({ cell, tableStyles, onCellFilterAdded, timeRange, use innerWidth, timeRange, userProps, + frame, }) as React.ReactElement; }; diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index fd2cb55dba9..22c0d1e9296 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -3,16 +3,11 @@ import { FC } from 'react'; import { CellProps, Column, Row, TableState, UseExpandedRowProps } from 'react-table'; import { DataFrame, Field, KeyValue, SelectableValue, TimeRange } from '@grafana/data'; -import { TableCellHeight } from '@grafana/schema'; +import * as schema from '@grafana/schema'; import { TableStyles } from './styles'; -export { - type TableFieldOptions, - TableCellDisplayMode, - type FieldTextAlignment, - TableCellBackgroundDisplayMode, -} from '@grafana/schema'; +export { type FieldTextAlignment, TableCellBackgroundDisplayMode, TableCellDisplayMode } from '@grafana/schema'; export interface TableRow { [x: string]: any; @@ -37,6 +32,7 @@ export interface TableCellProps extends CellProps { field: Field; onCellFilterAdded?: TableFilterActionCallback; innerWidth: number; + frame: DataFrame; } export type CellComponent = FC; @@ -84,9 +80,38 @@ export interface Props { footerOptions?: TableFooterCalc; footerValues?: FooterItem[]; enablePagination?: boolean; - cellHeight?: TableCellHeight; + cellHeight?: schema.TableCellHeight; /** @alpha */ subData?: DataFrame[]; /** @alpha Used by SparklineCell when provided */ 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; + 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 & { + cellOptions: TableCellOptions; +}; diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts index 599653087d3..7bd0ca96807 100644 --- a/packages/grafana-ui/src/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -21,7 +21,7 @@ import { BarGaugeDisplayMode, TableAutoCellOptions, TableCellBackgroundDisplayMode, - TableCellOptions, + TableCellDisplayMode, } from '@grafana/schema'; import { BarGaugeCell } from './BarGaugeCell'; @@ -34,7 +34,7 @@ import { RowExpander } from './RowExpander'; import { SparklineCell } from './SparklineCell'; import { CellComponent, - TableCellDisplayMode, + TableCellOptions, TableFieldOptions, FooterItem, GrafanaTableColumn, @@ -130,7 +130,7 @@ export function getColumns( Cell, id: fieldIndex.toString(), field: field, - Header: getFieldDisplayName(field, data), + Header: fieldTableOptions.hideHeader ? '' : getFieldDisplayName(field, data), accessor: (_row: any, i: number) => { return field.values[i]; }, @@ -169,6 +169,7 @@ export function getColumns( export function getCellComponent(displayMode: TableCellDisplayMode, field: Field): CellComponent { switch (displayMode) { + case TableCellDisplayMode.Custom: case TableCellDisplayMode.ColorText: case TableCellDisplayMode.ColorBackground: return DefaultCell; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 98969e5e40e..ca826cc946f 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -83,7 +83,9 @@ export { SetInterval } from './SetInterval/SetInterval'; export { Table } from './Table/Table'; export { - TableCellDisplayMode, + type TableCustomCellOptions, + type CustomCellRendererProps, + type TableFieldOptions, type TableSortByFieldState, type TableFooterCalc, type AdHocFilterItem, diff --git a/packages/grafana-ui/src/schema.ts b/packages/grafana-ui/src/schema.ts index 3bec62ab5c6..b52babdb542 100644 --- a/packages/grafana-ui/src/schema.ts +++ b/packages/grafana-ui/src/schema.ts @@ -34,7 +34,6 @@ export { LegendDisplayMode, type VizLegendOptions, type OptionsWithLegend, - type TableFieldOptions, TableCellDisplayMode, type FieldTextAlignment, type VizTextDisplayOptions, diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index f6d4e9ed962..bc32f4fe2d5 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -147,13 +147,23 @@ export const SearchResultsTable = React.memo( columnIndex={index} columnCount={row.cells.length} userProps={{ href: url, onClick: onClickItem }} + frame={response.view.dataFrame} /> ); })} ); }, - [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) {