mirror of
https://github.com/grafana/grafana.git
synced 2026-09-05 04:40:13 -05:00
Scenes / DashboardsLoader: Add variables migration (#60226)
* VizPanel - add variables dependencies definition * Migrate variables to scene variables * Constant variable migration * Update test * Lint fix
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user