From c14c7b6874aef732d4a6a73be26149bf40a12d98 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Fri, 15 Jan 2021 13:06:40 +0100 Subject: [PATCH] Table: migrate old-table config to new table config (#30142) * feat(tablepanel): add migration button to old table panel config * feat(tablepanel): migrate old table transformations * feat(tablepanel): migrate old styles to config overrides * feat(tablepanel): migrate catch all style override to panel defaults * refactor(tablepanel): clean up typings * refactor(tablepanel): base threshold as -Infinity * feat(tablepanel): migrate align to new table config overrides * feat(tablepanel): migrate links to new table overrides * refactor(tabelpanel): clean up threshold migrations * feat(tablepanel): introduce table transform to merge * feat(tablepanel): add note informing user to manually update links with cell values --- .../app/plugins/panel/table-old/editor.html | 18 ++ public/app/plugins/panel/table-old/module.ts | 9 + .../__snapshots__/migrations.test.ts.snap | 219 ++++++++++++++++++ .../plugins/panel/table/migrations.test.ts | 132 +++++++++++ public/app/plugins/panel/table/migrations.ts | 215 ++++++++++++++++- 5 files changed, 591 insertions(+), 2 deletions(-) create mode 100644 public/app/plugins/panel/table/__snapshots__/migrations.test.ts.snap create mode 100644 public/app/plugins/panel/table/migrations.test.ts diff --git a/public/app/plugins/panel/table-old/editor.html b/public/app/plugins/panel/table-old/editor.html index 2dcc1fa8590..bba1c9fa4fe 100644 --- a/public/app/plugins/panel/table-old/editor.html +++ b/public/app/plugins/panel/table-old/editor.html @@ -1,4 +1,22 @@
+
+
Table migration
+

+ This panel is deprecated. Please migrate to the new Table panel. +

+

+ +

+

NOTE: Sorting is not persisted after migration.

+

+ NOTE: Row color mode is no longer supported and will fallback to cell color mode. +

+

+ NOTE: Links that specify cell values will need to be updated manually after migration. +

+
Data
diff --git a/public/app/plugins/panel/table-old/module.ts b/public/app/plugins/panel/table-old/module.ts index 2b3660c99d5..37bcba40e87 100644 --- a/public/app/plugins/panel/table-old/module.ts +++ b/public/app/plugins/panel/table-old/module.ts @@ -18,6 +18,8 @@ export class TablePanelCtrl extends MetricsPanelCtrl { dataRaw: any; table: any; renderer: any; + panelHasRowColorMode: boolean; + panelHasLinks: boolean; panelDefaults: any = { targets: [{}], @@ -65,6 +67,9 @@ export class TablePanelCtrl extends MetricsPanelCtrl { _.defaults(this.panel, this.panelDefaults); + this.panelHasRowColorMode = Boolean(this.panel.styles.find((style: any) => style.colorMode === 'row')); + this.panelHasLinks = Boolean(this.panel.styles.find((style: any) => style.link)); + this.events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this)); this.events.on(PanelEvents.dataSnapshotLoad, this.onDataReceived.bind(this)); this.events.on(PanelEvents.editModeInitialized, this.onInitEditMode.bind(this)); @@ -75,6 +80,10 @@ export class TablePanelCtrl extends MetricsPanelCtrl { this.addEditorTab('Column Styles', columnOptionsTab, 3); } + migrateToPanel(type: string) { + this.onPluginTypeChange(config.panels[type]); + } + issueQueries(datasource: any) { this.pageIndex = 0; diff --git a/public/app/plugins/panel/table/__snapshots__/migrations.test.ts.snap b/public/app/plugins/panel/table/__snapshots__/migrations.test.ts.snap new file mode 100644 index 00000000000..992a95e9554 --- /dev/null +++ b/public/app/plugins/panel/table/__snapshots__/migrations.test.ts.snap @@ -0,0 +1,219 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table Migrations migrates styles to field config overrides and defaults 1`] = ` +Object { + "fieldConfig": Object { + "defaults": Object { + "custom": Object { + "align": "right", + "displayMode": undefined, + }, + "decimals": 2, + "displayName": "", + "unit": "short", + }, + "overrides": Array [ + Object { + "matcher": Object { + "id": "byName", + "options": "Time", + }, + "properties": Array [ + Object { + "id": "displayName", + "value": "Time", + }, + Object { + "id": "unit", + "value": "time: YYYY-MM-DD HH:mm:ss", + }, + Object { + "id": "custom.align", + "value": null, + }, + ], + }, + Object { + "matcher": Object { + "id": "byName", + "options": "ColorCell", + }, + "properties": Array [ + Object { + "id": "unit", + "value": "currencyUSD", + }, + Object { + "id": "decimals", + "value": 2, + }, + Object { + "id": "custom.displayMode", + "value": "color-background", + }, + Object { + "id": "custom.align", + "value": "left", + }, + Object { + "id": "thresholds", + "value": Object { + "mode": "absolute", + "steps": Array [ + Object { + "color": "rgba(245, 54, 54, 0.9)", + "value": -Infinity, + }, + Object { + "color": "rgba(237, 129, 40, 0.89)", + "value": 5, + }, + Object { + "color": "rgba(50, 172, 45, 0.97)", + "value": 10, + }, + ], + }, + }, + ], + }, + Object { + "matcher": Object { + "id": "byName", + "options": "ColorValue", + }, + "properties": Array [ + Object { + "id": "unit", + "value": "Bps", + }, + Object { + "id": "decimals", + "value": 2, + }, + Object { + "id": "links", + "value": Array [ + Object { + "targetBlank": true, + "title": "", + "url": "http://www.grafana.com", + }, + ], + }, + Object { + "id": "custom.displayMode", + "value": "color-text", + }, + Object { + "id": "custom.align", + "value": null, + }, + Object { + "id": "thresholds", + "value": Object { + "mode": "absolute", + "steps": Array [ + Object { + "color": "rgba(245, 54, 54, 0.9)", + "value": -Infinity, + }, + Object { + "color": "rgba(237, 129, 40, 0.89)", + "value": 5, + }, + Object { + "color": "rgba(50, 172, 45, 0.97)", + "value": 10, + }, + ], + }, + }, + ], + }, + ], + }, + "transformations": Array [], +} +`; + +exports[`Table Migrations migrates transform out to core transforms 1`] = ` +Object { + "fieldConfig": Object { + "defaults": Object { + "custom": Object {}, + }, + "overrides": Array [], + }, + "transformations": Array [ + Object { + "id": "seriesToColumns", + "options": Object { + "reducers": Array [], + }, + }, + ], +} +`; + +exports[`Table Migrations migrates transform out to core transforms 2`] = ` +Object { + "fieldConfig": Object { + "defaults": Object { + "custom": Object {}, + }, + "overrides": Array [], + }, + "transformations": Array [ + Object { + "id": "seriesToRows", + "options": Object { + "reducers": Array [], + }, + }, + ], +} +`; + +exports[`Table Migrations migrates transform out to core transforms 3`] = ` +Object { + "fieldConfig": Object { + "defaults": Object { + "custom": Object {}, + }, + "overrides": Array [], + }, + "transformations": Array [ + Object { + "id": "reduce", + "options": Object { + "includeTimeField": false, + "reducers": Array [ + "mean", + "max", + "last", + ], + }, + }, + ], +} +`; + +exports[`Table Migrations migrates transform out to core transforms 4`] = ` +Object { + "fieldConfig": Object { + "defaults": Object { + "custom": Object {}, + }, + "overrides": Array [], + }, + "transformations": Array [ + Object { + "id": "merge", + "options": Object { + "reducers": Array [], + }, + }, + ], +} +`; diff --git a/public/app/plugins/panel/table/migrations.test.ts b/public/app/plugins/panel/table/migrations.test.ts new file mode 100644 index 00000000000..2f767d0f95d --- /dev/null +++ b/public/app/plugins/panel/table/migrations.test.ts @@ -0,0 +1,132 @@ +import { PanelModel } from '@grafana/data'; +import { tablePanelChangedHandler } from './migrations'; + +describe('Table Migrations', () => { + it('migrates transform out to core transforms', () => { + const toColumns = { + angular: { + columns: [], + styles: [], + transform: 'timeseries_to_columns', + options: {}, + }, + }; + const toRows = { + angular: { + columns: [], + styles: [], + transform: 'timeseries_to_rows', + options: {}, + }, + }; + const aggregations = { + angular: { + columns: [ + { + text: 'Avg', + value: 'avg', + $$hashKey: 'object:82', + }, + { + text: 'Max', + value: 'max', + $$hashKey: 'object:83', + }, + { + text: 'Current', + value: 'current', + $$hashKey: 'object:84', + }, + ], + styles: [], + transform: 'timeseries_aggregations', + options: {}, + }, + }; + const table = { + angular: { + columns: [], + styles: [], + transform: 'table', + options: {}, + }, + }; + + const columnsPanel = {} as PanelModel; + tablePanelChangedHandler(columnsPanel, 'table-old', toColumns); + expect(columnsPanel).toMatchSnapshot(); + const rowsPanel = {} as PanelModel; + tablePanelChangedHandler(rowsPanel, 'table-old', toRows); + expect(rowsPanel).toMatchSnapshot(); + const aggregationsPanel = {} as PanelModel; + tablePanelChangedHandler(aggregationsPanel, 'table-old', aggregations); + expect(aggregationsPanel).toMatchSnapshot(); + const tablePanel = {} as PanelModel; + tablePanelChangedHandler(tablePanel, 'table-old', table); + expect(tablePanel).toMatchSnapshot(); + }); + + it('migrates styles to field config overrides and defaults', () => { + const oldStyles = { + angular: { + columns: [], + styles: [ + { + alias: 'Time', + align: 'auto', + dateFormat: 'YYYY-MM-DD HH:mm:ss', + pattern: 'Time', + type: 'date', + $$hashKey: 'object:195', + }, + { + alias: '', + align: 'left', + colorMode: 'cell', + colors: ['rgba(245, 54, 54, 0.9)', 'rgba(237, 129, 40, 0.89)', 'rgba(50, 172, 45, 0.97)'], + dateFormat: 'YYYY-MM-DD HH:mm:ss', + decimals: 2, + mappingType: 1, + pattern: 'ColorCell', + thresholds: ['5', '10'], + type: 'number', + unit: 'currencyUSD', + $$hashKey: 'object:196', + }, + { + alias: '', + align: 'auto', + colorMode: 'value', + colors: ['rgba(245, 54, 54, 0.9)', 'rgba(237, 129, 40, 0.89)', 'rgba(50, 172, 45, 0.97)'], + dateFormat: 'YYYY-MM-DD HH:mm:ss', + decimals: 2, + link: true, + linkTargetBlank: true, + linkTooltip: '', + linkUrl: 'http://www.grafana.com', + mappingType: 1, + pattern: 'ColorValue', + thresholds: ['5', '10'], + type: 'number', + unit: 'Bps', + $$hashKey: 'object:197', + }, + { + unit: 'short', + type: 'number', + alias: '', + decimals: 2, + colors: ['rgba(245, 54, 54, 0.9)', 'rgba(237, 129, 40, 0.89)', 'rgba(50, 172, 45, 0.97)'], + colorMode: null, + pattern: '/.*/', + thresholds: [], + align: 'right', + }, + ], + }, + }; + const panel = {} as PanelModel; + tablePanelChangedHandler(panel, 'table-old', oldStyles); + expect(panel).toMatchSnapshot(); + }); +}); diff --git a/public/app/plugins/panel/table/migrations.ts b/public/app/plugins/panel/table/migrations.ts index 557b819a9d2..dc2fac31529 100644 --- a/public/app/plugins/panel/table/migrations.ts +++ b/public/app/plugins/panel/table/migrations.ts @@ -1,4 +1,16 @@ -import { PanelModel } from '@grafana/data'; +import { + PanelModel, + FieldMatcherID, + ConfigOverrideRule, + ThresholdsMode, + ThresholdsConfig, + FieldConfig, +} from '@grafana/data'; +import { ReduceTransformerOptions } from '@grafana/data/src/transformations/transformers/reduce'; +import omitBy from 'lodash/omitBy'; +import isNil from 'lodash/isNil'; +import isNumber from 'lodash/isNumber'; +import defaultTo from 'lodash/defaultTo'; import { Options } from './types'; /** @@ -16,6 +28,195 @@ export const tableMigrationHandler = (panel: PanelModel): Partial { + return [-Infinity, ...thresholds].map((threshold, idx) => ({ + color: colors[idx], + value: isNumber(threshold) ? threshold : parseInt(threshold, 10), + })); +}; + +const migrateTransformations = ( + panel: PanelModel> | any, + oldOpts: { columns: any; transform: Transformations } +) => { + const transformations: Transformation[] = panel.transformations ?? []; + if (Object.keys(transformsMap).includes(oldOpts.transform)) { + const opts: ReduceTransformerOptions = { + reducers: [], + }; + if (oldOpts.transform === 'timeseries_aggregations') { + opts.includeTimeField = false; + opts.reducers = oldOpts.columns.map((column: Column) => columnsMap[column.value]); + } + transformations.push({ + id: transformsMap[oldOpts.transform], + options: opts, + }); + } + return transformations; +}; + +type Style = { + unit: string; + type: string; + alias: string; + decimals: number; + colors: string[]; + colorMode: ColorModes; + pattern: string; + thresholds: string[]; + align?: string; + dateFormat: string; + link: boolean; + linkTargetBlank?: boolean; + linkTooltip?: string; + linkUrl?: string; +}; + +const migrateTableStyleToOverride = (style: Style) => { + const fieldMatcherId = /^\/.*\/$/.test(style.pattern) ? FieldMatcherID.byRegexp : FieldMatcherID.byName; + const override: ConfigOverrideRule = { + matcher: { + id: fieldMatcherId, + options: style.pattern, + }, + properties: [], + }; + + if (style.alias) { + override.properties.push({ + id: 'displayName', + value: style.alias, + }); + } + + if (style.unit) { + override.properties.push({ + id: 'unit', + value: style.unit, + }); + } + + if (style.decimals) { + override.properties.push({ + id: 'decimals', + value: style.decimals, + }); + } + + if (style.type === 'date') { + override.properties.push({ + id: 'unit', + value: `time: ${style.dateFormat}`, + }); + } + + if (style.link) { + override.properties.push({ + id: 'links', + value: [ + { + title: defaultTo(style.linkTooltip, ''), + url: defaultTo(style.linkUrl, ''), + targetBlank: defaultTo(style.linkTargetBlank, false), + }, + ], + }); + } + + if (style.colorMode) { + override.properties.push({ + id: 'custom.displayMode', + value: colorModeMap[style.colorMode], + }); + } + + if (style.align) { + override.properties.push({ + id: 'custom.align', + value: style.align === 'auto' ? null : style.align, + }); + } + + if (style.thresholds?.length) { + override.properties.push({ + id: 'thresholds', + value: { + mode: ThresholdsMode.Absolute, + steps: generateThresholds(style.thresholds, style.colors), + }, + }); + } + + return override; +}; + +const migrateDefaults = (prevDefaults: Style) => { + let defaults: FieldConfig = { + custom: {}, + }; + if (prevDefaults) { + defaults = omitBy( + { + unit: prevDefaults.unit, + decimals: prevDefaults.decimals, + displayName: prevDefaults.alias, + custom: { + align: prevDefaults.align === 'auto' ? null : prevDefaults.align, + displayMode: colorModeMap[prevDefaults.colorMode], + }, + }, + isNil + ); + if (prevDefaults.thresholds.length) { + const thresholds: ThresholdsConfig = { + mode: ThresholdsMode.Absolute, + steps: generateThresholds(prevDefaults.thresholds, prevDefaults.colors), + }; + defaults.thresholds = thresholds; + } + } + return defaults; +}; + /** * This is called when the panel changes from another panel */ @@ -26,7 +227,17 @@ export const tablePanelChangedHandler = ( ) => { // Changing from angular table panel if (prevPluginId === 'table-old' && prevOptions.angular) { - // Todo write migration logic + const oldOpts = prevOptions.angular; + const transformations = migrateTransformations(panel, oldOpts); + const prevDefaults = oldOpts.styles.find((style: any) => style.pattern === '/.*/'); + const defaults = migrateDefaults(prevDefaults); + const overrides = oldOpts.styles.filter((style: any) => style.pattern !== '/.*/').map(migrateTableStyleToOverride); + + panel.transformations = transformations; + panel.fieldConfig = { + defaults, + overrides, + }; } return {};