From 168afa99d1cae8c33e73e3aa68600abca3a97a14 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 29 Dec 2022 05:34:22 -0800 Subject: [PATCH] Scenes / DashboardsLoader: Add variables migration (#60226) * VizPanel - add variables dependencies definition * Migrate variables to scene variables * Constant variable migration * Update test * Lint fix --- .betterer.results | 3 +- .../dashboard/state/DashboardMigrator.test.ts | 4 +- .../scenes/components/VizPanel/VizPanel.tsx | 3 + .../components/VizPanel/VizPanelRenderer.tsx | 5 +- .../scenes/dashboard/DashboardScene.tsx | 4 +- .../scenes/dashboard/DashboardsLoader.test.ts | 283 ++++++++++++++++++ .../scenes/dashboard/DashboardsLoader.ts | 118 +++++++- .../variables/variants/ConstantVariable.ts | 2 +- 8 files changed, 415 insertions(+), 7 deletions(-) create mode 100644 public/app/features/scenes/dashboard/DashboardsLoader.test.ts diff --git a/.betterer.results b/.betterer.results index ffce50e2068..5503228173f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4395,7 +4395,8 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/scenes/components/VizPanel/VizPanelRenderer.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"] ], "public/app/features/scenes/components/layout/SceneFlexLayout.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index ac976d8d296..01d5330b7de 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -64,10 +64,10 @@ describe('DashboardModel', () => { { type: 'annotations', enable: true, annotations: [{ name: 'old' }] }, ], panels: [ - // @ts-expect-error { type: 'graph', - legend: true, + legend: { show: true }, + // @ts-expect-error aliasYAxis: { test: 2 }, y_formats: ['kbyte', 'ms'], grid: { diff --git a/public/app/features/scenes/components/VizPanel/VizPanel.tsx b/public/app/features/scenes/components/VizPanel/VizPanel.tsx index 22d6b453a08..ee0773e6b5e 100644 --- a/public/app/features/scenes/components/VizPanel/VizPanel.tsx +++ b/public/app/features/scenes/components/VizPanel/VizPanel.tsx @@ -10,6 +10,7 @@ import { getPanelOptionsWithDefaults } from '../../../dashboard/state/getPanelOp import { SceneObjectBase } from '../../core/SceneObjectBase'; import { sceneGraph } from '../../core/sceneGraph'; import { SceneComponentProps, SceneLayoutChildState } from '../../core/types'; +import { VariableDependencyConfig } from '../../variables/VariableDependencyConfig'; import { VizPanelRenderer } from './VizPanelRenderer'; @@ -27,6 +28,8 @@ export class VizPanel extends SceneObjectBase< public static Component = VizPanelRenderer; public static Editor = VizPanelEditor; + protected _variableDependency = new VariableDependencyConfig(this, { statePaths: ['options', 'title'] }); + // Not part of state as this is not serializable private _plugin?: PanelPlugin; diff --git a/public/app/features/scenes/components/VizPanel/VizPanelRenderer.tsx b/public/app/features/scenes/components/VizPanel/VizPanelRenderer.tsx index 953f4130468..8877c8f0d22 100644 --- a/public/app/features/scenes/components/VizPanel/VizPanelRenderer.tsx +++ b/public/app/features/scenes/components/VizPanel/VizPanelRenderer.tsx @@ -9,6 +9,7 @@ import { useFieldOverrides } from 'app/features/panel/components/PanelRenderer'; import { sceneGraph } from '../../core/sceneGraph'; import { SceneComponentProps } from '../../core/types'; import { SceneQueryRunner } from '../../querying/SceneQueryRunner'; +import { CustomFormatterFn } from '../../variables/interpolation/sceneInterpolator'; import { SceneDragHandle } from '../SceneDragHandle'; import { VizPanel } from './VizPanel'; @@ -77,7 +78,9 @@ export function VizPanelRenderer({ model }: SceneComponentProps) { width={innerWidth} height={innerHeight} renderCounter={0} - replaceVariables={(str: string) => str} + replaceVariables={(str, scopedVars, format) => + sceneGraph.interpolate(model, str, scopedVars, format as string | CustomFormatterFn | undefined) + } onOptionsChange={model.onOptionsChange} onFieldConfigChange={model.onFieldConfigChange} onChangeTimeRange={model.onChangeTimeRange} diff --git a/public/app/features/scenes/dashboard/DashboardScene.tsx b/public/app/features/scenes/dashboard/DashboardScene.tsx index 2eaf00ffde8..04f65e48aa7 100644 --- a/public/app/features/scenes/dashboard/DashboardScene.tsx +++ b/public/app/features/scenes/dashboard/DashboardScene.tsx @@ -15,6 +15,7 @@ interface DashboardSceneState extends SceneObjectStatePlain { uid: string; body: SceneLayout; actions?: SceneObject[]; + subMenu?: SceneObject; } export class DashboardScene extends SceneObjectBase { @@ -43,7 +44,7 @@ export class DashboardScene extends SceneObjectBase { } function DashboardSceneRenderer({ model }: SceneComponentProps) { - const { title, body, actions = [], uid } = model.useState(); + const { title, body, actions = [], uid, subMenu } = model.useState(); const toolbarActions = (actions ?? []).map((action) => ); @@ -58,6 +59,7 @@ function DashboardSceneRenderer({ model }: SceneComponentProps) return ( + {subMenu && }
diff --git a/public/app/features/scenes/dashboard/DashboardsLoader.test.ts b/public/app/features/scenes/dashboard/DashboardsLoader.test.ts new file mode 100644 index 00000000000..432741a2ab2 --- /dev/null +++ b/public/app/features/scenes/dashboard/DashboardsLoader.test.ts @@ -0,0 +1,283 @@ +import { VariableType } from '@grafana/schema'; + +import { CustomVariable } from '../variables/variants/CustomVariable'; +import { DataSourceVariable } from '../variables/variants/DataSourceVariable'; +import { QueryVariable } from '../variables/variants/query/QueryVariable'; + +import { createVariableFromLegacyModel } from './DashboardsLoader'; + +describe('DashboardLoader', () => { + describe('variables migration', () => { + it('should migrate custom variable', () => { + const variable = { + current: { + selected: false, + text: 'a', + value: 'a', + }, + hide: 0, + includeAll: false, + multi: false, + name: 'query0', + options: [ + { + selected: true, + text: 'a', + value: 'a', + }, + { + selected: false, + text: 'b', + value: 'b', + }, + { + selected: false, + text: 'c', + value: 'c', + }, + { + selected: false, + text: 'd', + value: 'd', + }, + ], + query: 'a,b,c,d', + skipUrlSync: false, + type: 'custom' as VariableType, + rootStateKey: 'N4XLmH5Vz', + id: 'query0', + global: false, + index: 0, + state: 'Done', + error: null, + description: null, + allValue: null, + }; + + const migrated = createVariableFromLegacyModel(variable); + const { key, ...rest } = migrated.state; + + expect(migrated).toBeInstanceOf(CustomVariable); + expect(rest).toEqual({ + allValue: undefined, + defaultToAll: false, + description: null, + includeAll: false, + isMulti: false, + label: undefined, + name: 'query0', + options: [], + query: 'a,b,c,d', + skipUrlSync: false, + text: 'a', + type: 'custom', + value: 'a', + hide: 0, + }); + }); + it('should migrate query variable', () => { + const variable = { + allValue: null, + current: { + text: 'America', + value: 'America', + selected: false, + }, + datasource: { + uid: 'P15396BDD62B2BE29', + type: 'influxdb', + }, + definition: '', + hide: 0, + includeAll: false, + label: 'Datacenter', + multi: false, + name: 'datacenter', + options: [ + { + text: 'America', + value: 'America', + selected: true, + }, + { + text: 'Africa', + value: 'Africa', + selected: false, + }, + { + text: 'Asia', + value: 'Asia', + selected: false, + }, + { + text: 'Europe', + value: 'Europe', + selected: false, + }, + ], + query: 'SHOW TAG VALUES WITH KEY = "datacenter" ', + refresh: 1, + regex: '', + skipUrlSync: false, + sort: 0, + tagValuesQuery: null, + tagsQuery: null, + type: 'query' as VariableType, + useTags: false, + rootStateKey: '000000002', + id: 'datacenter', + global: false, + index: 0, + state: 'Done', + error: null, + description: null, + }; + + const migrated = createVariableFromLegacyModel(variable); + const { key, ...rest } = migrated.state; + + expect(migrated).toBeInstanceOf(QueryVariable); + expect(rest).toEqual({ + allValue: undefined, + datasource: { + type: 'influxdb', + uid: 'P15396BDD62B2BE29', + }, + defaultToAll: false, + description: null, + includeAll: false, + isMulti: false, + label: 'Datacenter', + name: 'datacenter', + options: [], + query: 'SHOW TAG VALUES WITH KEY = "datacenter" ', + refresh: 1, + regex: '', + skipUrlSync: false, + sort: 0, + text: 'America', + type: 'query', + value: 'America', + hide: 0, + }); + }); + + it('should migrate datasource variable', () => { + const variable = { + id: 'query1', + rootStateKey: 'N4XLmH5Vz', + name: 'query1', + type: 'datasource' as VariableType, + global: false, + index: 1, + hide: 0, + skipUrlSync: false, + state: 'Done', + error: null, + description: null, + current: { + value: ['gdev-prometheus', 'gdev-slow-prometheus'], + text: ['gdev-prometheus', 'gdev-slow-prometheus'], + selected: true, + }, + regex: '/^gdev/', + options: [ + { + text: 'All', + value: '$__all', + selected: false, + }, + { + text: 'gdev-prometheus', + value: 'gdev-prometheus', + selected: true, + }, + { + text: 'gdev-slow-prometheus', + value: 'gdev-slow-prometheus', + selected: false, + }, + ], + query: 'prometheus', + multi: true, + includeAll: true, + refresh: 1, + allValue: 'Custom all', + }; + + const migrated = createVariableFromLegacyModel(variable); + const { key, ...rest } = migrated.state; + + expect(migrated).toBeInstanceOf(DataSourceVariable); + expect(rest).toEqual({ + allValue: 'Custom all', + defaultToAll: true, + includeAll: true, + label: undefined, + name: 'query1', + options: [], + query: 'prometheus', + regex: '/^gdev/', + skipUrlSync: false, + text: ['gdev-prometheus', 'gdev-slow-prometheus'], + type: 'datasource', + value: ['gdev-prometheus', 'gdev-slow-prometheus'], + isMulti: true, + description: null, + hide: 0, + }); + }); + + it('should migrate constant variable', () => { + const variable = { + hide: 2, + label: 'constant', + name: 'constant', + skipUrlSync: false, + type: 'constant' as VariableType, + rootStateKey: 'N4XLmH5Vz', + current: { + selected: true, + text: 'test', + value: 'test', + }, + options: [ + { + selected: true, + text: 'test', + value: 'test', + }, + ], + query: 'test', + id: 'constant', + global: false, + index: 3, + state: 'Done', + error: null, + description: null, + }; + + const migrated = createVariableFromLegacyModel(variable); + const { key, ...rest } = migrated.state; + + expect(rest).toEqual({ + description: null, + hide: 2, + label: 'constant', + name: 'constant', + skipUrlSync: false, + type: 'constant', + value: 'test', + }); + }); + + it.each(['adhoc', 'interval', 'textbox', 'system'])('should throw for unsupported (yet) variables', (type) => { + const variable = { + name: 'query0', + type: type as VariableType, + }; + + expect(() => createVariableFromLegacyModel(variable)).toThrow(); + }); + }); +}); diff --git a/public/app/features/scenes/dashboard/DashboardsLoader.ts b/public/app/features/scenes/dashboard/DashboardsLoader.ts index 36a6fdce6e3..6da55aaeb65 100644 --- a/public/app/features/scenes/dashboard/DashboardsLoader.ts +++ b/public/app/features/scenes/dashboard/DashboardsLoader.ts @@ -1,12 +1,26 @@ +import { + ConstantVariableModel, + CustomVariableModel, + DataSourceVariableModel, + QueryVariableModel, + VariableModel, +} from '@grafana/data'; import { StateManagerBase } from 'app/core/services/StateManagerBase'; import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardDTO } from 'app/types'; -import { VizPanel, SceneTimePicker, SceneGridLayout, SceneGridRow } from '../components'; +import { VizPanel, SceneTimePicker, SceneGridLayout, SceneGridRow, SceneSubMenu } from '../components'; import { SceneTimeRange } from '../core/SceneTimeRange'; import { SceneObject } from '../core/types'; import { SceneQueryRunner } from '../querying/SceneQueryRunner'; +import { VariableValueSelectors } from '../variables/components/VariableValueSelectors'; +import { SceneVariableSet } from '../variables/sets/SceneVariableSet'; +import { SceneVariable } from '../variables/types'; +import { ConstantVariable } from '../variables/variants/ConstantVariable'; +import { CustomVariable } from '../variables/variants/CustomVariable'; +import { DataSourceVariable } from '../variables/variants/DataSourceVariable'; +import { QueryVariable } from '../variables/variants/query/QueryVariable'; import { DashboardScene } from './DashboardScene'; @@ -45,6 +59,19 @@ export class DashboardLoader extends StateManagerBase { // Just to have migrations run const oldModel = new DashboardModel(rsp.dashboard, rsp.meta); + let subMenu: SceneSubMenu | undefined = undefined; + let variables: SceneVariableSet | undefined = undefined; + + if (oldModel.templating.list.length) { + const variableObjects = this.migrateVariables(oldModel); + subMenu = new SceneSubMenu({ + children: [new VariableValueSelectors({})], + }); + variables = new SceneVariableSet({ + variables: variableObjects, + }); + } + const dashboard = new DashboardScene({ title: oldModel.title, uid: oldModel.uid, @@ -53,6 +80,8 @@ export class DashboardLoader extends StateManagerBase { }), $timeRange: new SceneTimeRange(), actions: [new SceneTimePicker({})], + $variables: variables, + subMenu, }); // We initialize URL sync here as it better to do that before mounting and doing any rendering. @@ -136,6 +165,88 @@ export class DashboardLoader extends StateManagerBase { return panels; } + + private migrateVariables(dashboard: DashboardModel) { + return ( + dashboard.templating.list + .map((v) => { + try { + return createVariableFromLegacyModel(v); + } catch (err) { + console.error(err); + return null; + } + }) + // TODO: Remove filter + // Added temporarily to allow skipping non-compatible variables + .filter((v): v is SceneVariable => Boolean(v)) + ); + } +} + +export function createVariableFromLegacyModel(variable: VariableModel): SceneVariable { + const commonProperties = { + name: variable.name, + label: variable.label, + }; + if (isCustomVariable(variable)) { + return new CustomVariable({ + ...commonProperties, + value: variable.current.value, + text: variable.current.text, + description: variable.description, + query: variable.query, + isMulti: variable.multi, + allValue: variable.allValue || undefined, + includeAll: variable.includeAll, + defaultToAll: Boolean(variable.includeAll), + skipUrlSync: variable.skipUrlSync, + hide: variable.hide, + }); + } else if (isQueryVariable(variable)) { + return new QueryVariable({ + ...commonProperties, + value: variable.current.value, + text: variable.current.text, + description: variable.description, + query: variable.query, + datasource: variable.datasource, + sort: variable.sort, + refresh: variable.refresh, + regex: variable.regex, + allValue: variable.allValue || undefined, + includeAll: variable.includeAll, + defaultToAll: Boolean(variable.includeAll), + isMulti: variable.multi, + skipUrlSync: variable.skipUrlSync, + hide: variable.hide, + }); + } else if (isDataSourceVariable(variable)) { + return new DataSourceVariable({ + ...commonProperties, + value: variable.current.value, + text: variable.current.text, + description: variable.description, + regex: variable.regex, + query: variable.query, + allValue: variable.allValue || undefined, + includeAll: variable.includeAll, + defaultToAll: Boolean(variable.includeAll), + skipUrlSync: variable.skipUrlSync, + isMulti: variable.multi, + hide: variable.hide, + }); + } else if (isConstantVariable(variable)) { + return new ConstantVariable({ + ...commonProperties, + description: variable.description, + value: variable.query, + skipUrlSync: variable.skipUrlSync, + hide: variable.hide, + }); + } else { + throw new Error(`Scenes: Unsupported variable type ${variable.type}`); + } } function createVizPanelFromPanelModel(panel: PanelModel) { @@ -166,3 +277,8 @@ export function getDashboardLoader(): DashboardLoader { return loader; } + +const isCustomVariable = (v: VariableModel): v is CustomVariableModel => v.type === 'custom'; +const isQueryVariable = (v: VariableModel): v is QueryVariableModel => v.type === 'query'; +const isDataSourceVariable = (v: VariableModel): v is DataSourceVariableModel => v.type === 'datasource'; +const isConstantVariable = (v: VariableModel): v is ConstantVariableModel => v.type === 'constant'; diff --git a/public/app/features/scenes/variables/variants/ConstantVariable.ts b/public/app/features/scenes/variables/variants/ConstantVariable.ts index f946cc9b13e..b6f65d9b572 100644 --- a/public/app/features/scenes/variables/variants/ConstantVariable.ts +++ b/public/app/features/scenes/variables/variants/ConstantVariable.ts @@ -2,7 +2,7 @@ import { SceneObjectBase } from '../../core/SceneObjectBase'; import { SceneVariable, SceneVariableState, VariableValue } from '../types'; export interface ConstantVariableState extends SceneVariableState { - value: string; + value: VariableValue; } export class ConstantVariable