Dashboard Scene: Fix snapshots not displaying variables values (#88967)

* Use new snapshot variables from scenes

* Add snapshotVariable implementation

* Refactor: Extract variables logic from transforSaveModelToScene file

---------

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Alexa V 2024-08-20 17:05:12 +02:00 committed by GitHub
parent 6f63def283
commit cd4b7ef9db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1286 additions and 830 deletions

View File

@ -294,7 +294,8 @@ lineage: schemas: [{
// `textbox`: Display a free text input field with an optional default value. // `textbox`: Display a free text input field with an optional default value.
// `custom`: Define the variable options manually using a comma-separated list. // `custom`: Define the variable options manually using a comma-separated list.
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables // `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
#VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" | "system" @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview) #VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
"system" | "snapshot" @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview)
// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value. // Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
// Continuous color interpolates a color using the percentage of a value relative to min and max. // Continuous color interpolates a color using the percentage of a value relative to min and max.

View File

@ -515,6 +515,7 @@ export {
type UserVariableModel, type UserVariableModel,
type SystemVariable, type SystemVariable,
type BaseVariableModel, type BaseVariableModel,
type SnapshotVariableModel,
} from './types/templateVars'; } from './types/templateVars';
export { type Threshold, ThresholdsMode, type ThresholdsConfig } from './types/thresholds'; export { type Threshold, ThresholdsMode, type ThresholdsConfig } from './types/thresholds';
export { export {

View File

@ -22,7 +22,8 @@ export type TypedVariableModel =
| CustomVariableModel | CustomVariableModel
| UserVariableModel | UserVariableModel
| OrgVariableModel | OrgVariableModel
| DashboardVariableModel; | DashboardVariableModel
| SnapshotVariableModel;
export enum VariableRefresh { export enum VariableRefresh {
never, // removed from the UI never, // removed from the UI
@ -178,3 +179,8 @@ export interface BaseVariableModel {
description: string | null; description: string | null;
usedInRepeat?: boolean; usedInRepeat?: boolean;
} }
export interface SnapshotVariableModel extends VariableWithOptions {
type: 'snapshot';
query: string;
}

View File

@ -349,7 +349,7 @@ export type DashboardLinkType = ('link' | 'dashboards');
* `custom`: Define the variable options manually using a comma-separated list. * `custom`: Define the variable options manually using a comma-separated list.
* `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables * `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
*/ */
export type VariableType = ('query' | 'adhoc' | 'groupby' | 'constant' | 'datasource' | 'interval' | 'textbox' | 'custom' | 'system'); export type VariableType = ('query' | 'adhoc' | 'groupby' | 'constant' | 'datasource' | 'interval' | 'textbox' | 'custom' | 'system' | 'snapshot');
/** /**
* Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value. * Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.

View File

@ -168,6 +168,7 @@ const (
VariableTypeGroupby VariableType = "groupby" VariableTypeGroupby VariableType = "groupby"
VariableTypeInterval VariableType = "interval" VariableTypeInterval VariableType = "interval"
VariableTypeQuery VariableType = "query" VariableTypeQuery VariableType = "query"
VariableTypeSnapshot VariableType = "snapshot"
VariableTypeSystem VariableType = "system" VariableTypeSystem VariableType = "system"
VariableTypeTextbox VariableType = "textbox" VariableTypeTextbox VariableType = "textbox"
) )

View File

@ -0,0 +1,33 @@
import { SnapshotVariable } from './SnapshotVariable';
describe('SnapshotVariable', () => {
describe('SnapshotVariable state', () => {
it('should create a new snapshotVariable when custom variable is passed', () => {
const { multiVariable } = setupScene();
const snapshot = new SnapshotVariable(multiVariable);
//expect snapshot to be defined
expect(snapshot).toBeDefined();
expect(snapshot.state).toBeDefined();
expect(snapshot.state.type).toBe('snapshot');
expect(snapshot.state.isReadOnly).toBe(true);
expect(snapshot.state.value).toBe(multiVariable.value);
expect(snapshot.state.text).toBe(multiVariable.text);
expect(snapshot.state.hide).toBe(multiVariable.hide);
});
});
});
function setupScene() {
// create custom variable type custom
const multiVariable = {
name: 'Multi',
description: 'Define variable values manually',
text: 'myMultiText',
value: 'myMultiValue',
multi: true,
hide: 0,
};
return { multiVariable };
}

View File

@ -0,0 +1,81 @@
import { Observable, map, of } from 'rxjs';
import {
MultiValueVariable,
MultiValueVariableState,
SceneComponentProps,
ValidateAndUpdateResult,
VariableDependencyConfig,
VariableValueOption,
renderSelectForVariable,
sceneGraph,
VariableGetOptionsArgs,
} from '@grafana/scenes';
export interface SnapshotVariableState extends MultiValueVariableState {
query?: string;
}
export class SnapshotVariable extends MultiValueVariable<SnapshotVariableState> {
protected _variableDependency = new VariableDependencyConfig(this, {
statePaths: [],
});
public constructor(initialState: Partial<SnapshotVariableState>) {
super({
name: '',
type: 'snapshot',
isReadOnly: true,
query: '',
value: '',
text: '',
options: [],
...initialState,
});
}
public getValueOptions(args: VariableGetOptionsArgs): Observable<VariableValueOption[]> {
const interpolated = sceneGraph.interpolate(this, this.state.query);
const match = interpolated.match(/(?:\\,|[^,])+/g) ?? [];
const options = match.map((text) => {
text = text.replace(/\\,/g, ',');
const textMatch = /^(.+)\s:\s(.+)$/g.exec(text) ?? [];
if (textMatch.length === 3) {
const [, key, value] = textMatch;
return { label: key.trim(), value: value.trim() };
} else {
return { label: text.trim(), value: text.trim() };
}
});
return of(options);
}
public validateAndUpdate(): Observable<ValidateAndUpdateResult> {
return this.getValueOptions({}).pipe(
map((options) => {
if (this.state.options !== options) {
this._updateValueGivenNewOptions(options);
}
return {};
})
);
}
public static Component = ({ model }: SceneComponentProps<MultiValueVariable<SnapshotVariableState>>) => {
return renderSelectForVariable(model);
};
// we will always preserve the current value and text for snapshots
private _updateValueGivenNewOptions(options: VariableValueOption[]) {
const { value: currentValue, text: currentText } = this.state;
const stateUpdate: Partial<MultiValueVariableState> = {
options,
loading: false,
value: currentValue ?? [],
text: currentText ?? [],
};
this.setState(stateUpdate);
}
}

View File

@ -1,24 +1,10 @@
import { import { LoadingState } from '@grafana/data';
LoadingState,
ConstantVariableModel,
CustomVariableModel,
DataSourceVariableModel,
QueryVariableModel,
IntervalVariableModel,
TypedVariableModel,
TextBoxVariableModel,
GroupByVariableModel,
} from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { import {
AdHocFiltersVariable, AdHocFiltersVariable,
behaviors, behaviors,
ConstantVariable, ConstantVariable,
CustomVariable,
DataSourceVariable,
GroupByVariable,
QueryVariable,
SceneDataLayerControls, SceneDataLayerControls,
SceneDataTransformer, SceneDataTransformer,
SceneGridLayout, SceneGridLayout,
@ -50,12 +36,12 @@ import { getQueryRunnerFor } from '../utils/utils';
import { buildNewDashboardSaveModel } from './buildNewDashboardSaveModel'; import { buildNewDashboardSaveModel } from './buildNewDashboardSaveModel';
import { GRAFANA_DATASOURCE_REF } from './const'; import { GRAFANA_DATASOURCE_REF } from './const';
import { SnapshotVariable } from './custom-variables/SnapshotVariable';
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json'; import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json'; import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json';
import { import {
createDashboardSceneFromDashboardModel, createDashboardSceneFromDashboardModel,
buildGridItemForPanel, buildGridItemForPanel,
createSceneVariableFromVariableModel,
transformSaveModelToScene, transformSaveModelToScene,
convertOldSnapshotToScenesSnapshot, convertOldSnapshotToScenesSnapshot,
buildGridItemForLibPanel, buildGridItemForLibPanel,
@ -193,6 +179,113 @@ describe('transformSaveModelToScene', () => {
}); });
}); });
describe('When creating a snapshot dashboard scene', () => {
it('should initialize a dashboard scene with SnapshotVariables', () => {
const customVariable = {
current: {
selected: false,
text: 'a',
value: 'a',
},
hide: 0,
includeAll: false,
multi: false,
name: 'custom0',
options: [],
query: 'a,b,c,d',
skipUrlSync: false,
type: 'custom' as VariableType,
rootStateKey: 'N4XLmH5Vz',
};
const intervalVariable = {
current: {
selected: false,
text: '10s',
value: '10s',
},
hide: 0,
includeAll: false,
multi: false,
name: 'interval0',
options: [],
query: '10s,20s,30s',
skipUrlSync: false,
type: 'interval' as VariableType,
rootStateKey: 'N4XLmH5Vz',
};
const adHocVariable = {
global: false,
name: 'CoolFilters',
label: 'CoolFilters Label',
type: 'adhoc' as VariableType,
datasource: {
uid: 'gdev-prometheus',
type: 'prometheus',
},
filters: [
{
key: 'filterTest',
operator: '=',
value: 'test',
},
],
baseFilters: [
{
key: 'baseFilterTest',
operator: '=',
value: 'test',
},
],
hide: 0,
index: 0,
};
const snapshot = {
...defaultDashboard,
title: 'snapshot dash',
uid: 'test-uid',
time: { from: 'now-10h', to: 'now' },
weekStart: 'saturday',
fiscalYearStartMonth: 2,
timezone: 'America/New_York',
timepicker: {
...defaultTimePickerConfig,
hidden: true,
},
links: [{ ...NEW_LINK, title: 'Link 1' }],
templating: {
list: [customVariable, adHocVariable, intervalVariable],
},
};
const oldModel = new DashboardModel(snapshot, { isSnapshot: true });
const scene = createDashboardSceneFromDashboardModel(oldModel, snapshot);
// check variables were converted to snapshot variables
expect(scene.state.$variables?.state.variables).toHaveLength(3);
expect(scene.state.$variables?.getByName('custom0')).toBeInstanceOf(SnapshotVariable);
expect(scene.state.$variables?.getByName('CoolFilters')).toBeInstanceOf(AdHocFiltersVariable);
expect(scene.state.$variables?.getByName('interval0')).toBeInstanceOf(SnapshotVariable);
// custom snapshot
const customSnapshot = scene.state.$variables?.getByName('custom0') as SnapshotVariable;
expect(customSnapshot.state.value).toBe('a');
expect(customSnapshot.state.text).toBe('a');
expect(customSnapshot.state.isReadOnly).toBe(true);
// adhoc snapshot
const adhocSnapshot = scene.state.$variables?.getByName('CoolFilters') as AdHocFiltersVariable;
expect(adhocSnapshot.state.filters).toEqual(adHocVariable.filters);
expect(adhocSnapshot.state.readOnly).toBe(true);
// interval snapshot
const intervalSnapshot = scene.state.$variables?.getByName('interval0') as SnapshotVariable;
expect(intervalSnapshot.state.value).toBe('10s');
expect(intervalSnapshot.state.text).toBe('10s');
expect(intervalSnapshot.state.isReadOnly).toBe(true);
});
});
describe('when organizing panels as scene children', () => { describe('when organizing panels as scene children', () => {
it('should create panels within collapsed rows', () => { it('should create panels within collapsed rows', () => {
const panel = createPanelSaveModel({ const panel = createPanelSaveModel({
@ -593,647 +686,6 @@ describe('transformSaveModelToScene', () => {
}); });
}); });
describe('when creating variables objects', () => {
it('should migrate custom variable', () => {
const variable: CustomVariableModel = {
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',
rootStateKey: 'N4XLmH5Vz',
id: 'query0',
global: false,
index: 0,
state: LoadingState.Done,
error: null,
description: null,
allValue: null,
};
const migrated = createSceneVariableFromVariableModel(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 with definition', () => {
const variable: QueryVariableModel = {
allValue: null,
current: {
text: 'America',
value: 'America',
selected: false,
},
datasource: {
uid: 'P15396BDD62B2BE29',
type: 'influxdb',
},
definition: 'SHOW TAG VALUES WITH KEY = "datacenter"',
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,
type: 'query',
rootStateKey: '000000002',
id: 'datacenter',
global: false,
index: 0,
state: LoadingState.Done,
error: null,
description: null,
};
const migrated = createSceneVariableFromVariableModel(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,
definition: 'SHOW TAG VALUES WITH KEY = "datacenter"',
});
});
it('should migrate datasource variable', () => {
const variable: DataSourceVariableModel = {
id: 'query1',
rootStateKey: 'N4XLmH5Vz',
name: 'query1',
type: 'datasource',
global: false,
index: 1,
hide: 0,
skipUrlSync: false,
state: LoadingState.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 = createSceneVariableFromVariableModel(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: [],
pluginId: '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: ConstantVariableModel = {
hide: 2,
label: 'constant',
name: 'constant',
skipUrlSync: false,
type: 'constant',
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: LoadingState.Done,
error: null,
description: null,
};
const migrated = createSceneVariableFromVariableModel(variable);
const { key, ...rest } = migrated.state;
expect(rest).toEqual({
description: null,
hide: 2,
label: 'constant',
name: 'constant',
skipUrlSync: true,
type: 'constant',
value: 'test',
});
});
it('should migrate interval variable', () => {
const variable: IntervalVariableModel = {
name: 'intervalVar',
label: 'Interval Label',
type: 'interval',
rootStateKey: 'N4XLmH5Vz',
auto: false,
refresh: 2,
auto_count: 30,
auto_min: '10s',
current: {
selected: true,
text: '1m',
value: '1m',
},
options: [
{
selected: true,
text: '1m',
value: '1m',
},
],
query: '1m, 5m, 15m, 30m, 1h, 6h, 12h, 1d, 7d, 14d, 30d',
id: 'intervalVar',
global: false,
index: 4,
hide: 0,
skipUrlSync: false,
state: LoadingState.Done,
error: null,
description: null,
};
const migrated = createSceneVariableFromVariableModel(variable);
const { key, ...rest } = migrated.state;
expect(rest).toEqual({
label: 'Interval Label',
autoEnabled: false,
autoMinInterval: '10s',
autoStepCount: 30,
description: null,
refresh: 2,
intervals: ['1m', '5m', '15m', '30m', '1h', '6h', '12h', '1d', '7d', '14d', '30d'],
hide: 0,
name: 'intervalVar',
skipUrlSync: false,
type: 'interval',
value: '1m',
});
});
it('should migrate textbox variable', () => {
const variable: TextBoxVariableModel = {
id: 'query0',
global: false,
index: 0,
state: LoadingState.Done,
error: null,
name: 'textboxVar',
label: 'Textbox Label',
description: 'Textbox Description',
type: 'textbox',
rootStateKey: 'N4XLmH5Vz',
current: {},
hide: 0,
options: [],
query: 'defaultValue',
originalQuery: 'defaultValue',
skipUrlSync: false,
};
const migrated = createSceneVariableFromVariableModel(variable);
const { key, ...rest } = migrated.state;
expect(rest).toEqual({
description: 'Textbox Description',
hide: 0,
label: 'Textbox Label',
name: 'textboxVar',
skipUrlSync: false,
type: 'textbox',
value: 'defaultValue',
});
});
it('should migrate adhoc variable', () => {
const variable: TypedVariableModel = {
id: 'adhoc',
global: false,
index: 0,
state: LoadingState.Done,
error: null,
name: 'adhoc',
label: 'Adhoc Label',
description: 'Adhoc Description',
type: 'adhoc',
rootStateKey: 'N4XLmH5Vz',
datasource: {
uid: 'gdev-prometheus',
type: 'prometheus',
},
filters: [
{
key: 'filterTest',
operator: '=',
value: 'test',
},
],
baseFilters: [
{
key: 'baseFilterTest',
operator: '=',
value: 'test',
},
],
hide: 0,
skipUrlSync: false,
};
const migrated = createSceneVariableFromVariableModel(variable) as AdHocFiltersVariable;
const filterVarState = migrated.state;
expect(migrated).toBeInstanceOf(AdHocFiltersVariable);
expect(filterVarState).toEqual({
key: expect.any(String),
description: 'Adhoc Description',
hide: 0,
label: 'Adhoc Label',
name: 'adhoc',
skipUrlSync: false,
type: 'adhoc',
filterExpression: 'filterTest="test"',
filters: [{ key: 'filterTest', operator: '=', value: 'test' }],
baseFilters: [{ key: 'baseFilterTest', operator: '=', value: 'test' }],
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
applyMode: 'auto',
useQueriesAsFilterForOptions: true,
});
});
it('should migrate adhoc variable with default keys', () => {
const variable: TypedVariableModel = {
id: 'adhoc',
global: false,
index: 0,
state: LoadingState.Done,
error: null,
name: 'adhoc',
label: 'Adhoc Label',
description: 'Adhoc Description',
type: 'adhoc',
rootStateKey: 'N4XLmH5Vz',
datasource: {
uid: 'gdev-prometheus',
type: 'prometheus',
},
filters: [
{
key: 'filterTest',
operator: '=',
value: 'test',
},
],
baseFilters: [
{
key: 'baseFilterTest',
operator: '=',
value: 'test',
},
],
defaultKeys: [
{
text: 'some',
value: '1',
},
{
text: 'static',
value: '2',
},
{
text: 'keys',
value: '3',
},
],
hide: 0,
skipUrlSync: false,
};
const migrated = createSceneVariableFromVariableModel(variable) as AdHocFiltersVariable;
const filterVarState = migrated.state;
expect(migrated).toBeInstanceOf(AdHocFiltersVariable);
expect(filterVarState).toEqual({
key: expect.any(String),
description: 'Adhoc Description',
hide: 0,
label: 'Adhoc Label',
name: 'adhoc',
skipUrlSync: false,
type: 'adhoc',
filterExpression: 'filterTest="test"',
filters: [{ key: 'filterTest', operator: '=', value: 'test' }],
baseFilters: [{ key: 'baseFilterTest', operator: '=', value: 'test' }],
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
applyMode: 'auto',
defaultKeys: [
{
text: 'some',
value: '1',
},
{
text: 'static',
value: '2',
},
{
text: 'keys',
value: '3',
},
],
useQueriesAsFilterForOptions: true,
});
});
describe('when groupByVariable feature toggle is enabled', () => {
beforeAll(() => {
config.featureToggles.groupByVariable = true;
});
afterAll(() => {
config.featureToggles.groupByVariable = false;
});
it('should migrate groupby variable', () => {
const variable: GroupByVariableModel = {
id: 'groupby',
global: false,
index: 0,
state: LoadingState.Done,
error: null,
name: 'groupby',
label: 'GroupBy Label',
description: 'GroupBy Description',
type: 'groupby',
rootStateKey: 'N4XLmH5Vz',
datasource: {
uid: 'gdev-prometheus',
type: 'prometheus',
},
multi: true,
options: [
{
selected: false,
text: 'Foo',
value: 'foo',
},
{
selected: false,
text: 'Bar',
value: 'bar',
},
],
current: {},
query: '',
hide: 0,
skipUrlSync: false,
};
const migrated = createSceneVariableFromVariableModel(variable) as GroupByVariable;
const groupbyVarState = migrated.state;
expect(migrated).toBeInstanceOf(GroupByVariable);
expect(groupbyVarState).toEqual({
key: expect.any(String),
description: 'GroupBy Description',
hide: 0,
defaultOptions: [
{
selected: false,
text: 'Foo',
value: 'foo',
},
{
selected: false,
text: 'Bar',
value: 'bar',
},
],
isMulti: true,
layout: 'horizontal',
noValueOnClear: true,
label: 'GroupBy Label',
name: 'groupby',
skipUrlSync: false,
type: 'groupby',
baseFilters: [],
options: [],
text: [],
value: [],
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
applyMode: 'auto',
});
});
});
describe('when groupByVariable feature toggle is disabled', () => {
it('should not migrate groupby variable and throw an error instead', () => {
const variable: GroupByVariableModel = {
id: 'groupby',
global: false,
index: 0,
state: LoadingState.Done,
error: null,
name: 'groupby',
label: 'GroupBy Label',
description: 'GroupBy Description',
type: 'groupby',
rootStateKey: 'N4XLmH5Vz',
datasource: {
uid: 'gdev-prometheus',
type: 'prometheus',
},
multi: true,
options: [],
current: {},
query: '',
hide: 0,
skipUrlSync: false,
};
expect(() => createSceneVariableFromVariableModel(variable)).toThrow('Scenes: Unsupported variable type');
});
});
it.each(['system'])('should throw for unsupported (yet) variables', (type) => {
const variable = {
name: 'query0',
type: type as VariableType,
};
expect(() => createSceneVariableFromVariableModel(variable as TypedVariableModel)).toThrow();
});
it('should handle variable without current', () => {
// @ts-expect-error
const variable: TypedVariableModel = {
id: 'query1',
name: 'query1',
type: 'datasource',
global: false,
regex: '/^gdev/',
options: [],
query: 'prometheus',
multi: true,
includeAll: true,
refresh: 1,
allValue: 'Custom all',
};
const migrated = createSceneVariableFromVariableModel(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: [],
pluginId: 'prometheus',
regex: '/^gdev/',
text: '',
type: 'datasource',
value: '',
isMulti: true,
});
});
});
describe('Repeating rows', () => { describe('Repeating rows', () => {
it('Should build correct scene model', () => { it('Should build correct scene model', () => {
const scene = transformSaveModelToScene({ dashboard: repeatingRowsAndPanelsDashboardJson as any, meta: {} }); const scene = transformSaveModelToScene({ dashboard: repeatingRowsAndPanelsDashboardJson as any, meta: {} });

View File

@ -1,6 +1,6 @@
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import { DataFrameDTO, DataFrameJSON, TypedVariableModel } from '@grafana/data'; import { DataFrameDTO, DataFrameJSON } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { import {
VizPanel, VizPanel,
@ -10,12 +10,6 @@ import {
SceneTimeRange, SceneTimeRange,
SceneVariableSet, SceneVariableSet,
VariableValueSelectors, VariableValueSelectors,
SceneVariable,
CustomVariable,
DataSourceVariable,
QueryVariable,
ConstantVariable,
IntervalVariable,
SceneRefreshPicker, SceneRefreshPicker,
SceneObject, SceneObject,
VizPanelMenu, VizPanelMenu,
@ -24,10 +18,7 @@ import {
SceneGridItemLike, SceneGridItemLike,
SceneDataLayerProvider, SceneDataLayerProvider,
SceneDataLayerControls, SceneDataLayerControls,
TextBoxVariable,
UserActionEvent, UserActionEvent,
GroupByVariable,
AdHocFiltersVariable,
sceneGraph, sceneGraph,
} from '@grafana/scenes'; } from '@grafana/scenes';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
@ -52,12 +43,8 @@ import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
import { createPanelDataProvider } from '../utils/createPanelDataProvider'; import { createPanelDataProvider } from '../utils/createPanelDataProvider';
import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState'; import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState';
import { DashboardInteractions } from '../utils/interactions'; import { DashboardInteractions } from '../utils/interactions';
import { import { getDashboardSceneFor, getVizPanelKeyForPanelId } from '../utils/utils';
getCurrentValueForOldIntervalModel, import { createVariablesForDashboard, createVariablesForSnapshot } from '../utils/variables';
getDashboardSceneFor,
getIntervalsFromQueryString,
getVizPanelKeyForPanelId,
} from '../utils/utils';
import { getAngularPanelMigrationHandler } from './angularMigration'; import { getAngularPanelMigrationHandler } from './angularMigration';
import { GRAFANA_DATASOURCE_REF } from './const'; import { GRAFANA_DATASOURCE_REF } from './const';
@ -198,22 +185,11 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
let alertStatesLayer: AlertStatesDataLayer | undefined; let alertStatesLayer: AlertStatesDataLayer | undefined;
if (oldModel.templating?.list?.length) { if (oldModel.templating?.list?.length) {
const variableObjects = oldModel.templating.list if (oldModel.meta.isSnapshot) {
.map((v) => { variables = createVariablesForSnapshot(oldModel);
try { } else {
return createSceneVariableFromVariableModel(v); variables = createVariablesForDashboard(oldModel);
} 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));
variables = new SceneVariableSet({
variables: variableObjects,
});
} else { } else {
// Create empty variable set // Create empty variable set
variables = new SceneVariableSet({ variables = new SceneVariableSet({
@ -303,128 +279,6 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
return dashboardScene; return dashboardScene;
} }
export function createSceneVariableFromVariableModel(variable: TypedVariableModel): SceneVariable {
const commonProperties = {
name: variable.name,
label: variable.label,
description: variable.description,
};
if (variable.type === 'adhoc') {
return new AdHocFiltersVariable({
...commonProperties,
description: variable.description,
skipUrlSync: variable.skipUrlSync,
hide: variable.hide,
datasource: variable.datasource,
applyMode: 'auto',
filters: variable.filters ?? [],
baseFilters: variable.baseFilters ?? [],
defaultKeys: variable.defaultKeys,
useQueriesAsFilterForOptions: true,
});
}
if (variable.type === 'custom') {
return new CustomVariable({
...commonProperties,
value: variable.current?.value ?? '',
text: variable.current?.text ?? '',
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 (variable.type === 'query') {
return new QueryVariable({
...commonProperties,
value: variable.current?.value ?? '',
text: variable.current?.text ?? '',
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,
definition: variable.definition,
});
} else if (variable.type === 'datasource') {
return new DataSourceVariable({
...commonProperties,
value: variable.current?.value ?? '',
text: variable.current?.text ?? '',
regex: variable.regex,
pluginId: variable.query,
allValue: variable.allValue || undefined,
includeAll: variable.includeAll,
defaultToAll: Boolean(variable.includeAll),
skipUrlSync: variable.skipUrlSync,
isMulti: variable.multi,
hide: variable.hide,
});
} else if (variable.type === 'interval') {
const intervals = getIntervalsFromQueryString(variable.query);
const currentInterval = getCurrentValueForOldIntervalModel(variable, intervals);
return new IntervalVariable({
...commonProperties,
value: currentInterval,
intervals: intervals,
autoEnabled: variable.auto,
autoStepCount: variable.auto_count,
autoMinInterval: variable.auto_min,
refresh: variable.refresh,
skipUrlSync: variable.skipUrlSync,
hide: variable.hide,
});
} else if (variable.type === 'constant') {
return new ConstantVariable({
...commonProperties,
value: variable.query,
skipUrlSync: variable.skipUrlSync,
hide: variable.hide,
});
} else if (variable.type === 'textbox') {
let val;
if (!variable?.current?.value) {
val = variable.query;
} else {
if (typeof variable.current.value === 'string') {
val = variable.current.value;
} else {
val = variable.current.value[0];
}
}
return new TextBoxVariable({
...commonProperties,
value: val,
skipUrlSync: variable.skipUrlSync,
hide: variable.hide,
});
} else if (config.featureToggles.groupByVariable && variable.type === 'groupby') {
return new GroupByVariable({
...commonProperties,
datasource: variable.datasource,
value: variable.current?.value || [],
text: variable.current?.text || [],
skipUrlSync: variable.skipUrlSync,
hide: variable.hide,
// @ts-expect-error
defaultOptions: variable.options,
});
} else {
throw new Error(`Scenes: Unsupported variable type ${variable.type}`);
}
}
export function buildGridItemForLibPanel(panel: PanelModel) { export function buildGridItemForLibPanel(panel: PanelModel) {
if (!panel.libraryPanel) { if (!panel.libraryPanel) {
return null; return null;

View File

@ -36,7 +36,8 @@ interface EditableVariableConfig {
editor: React.ComponentType<any>; editor: React.ComponentType<any>;
} }
export type EditableVariableType = Exclude<VariableType, 'system'>; //exclude system variable type and snapshot variable type
export type EditableVariableType = Exclude<VariableType, 'system' | 'snapshot'>;
export function isEditableVariableType(type: VariableType): type is EditableVariableType { export function isEditableVariableType(type: VariableType): type is EditableVariableType {
return type !== 'system'; return type !== 'system';

View File

@ -0,0 +1,775 @@
import {
ConstantVariableModel,
CustomVariableModel,
DataSourceVariableModel,
GroupByVariableModel,
IntervalVariableModel,
LoadingState,
QueryVariableModel,
TextBoxVariableModel,
TypedVariableModel,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import {
AdHocFiltersVariable,
CustomVariable,
DataSourceVariable,
GroupByVariable,
QueryVariable,
SceneVariableSet,
} from '@grafana/scenes';
import { defaultDashboard, defaultTimePickerConfig, VariableType } from '@grafana/schema';
import { DashboardModel } from 'app/features/dashboard/state';
import { SnapshotVariable } from '../serialization/custom-variables/SnapshotVariable';
import { NEW_LINK } from '../settings/links/utils';
import { createSceneVariableFromVariableModel, createVariablesForSnapshot } from './variables';
describe('when creating variables objects', () => {
it('should migrate custom variable', () => {
const variable: CustomVariableModel = {
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',
rootStateKey: 'N4XLmH5Vz',
id: 'query0',
global: false,
index: 0,
state: LoadingState.Done,
error: null,
description: null,
allValue: null,
};
const migrated = createSceneVariableFromVariableModel(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 with definition', () => {
const variable: QueryVariableModel = {
allValue: null,
current: {
text: 'America',
value: 'America',
selected: false,
},
datasource: {
uid: 'P15396BDD62B2BE29',
type: 'influxdb',
},
definition: 'SHOW TAG VALUES WITH KEY = "datacenter"',
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,
type: 'query',
rootStateKey: '000000002',
id: 'datacenter',
global: false,
index: 0,
state: LoadingState.Done,
error: null,
description: null,
};
const migrated = createSceneVariableFromVariableModel(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,
definition: 'SHOW TAG VALUES WITH KEY = "datacenter"',
});
});
it('should migrate datasource variable', () => {
const variable: DataSourceVariableModel = {
id: 'query1',
rootStateKey: 'N4XLmH5Vz',
name: 'query1',
type: 'datasource',
global: false,
index: 1,
hide: 0,
skipUrlSync: false,
state: LoadingState.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 = createSceneVariableFromVariableModel(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: [],
pluginId: '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: ConstantVariableModel = {
hide: 2,
label: 'constant',
name: 'constant',
skipUrlSync: false,
type: 'constant',
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: LoadingState.Done,
error: null,
description: null,
};
const migrated = createSceneVariableFromVariableModel(variable);
const { key, ...rest } = migrated.state;
expect(rest).toEqual({
description: null,
hide: 2,
label: 'constant',
name: 'constant',
skipUrlSync: true,
type: 'constant',
value: 'test',
});
});
it('should migrate interval variable', () => {
const variable: IntervalVariableModel = {
name: 'intervalVar',
label: 'Interval Label',
type: 'interval',
rootStateKey: 'N4XLmH5Vz',
auto: false,
refresh: 2,
auto_count: 30,
auto_min: '10s',
current: {
selected: true,
text: '1m',
value: '1m',
},
options: [
{
selected: true,
text: '1m',
value: '1m',
},
],
query: '1m, 5m, 15m, 30m, 1h, 6h, 12h, 1d, 7d, 14d, 30d',
id: 'intervalVar',
global: false,
index: 4,
hide: 0,
skipUrlSync: false,
state: LoadingState.Done,
error: null,
description: null,
};
const migrated = createSceneVariableFromVariableModel(variable);
const { key, ...rest } = migrated.state;
expect(rest).toEqual({
label: 'Interval Label',
autoEnabled: false,
autoMinInterval: '10s',
autoStepCount: 30,
description: null,
refresh: 2,
intervals: ['1m', '5m', '15m', '30m', '1h', '6h', '12h', '1d', '7d', '14d', '30d'],
hide: 0,
name: 'intervalVar',
skipUrlSync: false,
type: 'interval',
value: '1m',
});
});
it('should migrate textbox variable', () => {
const variable: TextBoxVariableModel = {
id: 'query0',
global: false,
index: 0,
state: LoadingState.Done,
error: null,
name: 'textboxVar',
label: 'Textbox Label',
description: 'Textbox Description',
type: 'textbox',
rootStateKey: 'N4XLmH5Vz',
current: {},
hide: 0,
options: [],
query: 'defaultValue',
originalQuery: 'defaultValue',
skipUrlSync: false,
};
const migrated = createSceneVariableFromVariableModel(variable);
const { key, ...rest } = migrated.state;
expect(rest).toEqual({
description: 'Textbox Description',
hide: 0,
label: 'Textbox Label',
name: 'textboxVar',
skipUrlSync: false,
type: 'textbox',
value: 'defaultValue',
});
});
it('should migrate adhoc variable', () => {
const variable: TypedVariableModel = {
id: 'adhoc',
global: false,
index: 0,
state: LoadingState.Done,
error: null,
name: 'adhoc',
label: 'Adhoc Label',
description: 'Adhoc Description',
type: 'adhoc',
rootStateKey: 'N4XLmH5Vz',
datasource: {
uid: 'gdev-prometheus',
type: 'prometheus',
},
filters: [
{
key: 'filterTest',
operator: '=',
value: 'test',
},
],
baseFilters: [
{
key: 'baseFilterTest',
operator: '=',
value: 'test',
},
],
hide: 0,
skipUrlSync: false,
};
const migrated = createSceneVariableFromVariableModel(variable) as AdHocFiltersVariable;
const filterVarState = migrated.state;
expect(migrated).toBeInstanceOf(AdHocFiltersVariable);
expect(filterVarState).toEqual({
key: expect.any(String),
description: 'Adhoc Description',
hide: 0,
label: 'Adhoc Label',
name: 'adhoc',
skipUrlSync: false,
type: 'adhoc',
filterExpression: 'filterTest="test"',
filters: [{ key: 'filterTest', operator: '=', value: 'test' }],
baseFilters: [{ key: 'baseFilterTest', operator: '=', value: 'test' }],
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
applyMode: 'auto',
useQueriesAsFilterForOptions: true,
});
});
it('should migrate adhoc variable with default keys', () => {
const variable: TypedVariableModel = {
id: 'adhoc',
global: false,
index: 0,
state: LoadingState.Done,
error: null,
name: 'adhoc',
label: 'Adhoc Label',
description: 'Adhoc Description',
type: 'adhoc',
rootStateKey: 'N4XLmH5Vz',
datasource: {
uid: 'gdev-prometheus',
type: 'prometheus',
},
filters: [
{
key: 'filterTest',
operator: '=',
value: 'test',
},
],
baseFilters: [
{
key: 'baseFilterTest',
operator: '=',
value: 'test',
},
],
defaultKeys: [
{
text: 'some',
value: '1',
},
{
text: 'static',
value: '2',
},
{
text: 'keys',
value: '3',
},
],
hide: 0,
skipUrlSync: false,
};
const migrated = createSceneVariableFromVariableModel(variable) as AdHocFiltersVariable;
const filterVarState = migrated.state;
expect(migrated).toBeInstanceOf(AdHocFiltersVariable);
expect(filterVarState).toEqual({
key: expect.any(String),
description: 'Adhoc Description',
hide: 0,
label: 'Adhoc Label',
name: 'adhoc',
skipUrlSync: false,
type: 'adhoc',
filterExpression: 'filterTest="test"',
filters: [{ key: 'filterTest', operator: '=', value: 'test' }],
baseFilters: [{ key: 'baseFilterTest', operator: '=', value: 'test' }],
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
applyMode: 'auto',
defaultKeys: [
{
text: 'some',
value: '1',
},
{
text: 'static',
value: '2',
},
{
text: 'keys',
value: '3',
},
],
useQueriesAsFilterForOptions: true,
});
});
describe('when groupByVariable feature toggle is enabled', () => {
beforeAll(() => {
config.featureToggles.groupByVariable = true;
});
afterAll(() => {
config.featureToggles.groupByVariable = false;
});
it('should migrate groupby variable', () => {
const variable: GroupByVariableModel = {
id: 'groupby',
global: false,
index: 0,
state: LoadingState.Done,
error: null,
name: 'groupby',
label: 'GroupBy Label',
description: 'GroupBy Description',
type: 'groupby',
rootStateKey: 'N4XLmH5Vz',
datasource: {
uid: 'gdev-prometheus',
type: 'prometheus',
},
multi: true,
options: [
{
selected: false,
text: 'Foo',
value: 'foo',
},
{
selected: false,
text: 'Bar',
value: 'bar',
},
],
current: {},
query: '',
hide: 0,
skipUrlSync: false,
};
const migrated = createSceneVariableFromVariableModel(variable) as GroupByVariable;
const groupbyVarState = migrated.state;
expect(migrated).toBeInstanceOf(GroupByVariable);
expect(groupbyVarState).toEqual({
key: expect.any(String),
description: 'GroupBy Description',
hide: 0,
defaultOptions: [
{
selected: false,
text: 'Foo',
value: 'foo',
},
{
selected: false,
text: 'Bar',
value: 'bar',
},
],
isMulti: true,
layout: 'horizontal',
noValueOnClear: true,
label: 'GroupBy Label',
name: 'groupby',
skipUrlSync: false,
type: 'groupby',
baseFilters: [],
options: [],
text: [],
value: [],
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
applyMode: 'auto',
});
});
});
describe('when groupByVariable feature toggle is disabled', () => {
it('should not migrate groupby variable and throw an error instead', () => {
const variable: GroupByVariableModel = {
id: 'groupby',
global: false,
index: 0,
state: LoadingState.Done,
error: null,
name: 'groupby',
label: 'GroupBy Label',
description: 'GroupBy Description',
type: 'groupby',
rootStateKey: 'N4XLmH5Vz',
datasource: {
uid: 'gdev-prometheus',
type: 'prometheus',
},
multi: true,
options: [],
current: {},
query: '',
hide: 0,
skipUrlSync: false,
};
expect(() => createSceneVariableFromVariableModel(variable)).toThrow('Scenes: Unsupported variable type');
});
});
it.each(['system'])('should throw for unsupported (yet) variables', (type) => {
const variable = {
name: 'query0',
type: type as VariableType,
};
expect(() => createSceneVariableFromVariableModel(variable as TypedVariableModel)).toThrow();
});
it('should handle variable without current', () => {
// @ts-expect-error
const variable: TypedVariableModel = {
id: 'query1',
name: 'query1',
type: 'datasource',
global: false,
regex: '/^gdev/',
options: [],
query: 'prometheus',
multi: true,
includeAll: true,
refresh: 1,
allValue: 'Custom all',
};
const migrated = createSceneVariableFromVariableModel(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: [],
pluginId: 'prometheus',
regex: '/^gdev/',
text: '',
type: 'datasource',
value: '',
isMulti: true,
});
});
});
describe('when creating snapshot variables from dashboard model', () => {
it('should create SnapshotVariables when required', () => {
const customVariable = {
current: {
selected: false,
text: 'a',
value: 'a',
},
hide: 0,
includeAll: false,
multi: false,
name: 'custom0',
options: [],
query: 'a,b,c,d',
skipUrlSync: false,
type: 'custom' as VariableType,
rootStateKey: 'N4XLmH5Vz',
};
const intervalVariable = {
current: {
selected: false,
text: '10s',
value: '10s',
},
hide: 0,
includeAll: false,
multi: false,
name: 'interval0',
options: [],
query: '10s,20s,30s',
skipUrlSync: false,
type: 'interval' as VariableType,
rootStateKey: 'N4XLmH5Vz',
};
const adHocVariable = {
global: false,
name: 'CoolFilters',
label: 'CoolFilters Label',
type: 'adhoc' as VariableType,
datasource: {
uid: 'gdev-prometheus',
type: 'prometheus',
},
filters: [
{
key: 'filterTest',
operator: '=',
value: 'test',
},
],
baseFilters: [
{
key: 'baseFilterTest',
operator: '=',
value: 'test',
},
],
hide: 0,
index: 0,
};
const snapshot = {
...defaultDashboard,
title: 'snapshot dash',
uid: 'test-uid',
time: { from: 'now-10h', to: 'now' },
weekStart: 'saturday',
fiscalYearStartMonth: 2,
timezone: 'America/New_York',
timepicker: {
...defaultTimePickerConfig,
hidden: true,
},
links: [{ ...NEW_LINK, title: 'Link 1' }],
templating: {
list: [customVariable, adHocVariable, intervalVariable],
},
};
const oldModel = new DashboardModel(snapshot, { isSnapshot: true });
const variables = createVariablesForSnapshot(oldModel);
// check variables were converted to snapshot variables
expect(variables).toBeInstanceOf(SceneVariableSet);
expect(variables.getByName('custom0')).toBeInstanceOf(SnapshotVariable);
expect(variables?.getByName('CoolFilters')).toBeInstanceOf(AdHocFiltersVariable);
expect(variables?.getByName('interval0')).toBeInstanceOf(SnapshotVariable);
// // custom snapshot
const customSnapshot = variables?.getByName('custom0') as SnapshotVariable;
expect(customSnapshot.state.value).toBe('a');
expect(customSnapshot.state.text).toBe('a');
expect(customSnapshot.state.isReadOnly).toBe(true);
// // adhoc snapshot
const adhocSnapshot = variables?.getByName('CoolFilters') as AdHocFiltersVariable;
expect(adhocSnapshot.state.filters).toEqual(adHocVariable.filters);
expect(adhocSnapshot.state.readOnly).toBe(true);
//
// // interval snapshot
const intervalSnapshot = variables?.getByName('interval0') as SnapshotVariable;
expect(intervalSnapshot.state.value).toBe('10s');
expect(intervalSnapshot.state.text).toBe('10s');
expect(intervalSnapshot.state.isReadOnly).toBe(true);
});
});

View File

@ -0,0 +1,238 @@
import { TypedVariableModel } from '@grafana/data';
import { config } from '@grafana/runtime';
import {
AdHocFiltersVariable,
ConstantVariable,
CustomVariable,
DataSourceVariable,
GroupByVariable,
IntervalVariable,
QueryVariable,
SceneVariable,
SceneVariableSet,
TextBoxVariable,
} from '@grafana/scenes';
import { DashboardModel } from 'app/features/dashboard/state';
import { SnapshotVariable } from '../serialization/custom-variables/SnapshotVariable';
import { getCurrentValueForOldIntervalModel, getIntervalsFromQueryString } from './utils';
export function createVariablesForDashboard(oldModel: DashboardModel) {
const variableObjects = oldModel.templating.list
.map((v) => {
try {
return createSceneVariableFromVariableModel(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));
return new SceneVariableSet({
variables: variableObjects,
});
}
export function createVariablesForSnapshot(oldModel: DashboardModel) {
const variableObjects = oldModel.templating.list
.map((v) => {
try {
// for adhoc we are using the AdHocFiltersVariable from scenes becuase of its complexity
if (v.type === 'adhoc') {
return new AdHocFiltersVariable({
name: v.name,
label: v.label,
readOnly: true,
description: v.description,
skipUrlSync: v.skipUrlSync,
hide: v.hide,
datasource: v.datasource,
applyMode: 'auto',
filters: v.filters ?? [],
baseFilters: v.baseFilters ?? [],
defaultKeys: v.defaultKeys,
useQueriesAsFilterForOptions: true,
});
}
// for other variable types we are using the SnapshotVariable
return createSnapshotVariable(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));
return new SceneVariableSet({
variables: variableObjects,
});
}
/** Snapshots variables are read-only and should not be updated */
export function createSnapshotVariable(variable: TypedVariableModel): SceneVariable {
let snapshotVariable: SnapshotVariable;
let current: { value: string | string[]; text: string | string[] };
if (variable.type === 'interval') {
const intervals = getIntervalsFromQueryString(variable.query);
const currentInterval = getCurrentValueForOldIntervalModel(variable, intervals);
snapshotVariable = new SnapshotVariable({
name: variable.name,
label: variable.label,
description: variable.description,
value: currentInterval,
text: currentInterval,
hide: variable.hide,
});
return snapshotVariable;
}
if (variable.type === 'system' || variable.type === 'constant' || variable.type === 'adhoc') {
current = {
value: '',
text: '',
};
} else {
current = {
value: variable.current?.value ?? '',
text: variable.current?.text ?? '',
};
}
snapshotVariable = new SnapshotVariable({
name: variable.name,
label: variable.label,
description: variable.description,
value: current?.value ?? '',
text: current?.text ?? '',
hide: variable.hide,
});
return snapshotVariable;
}
export function createSceneVariableFromVariableModel(variable: TypedVariableModel): SceneVariable {
const commonProperties = {
name: variable.name,
label: variable.label,
description: variable.description,
};
if (variable.type === 'adhoc') {
return new AdHocFiltersVariable({
...commonProperties,
description: variable.description,
skipUrlSync: variable.skipUrlSync,
hide: variable.hide,
datasource: variable.datasource,
applyMode: 'auto',
filters: variable.filters ?? [],
baseFilters: variable.baseFilters ?? [],
defaultKeys: variable.defaultKeys,
useQueriesAsFilterForOptions: true,
});
}
if (variable.type === 'custom') {
return new CustomVariable({
...commonProperties,
value: variable.current?.value ?? '',
text: variable.current?.text ?? '',
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 (variable.type === 'query') {
return new QueryVariable({
...commonProperties,
value: variable.current?.value ?? '',
text: variable.current?.text ?? '',
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,
definition: variable.definition,
});
} else if (variable.type === 'datasource') {
return new DataSourceVariable({
...commonProperties,
value: variable.current?.value ?? '',
text: variable.current?.text ?? '',
regex: variable.regex,
pluginId: variable.query,
allValue: variable.allValue || undefined,
includeAll: variable.includeAll,
defaultToAll: Boolean(variable.includeAll),
skipUrlSync: variable.skipUrlSync,
isMulti: variable.multi,
hide: variable.hide,
});
} else if (variable.type === 'interval') {
const intervals = getIntervalsFromQueryString(variable.query);
const currentInterval = getCurrentValueForOldIntervalModel(variable, intervals);
return new IntervalVariable({
...commonProperties,
value: currentInterval,
intervals: intervals,
autoEnabled: variable.auto,
autoStepCount: variable.auto_count,
autoMinInterval: variable.auto_min,
refresh: variable.refresh,
skipUrlSync: variable.skipUrlSync,
hide: variable.hide,
});
} else if (variable.type === 'constant') {
return new ConstantVariable({
...commonProperties,
value: variable.query,
skipUrlSync: variable.skipUrlSync,
hide: variable.hide,
});
} else if (variable.type === 'textbox') {
let val;
if (!variable?.current?.value) {
val = variable.query;
} else {
if (typeof variable.current.value === 'string') {
val = variable.current.value;
} else {
val = variable.current.value[0];
}
}
return new TextBoxVariable({
...commonProperties,
value: val,
skipUrlSync: variable.skipUrlSync,
hide: variable.hide,
});
} else if (config.featureToggles.groupByVariable && variable.type === 'groupby') {
return new GroupByVariable({
...commonProperties,
datasource: variable.datasource,
value: variable.current?.value || [],
text: variable.current?.text || [],
skipUrlSync: variable.skipUrlSync,
hide: variable.hide,
// @ts-expect-error
defaultOptions: variable.options,
});
} else {
throw new Error(`Scenes: Unsupported variable type ${variable.type}`);
}
}

View File

@ -23,6 +23,7 @@ import {
createIntervalVariable, createIntervalVariable,
createOrgVariable, createOrgVariable,
createQueryVariable, createQueryVariable,
createSnapshotVariable,
createTextBoxVariable, createTextBoxVariable,
createUserVariable, createUserVariable,
} from './state/__tests__/fixtures'; } from './state/__tests__/fixtures';
@ -175,6 +176,7 @@ describe('type guards', () => {
org: { variable: createOrgVariable(), isMulti: false, hasOptions: false, hasCurrent: true }, org: { variable: createOrgVariable(), isMulti: false, hasOptions: false, hasCurrent: true },
dashboard: { variable: createDashboardVariable(), isMulti: false, hasOptions: false, hasCurrent: true }, dashboard: { variable: createDashboardVariable(), isMulti: false, hasOptions: false, hasCurrent: true },
custom: { variable: createCustomVariable(), isMulti: true, hasOptions: true, hasCurrent: true }, custom: { variable: createCustomVariable(), isMulti: true, hasOptions: true, hasCurrent: true },
snapshot: { variable: createSnapshotVariable(), isMulti: false, hasOptions: true, hasCurrent: true },
}; };
const variableFacts = Object.values(variableFactsObj); const variableFacts = Object.values(variableFactsObj);

View File

@ -10,6 +10,7 @@ import {
LoadingState, LoadingState,
OrgVariableModel, OrgVariableModel,
QueryVariableModel, QueryVariableModel,
SnapshotVariableModel,
TextBoxVariableModel, TextBoxVariableModel,
UserVariableModel, UserVariableModel,
VariableHide, VariableHide,
@ -198,3 +199,13 @@ export function createCustomVariable(input: Partial<CustomVariableModel> = {}):
...input, ...input,
}; };
} }
export function createSnapshotVariable(input: Partial<SnapshotVariableModel> = {}): SnapshotVariableModel {
return {
...createBaseVariableModel('snapshot'),
query: '',
current: createVariableOption('prom-prod', { text: 'Prometheus (main)', selected: true }),
options: [],
...input,
};
}