Scenes / DashboardsLoader: Add variables migration (#60226)

* VizPanel - add variables dependencies definition

* Migrate variables to scene variables

* Constant variable migration

* Update test

* Lint fix
pull/60820/head
Dominik Prokop 3 years ago committed by GitHub
parent 53d3a810dd
commit 168afa99d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .betterer.results
  2. 4
      public/app/features/dashboard/state/DashboardMigrator.test.ts
  3. 3
      public/app/features/scenes/components/VizPanel/VizPanel.tsx
  4. 5
      public/app/features/scenes/components/VizPanel/VizPanelRenderer.tsx
  5. 4
      public/app/features/scenes/dashboard/DashboardScene.tsx
  6. 283
      public/app/features/scenes/dashboard/DashboardsLoader.test.ts
  7. 118
      public/app/features/scenes/dashboard/DashboardsLoader.ts
  8. 2
      public/app/features/scenes/variables/variants/ConstantVariable.ts

@ -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"]

@ -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: {

@ -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<TOptions = {}, TFieldConfig = {}> 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;

@ -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<VizPanel>) {
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}

@ -15,6 +15,7 @@ interface DashboardSceneState extends SceneObjectStatePlain {
uid: string;
body: SceneLayout;
actions?: SceneObject[];
subMenu?: SceneObject;
}
export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
@ -43,7 +44,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
}
function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
const { title, body, actions = [], uid } = model.useState();
const { title, body, actions = [], uid, subMenu } = model.useState();
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />);
@ -58,6 +59,7 @@ function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>)
return (
<Page navId="scenes" pageNav={{ text: title }} layout={PageLayoutType.Canvas} toolbar={pageToolbar}>
{subMenu && <subMenu.Component model={subMenu} />}
<div style={{ flexGrow: 1, display: 'flex', gap: '8px', overflow: 'auto' }}>
<body.Component model={body} />
</div>

@ -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();
});
});
});

@ -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<DashboardLoaderState> {
// 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<DashboardLoaderState> {
}),
$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<DashboardLoaderState> {
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';

@ -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

Loading…
Cancel
Save