Dashboards Schema V2: ResponseTransformers: Transform remaining variables when ensuring v2 (#98777)

* add missing vars

* Don't create undefined variable fields

* tests

* Fix test; remove allValue from groupBy

* Fix tests

* betterer

* Use @ts-expect-error

* betterer
This commit is contained in:
Haris Rozajac 2025-01-17 09:54:42 -07:00 committed by GitHub
parent 40baca699a
commit 4f337b99d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 378 additions and 30 deletions

View File

@ -753,8 +753,6 @@ GroupByVariableSpec: {
}
options: [...VariableOption] | *[]
multi: bool | *false
includeAll: bool | *false
allValue?: string
label?: string
hide: VariableHide
skipUrlSync: bool | *false

View File

@ -380,7 +380,6 @@ export const handyTestingSchema: DashboardV2Spec = {
},
description: 'A group by variable',
hide: 'dontHide',
includeAll: false,
label: 'Group By Variable',
multi: false,
name: 'groupByVar',

View File

@ -1109,8 +1109,6 @@ export interface GroupByVariableSpec {
current: VariableOption;
options: VariableOption[];
multi: boolean;
includeAll: boolean;
allValue?: string;
label?: string;
hide: VariableHide;
skipUrlSync: boolean;
@ -1122,7 +1120,6 @@ export const defaultGroupByVariableSpec = (): GroupByVariableSpec => ({
current: { text: "", value: "", },
options: [],
multi: false,
includeAll: false,
hide: "dontHide",
skipUrlSync: false,
});

View File

@ -488,7 +488,6 @@ describe('DashboardSceneSerializer', () => {
},
],
multi: false,
includeAll: false,
hide: 'dontHide',
skipUrlSync: false,
},

View File

@ -305,7 +305,6 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model
},
"description": "A group by variable",
"hide": "dontHide",
"includeAll": false,
"label": "Group By Variable",
"multi": false,
"name": "groupByVar",

View File

@ -1230,7 +1230,6 @@ describe('sceneVariablesSetToVariables', () => {
},
"description": "test-desc",
"hide": "dontHide",
"includeAll": false,
"label": "test-label",
"multi": true,
"name": "test",

View File

@ -407,7 +407,6 @@ export function sceneVariablesSetToSchemaV2Variables(
})) || [],
current: currentVariableOption,
multi: variable.state.isMulti || false,
includeAll: variable.state.includeAll || false,
},
};
variables.push(groupVariable);

View File

@ -1,5 +1,5 @@
import { DataQuery } from '@grafana/schema';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { DataQuery, VariableModel, VariableRefresh } from '@grafana/schema';
import { DashboardV2Spec, VariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import {
AnnoKeyCreatedBy,
AnnoKeyDashboardGnetId,
@ -10,6 +10,10 @@ import {
AnnoKeyUpdatedTimestamp,
} from 'app/features/apiserver/types';
import { getDefaultDataSourceRef } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2';
import {
transformVariableHideToEnum,
transformVariableRefreshToEnum,
} from 'app/features/dashboard-scene/serialization/transformToV2TypesUtils';
import { DashboardDataDTO, DashboardDTO } from 'app/types';
import { getDefaultDatasource, getPanelQueries, ResponseTransformers } from './ResponseTransformers';
@ -119,6 +123,164 @@ describe('ResponseTransformers', () => {
annotations: {
list: [],
},
templating: {
list: [
{
type: 'query',
name: 'var1',
label: 'query var',
description: 'query var description',
skipUrlSync: false,
hide: 0,
multi: true,
includeAll: true,
current: { value: '1', text: '1' },
options: [
{ selected: true, text: '1', value: '1' },
{ selected: false, text: '2', value: '2' },
],
refresh: VariableRefresh.onTimeRangeChanged,
datasource: {
type: 'prometheus',
uid: 'abc',
},
regex: '.*',
sort: 1,
query: {
expr: 'sum(query)',
},
},
{
type: 'datasource',
name: 'var2',
label: 'datasource var',
description: 'datasource var description',
skipUrlSync: false,
hide: 0,
multi: true,
includeAll: true,
current: { value: 'PromTest', text: 'PromTest' },
options: [
{ selected: true, text: 'PromTest', value: 'PromTest' },
{ selected: false, text: 'Grafana', value: 'Grafana' },
],
refresh: VariableRefresh.onTimeRangeChanged,
regex: '.*',
sort: 1,
query: 'sum(query)',
},
{
type: 'custom',
name: 'var3',
label: 'custom var',
description: 'custom var description',
skipUrlSync: false,
hide: 0,
multi: true,
includeAll: true,
current: { value: '1', text: '1' },
query: '1,2,3',
options: [
{ selected: true, text: '1', value: '1' },
{ selected: false, text: '2', value: '2' },
],
allValue: '1,2,3',
},
{
type: 'adhoc',
name: 'var4',
label: 'adhoc var',
description: 'adhoc var description',
skipUrlSync: false,
hide: 0,
datasource: {
type: 'prometheus',
uid: 'abc',
},
// @ts-expect-error
baseFilters: [{ key: 'key1', operator: 'AND' }],
filters: [],
defaultKeys: [],
},
{
type: 'constant',
name: 'var5',
label: 'constant var',
description: 'constant var description',
skipUrlSync: false,
hide: 0,
current: { value: '1', text: '0' },
query: '1',
},
{
type: 'interval',
name: 'var6',
label: 'interval var',
description: 'interval var description',
skipUrlSync: false,
query: '1m,10m,30m,1h',
hide: 0,
current: {
value: 'auto',
text: 'auto',
},
refresh: VariableRefresh.onTimeRangeChanged,
options: [
{
selected: true,
text: '1m',
value: '1m',
},
{
selected: false,
text: '10m',
value: '10m',
},
{
selected: false,
text: '30m',
value: '30m',
},
{
selected: false,
text: '1h',
value: '1h',
},
],
// @ts-expect-error
auto: false,
auto_min: '1s',
auto_count: 1,
},
{
type: 'textbox',
name: 'var7',
label: 'textbox var',
description: 'textbox var description',
skipUrlSync: false,
hide: 0,
current: { value: '1', text: '1' },
query: '1',
},
{
type: 'groupby',
name: 'var8',
label: 'groupby var',
description: 'groupby var description',
skipUrlSync: false,
hide: 0,
datasource: {
type: 'prometheus',
uid: 'abc',
},
options: [
{ selected: true, text: '1', value: '1' },
{ selected: false, text: '2', value: '2' },
],
current: { value: ['1'], text: ['1'] },
},
],
},
};
const dto: DashboardWithAccessInfo<DashboardDataDTO> = {
@ -142,7 +304,6 @@ describe('ResponseTransformers', () => {
metadata: {
name: 'dashboard-uid',
resourceVersion: '1',
creationTimestamp: '2023-01-01T00:00:00Z',
annotations: {
[AnnoKeyCreatedBy]: 'user1',
@ -188,6 +349,14 @@ describe('ResponseTransformers', () => {
expect(spec.timeSettings.weekStart).toBe(dashboardV1.weekStart);
expect(spec.links).toEqual(dashboardV1.links);
expect(spec.annotations).toEqual([]);
validateVariablesV1ToV2(spec.variables[0], dashboardV1.templating?.list?.[0]);
validateVariablesV1ToV2(spec.variables[1], dashboardV1.templating?.list?.[1]);
validateVariablesV1ToV2(spec.variables[2], dashboardV1.templating?.list?.[2]);
validateVariablesV1ToV2(spec.variables[3], dashboardV1.templating?.list?.[3]);
validateVariablesV1ToV2(spec.variables[4], dashboardV1.templating?.list?.[4]);
validateVariablesV1ToV2(spec.variables[5], dashboardV1.templating?.list?.[5]);
validateVariablesV1ToV2(spec.variables[6], dashboardV1.templating?.list?.[6]);
validateVariablesV1ToV2(spec.variables[7], dashboardV1.templating?.list?.[7]);
});
});
@ -399,3 +568,79 @@ describe('ResponseTransformers', () => {
});
});
});
function validateVariablesV1ToV2(v2: VariableKind, v1: VariableModel | undefined) {
if (!v1) {
return expect(v1).toBeDefined();
}
const v1Common = {
name: v1.name,
label: v1.label,
description: v1.description,
hide: transformVariableHideToEnum(v1.hide),
skipUrlSync: v1.skipUrlSync,
};
const v2Common = {
name: v2.spec.name,
label: v2.spec.label,
description: v2.spec.description,
hide: v2.spec.hide,
skipUrlSync: v2.spec.skipUrlSync,
};
expect(v2Common).toEqual(v1Common);
if (v2.kind === 'QueryVariable') {
expect(v2.spec.datasource).toEqual(v1.datasource);
expect(v2.spec.query).toEqual({
kind: v1.datasource?.type,
spec: {
...(typeof v1.query === 'object' ? v1.query : {}),
},
});
}
if (v2.kind === 'DatasourceVariable') {
expect(v2.spec.pluginId).toBe(v1.query);
expect(v2.spec.refresh).toBe(transformVariableRefreshToEnum(v1.refresh));
}
if (v2.kind === 'CustomVariable') {
expect(v2.spec.query).toBe(v1.query);
expect(v2.spec.options).toEqual(v1.options);
}
if (v2.kind === 'AdhocVariable') {
expect(v2.spec.datasource).toEqual(v1.datasource);
expect(v2.spec.filters).toEqual([]);
// @ts-expect-error
expect(v2.spec.baseFilters).toEqual(v1.baseFilters);
}
if (v2.kind === 'ConstantVariable') {
expect(v2.spec.query).toBe(v1.query);
}
if (v2.kind === 'IntervalVariable') {
expect(v2.spec.query).toBe(v1.query);
expect(v2.spec.options).toEqual(v1.options);
expect(v2.spec.current).toEqual(v1.current);
// @ts-expect-error
expect(v2.spec.auto).toBe(v1.auto);
// @ts-expect-error
expect(v2.spec.auto_min).toBe(v1.auto_min);
// @ts-expect-error
expect(v2.spec.auto_count).toBe(v1.auto_count);
}
if (v2.kind === 'TextVariable') {
expect(v2.spec.query).toBe(v1.query);
expect(v2.spec.current).toEqual(v1.current);
}
if (v2.kind === 'GroupByVariable') {
expect(v2.spec.datasource).toEqual(v1.datasource);
expect(v2.spec.options).toEqual(v1.options);
}
}

View File

@ -1,5 +1,6 @@
import { TypedVariableModel } from '@grafana/data';
import { config } from '@grafana/runtime';
import { AnnotationQuery, DataQuery, DataSourceRef, Panel, VariableModel } from '@grafana/schema';
import { AnnotationQuery, DataQuery, DataSourceRef, Panel } from '@grafana/schema';
import {
AnnotationQueryKind,
DashboardV2Spec,
@ -11,6 +12,12 @@ import {
PanelQueryKind,
QueryVariableKind,
TransformationKind,
AdhocVariableKind,
CustomVariableKind,
ConstantVariableKind,
IntervalVariableKind,
TextVariableKind,
GroupByVariableKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { DataTransformerConfig } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen';
import {
@ -55,6 +62,8 @@ export function ensureV2Response(
const timeSettingsDefaults = defaultTimeSettingsSpec();
const dashboardDefaults = defaultDashboardV2Spec();
const [elements, layout] = getElementsFromPanels(dashboard.panels || []);
// @ts-expect-error - dashboard.templating.list is VariableModel[] and we need TypedVariableModel[] here
// that would allow accessing unique properties for each variable type that the API returns
const variables = getVariables(dashboard.templating?.list || []);
const annotations = getAnnotations(dashboard.annotations?.list || []);
@ -368,9 +377,17 @@ function getPanelTransformations(transformations: DataTransformerConfig[]): Tran
});
}
function getVariables(vars: VariableModel[]): DashboardV2Spec['variables'] {
function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables'] {
const variables: DashboardV2Spec['variables'] = [];
for (const v of vars) {
const commonProperties = {
name: v.name,
label: v.label,
...(v.description && { description: v.description }),
skipUrlSync: Boolean(v.skipUrlSync),
hide: transformVariableHideToEnum(v.hide),
};
switch (v.type) {
case 'query':
let query = v.query || {};
@ -383,17 +400,17 @@ function getVariables(vars: VariableModel[]): DashboardV2Spec['variables'] {
const qv: QueryVariableKind = {
kind: 'QueryVariable',
spec: {
name: v.name,
label: v.label,
hide: transformVariableHideToEnum(v.hide),
skipUrlSync: Boolean(v.skipUrlSync),
...commonProperties,
multi: Boolean(v.multi),
includeAll: Boolean(v.includeAll),
allValue: v.allValue,
current: v.current || { text: '', value: '' },
...(v.allValue && { allValue: v.allValue }),
current: {
value: v.current.value,
text: v.current.text,
},
options: v.options || [],
refresh: transformVariableRefreshToEnum(v.refresh),
datasource: v.datasource ?? undefined,
...(v.datasource && { datasource: v.datasource }),
regex: v.regex || '',
sort: transformSortVariableToEnum(v.sort),
query: {
@ -416,23 +433,119 @@ function getVariables(vars: VariableModel[]): DashboardV2Spec['variables'] {
const dv: DatasourceVariableKind = {
kind: 'DatasourceVariable',
spec: {
name: v.name,
label: v.label,
hide: transformVariableHideToEnum(v.hide),
skipUrlSync: Boolean(v.skipUrlSync),
...commonProperties,
multi: Boolean(v.multi),
includeAll: Boolean(v.includeAll),
allValue: v.allValue,
current: v.current || { text: '', value: '' },
...(v.allValue && { allValue: v.allValue }),
current: {
value: v.current.value,
text: v.current.text,
},
options: v.options || [],
refresh: transformVariableRefreshToEnum(v.refresh),
pluginId,
regex: v.regex || '',
description: v.description || '',
},
};
variables.push(dv);
break;
case 'custom':
const cv: CustomVariableKind = {
kind: 'CustomVariable',
spec: {
...commonProperties,
query: v.query,
current: {
value: v.current.value,
text: v.current.text,
},
options: v.options,
multi: v.multi,
includeAll: v.includeAll,
...(v.allValue && { allValue: v.allValue }),
},
};
variables.push(cv);
break;
case 'adhoc':
const av: AdhocVariableKind = {
kind: 'AdhocVariable',
spec: {
...commonProperties,
datasource: v.datasource || getDefaultDatasource(),
baseFilters: v.baseFilters || [],
filters: v.filters || [],
defaultKeys: v.defaultKeys || [],
},
};
variables.push(av);
break;
case 'constant':
const cnts: ConstantVariableKind = {
kind: 'ConstantVariable',
spec: {
...commonProperties,
current: {
value: v.current.value,
// Constant variable doesn't use text state
text: v.current.value,
},
query: v.query,
},
};
variables.push(cnts);
break;
case 'interval':
const intrv: IntervalVariableKind = {
kind: 'IntervalVariable',
spec: {
...commonProperties,
current: {
value: v.current.value,
// Interval variable doesn't use text state
text: v.current.value,
},
query: v.query,
refresh: 'onTimeRangeChanged',
options: v.options,
auto: v.auto,
auto_min: v.auto_min,
auto_count: v.auto_count,
},
};
variables.push(intrv);
break;
case 'textbox':
const tx: TextVariableKind = {
kind: 'TextVariable',
spec: {
...commonProperties,
current: {
value: v.current.value,
// Text variable doesn't use text state
text: v.current.value,
},
query: v.query,
},
};
variables.push(tx);
break;
case 'groupby':
const gb: GroupByVariableKind = {
kind: 'GroupByVariable',
spec: {
...commonProperties,
datasource: v.datasource || getDefaultDatasource(),
options: v.options,
current: {
value: v.current.value,
text: v.current.text,
},
multi: v.multi,
},
};
variables.push(gb);
break;
default:
// do not throw error, just log it
console.error(`Variable transformation not implemented: ${v.type}`);
@ -447,7 +560,7 @@ function getAnnotations(annotations: AnnotationQuery[]): DashboardV2Spec['annota
kind: 'AnnotationQuery',
spec: {
name: a.name,
datasource: a.datasource ?? undefined,
...(a.datasource && { datasource: a.datasource }),
enable: a.enable,
hide: Boolean(a.hide),
iconColor: a.iconColor,