Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2383d0b
feat: Add support in CustomVariable and QueryVariable
grafakus Sep 4, 2025
51bfbdf
chore(QueryVariable): Remove unused class property
grafakus Sep 4, 2025
e49743e
fix: Add missing properties when building options
grafakus Sep 4, 2025
da04a3b
refactor(MultiValueVariable): Add support valueProp and textProp
grafakus Sep 4, 2025
c32c895
Update packages/scenes/src/variables/types.ts
grafakus Sep 5, 2025
4d065f1
chore: Fix type
grafakus Sep 5, 2025
7a39e48
fix(MultiValueVariable): Fix incorrect valueProperties state update
grafakus Sep 12, 2025
b83b465
refactor: Introduce CustomOptionsProviders
grafakus Sep 12, 2025
c52ad61
chore: Remove comments
grafakus Sep 12, 2025
339cc7c
refactor(CustomVariable): Remove isJson config option
grafakus Sep 12, 2025
fcf42f3
refactor: Introduce options provider builder
grafakus Sep 12, 2025
86ac6ab
feat: Enable custom options provider registration
grafakus Sep 12, 2025
5a2b438
refactor: Early returns
grafakus Sep 12, 2025
901a7ff
refactor
grafakus Sep 19, 2025
5355a08
feat: Add demo
grafakus Sep 19, 2025
29c0bdf
fix
grafakus Sep 19, 2025
82a780a
feat: Better options provider + add QueryOptionsProvider
grafakus Sep 19, 2025
c6c76f4
refactor
grafakus Sep 19, 2025
20f655b
feat(QueryVariable): Support properties received in data frames
grafakus Sep 19, 2025
0fe0503
chore: Better naming
grafakus Sep 19, 2025
932bd3c
fix: Fix typecheck issue
grafakus Sep 19, 2025
ce11620
fix: Remove unnecessary code
grafakus Sep 23, 2025
0a39fea
fix: Remove unnecessary logic
grafakus Sep 23, 2025
f27e56a
chore: Simplify code
grafakus Sep 23, 2025
34e9ca0
test(MultiVarlueVariable): Simplify unit test
grafakus Sep 30, 2025
4b3b50f
chore: Remove unnecessary code
grafakus Sep 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/scenes/src/variables/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface VariableValueOption {
label: string;
value: VariableValueSingle;
group?: string;
properties?: Record<string, any>;
}

export interface SceneVariableSetState extends SceneObjectState {
Expand Down
41 changes: 41 additions & 0 deletions packages/scenes/src/variables/variants/CustomVariable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,45 @@ label-3 : value-3,`,
expect(B.state.options[2].value).toBe('value1');
});
});

describe('multi prop / object support', () => {
it('Can have object values (JSON array of objects)', async () => {
const variable = new CustomVariable({
name: 'test',
isMulti: false,
isJson: true,
query: `
[
{ "label": "Test", "value": "test", "properties": { "id": "test", "display": "Test", "location": "US" } },
{ "label": "Prod", "value": "prod", "properties": { "id": "prod", "display": "Prod", "location": "EU" } }
]
`,
value: 'prod',
text: 'Prod',
options: [],
});

await lastValueFrom(variable.validateAndUpdate());

expect(variable.getValue()).toEqual('prod');
expect(variable.getValue('location')).toEqual('EU');
});

it('Can have object values (JSON array of strings)', async () => {
const variable = new CustomVariable({
name: 'test',
isMulti: false,
isJson: true,
query: `["test", "prod"]`,
value: 'prod',
text: 'prod',
options: [],
});

await lastValueFrom(variable.validateAndUpdate());

expect(variable.state.value).toBe('prod');
expect(variable.state.text).toBe('prod');
});
});
});
47 changes: 36 additions & 11 deletions packages/scenes/src/variables/variants/CustomVariable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import React from 'react';

export interface CustomVariableState extends MultiValueVariableState {
query: string;
isJson: boolean;
}

export class CustomVariable extends MultiValueVariable<CustomVariableState> {
Expand All @@ -26,24 +27,48 @@ export class CustomVariable extends MultiValueVariable<CustomVariableState> {
text: '',
options: [],
name: '',
isJson: false,
...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:\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() };

let options: VariableValueOption[] = [];

if (this.state.isJson) {
try {
const parsedOptions = JSON.parse(interpolated);

if (!Array.isArray(parsedOptions)) {
throw new Error('Query must be a JSON array');
}

if (typeof parsedOptions[0] === 'string') {
options = parsedOptions.map((value) => ({ label: value.trim(), value: value.trim() }));
} else if (typeof parsedOptions[0] === 'object' && parsedOptions[0] !== null) {
options = parsedOptions;
} else {
throw new Error('Query must be a JSON array of strings or objects');
}
} catch (error) {
throw error;
}
});
} else {
const match = interpolated.match(/(?:\\,|[^,])+/g) ?? [];

options = match.map((text) => {
text = text.replace(/\\,/g, ',');
const textMatch = /^\s*(.+)\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() };
}
});
}

if (!options.length) {
this.skipNextValidation = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,7 @@ describe('MultiValueVariable', () => {
skipUrlSync: true,
optionsToReturn: [
{ label: 'Test', value: 'test', properties: { id: 'test', display: 'Test', location: 'US' } },
{ label: 'Prod', value: 'pod', properties: { id: 'prod', display: 'Prod', location: 'EU' } },
{ label: 'Prod', value: 'prod', properties: { id: 'prod', display: 'Prod', location: 'EU' } },
],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,51 @@ describe.each(['11.1.2', '11.1.1'])('QueryVariable', (v) => {
expect(variable.state.loading).toEqual(true);
});

describe('multi prop / object support', () => {
it('Can have object values', async () => {
setCreateQueryVariableRunnerFactory(
() =>
new FakeQueryRunner(
fakeDsMock,
jest.fn().mockReturnValue(
of<PanelData>({
state: LoadingState.Done,
series: [
toDataFrame({
fields: [
{ name: 'text', type: FieldType.string, values: ['Test', 'Prod'] },
{ name: 'value', type: FieldType.string, values: ['test', 'prod'] },
{
name: 'properties',
type: FieldType.other,
values: [
{ id: 'test', display: 'Test', location: 'US' },
{ id: 'prod', display: 'Prod', location: 'EU' },
],
},
],
}),
],
timeRange: getDefaultTimeRange(),
})
)
)
);

const variable = new QueryVariable({
name: 'test',
datasource: { uid: 'fake-std', type: 'fake-std' },
query: 'query',
isMultiDimensions: true,
});

await lastValueFrom(variable.validateAndUpdate());

expect(variable.getValue('location')).toEqual('US');
expect(variable.getValue()).toEqual('test');
});
});

it('Should pass variable scene object when resolving data source and via request scoped vars', async () => {
const variable = new QueryVariable({
name: 'test',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface QueryVariableState extends MultiValueVariableState {
type: 'query';
datasource: DataSourceRef | null;
query: string | SceneDataQuery;
isMultiDimensions: boolean;
regex: string;
refresh: VariableRefresh;
sort: VariableSort;
Expand Down Expand Up @@ -62,6 +63,7 @@ export class QueryVariable extends MultiValueVariable<QueryVariableState> {
datasource: null,
regex: '',
query: '',
isMultiDimensions: false,
refresh: VariableRefresh.onDashboardLoad,
sort: VariableSort.disabled,
...initialState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ describe('toMetricFindValues', () => {
{ name: 'expandable', type: FieldType.boolean, values: [true, false, true] },
],
});
const frameWithPropertiesField = toDataFrame({
fields: [
{ name: 'label', type: FieldType.string, values: ['A', 'B', 'C'] },
{
name: 'properties',
type: FieldType.other,
values: [
{ value: 'A', displayvalue: 'Alpha' },
{ value: 'B', displayvalue: 'Beta' },
{ value: 'C', displayvalue: 'Gamma' },
],
},
],
});

// it.each wouldn't work here as we need the done callback
[
Expand Down Expand Up @@ -72,6 +86,14 @@ describe('toMetricFindValues', () => {
{ text: 'C', value: 'C', expandable: true },
],
},
{
series: [frameWithPropertiesField],
expected: [
{ text: 'A', value: 'A', properties: { displayvalue: 'Alpha', value: 'A' } },
{ text: 'B', value: 'B', properties: { displayvalue: 'Beta', value: 'B' } },
{ text: 'C', value: 'C', properties: { displayvalue: 'Gamma', value: 'C' } },
],
},
].map((scenario) => {
it(`when called with series:${JSON.stringify(scenario.series, null, 0)}`, async () => {
const { series, expected } = scenario;
Expand Down
22 changes: 16 additions & 6 deletions packages/scenes/src/variables/variants/query/toMetricFindValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
} from '@grafana/data';
import { map, OperatorFunction } from 'rxjs';

export function toMetricFindValues(): OperatorFunction<PanelData, MetricFindValue[]> {
interface MetricFindValueWithProperties extends MetricFindValue {
properties?: Record<string, any>;
}

export function toMetricFindValues(): OperatorFunction<PanelData, MetricFindValueWithProperties[]> {
return (source) =>
source.pipe(
map((panelData) => {
Expand All @@ -26,12 +30,13 @@ export function toMetricFindValues(): OperatorFunction<PanelData, MetricFindValu
}

const processedDataFrames = getProcessedDataFrames(frames);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both elastic and postgres use the metricFindQuery so already return MetricFindValue, so this function will return already on line 29 (unless we update those data sources to handle variables differently)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was mentioned when we reached the consensus on the design doc. Besides pinging the owners of the Elastic and Postgres datasources, is the change in this PR enough?

const metrics: MetricFindValue[] = [];
const metrics: MetricFindValueWithProperties[] = [];

let valueIndex = -1;
let textIndex = -1;
let stringIndex = -1;
let expandableIndex = -1;
let propertiesIndex = -1;

for (const frame of processedDataFrames) {
for (let index = 0; index < frame.fields.length; index++) {
Expand All @@ -42,6 +47,10 @@ export function toMetricFindValues(): OperatorFunction<PanelData, MetricFindValu
stringIndex = index;
}

if (field.type === FieldType.other && propertiesIndex === -1) {
propertiesIndex = index;
}

if (fieldName === 'text' && field.type === FieldType.string && textIndex === -1) {
textIndex = index;
}
Expand Down Expand Up @@ -70,23 +79,24 @@ export function toMetricFindValues(): OperatorFunction<PanelData, MetricFindValu
const string = frame.fields[stringIndex].values.get(index);
const text = textIndex !== -1 ? frame.fields[textIndex].values.get(index) : '';
const value = valueIndex !== -1 ? frame.fields[valueIndex].values.get(index) : '';
const properties = propertiesIndex !== -1 ? frame.fields[propertiesIndex].values.get(index) : undefined;

if (valueIndex === -1 && textIndex === -1) {
metrics.push({ text: string, value: string, expandable });
metrics.push({ text: string, value: string, expandable, properties });
continue;
}

if (valueIndex === -1 && textIndex !== -1) {
metrics.push({ text, value: text, expandable });
metrics.push({ text, value: text, expandable, properties });
continue;
}

if (valueIndex !== -1 && textIndex === -1) {
metrics.push({ text: value, value, expandable });
metrics.push({ text: value, value, expandable, properties });
continue;
}

metrics.push({ text, value, expandable });
metrics.push({ text, value, expandable, properties });
}
}

Expand Down
Loading