From 2b94a82baaf54977d25e0c84135a80948ce82e10 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:42:59 -0600 Subject: [PATCH] AngularMigration: Migrate series Graph to BarGauge (#92609) --- .../dev-dashboards/migrations/migrations.json | 145 +++++++++++++++++- .../SingleStatBaseOptions.test.ts | 26 ++++ .../SingleStatShared/SingleStatBaseOptions.ts | 119 +++++++++++++- .../state/getPanelPluginToMigrateTo.ts | 4 + .../plugins/panel/barchart/migrations.test.ts | 35 ++++- .../app/plugins/panel/barchart/migrations.ts | 72 +++++++-- .../plugins/panel/bargauge/BarGaugeLegend.tsx | 4 +- 7 files changed, 381 insertions(+), 24 deletions(-) diff --git a/devenv/dev-dashboards/migrations/migrations.json b/devenv/dev-dashboards/migrations/migrations.json index 3d13fe327f6..5abfe8be420 100644 --- a/devenv/dev-dashboards/migrations/migrations.json +++ b/devenv/dev-dashboards/migrations/migrations.json @@ -389,9 +389,9 @@ "type": "testdata", "uid": "PD8C576611E62080A" }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 2.260951647569786,\n 1.887442338051216,\n 2.1526144400893514,\n 1.7287721375237766,\n 1.7262902137793208\n ]\n ]\n }\n },\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod1\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 1.589539045095202,\n 1.5914283506847613,\n 1.8976990616650726,\n 1.758223085999124,\n 2.2294649594813816\n ]\n ]\n }\n },\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod2\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 2.0914263380328766,\n 1.8164545521094575,\n 1.621111084665713,\n 1.3902653996444705,\n 1.482803315949775\n ]\n ]\n }\n }\n]", "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 3 + "scenarioId": "raw_frame" } ], "thresholds": [], @@ -462,6 +462,145 @@ "title": "Status + Notes", "type": "text" }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": { + "default": false, + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 16, + "x": 0, + "y": 22 + }, + "hiddenSeries": false, + "id": 32, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "percentage": false, + "pluginVersion": "11.3.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 28, + "refId": "A" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Flot graph - x axis series mode (with legend calcs)", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "$$hashKey": "object:88", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:89", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 33, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel >> Bar gauge panel\n", + "mode": "markdown" + }, + "pluginVersion": "11.3.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, { "aliasColors": {}, "bars": true, @@ -1183,4 +1322,4 @@ "uid": "cdd412c4", "version": 68, "weekStart": "" -} \ No newline at end of file +} diff --git a/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.test.ts b/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.test.ts index 0fc3a03ab5b..490945ed908 100644 --- a/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.test.ts +++ b/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.test.ts @@ -249,3 +249,29 @@ describe('sharedSingleStatMigrationHandler', () => { expect(panel.fieldConfig.defaults.max).toBe(1); }); }); + +describe('BarGauge migrations', () => { + it('Should migrate from old graph', () => { + const old = { + angular: { + xaxis: { + mode: 'series', + }, + legend: { + show: true, + values: true, + min: false, + max: true, + current: true, + total: false, + avg: false, + }, + }, + }; + + const panel = {} as PanelModel; + panel.options = sharedSingleStatPanelChangedHandler(panel, 'graph', old); + expect(panel.options.legend.showLegend).toBe(true); + expect(panel.options.legend.calcs).toHaveLength(2); + }); +}); diff --git a/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts b/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts index 49ce29f6818..37f4fe00e04 100644 --- a/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts +++ b/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts @@ -1,4 +1,4 @@ -import { cloneDeep, isNumber, omit } from 'lodash'; +import { cloneDeep, identity, isNumber, omit, pickBy } from 'lodash'; import { convertOldAngularValueMappings, @@ -16,7 +16,7 @@ import { ValueMapping, VizOrientation, } from '@grafana/data'; -import { OptionsWithTextFormatting } from '@grafana/schema'; +import { LegendDisplayMode, OptionsWithLegend, OptionsWithTextFormatting } from '@grafana/schema'; export interface SingleStatBaseOptions extends OptionsWithTextFormatting { reduceOptions: ReduceDataOptions; @@ -25,6 +25,7 @@ export interface SingleStatBaseOptions extends OptionsWithTextFormatting { const optionsToKeep = ['reduceOptions', 'orientation']; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function sharedSingleStatPanelChangedHandler( panel: PanelModel> | any, prevPluginId: string, @@ -40,6 +41,9 @@ export function sharedSingleStatPanelChangedHandler( // Migrating from angular singlestat if (prevPluginId === 'singlestat' && prevOptions.angular) { return migrateFromAngularSinglestat(panel, prevOptions); + } else if (prevPluginId === 'graph') { + // Migrating from Graph panel + return migrateFromGraphPanel(panel, prevOptions); } for (const k of optionsToKeep) { @@ -51,6 +55,65 @@ export function sharedSingleStatPanelChangedHandler( return options; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function migrateFromGraphPanel(panel: PanelModel> | any, prevOptions: any) { + const graphOptions: GraphOptions = prevOptions.angular; + + const options: SingleStatBaseOptions & OptionsWithLegend = { + orientation: VizOrientation.Auto, + reduceOptions: { + values: false, + calcs: [], + }, + legend: { + displayMode: LegendDisplayMode.List, + showLegend: true, + placement: 'bottom', + calcs: [], + }, + }; + + if (graphOptions.xaxis?.mode === 'series') { + panel.fieldConfig = { + ...panel.fieldConfig, + defaults: { + ...panel.fieldConfig.defaults, + color: { mode: 'palette-classic' }, + }, + }; + + // Value options calculation migration + if (graphOptions.xaxis.values) { + options.reduceOptions.calcs = getReducerForMigration(graphOptions.xaxis.values); + } + + // Legend migration + const legendConfig = graphOptions.legend; + if (legendConfig) { + if (legendConfig.show) { + options.legend.displayMode = legendConfig.alignAsTable ? LegendDisplayMode.Table : LegendDisplayMode.List; + } else { + options.legend.showLegend = false; + } + + if (legendConfig.rightSide) { + options.legend.placement = 'right'; + } + + if (legendConfig.values) { + const enabledLegendValues = pickBy(legendConfig, identity); + options.legend.calcs = getReducersFromLegend(enabledLegendValues); + } + + if (legendConfig.sideWidth) { + options.legend.width = legendConfig.sideWidth; + } + } + } + + return options; +} + function migrateFromAngularSinglestat(panel: PanelModel> | any, prevOptions: any) { const prevPanel = prevOptions.angular; const reducer = fieldReducers.getIfExists(prevPanel.valueName); @@ -331,3 +394,55 @@ export function migrateOldThresholds(thresholds?: any[]): Threshold[] | undefine export function convertOldAngularValueMapping(panel: any): ValueMapping[] { return convertOldAngularValueMappings(panel); } + +interface GraphOptions { + xaxis: { + mode: 'series' | 'time' | 'histogram'; + values?: string[]; + }; + legend: { + show: boolean; + alignAsTable: boolean; + rightSide: boolean; + values: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; + sideWidth?: number; + }; +} + +function getReducersFromLegend(obj: Record): string[] { + const ids: string[] = []; + for (const key in obj) { + const reducer = fieldReducers.getIfExists(key); + if (reducer) { + ids.push(reducer.id); + } + } + return ids; +} + +// same as public/app/plugins/panel/barchart/migrations.ts +function getReducerForMigration(reducers: string[] | undefined) { + const transformReducers: string[] = []; + + reducers?.forEach((reducer) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + if (!Object.values(ReducerID).includes(reducer as ReducerID)) { + if (reducer === 'current') { + transformReducers.push(ReducerID.lastNotNull); + } else if (reducer === 'total') { + transformReducers.push(ReducerID.sum); + } else if (reducer === 'avg') { + transformReducers.push(ReducerID.mean); + } + } else { + transformReducers.push(reducer); + } + }); + + return reducers ? transformReducers : [ReducerID.sum]; +} diff --git a/public/app/features/dashboard/state/getPanelPluginToMigrateTo.ts b/public/app/features/dashboard/state/getPanelPluginToMigrateTo.ts index 5eb7c201ee4..e4cc5b6dac2 100644 --- a/public/app/features/dashboard/state/getPanelPluginToMigrateTo.ts +++ b/public/app/features/dashboard/state/getPanelPluginToMigrateTo.ts @@ -32,6 +32,10 @@ export function getPanelPluginToMigrateTo(panel: any, forceMigration?: boolean): isUrlFeatureFlagEnabled('autoMigrateGraphPanel')) ) { if (panel.xaxis?.mode === 'series') { + if (panel.legend?.values) { + return 'bargauge'; + } + return 'barchart'; } diff --git a/public/app/plugins/panel/barchart/migrations.test.ts b/public/app/plugins/panel/barchart/migrations.test.ts index 00af18aec7b..5f172cdf380 100644 --- a/public/app/plugins/panel/barchart/migrations.test.ts +++ b/public/app/plugins/panel/barchart/migrations.test.ts @@ -1,4 +1,4 @@ -import { FieldConfigSource, PanelModel } from '@grafana/data'; +import { FieldConfigSource, FieldMatcherID, PanelModel } from '@grafana/data'; import { changeToBarChartPanelMigrationHandler } from './migrations'; @@ -17,16 +17,41 @@ describe('Bar chart Migrations', () => { angular: { xaxis: { mode: 'series', - values: 'avg', + values: ['avg'], }, }, }; const panel = {} as PanelModel; panel.options = changeToBarChartPanelMigrationHandler(panel, 'graph', old, prevFieldConfig); + const transformations = panel.transformations || []; + expect(transformations).toHaveLength(2); - const transform = panel.transformations![0]; - expect(transform.id).toBe('reduce'); - expect(transform.options.reducers).toBe('avg'); + const reduceTransform = transformations[0]; + expect(reduceTransform.id).toBe('reduce'); + expect(reduceTransform.options.reducers).toHaveLength(1); + expect(reduceTransform.options.reducers[0]).toBe('mean'); + + const transposeTransform = transformations[1]; + expect(transposeTransform.id).toBe('transpose'); + + expect(panel.fieldConfig.overrides).toHaveLength(1); + expect(panel.fieldConfig.overrides[0].matcher.id).toBe(FieldMatcherID.byName); + expect(panel.fieldConfig.overrides).toMatchInlineSnapshot(` + [ + { + "matcher": { + "id": "byName", + "options": "Field", + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "hidden", + }, + ], + }, + ] + `); }); }); diff --git a/public/app/plugins/panel/barchart/migrations.ts b/public/app/plugins/panel/barchart/migrations.ts index 4dba05de840..100b1a138ec 100644 --- a/public/app/plugins/panel/barchart/migrations.ts +++ b/public/app/plugins/panel/barchart/migrations.ts @@ -1,33 +1,79 @@ -import { PanelTypeChangedHandler } from '@grafana/data'; +import { FieldMatcherID, PanelTypeChangedHandler, ReducerID } from '@grafana/data'; +import { AxisPlacement } from '@grafana/ui'; /* * This is called when the panel changes from another panel */ -export const changeToBarChartPanelMigrationHandler: PanelTypeChangedHandler = ( - panel, - prevPluginId, - prevOptions, - prevFieldConfig -) => { +export const changeToBarChartPanelMigrationHandler: PanelTypeChangedHandler = (panel, prevPluginId, prevOptions) => { if (prevPluginId === 'graph') { const graphOptions: GraphOptions = prevOptions.angular; + const fieldConfig = panel.fieldConfig ?? { defaults: {}, overrides: [] }; + if (graphOptions.xaxis?.mode === 'series') { - const tranformations = panel.transformations || []; - tranformations.push({ - id: 'reduce', - options: { - reducers: graphOptions.xaxis?.values ?? ['sum'], + const transformations = panel.transformations || []; + transformations.push( + { + id: 'reduce', + options: { + reducers: getReducer(graphOptions.xaxis?.values), + }, }, + { + id: 'transpose', + options: {}, + } + ); + + panel.transformations = transformations; + + // temporary, until we have a bar chart with per bar labels + fieldConfig.overrides.push({ + matcher: { + id: FieldMatcherID.byName, + options: 'Field', + }, + properties: [ + { + id: 'custom.axisPlacement', + value: AxisPlacement.Hidden, + }, + ], }); - panel.transformations = tranformations; + panel.fieldConfig = fieldConfig; + panel.options = { + ...panel.options, + groupWidth: 1, + }; } } return {}; }; +// same as grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts +const getReducer = (reducers: string[] | undefined) => { + const transformReducers: string[] = []; + + reducers?.forEach((reducer) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + if (!Object.values(ReducerID).includes(reducer as ReducerID)) { + if (reducer === 'current') { + transformReducers.push(ReducerID.lastNotNull); + } else if (reducer === 'total') { + transformReducers.push(ReducerID.sum); + } else if (reducer === 'avg') { + transformReducers.push(ReducerID.mean); + } + } else { + transformReducers.push(reducer); + } + }); + + return reducers ? transformReducers : [ReducerID.sum]; +}; + interface GraphOptions { xaxis: { mode: 'series' | 'time' | 'histogram'; diff --git a/public/app/plugins/panel/bargauge/BarGaugeLegend.tsx b/public/app/plugins/panel/bargauge/BarGaugeLegend.tsx index 6aa9609656d..8d88eac3ee3 100644 --- a/public/app/plugins/panel/bargauge/BarGaugeLegend.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugeLegend.tsx @@ -1,6 +1,6 @@ import { memo } from 'react'; -import { DataFrame, FieldType, getFieldSeriesColor } from '@grafana/data'; +import { cacheFieldDisplayNames, DataFrame, FieldType, getFieldSeriesColor } from '@grafana/data'; import { Field } from '@grafana/data/'; import { AxisPlacement, VizLegendOptions } from '@grafana/schema'; import { useTheme2, VizLayout, VizLayoutLegendProps, VizLegend, VizLegendItem } from '@grafana/ui'; @@ -16,6 +16,8 @@ export const BarGaugeLegend = memo( const theme = useTheme2(); let legendItems: VizLegendItem[] = []; + cacheFieldDisplayNames(data); + data.forEach((series, frameIndex) => { series.fields.forEach((field, i) => { const fieldIndex = i + 1;