diff --git a/public/app/plugins/panel/graph3/__snapshots__/migrations.test.ts.snap b/public/app/plugins/panel/graph3/__snapshots__/migrations.test.ts.snap new file mode 100644 index 00000000000..81ff3500999 --- /dev/null +++ b/public/app/plugins/panel/graph3/__snapshots__/migrations.test.ts.snap @@ -0,0 +1,122 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Graph Migrations simple bars 1`] = ` +Object { + "fieldConfig": Object { + "defaults": Object { + "custom": Object { + "drawStyle": "bars", + "fillOpacity": 1, + "showPoints": "never", + "spanNulls": false, + }, + }, + "overrides": Array [], + }, + "options": Object { + "graph": Object {}, + "legend": Object { + "displayMode": "list", + "placement": "bottom", + }, + "tooltipOptions": Object { + "mode": "single", + }, + }, +} +`; + +exports[`Graph Migrations stairscase 1`] = ` +Object { + "fieldConfig": Object { + "defaults": Object { + "custom": Object { + "axisPlacement": "hidden", + "drawStyle": "line", + "fillOpacity": 0.5, + "lineInterpolation": "stepAfter", + "pointSize": 2, + "showPoints": "never", + "spanNulls": true, + }, + "displayName": "DISPLAY NAME", + "nullValueMode": "null", + "unit": "short", + }, + "overrides": Array [], + }, + "options": Object { + "graph": Object {}, + "legend": Object { + "displayMode": "list", + "placement": "bottom", + }, + "tooltipOptions": Object { + "mode": "single", + }, + }, +} +`; + +exports[`Graph Migrations twoYAxis 1`] = ` +Object { + "fieldConfig": Object { + "defaults": Object { + "custom": Object { + "axisLabel": "Y111", + "axisPlacement": "auto", + "drawStyle": "line", + "fillOpacity": 0.1, + "pointSize": 2, + "showPoints": "never", + "spanNulls": true, + }, + "decimals": 3, + "max": 1000, + "min": 0, + "nullValueMode": "null", + "unit": "areaMI2", + }, + "overrides": Array [ + Object { + "matcher": Object { + "id": "byName", + "options": "B-series", + }, + "properties": Array [ + Object { + "id": "unit", + "value": "degree", + }, + Object { + "id": "decimals", + "value": 2, + }, + Object { + "id": "min", + "value": -10, + }, + Object { + "id": "max", + "value": 25, + }, + Object { + "id": "custom.axisLabel", + "value": "Y222", + }, + ], + }, + ], + }, + "options": Object { + "graph": Object {}, + "legend": Object { + "displayMode": "list", + "placement": "bottom", + }, + "tooltipOptions": Object { + "mode": "single", + }, + }, +} +`; diff --git a/public/app/plugins/panel/graph3/migrations.test.ts b/public/app/plugins/panel/graph3/migrations.test.ts new file mode 100644 index 00000000000..ddd9c0e3018 --- /dev/null +++ b/public/app/plugins/panel/graph3/migrations.test.ts @@ -0,0 +1,203 @@ +import { PanelModel } from '@grafana/data'; +import { graphPanelChangedHandler } from './migrations'; + +describe('Graph Migrations', () => { + it('simple bars', () => { + const old: any = { + angular: { + bars: true, + }, + }; + const panel = {} as PanelModel; + panel.options = graphPanelChangedHandler(panel, 'graph', old); + expect(panel).toMatchSnapshot(); + }); + + it('stairscase', () => { + const old: any = { + angular: stairscase, + }; + const panel = {} as PanelModel; + panel.options = graphPanelChangedHandler(panel, 'graph', old); + expect(panel).toMatchSnapshot(); + }); + + it('twoYAxis', () => { + const old: any = { + angular: twoYAxis, + }; + const panel = {} as PanelModel; + panel.options = graphPanelChangedHandler(panel, 'graph', old); + expect(panel).toMatchSnapshot(); + }); +}); + +const stairscase = { + fieldConfig: { + defaults: { + custom: {}, + unit: 'areaF2', + displayName: 'DISPLAY NAME', + }, + overrides: [], + }, + aliasColors: {}, + dashLength: 10, + fill: 5, + fillGradient: 6, + legend: { + avg: true, + current: true, + max: true, + min: true, + show: true, + total: true, + values: true, + alignAsTable: true, + }, + lines: true, + linewidth: 1, + nullPointMode: 'null', + options: { + alertThreshold: true, + }, + pointradius: 2, + seriesOverrides: [], + spaceLength: 10, + steppedLine: true, + thresholds: [], + timeRegions: [], + title: 'Panel Title', + tooltip: { + shared: true, + sort: 0, + value_type: 'individual', + }, + type: 'graph', + xaxis: { + buckets: null, + mode: 'time', + name: null, + show: true, + values: [], + }, + yaxes: [ + { + $$hashKey: 'object:42', + format: 'short', + label: null, + logBase: 1, + max: null, + min: null, + show: false, + }, + { + $$hashKey: 'object:43', + format: 'short', + label: null, + logBase: 1, + max: null, + min: null, + show: true, + }, + ], + yaxis: { + align: false, + alignLevel: null, + }, + timeFrom: null, + timeShift: null, + bars: false, + dashes: false, + hiddenSeries: false, + percentage: false, + points: false, + stack: false, + decimals: 1, + datasource: null, +}; + +const twoYAxis = { + yaxes: [ + { + label: 'Y111', + show: true, + logBase: 10, + min: '0', + max: '1000', + format: 'areaMI2', + $$hashKey: 'object:19', + decimals: 3, + }, + { + label: 'Y222', + show: true, + logBase: 1, + min: '-10', + max: '25', + format: 'degree', + $$hashKey: 'object:20', + decimals: 2, + }, + ], + xaxis: { + show: true, + mode: 'time', + name: null, + values: [], + buckets: null, + }, + yaxis: { + align: false, + alignLevel: null, + }, + lines: true, + fill: 1, + linewidth: 1, + dashLength: 10, + spaceLength: 10, + pointradius: 2, + legend: { + show: true, + values: false, + min: false, + max: false, + current: false, + total: false, + avg: false, + }, + nullPointMode: 'null', + tooltip: { + value_type: 'individual', + shared: true, + sort: 0, + }, + aliasColors: {}, + seriesOverrides: [ + { + alias: 'B-series', + yaxis: 2, + }, + ], + thresholds: [], + timeRegions: [], + targets: [ + { + refId: 'A', + }, + { + refId: 'B', + }, + ], + fillGradient: 0, + dashes: false, + hiddenSeries: false, + points: false, + bars: false, + stack: false, + percentage: false, + steppedLine: false, + timeFrom: null, + timeShift: null, + datasource: null, +}; diff --git a/public/app/plugins/panel/graph3/migrations.ts b/public/app/plugins/panel/graph3/migrations.ts new file mode 100644 index 00000000000..79d8f2f15fc --- /dev/null +++ b/public/app/plugins/panel/graph3/migrations.ts @@ -0,0 +1,266 @@ +import { + FieldConfig, + FieldConfigSource, + NullValueMode, + PanelModel, + fieldReducers, + ConfigOverrideRule, + FieldMatcherID, + DynamicConfigValue, +} from '@grafana/data'; +import { GraphFieldConfig, LegendDisplayMode } from '@grafana/ui'; +import { AxisPlacement, DrawStyle, LineInterpolation, PointVisibility } from '@grafana/ui/src/components/uPlot/config'; +import { Options } from './types'; +import omitBy from 'lodash/omitBy'; +import isNil from 'lodash/isNil'; +import { isNumber, isString } from 'lodash'; + +/** + * This is called when the panel changes from another panel + */ +export const graphPanelChangedHandler = ( + panel: PanelModel> | any, + prevPluginId: string, + prevOptions: any +) => { + // Changing from angular/flot panel to react/uPlot + if (prevPluginId === 'graph' && prevOptions.angular) { + const { fieldConfig, options } = flotToGraphOptions(prevOptions.angular); + panel.fieldConfig = fieldConfig; // Mutates the incoming panel + return options; + } + + return {}; +}; + +export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSource; options: Options } { + const overrides: ConfigOverrideRule[] = angular.fieldConfig?.overrides ?? []; + const yaxes = angular.yaxes ?? []; + let y1 = getFieldConfigFromOldAxis(yaxes[0]); + if (angular.fieldConfig?.defaults) { + y1 = { + ...angular.fieldConfig?.defaults, + ...y1, // Keep the y-axis unit and custom + }; + } + + // "seriesOverrides": [ + // { + // "$$hashKey": "object:183", + // "alias": "B-series", + // "fill": 3, + // "nullPointMode": "null as zero", + // "lines": true, + // "linewidth": 2 + // } + // ], + if (angular.seriesOverrides?.length) { + for (const seriesOverride of angular.seriesOverrides) { + if (!seriesOverride.alias) { + continue; // the matcher config + } + const rule: ConfigOverrideRule = { + matcher: { + id: FieldMatcherID.byName, + options: seriesOverride.alias, + }, + properties: [], + }; + for (const p of Object.keys(seriesOverride)) { + const v = seriesOverride[p]; + switch (p) { + // Ignore + case 'alias': + case '$$hashKey': + break; + // Link to y axis settings + case 'yaxis': + if (2 === v) { + const y2 = getFieldConfigFromOldAxis(yaxes[1]); + fillY2DynamicValues(y1, y2, rule.properties); + } + break; + case 'fill': + rule.properties.push({ + id: 'custom.fillOpacity', + value: v / 10.0, // was 0-10 + }); + break; + case 'points': + rule.properties.push({ + id: 'custom.showPoints', + value: v ? PointVisibility.Always : PointVisibility.Never, + }); + break; + case 'bars': + if (v) { + rule.properties.push({ + id: 'custom.drawStyle', + value: DrawStyle.Bars, + }); + rule.properties.push({ + id: 'custom.fillOpacity', + value: 1, // solid bars + }); + } else { + rule.properties.push({ + id: 'custom.drawStyle', + value: DrawStyle.Line, // Change from bars + }); + } + break; + case 'lineWidth': + rule.properties.push({ + id: 'custom.lineWidth', + value: v, + }); + break; + case 'pointradius': + rule.properties.push({ + id: 'custom.pointSize', + value: v, + }); + break; + default: + console.log('Ignore override migration:', seriesOverride.alias, p, v); + } + } + if (rule.properties.length) { + overrides.push(rule); + } + } + } + + const graph = y1.custom ?? ({} as GraphFieldConfig); + graph.drawStyle = angular.bars ? DrawStyle.Bars : angular.lines ? DrawStyle.Line : DrawStyle.Points; + if (angular.points) { + graph.showPoints = PointVisibility.Always; + } else if (graph.drawStyle !== DrawStyle.Points) { + graph.showPoints = PointVisibility.Never; + } + if (graph.drawStyle === DrawStyle.Bars) { + graph.fillOpacity = 1.0; // bars were always + } + + graph.lineWidth = angular.lineWidth; + graph.pointSize = angular.pointradius; + if (isNumber(angular.fill)) { + graph.fillOpacity = angular.fill / 10; // fill is 0-10 + } + graph.spanNulls = angular.nullPointMode === NullValueMode.Null; + if (angular.steppedLine) { + graph.lineInterpolation = LineInterpolation.StepAfter; + } + y1.custom = omitBy(graph, isNil); + y1.nullValueMode = angular.nullPointMode as NullValueMode; + + const options: Options = { + graph: {}, + legend: { + displayMode: LegendDisplayMode.List, + placement: 'bottom', + }, + tooltipOptions: { + mode: 'single', + }, + }; + + if (angular.legend?.values) { + const show = getReducersFromLegend(angular.legend?.values); + console.log('Migrate Legend', show); + } + + return { + fieldConfig: { + defaults: omitBy(y1, isNil), + overrides, + }, + options, + }; +} + +// { +// "label": "Y111", +// "show": true, +// "logBase": 10, +// "min": "0", +// "max": "1000", +// "format": "areaMI2", +// "$$hashKey": "object:19", +// "decimals": 3 +// }, +function getFieldConfigFromOldAxis(obj: any): FieldConfig { + if (!obj) { + return {}; + } + const graph: GraphFieldConfig = { + axisPlacement: obj.show ? AxisPlacement.Auto : AxisPlacement.Hidden, + }; + if (obj.label) { + graph.axisLabel = obj.label; + } + return omitBy( + { + unit: obj.format, + decimals: validNumber(obj.decimals), + min: validNumber(obj.min), + max: validNumber(obj.max), + custom: graph, + }, + isNil + ); +} + +function fillY2DynamicValues( + y1: FieldConfig, + y2: FieldConfig, + props: DynamicConfigValue[] +) { + // The standard properties + for (const key of Object.keys(y2)) { + const value = (y2 as any)[key]; + if (key !== 'custom' && value !== (y1 as any)[key]) { + props.push({ + id: key, + value, + }); + } + } + + // Add any custom property + const y1G = y1.custom ?? {}; + const y2G = y2.custom ?? {}; + for (const key of Object.keys(y2G)) { + const value = (y2G as any)[key]; + if (value !== (y1G as any)[key]) { + props.push({ + id: `custom.${key}`, + value, + }); + } + } +} + +function validNumber(val: any): number | undefined { + if (isNumber(val)) { + return val; + } + if (isString(val)) { + const num = Number(val); + if (!isNaN(num)) { + return num; + } + } + return undefined; +} + +function getReducersFromLegend(obj: Record): string[] { + const ids: string[] = []; + for (const key of Object.keys(obj)) { + const r = fieldReducers.getIfExists(key); + if (r) { + ids.push(r.id); + } + } + return ids; +} diff --git a/public/app/plugins/panel/graph3/module.tsx b/public/app/plugins/panel/graph3/module.tsx index 112f06a63b7..512db0c56c6 100644 --- a/public/app/plugins/panel/graph3/module.tsx +++ b/public/app/plugins/panel/graph3/module.tsx @@ -8,9 +8,11 @@ import { graphFieldOptions, } from '@grafana/ui/src/components/uPlot/config'; import { GraphPanel } from './GraphPanel'; +import { graphPanelChangedHandler } from './migrations'; import { Options } from './types'; export const plugin = new PanelPlugin(GraphPanel) + .setPanelChangeHandler(graphPanelChangedHandler) .useFieldConfig({ standardOptions: { [FieldConfigProperty.Color]: {