Table Panel: Refactor Cell Options to Allow for Options per Cell Type (#59363)

* Update table data structure schema.

* Update table panel configuration options for new structure

* Fix TS errors from refactor

* Separate background and gauge display modes

* Remove the now used Bar Gauge display from the mud

* Fix types up

* Reorganize data structures

* Fix type issues.

* Start stubbing necessary code.

* Continue implementing option refactor

* Change category for cell type selection.

* Consolidate cell options

* Fix various typing issues

* Clean up dead code

* Stub handling display mode changes

* Make subOption editor dynamic

* Setup interface for sub-option editor props

* Remove unused imports

* Remove console.log call

* Persist display mode changes, stub sub options change, update comments.

* Make sure updates from cells are persisted

* Persist sub-option changes

* Update BarGaugeCell to take into account new settings.

* Add deprecated field back

* Remove unecessary options in configuration

* Update default cell to accept new settings

* Make sure color text display works

* Add deprecated property notice

* Use constant as opposed to string

* Make sure we name globally namespaced things uniquely

* Update to use unique name

* Use union type with discriminator.

* Simplify types and operation

* Update type definitons

* Update types

* Update property names in cells

* Remove React.FC usage

* Update option editor signature

* Update options structure

* Change variable name

* Fix "Color Text" display

* Remove debug statement

* Make sure we remain backwards compatible with display mode.

* Add migration for configuration.

* Export BarGaugeDisplayMode from grafana-ui

* Update import

* Fix bar gauge and dashboard migrator tests

* Fix potential undefined references causing test failures

* Fix another potential reference error in DefaultCell

* Try to fix breaking change detection.

* Cache setting changes

* Make sure we return with onChange invocation

* Fixed migrating overrides

* Fix a number of review comments

* Simplify option editors

* Fix unused imports

* Fill out comments for types

* Actually use defaultPanelConfig for editor default

* Move TableCellEditorProps alongside TableCellOptionEditor

* Update docs for table panel

* Also make sure we remove TableCellEditorProps from model file

* Stub migration tests

* Add tests for default config migration

* Add basic overrides test

* Flesh out tests a bit more

* Add inspect to same category as cell editor

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Kyle Cunningham 2023-01-12 18:42:57 +07:00 committed by GitHub
parent d5da42bbbc
commit 80e7f54166
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 697 additions and 88 deletions

View File

@ -68,11 +68,11 @@ Choose how Grafana should align cell contents:
- Center
- Right
## Cell display mode
## Cell type
By default, Grafana automatically chooses display settings. You can override the settings by choosing one of the following options to change all fields.
By default, Grafana automatically chooses display settings. You can override the settings by choosing one of the following options to set the default for all fields. Additional configuration is available for some cell types.
> **Note:** If you set these in the Field tab, then the display modes will apply to all fields, including the time field. Many options will work best if you set them in the Override tab.
> **Note:** If you set these in the Field tab, then the type will apply to all fields, including the time field. Many options will work best if you set them in the Override tab so that they can be restricted to one or more fields.
### Color text
@ -86,13 +86,23 @@ If thresholds are set, then the field background is displayed in the appropriate
{{< figure src="/static/img/docs/tables/color-background.png" max-width="500px" caption="Color background" class="docs-image--no-shadow" >}}
### Gradient gauge
### Gauge
Cells can be displayed as a graphical gauge, with several different presentation types.
#### Basic
The basic mode will show a simple gauge with the threshold levels defining the color of gauge.
{{< figure src="/static/img/docs/tables/basic-gauge.png" max-width="500px" caption="Gradient gauge" class="docs-image--no-shadow" >}}
#### Gradient
The threshold levels define a gradient.
{{< figure src="/static/img/docs/tables/gradient-gauge.png" max-width="500px" caption="Gradient gauge" class="docs-image--no-shadow" >}}
### LCD gauge
#### LCD
The gauge is split up in small cells that are lit or unlit.

View File

@ -201,8 +201,18 @@ BigValueTextMode: "auto" | "value" | "value_and_name" | "name" | "none" @cuetsy(
// TODO docs
FieldTextAlignment: "auto" | "left" | "right" | "center" @cuetsy(kind="type")
// TODO docs
TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image")
// Internally, this is the "type" of cell that's being displayed
// 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" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge")
// : TableCellDisplayMode @cuetsy(kind="enum")
// Display mode to the "Colored Background" display
// mode for table cells. Either displays a solid color (basic mode)
// or a gradient.
TableCellBackgroundDisplayMode: "basic" | "gradient" @cuetsy(kind="enum",memberNames="Basic|Gradient")
// TODO docs
VizTextDisplayOptions: {
@ -246,15 +256,40 @@ VizLegendOptions: {
calcs: [...string]
} @cuetsy(kind="interface")
// TODO docs
// Enum expressing the possible display modes
// for the bar gauge component of Grafana UI
BarGaugeDisplayMode: "basic" | "lcd" | "gradient" @cuetsy(kind="enum")
// TODO docs
// Interface for table cell types that have no additional options.
TableAutoCellOptions: {
type: TableCellDisplayMode
} @cuetsy(kind="interface")
// Allows for the table cell gauge display type to set the gauge mode.
TableBarGaugeCellOptions: {
type: TableCellDisplayMode & "gauge"
mode: BarGaugeDisplayMode
} @cuetsy(kind="interface")
// Allows for the background display mode to be set for the color background cell.
TableColoredBackgroundCellOptions: {
type: TableCellDisplayMode & "color-background"
mode: TableCellBackgroundDisplayMode
} @cuetsy(kind="interface")
// Table cell options. Each cell has a display mode
// and other potential options for that display.
TableCellOptions: TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions @cuetsy(kind="type")
// Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
// Generally defines alignment, filtering capabilties, display options, etc.
TableFieldOptions: {
width?: number
minWidth?: number
align: FieldTextAlignment | *"auto"
displayMode: TableCellDisplayMode | *"auto"
// This field is deprecated in favor of using cellOptions
displayMode?: TableCellDisplayMode
cellOptions: TableCellOptions
hidden?: bool // ?? default is missing or false ??
inspect: bool | *false
filterable?: bool

View File

@ -389,7 +389,10 @@ export enum BigValueTextMode {
export type FieldTextAlignment = ('auto' | 'left' | 'right' | 'center');
/**
* TODO docs
* Internally, this is the "type" of cell that's being displayed
* 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
*/
export enum TableCellDisplayMode {
Auto = 'auto',
@ -397,12 +400,23 @@ export enum TableCellDisplayMode {
ColorBackground = 'color-background',
ColorBackgroundSolid = 'color-background-solid',
ColorText = 'color-text',
Gauge = 'gauge',
GradientGauge = 'gradient-gauge',
Image = 'image',
JSONView = 'json-view',
LcdGauge = 'lcd-gauge',
}
/**
* Display mode to the "Colored Background" display
* mode for table cells. Either displays a solid color (basic mode)
* or a gradient.
*/
export enum TableCellBackgroundDisplayMode {
Basic = 'basic',
Gradient = 'gradient',
}
/**
* TODO docs
*/
@ -465,7 +479,8 @@ export const defaultVizLegendOptions: Partial<VizLegendOptions> = {
};
/**
* TODO docs
* Enum expressing the possible display modes
* for the bar gauge component of Grafana UI
*/
export enum BarGaugeDisplayMode {
Basic = 'basic',
@ -474,11 +489,45 @@ export enum BarGaugeDisplayMode {
}
/**
* TODO docs
* Interface for table cell types that have no additional options.
*/
export interface TableAutoCellOptions {
type: TableCellDisplayMode;
}
/**
* Allows for the table cell gauge display type to set the gauge mode.
*/
export interface TableBarGaugeCellOptions {
mode: BarGaugeDisplayMode;
type: TableCellDisplayMode.Gauge;
}
/**
* Allows for the background display mode to be set for the color background cell.
*/
export interface TableColoredBackgroundCellOptions {
mode: TableCellBackgroundDisplayMode;
type: TableCellDisplayMode.ColorBackground;
}
/**
* Table cell options. Each cell has a display mode
* and other potential options for that display.
*/
export type TableCellOptions = (TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions);
/**
* Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
* Generally defines alignment, filtering capabilties, display options, etc.
*/
export interface TableFieldOptions {
align: FieldTextAlignment;
displayMode: TableCellDisplayMode;
cellOptions: TableCellOptions;
/**
* This field is deprecated in favor of using cellOptions
*/
displayMode?: TableCellDisplayMode;
filterable?: boolean;
hidden?: boolean;
inspect: boolean;
@ -488,7 +537,6 @@ export interface TableFieldOptions {
export const defaultTableFieldOptions: Partial<TableFieldOptions> = {
align: 'auto',
displayMode: TableCellDisplayMode.Auto,
inspect: false,
};

View File

@ -11,6 +11,7 @@ import {
getDisplayProcessor,
createTheme,
} from '@grafana/data';
import { BarGaugeDisplayMode } from '@grafana/schema';
import {
BarGauge,
@ -21,7 +22,6 @@ import {
getBarGradient,
getTitleStyles,
getValuePercent,
BarGaugeDisplayMode,
calculateBarAndValueDimensions,
} from './BarGauge';

View File

@ -20,7 +20,7 @@ import {
VizOrientation,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { VizTextDisplayOptions } from '@grafana/schema';
import { BarGaugeDisplayMode, VizTextDisplayOptions } from '@grafana/schema';
import { Themeable2 } from '../../types';
import { calculateFontSize, measureText } from '../../utils/measureText';
@ -51,12 +51,6 @@ export interface Props extends Themeable2 {
alignmentFactors?: DisplayValueAlignmentFactors;
}
export enum BarGaugeDisplayMode {
Basic = 'basic',
Lcd = 'lcd',
Gradient = 'gradient',
}
export class BarGauge extends PureComponent<Props> {
static defaultProps: Partial<Props> = {
lcdCellWidth: 12,

View File

@ -2,8 +2,9 @@ import { isFunction } from 'lodash';
import React, { FC } from 'react';
import { ThresholdsConfig, ThresholdsMode, VizOrientation, getFieldConfigWithMinMax } from '@grafana/data';
import { BarGaugeDisplayMode } from '@grafana/schema';
import { BarGauge, BarGaugeDisplayMode } from '../BarGauge/BarGauge';
import { BarGauge } from '../BarGauge/BarGauge';
import { DataLinksContextMenu, DataLinksContextMenuApi } from '../DataLinks/DataLinksContextMenu';
import { TableCellProps, TableCellDisplayMode } from './types';
@ -34,12 +35,32 @@ export const BarGaugeCell: FC<TableCellProps> = (props) => {
}
const displayValue = field.display!(cell.value);
let barGaugeMode = BarGaugeDisplayMode.Gradient;
if (field.config.custom && field.config.custom.displayMode === TableCellDisplayMode.LcdGauge) {
barGaugeMode = BarGaugeDisplayMode.Lcd;
} else if (field.config.custom && field.config.custom.displayMode === TableCellDisplayMode.BasicGauge) {
barGaugeMode = BarGaugeDisplayMode.Basic;
// Set default display mode
let barGaugeMode: BarGaugeDisplayMode = BarGaugeDisplayMode.Gradient;
// Support deprecated settings
const usingDeprecatedSettings = field.config.custom.displayMode !== undefined;
// If we're using the old settings format we read the displayMode directly from
// the cell options
if (usingDeprecatedSettings) {
if (
(field.config.custom && field.config.custom.cellOptions.displayMode === TableCellDisplayMode.Gauge) ||
(field.config.custom && field.config.custom.cellOptions.displayMode === BarGaugeDisplayMode.Lcd)
) {
barGaugeMode = BarGaugeDisplayMode.Lcd;
} else if (
(field.config.custom && field.config.custom.cellOptions.displayMode === TableCellDisplayMode.Gauge) ||
(field.config.custom && field.config.custom.cellOptions.displayMode === BarGaugeDisplayMode.Basic)
) {
barGaugeMode = BarGaugeDisplayMode.Basic;
}
}
// Otherwise in the case of sub-options we read specifically from the sub-options
// object in order to get the display mode
else {
barGaugeMode = field.config.custom.cellOptions.mode;
}
const getLinks = () => {

View File

@ -3,6 +3,7 @@ import React, { FC, ReactElement } from 'react';
import tinycolor from 'tinycolor2';
import { DisplayValue, Field, formattedValueToString } from '@grafana/data';
import { TableCellBackgroundDisplayMode } from '@grafana/schema';
import { getCellLinks, getTextColorForAlphaBackground } from '../../utils';
import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu';
@ -56,30 +57,57 @@ function getCellStyle(
displayValue: DisplayValue,
disableOverflowOnHover = false
) {
if (field.config.custom?.displayMode === TableCellDisplayMode.ColorText) {
return tableStyles.buildCellContainerStyle(displayValue.color, undefined, !disableOverflowOnHover);
// How much to darken elements depends upon if we're in dark mode
const darkeningFactor = tableStyles.theme.isDark ? 1 : -0.7;
// See if we're using deprecated settings
const usingDeprecatedSettings = field.config.custom?.displayMode !== undefined;
// Setup color variables
let textColor: string | undefined = undefined;
let bgColor: string | undefined = undefined;
// Set colors using deprecated settings format
if (usingDeprecatedSettings) {
if (field.config.custom?.displayMode === TableCellDisplayMode.ColorText) {
textColor = displayValue.color;
} else if (field.config.custom?.displayMode === TableCellDisplayMode.ColorBackground) {
textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark);
bgColor = tinycolor(displayValue.color).toRgbString();
} else if (
field.config.custom?.displayMode === TableCellDisplayMode.ColorBackground &&
field.config.custom?.backgroundDisplayMode === TableCellBackgroundDisplayMode.Gradient
) {
const bgColor2 = tinycolor(displayValue.color)
.darken(10 * darkeningFactor)
.spin(5);
textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark);
bgColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${displayValue.color})`;
}
}
// Set colors using updated sub-options format
else {
const cellDisplayMode = field.config.custom?.cellOptions?.mode;
const cellDisplayType = field.config.custom?.cellOptions?.type;
if (cellDisplayType === TableCellDisplayMode.ColorText) {
textColor = displayValue.color;
} else if (cellDisplayMode === TableCellBackgroundDisplayMode.Basic) {
textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark);
bgColor = tinycolor(displayValue.color).toRgbString();
} else if (cellDisplayMode === TableCellBackgroundDisplayMode.Gradient) {
const bgColor2 = tinycolor(displayValue.color)
.darken(10 * darkeningFactor)
.spin(5);
textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark);
bgColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${displayValue.color})`;
}
}
if (field.config.custom?.displayMode === TableCellDisplayMode.ColorBackgroundSolid) {
const bgColor = tinycolor(displayValue.color);
const textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark);
return tableStyles.buildCellContainerStyle(textColor, bgColor.toRgbString(), !disableOverflowOnHover);
}
if (field.config.custom?.displayMode === TableCellDisplayMode.ColorBackground) {
const themeFactor = tableStyles.theme.isDark ? 1 : -0.7;
const bgColor2 = tinycolor(displayValue.color)
.darken(10 * themeFactor)
.spin(5)
.toRgbString();
const textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark);
return tableStyles.buildCellContainerStyle(
textColor,
`linear-gradient(120deg, ${bgColor2}, ${displayValue.color})`,
!disableOverflowOnHover
);
// If we have definied colors return those styles
// Otherwise we return default styles
if (textColor !== undefined || bgColor !== undefined) {
return tableStyles.buildCellContainerStyle(textColor, bgColor, !disableOverflowOnHover);
}
return disableOverflowOnHover ? tableStyles.cellContainerNoOverflow : tableStyles.cellContainer;

View File

@ -6,7 +6,12 @@ import { DataFrame, Field, KeyValue, SelectableValue } from '@grafana/data';
import { TableStyles } from './styles';
export { type TableFieldOptions, TableCellDisplayMode, type FieldTextAlignment } from '@grafana/schema';
export {
type TableFieldOptions,
TableCellDisplayMode,
type FieldTextAlignment,
TableCellBackgroundDisplayMode,
} from '@grafana/schema';
export interface TableRow {
[x: string]: any;

View File

@ -114,7 +114,7 @@ export function getColumns(
}
};
const Cell = getCellComponent(fieldTableOptions.displayMode, field);
const Cell = getCellComponent(fieldTableOptions.cellOptions?.type, field);
columns.push({
Cell,
id: fieldIndex.toString(),
@ -163,9 +163,7 @@ export function getCellComponent(displayMode: TableCellDisplayMode, field: Field
return DefaultCell;
case TableCellDisplayMode.Image:
return ImageCell;
case TableCellDisplayMode.LcdGauge:
case TableCellDisplayMode.BasicGauge:
case TableCellDisplayMode.GradientGauge:
case TableCellDisplayMode.Gauge:
return BarGaugeCell;
case TableCellDisplayMode.JSONView:
return JSONViewCell;

View File

@ -102,7 +102,7 @@ export { Gauge } from './Gauge/Gauge';
export { Graph } from './Graph/Graph';
export { GraphWithLegend } from './Graph/GraphWithLegend';
export { GraphContextMenu, GraphContextMenuHeader } from './Graph/GraphContextMenu';
export { BarGauge, BarGaugeDisplayMode } from './BarGauge/BarGauge';
export { BarGauge } from './BarGauge/BarGauge';
export {
VizTooltip,
VizTooltipContainer,
@ -261,7 +261,7 @@ export { LegacyForms, LegacyInputStatus };
// WIP, need renames and exports cleanup
export * from './uPlot/config';
export { ScaleDistribution } from '@grafana/schema';
export { ScaleDistribution, BarGaugeDisplayMode } from '@grafana/schema';
export { UPlotConfigBuilder } from './uPlot/config/UPlotConfigBuilder';
export { UPLOT_AXIS_FONT_SIZE } from './uPlot/config/UPlotAxisBuilder';
export { UPlotChart } from './uPlot/Plot';

View File

@ -197,7 +197,7 @@ describe('DashboardModel', () => {
});
it('dashboard schema version should be set to latest', () => {
expect(model.schemaVersion).toBe(37);
expect(model.schemaVersion).toBe(38);
});
it('graph thresholds should be migrated', () => {
@ -2178,6 +2178,230 @@ describe('when generating the legend for a panel', () => {
});
});
describe('when migrating table cell display mode to cell options', () => {
let model: DashboardModel;
beforeEach(() => {
model = new DashboardModel({
panels: [
// @ts-expect-error
{
id: 1,
type: 'table',
fieldConfig: {
defaults: {
custom: {
align: 'auto',
displayMode: 'color-background',
inspect: false,
},
},
overrides: [],
},
},
// @ts-expect-error
{
id: 2,
type: 'table',
fieldConfig: {
defaults: {
custom: {
align: 'auto',
displayMode: 'color-background-solid',
inspect: false,
},
},
overrides: [],
},
},
// @ts-expect-error
{
id: 3,
type: 'table',
fieldConfig: {
defaults: {
custom: {
align: 'auto',
displayMode: 'lcd-gauge',
inspect: false,
},
},
overrides: [],
},
},
// @ts-expect-error
{
id: 4,
type: 'table',
fieldConfig: {
defaults: {
custom: {
align: 'auto',
displayMode: 'gradient-gauge',
inspect: false,
},
},
overrides: [],
},
},
// @ts-expect-error
{
id: 5,
type: 'table',
fieldConfig: {
defaults: {
custom: {
align: 'auto',
displayMode: 'basic',
inspect: false,
},
},
overrides: [],
},
},
// @ts-expect-error
{
id: 6,
type: 'table',
fieldConfig: {
defaults: {
custom: {
align: 'auto',
displayMode: 'auto',
inspect: false,
},
},
overrides: [
{
matcher: {
id: 'byName',
options: 'value',
},
properties: [
{
id: 'custom.displayMode',
value: 'color-background',
},
],
},
{
matcher: {
id: 'byName',
options: 'value2',
},
properties: [
{
id: 'custom.displayMode',
value: 'lcd-gauge',
},
],
},
{
matcher: {
id: 'byName',
options: 'value3',
},
properties: [
{
id: 'custom.displayMode',
value: 'gradient-gauge',
},
],
},
{
matcher: {
id: 'byName',
options: 'value4',
},
properties: [
{
id: 'custom.align',
value: 'left',
},
{
id: 'custom.displayMode',
value: 'gradient-gauge',
},
],
},
],
},
},
// @ts-expect-error
{
id: 7,
type: 'table',
fieldConfig: {
defaults: {
custom: {
align: 'auto',
displayMode: 'auto',
inspect: false,
},
},
overrides: [],
},
},
],
schemaVersion: 37,
});
});
it('should migrate gradient color background option to the new option format', () => {
const cellOptions = model.panels[0].fieldConfig.defaults.custom.cellOptions;
expect(cellOptions).toEqual({ type: 'color-background', mode: 'gradient' });
});
it('should migrate solid color background option to the new option format', () => {
const cellOptions = model.panels[1].fieldConfig.defaults.custom.cellOptions;
expect(cellOptions).toEqual({ type: 'color-background', mode: 'basic' });
});
it('should migrate LCD gauge option to the new option format', () => {
const cellOptions = model.panels[2].fieldConfig.defaults.custom.cellOptions;
expect(cellOptions).toEqual({ type: 'gauge', mode: 'lcd' });
});
it('should migrate gradient gauge option to the new option format', () => {
const cellOptions = model.panels[3].fieldConfig.defaults.custom.cellOptions;
expect(cellOptions).toEqual({ type: 'gauge', mode: 'gradient' });
});
it('should migrate basic gauge option to the new option format', () => {
const cellOptions = model.panels[4].fieldConfig.defaults.custom.cellOptions;
expect(cellOptions).toEqual({ type: 'gauge', mode: 'basic' });
});
it('should migrate from display mode to cell options in field overrides', () => {
const fieldConfig = model.panels[5].fieldConfig;
expect(fieldConfig.overrides[0].properties[0]).toEqual({
id: 'custom.cellOptions',
value: { type: 'color-background', mode: 'gradient' },
});
expect(fieldConfig.overrides[1].properties[0]).toEqual({
id: 'custom.cellOptions',
value: { type: 'gauge', mode: 'lcd' },
});
expect(fieldConfig.overrides[2].properties[0]).toEqual({
id: 'custom.cellOptions',
value: { type: 'gauge', mode: 'gradient' },
});
});
it('should migrate from display mode to cell options in field overrides with other overrides present', () => {
const override = model.panels[5].fieldConfig.overrides[3];
expect(override.properties[1]).toEqual({ id: 'custom.cellOptions', value: { type: 'gauge', mode: 'gradient' } });
});
it('should migrate cell display modes without options', () => {
const fieldConfig = model.panels[6].fieldConfig;
expect(fieldConfig.defaults.custom.cellOptions).toEqual({ type: 'auto' });
});
});
function createRow(options: any, panelDescriptions: any[]) {
const PANEL_HEIGHT_STEP = GRID_CELL_HEIGHT + GRID_CELL_VMARGIN;
const { collapse, showTitle, title, repeat, repeatIteration } = options;

View File

@ -26,7 +26,8 @@ import {
import { labelsToFieldsTransformer } from '@grafana/data/src/transformations/transformers/labelsToFields';
import { mergeTransformer } from '@grafana/data/src/transformations/transformers/merge';
import { getDataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
import { AxisPlacement, GraphFieldConfig } from '@grafana/ui';
import { BarGaugeDisplayMode, TableCellBackgroundDisplayMode, TableCellOptions } from '@grafana/schema';
import { AxisPlacement, GraphFieldConfig, TableCellDisplayMode } from '@grafana/ui';
import { getAllOptionEditors, getAllStandardFieldConfigs } from 'app/core/components/OptionsUI/registry';
import { config } from 'app/core/config';
import {
@ -77,7 +78,7 @@ export class DashboardMigrator {
let i, j, k, n;
const oldVersion = this.dashboard.schemaVersion;
const panelUpgrades: PanelSchemeUpgradeHandler[] = [];
this.dashboard.schemaVersion = 37;
this.dashboard.schemaVersion = 38;
if (oldVersion === this.dashboard.schemaVersion) {
return;
@ -807,6 +808,39 @@ export class DashboardMigrator {
});
}
// Update old table cell display configuration to the new
// format which uses an object for configuration
if (oldVersion < 38) {
panelUpgrades.push((panel: PanelModel) => {
if (panel.type === 'table' && panel.fieldConfig !== undefined) {
const displayMode = panel.fieldConfig.defaults?.custom?.displayMode;
// Update field configuration
if (displayMode !== undefined) {
// Migrate any options for the panel
panel.fieldConfig.defaults.custom.cellOptions = migrateTableCellConfig(displayMode);
// Delete the legacy field
delete panel.fieldConfig.defaults.custom.displayMode;
}
// Update any overrides referencing the cell display mode
for (let i = 0; i < panel.fieldConfig.overrides.length; i++) {
for (let j = 0; j < panel.fieldConfig.overrides[i].properties.length; j++) {
let overrideDisplayMode = panel.fieldConfig.overrides[i].properties[j].value;
if (panel.fieldConfig.overrides[i].properties[j].id === 'custom.displayMode') {
panel.fieldConfig.overrides[i].properties[j].id = 'custom.cellOptions';
panel.fieldConfig.overrides[i].properties[j].value = migrateTableCellConfig(overrideDisplayMode);
}
}
}
}
return panel;
});
}
if (panelUpgrades.length === 0) {
return;
}
@ -1330,3 +1364,50 @@ function ensureXAxisVisibility(panel: PanelModel) {
return panel;
}
/**
* Migrates table cell display mode to new object format.
*
* @param displayMode The display mode of the cell
* @returns TableCellOptions object in the correct format
* relative to the old display mode.
*/
function migrateTableCellConfig(displayMode: TableCellDisplayMode): TableCellOptions {
switch (displayMode) {
// In the case of the gauge we move to a different option
case 'basic':
case 'gradient-gauge':
case 'lcd-gauge':
let gaugeMode = BarGaugeDisplayMode.Basic;
if (displayMode === 'gradient-gauge') {
gaugeMode = BarGaugeDisplayMode.Gradient;
} else if (displayMode === 'lcd-gauge') {
gaugeMode = BarGaugeDisplayMode.Lcd;
}
return {
type: TableCellDisplayMode.Gauge,
mode: gaugeMode,
};
// Also true in the case of the color background
case 'color-background':
case 'color-background-solid':
let mode = TableCellBackgroundDisplayMode.Basic;
// Set the new mode field, somewhat confusingly the
// color-background mode is for gradient display
if (displayMode === 'color-background') {
mode = TableCellBackgroundDisplayMode.Gradient;
}
return {
type: TableCellDisplayMode.ColorBackground,
mode: mode,
};
default:
return {
type: displayMode,
};
}
}

View File

@ -4,7 +4,7 @@ import React from 'react';
import { dateMath, dateTime, EventBus, LoadingState, TimeRange, toDataFrame, VizOrientation } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { BarGaugeDisplayMode } from '@grafana/ui';
import { BarGaugeDisplayMode } from '@grafana/schema';
import { BarGaugePanel, BarGaugePanelProps } from './BarGaugePanel';

View File

@ -0,0 +1,106 @@
import { merge } from 'lodash';
import React, { ReactNode, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { TableCellOptions } from '@grafana/schema';
import { Field, HorizontalGroup, Select, TableCellDisplayMode } from '@grafana/ui';
import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor';
import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor';
const cellDisplayModeOptions = [
{ value: TableCellDisplayMode.Auto, label: 'Auto' },
{ value: TableCellDisplayMode.ColorText, label: 'Colored text' },
{ value: TableCellDisplayMode.ColorBackground, label: 'Colored background' },
{ value: TableCellDisplayMode.Gauge, label: 'Gauge' },
{ value: TableCellDisplayMode.JSONView, label: 'JSON View' },
{ value: TableCellDisplayMode.Image, label: 'Image' },
];
// The props that any cell type editor are expected
// to handle. In this case the generic type should
// be a discriminated interface of TableCellOptions
export interface TableCellEditorProps<T> {
cellOptions: T;
onChange: (value: T) => void;
}
// Maps display modes to editor components
interface ComponentMap {
[key: string]: Function;
}
// Maps cell type to options for caching
interface SettingMap {
[key: string]: TableCellOptions;
}
/*
Map of display modes to editor components
Additional cell types can be placed here
---
A cell editor is expected to be a functional
component that accepts options and displays
them in a form.
*/
const displayModeComponentMap: ComponentMap = {
[TableCellDisplayMode.Gauge]: BarGaugeCellOptionsEditor,
[TableCellDisplayMode.ColorBackground]: ColorBackgroundCellOptionsEditor,
};
interface Props {
value: TableCellOptions;
onChange: (v: TableCellOptions) => void;
}
export const TableCellOptionEditor = ({ value, onChange }: Props) => {
const cellType = value.type;
let editor: ReactNode | null = null;
let [settingCache, setSettingCache] = useState<SettingMap>({});
// Update display mode on change
const onCellTypeChange = (v: SelectableValue<TableCellDisplayMode>) => {
if (v.value !== undefined) {
// Set the new type of cell starting
// with default settings
value = {
type: v.value,
};
// When changing cell type see if there were previously stored
// settings and merge those with the changed value
if (settingCache[v.value] !== undefined && Object.keys(settingCache[v.value]).length > 1) {
settingCache[v.value] = merge(value, settingCache[v.value]);
setSettingCache(settingCache);
onChange(settingCache[v.value]);
return;
}
onChange(value);
}
};
// When options for a cell change we merge
// any option changes with our options object
const onCellOptionsChange = (options: TableCellOptions) => {
settingCache[value.type] = merge(value, options);
setSettingCache(settingCache);
onChange(settingCache[value.type]);
};
// Setup specific cell editor
if (cellType !== undefined && displayModeComponentMap[cellType] !== undefined) {
let Comp: Function = displayModeComponentMap[cellType];
editor = <Comp cellOptions={value} onChange={onCellOptionsChange} />;
}
// Setup and inject editor
return (
<>
<Field>
<Select options={cellDisplayModeOptions} value={cellType} onChange={onCellTypeChange} />
</Field>
<HorizontalGroup>{editor}</HorizontalGroup>
</>
);
};

View File

@ -0,0 +1,32 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { BarGaugeDisplayMode, TableBarGaugeCellOptions } from '@grafana/schema';
import { Field, HorizontalGroup, Select } from '@grafana/ui';
import { TableCellEditorProps } from '../TableCellOptionEditor';
const barGaugeOpts: SelectableValue[] = [
{ value: BarGaugeDisplayMode.Basic, label: 'Basic' },
{ value: BarGaugeDisplayMode.Gradient, label: 'Gradient' },
{ value: BarGaugeDisplayMode.Lcd, label: 'Retro LCD' },
];
export const BarGaugeCellOptionsEditor = ({
cellOptions,
onChange,
}: TableCellEditorProps<TableBarGaugeCellOptions>) => {
// Set the display mode on change
const onCellOptionsChange = (v: SelectableValue) => {
cellOptions.mode = v.value;
onChange(cellOptions);
};
return (
<HorizontalGroup>
<Field label="Gauge Display Mode">
<Select value={cellOptions?.mode} onChange={onCellOptionsChange} options={barGaugeOpts} />
</Field>
</HorizontalGroup>
);
};

View File

@ -0,0 +1,31 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { TableCellBackgroundDisplayMode, TableColoredBackgroundCellOptions } from '@grafana/schema';
import { HorizontalGroup, Select, Field } from '@grafana/ui';
import { TableCellEditorProps } from '../TableCellOptionEditor';
const colorBackgroundOpts: SelectableValue[] = [
{ value: TableCellBackgroundDisplayMode.Basic, label: 'Basic' },
{ value: TableCellBackgroundDisplayMode.Gradient, label: 'Gradient' },
];
export const ColorBackgroundCellOptionsEditor = ({
cellOptions,
onChange,
}: TableCellEditorProps<TableColoredBackgroundCellOptions>) => {
// Set the display mode on change
const onCellOptionsChange = (v: SelectableValue) => {
cellOptions.mode = v.value;
onChange(cellOptions);
};
return (
<HorizontalGroup>
<Field label="Background display mode">
<Select value={cellOptions?.mode} onChange={onCellOptionsChange} options={colorBackgroundOpts} />
</Field>
</HorizontalGroup>
);
};

View File

@ -32,7 +32,9 @@ export const defaultPanelOptions: PanelOptions = {
};
export const defaultPanelFieldConfig: TableFieldOptions = {
displayMode: TableCellDisplayMode.Auto,
align: 'auto',
cellOptions: {
type: TableCellDisplayMode.Auto,
},
inspect: false,
};

View File

@ -5,17 +5,19 @@ import {
PanelPlugin,
ReducerID,
standardEditorsRegistry,
identityOverrideProcessor,
} from '@grafana/data';
import { TableFieldOptions } from '@grafana/schema';
import { TableCellDisplayMode } from '@grafana/ui';
import { TableFieldOptions, TableCellOptions, TableCellDisplayMode } from '@grafana/schema';
import { PaginationEditor } from './PaginationEditor';
import { TableCellOptionEditor } from './TableCellOptionEditor';
import { TablePanel } from './TablePanel';
import { tableMigrationHandler, tablePanelChangedHandler } from './migrations';
import { PanelOptions, defaultPanelOptions, defaultPanelFieldConfig } from './models.gen';
import { TableSuggestionsSupplier } from './suggestions';
const footerCategory = 'Table footer';
const cellCategory = ['Cell Options'];
export const plugin = new PanelPlugin<PanelOptions, TableFieldOptions>(TablePanel)
.setPanelChangeHandler(tablePanelChangedHandler)
@ -59,37 +61,29 @@ export const plugin = new PanelPlugin<PanelOptions, TableFieldOptions>(TablePane
},
defaultValue: defaultPanelFieldConfig.align,
})
.addSelect({
path: 'displayMode',
name: 'Cell display mode',
description: 'Color text, background, show as gauge, etc',
settings: {
options: [
{ value: TableCellDisplayMode.Auto, label: 'Auto' },
{ value: TableCellDisplayMode.ColorText, label: 'Color text' },
{ value: TableCellDisplayMode.ColorBackground, label: 'Color background (gradient)' },
{ value: TableCellDisplayMode.ColorBackgroundSolid, label: 'Color background (solid)' },
{ value: TableCellDisplayMode.GradientGauge, label: 'Gradient gauge' },
{ value: TableCellDisplayMode.LcdGauge, label: 'LCD gauge' },
{ value: TableCellDisplayMode.BasicGauge, label: 'Basic gauge' },
{ value: TableCellDisplayMode.JSONView, label: 'JSON View' },
{ value: TableCellDisplayMode.Image, label: 'Image' },
],
},
defaultValue: defaultPanelFieldConfig.displayMode,
.addCustomEditor<void, TableCellOptions>({
id: 'cellOptions',
path: 'cellOptions',
name: 'Cell Type',
editor: TableCellOptionEditor,
override: TableCellOptionEditor,
defaultValue: defaultPanelFieldConfig.cellOptions,
process: identityOverrideProcessor,
category: cellCategory,
shouldApply: () => true,
})
.addBooleanSwitch({
path: 'inspect',
name: 'Cell value inspect',
description: 'Enable cell value inspection in a modal window',
defaultValue: false,
category: cellCategory,
showIf: (cfg) => {
return (
cfg.displayMode === TableCellDisplayMode.Auto ||
cfg.displayMode === TableCellDisplayMode.JSONView ||
cfg.displayMode === TableCellDisplayMode.ColorText ||
cfg.displayMode === TableCellDisplayMode.ColorBackground ||
cfg.displayMode === TableCellDisplayMode.ColorBackgroundSolid
cfg.cellOptions.type === TableCellDisplayMode.Auto ||
cfg.cellOptions.type === TableCellDisplayMode.JSONView ||
cfg.cellOptions.type === TableCellDisplayMode.ColorText ||
cfg.cellOptions.type === TableCellDisplayMode.ColorBackground
);
},
})