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