Skip to content

Commit f98ec2c

Browse files
Scenes: Add custom variable support (grafana#59057)
1 parent c43e1a7 commit f98ec2c

File tree

3 files changed

+321
-0
lines changed

3 files changed

+321
-0
lines changed

public/app/features/scenes/scenes/variablesDemo.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
77
import { SceneTimeRange } from '../core/SceneTimeRange';
88
import { VariableValueSelectors } from '../variables/components/VariableValueSelectors';
99
import { SceneVariableSet } from '../variables/sets/SceneVariableSet';
10+
import { CustomVariable } from '../variables/variants/CustomVariable';
1011
import { TestVariable } from '../variables/variants/TestVariable';
1112

1213
import { getQueryRunnerWithRandomWalkQuery } from './queries';
@@ -42,6 +43,17 @@ export function getVariablesDemo(): Scene {
4243
text: '',
4344
options: [],
4445
}),
46+
new CustomVariable({
47+
name: 'Single Custom',
48+
query: 'A : 10,B : 20',
49+
options: [],
50+
}),
51+
new CustomVariable({
52+
name: 'Multi Custom',
53+
query: 'A : 10,B : 20',
54+
isMulti: true,
55+
options: [],
56+
}),
4557
],
4658
}),
4759
layout: new SceneFlexLayout({
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import { lastValueFrom } from 'rxjs';
2+
3+
import { CustomVariable } from './CustomVariable';
4+
5+
describe('CustomVariable', () => {
6+
describe('When empty query is provided', () => {
7+
it('Should default to empty options', async () => {
8+
const variable = new CustomVariable({
9+
name: 'test',
10+
options: [],
11+
value: '',
12+
text: '',
13+
query: '',
14+
});
15+
16+
await lastValueFrom(variable.validateAndUpdate());
17+
18+
expect(variable.state.value).toEqual('');
19+
expect(variable.state.text).toEqual('');
20+
expect(variable.state.options).toEqual([]);
21+
});
22+
});
23+
24+
describe('When invalid query is provided', () => {
25+
it('Should default to empty options', async () => {
26+
const variable = new CustomVariable({
27+
name: 'test',
28+
options: [],
29+
value: '',
30+
text: '',
31+
query: 'A - B',
32+
});
33+
34+
// TODO: Be able to triggger the state update to get the options
35+
await lastValueFrom(variable.getValueOptions({}));
36+
37+
expect(variable.state.value).toEqual('');
38+
expect(variable.state.text).toEqual('');
39+
expect(variable.state.options).toEqual([]);
40+
});
41+
});
42+
43+
describe('When valid query is provided', () => {
44+
it('Should generate correctly the options for only value queries', async () => {
45+
const variable = new CustomVariable({
46+
name: 'test',
47+
options: [],
48+
value: '',
49+
text: '',
50+
query: 'A,B,C',
51+
});
52+
53+
await lastValueFrom(variable.validateAndUpdate());
54+
55+
expect(variable.state.value).toEqual('A');
56+
expect(variable.state.text).toEqual('A');
57+
expect(variable.state.options).toEqual([
58+
{ label: 'A', value: 'A' },
59+
{ label: 'B', value: 'B' },
60+
{ label: 'C', value: 'C' },
61+
]);
62+
});
63+
64+
it('Should generate correctly the options for key:value pairs', async () => {
65+
const variable = new CustomVariable({
66+
name: 'test',
67+
options: [],
68+
value: '',
69+
text: '',
70+
query: 'label-1 : value-1,label-2 : value-2, label-3 : value-3',
71+
});
72+
73+
await lastValueFrom(variable.validateAndUpdate());
74+
75+
expect(variable.state.value).toEqual('value-1');
76+
expect(variable.state.text).toEqual('label-1');
77+
expect(variable.state.options).toEqual([
78+
{ label: 'label-1', value: 'value-1' },
79+
{ label: 'label-2', value: 'value-2' },
80+
{ label: 'label-3', value: 'value-3' },
81+
]);
82+
});
83+
84+
it('Should generate correctly the options for key:value pairs with special characters', async () => {
85+
const variable = new CustomVariable({
86+
name: 'test',
87+
options: [],
88+
value: '',
89+
text: '',
90+
query: 'label\\,1 : value\\,1',
91+
});
92+
93+
await lastValueFrom(variable.validateAndUpdate());
94+
95+
expect(variable.state.value).toEqual('value,1');
96+
expect(variable.state.text).toEqual('label,1');
97+
expect(variable.state.options).toEqual([{ label: 'label,1', value: 'value,1' }]);
98+
});
99+
100+
it('Should generate correctly the options for key:value and only values combined', async () => {
101+
const variable = new CustomVariable({
102+
name: 'test',
103+
options: [],
104+
value: '',
105+
text: '',
106+
query: 'label-1 : value-1, value-2, label\\,3 : value-3,value\\,4',
107+
});
108+
109+
await lastValueFrom(variable.validateAndUpdate());
110+
111+
expect(variable.state.value).toEqual('value-1');
112+
expect(variable.state.text).toEqual('label-1');
113+
expect(variable.state.options).toEqual([
114+
{ label: 'label-1', value: 'value-1' },
115+
{ label: 'value-2', value: 'value-2' },
116+
{ label: 'label,3', value: 'value-3' },
117+
{ label: 'value,4', value: 'value,4' },
118+
]);
119+
});
120+
121+
it('Should generate correctly the options for key:value pairs with extra spaces', async () => {
122+
const variable = new CustomVariable({
123+
name: 'test',
124+
options: [],
125+
value: '',
126+
text: '',
127+
query: 'a, b, c, d : e',
128+
});
129+
130+
await lastValueFrom(variable.validateAndUpdate());
131+
132+
expect(variable.state.value).toEqual('a');
133+
expect(variable.state.text).toEqual('a');
134+
expect(variable.state.options).toEqual([
135+
{
136+
label: 'a',
137+
value: 'a',
138+
},
139+
{
140+
label: 'b',
141+
value: 'b',
142+
},
143+
{
144+
label: 'c',
145+
value: 'c',
146+
},
147+
{
148+
label: 'd',
149+
value: 'e',
150+
},
151+
]);
152+
});
153+
154+
it('Should generate correctly the options for only values as URLs', async () => {
155+
const variable = new CustomVariable({
156+
name: 'test',
157+
options: [],
158+
value: '',
159+
text: '',
160+
query: 'http://www.google.com/, http://www.amazon.com/',
161+
});
162+
163+
await lastValueFrom(variable.validateAndUpdate());
164+
165+
expect(variable.state.value).toEqual('http://www.google.com/');
166+
expect(variable.state.text).toEqual('http://www.google.com/');
167+
expect(variable.state.options).toEqual([
168+
{
169+
label: 'http://www.google.com/',
170+
value: 'http://www.google.com/',
171+
},
172+
{
173+
label: 'http://www.amazon.com/',
174+
value: 'http://www.amazon.com/',
175+
},
176+
]);
177+
});
178+
179+
it('Should generate correctly the options for key/values as URLs', async () => {
180+
const variable = new CustomVariable({
181+
name: 'test',
182+
options: [],
183+
value: '',
184+
text: '',
185+
query: 'google : http://www.google.com/, amazon : http://www.amazon.com/',
186+
});
187+
188+
await lastValueFrom(variable.validateAndUpdate());
189+
190+
expect(variable.state.value).toEqual('http://www.google.com/');
191+
expect(variable.state.text).toEqual('google');
192+
expect(variable.state.options).toEqual([
193+
{
194+
label: 'google',
195+
value: 'http://www.google.com/',
196+
},
197+
{
198+
label: 'amazon',
199+
value: 'http://www.amazon.com/',
200+
},
201+
]);
202+
});
203+
});
204+
205+
describe('When value is provided', () => {
206+
it('Should keep current value if current value is valid', async () => {
207+
const variable = new CustomVariable({
208+
name: 'test',
209+
options: [],
210+
query: 'A,B',
211+
value: 'B',
212+
text: 'B',
213+
});
214+
215+
await lastValueFrom(variable.validateAndUpdate());
216+
217+
expect(variable.state.value).toBe('B');
218+
expect(variable.state.text).toBe('B');
219+
});
220+
221+
it('Should maintain the valid values when multiple selected', async () => {
222+
const variable = new CustomVariable({
223+
name: 'test',
224+
options: [],
225+
isMulti: true,
226+
query: 'A,C',
227+
value: ['A', 'B', 'C'],
228+
text: ['A', 'B', 'C'],
229+
});
230+
231+
await lastValueFrom(variable.validateAndUpdate());
232+
233+
expect(variable.state.value).toEqual(['A', 'C']);
234+
expect(variable.state.text).toEqual(['A', 'C']);
235+
});
236+
237+
it('Should pick first option if none of the current values are valid', async () => {
238+
const variable = new CustomVariable({
239+
name: 'test',
240+
options: [],
241+
isMulti: true,
242+
query: 'A,C',
243+
value: ['D', 'E'],
244+
text: ['E', 'E'],
245+
});
246+
247+
await lastValueFrom(variable.validateAndUpdate());
248+
249+
expect(variable.state.value).toEqual(['A']);
250+
expect(variable.state.text).toEqual(['A']);
251+
});
252+
});
253+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import { Observable, of } from 'rxjs';
3+
4+
import { SceneComponentProps } from '../../core/types';
5+
import { VariableDependencyConfig } from '../VariableDependencyConfig';
6+
import { VariableValueSelect } from '../components/VariableValueSelect';
7+
import { VariableValueOption } from '../types';
8+
9+
import { MultiValueVariable, MultiValueVariableState, VariableGetOptionsArgs } from './MultiValueVariable';
10+
11+
export interface CustomVariableState extends MultiValueVariableState {
12+
query: string;
13+
}
14+
15+
export class CustomVariable extends MultiValueVariable<CustomVariableState> {
16+
protected _variableDependency = new VariableDependencyConfig(this, {
17+
statePaths: ['query'],
18+
});
19+
20+
public constructor(initialState: Partial<CustomVariableState>) {
21+
super({
22+
query: '',
23+
value: '',
24+
text: '',
25+
options: [],
26+
name: '',
27+
...initialState,
28+
});
29+
}
30+
31+
public getValueOptions(args: VariableGetOptionsArgs): Observable<VariableValueOption[]> {
32+
const match = this.state.query.match(/(?:\\,|[^,])+/g) ?? [];
33+
34+
const options = match.map((text) => {
35+
text = text.replace(/\\,/g, ',');
36+
const textMatch = /^(.+)\s:\s(.+)$/g.exec(text) ?? [];
37+
if (textMatch.length === 3) {
38+
const [, key, value] = textMatch;
39+
return { label: key.trim(), value: value.trim() };
40+
} else {
41+
return { label: text.trim(), value: text.trim() };
42+
}
43+
});
44+
45+
return of(options);
46+
47+
// TODO: Support 'All'
48+
//if (this.state.includeAll) {
49+
// options.unshift({ text: ALL_VARIABLE_TEXT, value: ALL_VARIABLE_VALUE, selected: false });
50+
//}
51+
}
52+
53+
public static Component = ({ model }: SceneComponentProps<MultiValueVariable>) => {
54+
return <VariableValueSelect model={model} />;
55+
};
56+
}

0 commit comments

Comments
 (0)