diff --git a/docs/sources/panels-visualizations/visualizations/table/index.md b/docs/sources/panels-visualizations/visualizations/table/index.md index 7f1d15aee2f..8e3e6b13d6c 100644 --- a/docs/sources/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/panels-visualizations/visualizations/table/index.md @@ -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. diff --git a/packages/grafana-schema/src/schema/mudball.cue b/packages/grafana-schema/src/schema/mudball.cue index ccc36703b7a..4a26d49bb9b 100644 --- a/packages/grafana-schema/src/schema/mudball.cue +++ b/packages/grafana-schema/src/schema/mudball.cue @@ -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 diff --git a/packages/grafana-schema/src/schema/mudball.gen.ts b/packages/grafana-schema/src/schema/mudball.gen.ts index 803c8aced88..f209b5bf666 100644 --- a/packages/grafana-schema/src/schema/mudball.gen.ts +++ b/packages/grafana-schema/src/schema/mudball.gen.ts @@ -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 = { }; /** - * 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 = { align: 'auto', - displayMode: TableCellDisplayMode.Auto, inspect: false, }; diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx index 3f0cfc2de07..0a8e36dec9d 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx @@ -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'; diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index d3065703a30..a38179b6417 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -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 { static defaultProps: Partial = { lcdCellWidth: 12, diff --git a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx index a140a7e6840..cdb831d81ab 100644 --- a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx @@ -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 = (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 = () => { diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index 1129e0796ee..1e2c0bcb953 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -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; diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index cec6034cd5c..399466704da 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -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; diff --git a/packages/grafana-ui/src/components/Table/utils.tsx b/packages/grafana-ui/src/components/Table/utils.tsx index 89fb0f11dfa..edaa526e0b1 100644 --- a/packages/grafana-ui/src/components/Table/utils.tsx +++ b/packages/grafana-ui/src/components/Table/utils.tsx @@ -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; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index f50b4610d48..6adfe0fd6c4 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index fcaa3be482a..8be8111780f 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -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; diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index 98a02990e15..fb6073d83e7 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -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, + }; + } +} diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.test.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.test.tsx index 5b5b64436b4..3b74a48a67c 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanel.test.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanel.test.tsx @@ -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'; diff --git a/public/app/plugins/panel/table/TableCellOptionEditor.tsx b/public/app/plugins/panel/table/TableCellOptionEditor.tsx new file mode 100644 index 00000000000..02b21973316 --- /dev/null +++ b/public/app/plugins/panel/table/TableCellOptionEditor.tsx @@ -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 { + 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({}); + + // Update display mode on change + const onCellTypeChange = (v: SelectableValue) => { + 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 = ; + } + + // Setup and inject editor + return ( + <> + + + + + ); +}; diff --git a/public/app/plugins/panel/table/cells/ColorBackgroundCellOptionsEditor.tsx b/public/app/plugins/panel/table/cells/ColorBackgroundCellOptionsEditor.tsx new file mode 100644 index 00000000000..7650d51ec78 --- /dev/null +++ b/public/app/plugins/panel/table/cells/ColorBackgroundCellOptionsEditor.tsx @@ -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) => { + // Set the display mode on change + const onCellOptionsChange = (v: SelectableValue) => { + cellOptions.mode = v.value; + onChange(cellOptions); + }; + + return ( + + +