mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Table: Add custom cell rendering option (#70999)
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user