mirror of https://github.com/grafana/grafana
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>pull/92147/head
parent
6f63def283
commit
cd4b7ef9db
@ -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 }; |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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); |
||||
}); |
||||
}); |
@ -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}`); |
||||
} |
||||
} |
Loading…
Reference in new issue