grafana/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts

599 lines
20 KiB
TypeScript

import { config } from '@grafana/runtime';
import {
AdHocFilterWithLabels as SceneAdHocFilterWithLabels,
MultiValueVariable,
SceneVariables,
sceneUtils,
SceneVariable,
} from '@grafana/scenes';
import {
VariableModel,
VariableRefresh as OldVariableRefresh,
VariableHide as OldVariableHide,
VariableSort as OldVariableSort,
} from '@grafana/schema';
import {
AdhocVariableKind,
ConstantVariableKind,
CustomVariableKind,
DataQueryKind,
DatasourceVariableKind,
IntervalVariableKind,
QueryVariableKind,
TextVariableKind,
GroupByVariableKind,
defaultVariableHide,
VariableOption,
defaultDataQueryKind,
AdHocFilterWithLabels,
SwitchVariableKind,
defaultIntervalVariableSpec,
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { getDefaultDatasource } from 'app/features/dashboard/api/ResponseTransformers';
import { getIntervalsQueryFromNewIntervalModel } from '../utils/utils';
import { DSReferencesMapping } from './DashboardSceneSerializer';
import { getDataSourceForQuery } from './layoutSerializers/utils';
import { getDataQueryKind, getElementDatasource } from './transformSceneToSaveModelSchemaV2';
import {
transformVariableRefreshToEnum,
transformVariableHideToEnum,
transformSortVariableToEnum,
LEGACY_STRING_VALUE_KEY,
} from './transformToV2TypesUtils';
/**
* Converts a SceneVariables object into an array of VariableModel objects.
* @param set - The SceneVariables object containing the variables to convert.
* @param keepQueryOptions - (Optional) A boolean flag indicating whether to keep the options for query variables.
* This should be set to `false` when variables are saved in the dashboard model,
* but should be set to `true` when variables are used in the templateSrv to keep them in sync.
* If `true`, the options for query variables are kept.
* */
export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptions?: boolean) {
const variables: VariableModel[] = [];
for (const variable of set.state.variables) {
const commonProperties = {
name: variable.state.name,
label: variable.state.label,
description: variable.state.description ?? undefined,
skipUrlSync: Boolean(variable.state.skipUrlSync),
hide: variable.state.hide || OldVariableHide.dontHide,
type: variable.state.type,
};
if (sceneUtils.isQueryVariable(variable)) {
let options: VariableOption[] = [];
if (keepQueryOptions) {
options = variableValueOptionsToVariableOptions(variable.state);
}
const datasource = getElementDatasource(set, variable, 'variable');
const variableObj: VariableModel = {
...commonProperties,
current: {
// @ts-expect-error
value: variable.state.value,
// @ts-expect-error
text: variable.state.text,
},
options,
query: variable.state.query,
definition: variable.state.definition,
sort: variable.state.sort,
refresh: variable.state.refresh,
regex: variable.state.regex,
regexApplyTo: variable.state.regexApplyTo,
allValue: variable.state.allValue,
includeAll: variable.state.includeAll,
multi: variable.state.isMulti,
...(variable.state.allowCustomValue !== undefined && { allowCustomValue: variable.state.allowCustomValue }),
skipUrlSync: variable.state.skipUrlSync,
staticOptions: variable.state.staticOptions?.map((option) => ({
text: option.label,
value: String(option.value),
...(option.properties && { properties: option.properties }),
})),
staticOptionsOrder: variable.state.staticOptionsOrder,
};
// Only add datasource if it exists and is not empty
if (datasource && Object.keys(datasource).length > 0 && (datasource.uid || datasource.type)) {
variableObj.datasource = datasource;
}
variables.push(variableObj);
} else if (sceneUtils.isCustomVariable(variable)) {
let options: VariableOption[] = [];
if (keepQueryOptions) {
options = variableValueOptionsToVariableOptions(variable.state);
}
const customVariable: VariableModel = {
...commonProperties,
current: {
// @ts-expect-error
text: variable.state.value,
// @ts-expect-error
value: variable.state.value,
},
options,
query: variable.state.query,
multi: variable.state.isMulti,
allValue: variable.state.allValue,
includeAll: variable.state.includeAll,
...(variable.state.allowCustomValue !== undefined && { allowCustomValue: variable.state.allowCustomValue }),
// Ensure we persist the backend default when not specified to stay aligned with
// transformSaveModelSchemaV2ToScene which injects 'csv' on load.
valuesFormat: variable.state.valuesFormat ?? 'csv',
};
variables.push(customVariable);
} else if (sceneUtils.isDataSourceVariable(variable)) {
variables.push({
...commonProperties,
current: {
// @ts-expect-error
value: variable.state.value,
// @ts-expect-error
text: variable.state.text,
},
options: [],
regex: variable.state.regex,
refresh: OldVariableRefresh.onDashboardLoad,
query: variable.state.pluginId,
multi: variable.state.isMulti,
allValue: variable.state.allValue,
includeAll: variable.state.includeAll,
...(variable.state.allowCustomValue !== undefined && { allowCustomValue: variable.state.allowCustomValue }),
});
} else if (sceneUtils.isConstantVariable(variable)) {
variables.push({
...commonProperties,
type: 'constant', // Explicitly set type to constant
current: {
// @ts-expect-error
value: variable.state.value,
// @ts-expect-error
text: variable.state.value,
},
// @ts-expect-error
query: variable.state.value,
hide: OldVariableHide.hideVariable,
});
} else if (sceneUtils.isIntervalVariable(variable)) {
const intervals = getIntervalsQueryFromNewIntervalModel(variable.state.intervals);
variables.push({
...commonProperties,
current: {
text: variable.state.value,
value: variable.state.value,
},
query: intervals,
// V2 schema mandates refresh: "onTimeRangeChanged" for interval variables,
// which maps to OldVariableRefresh.onTimeRangeChanged (2) in V1
refresh: variable.state.refresh ?? OldVariableRefresh.onTimeRangeChanged,
options: variable.state.intervals.map((interval) => ({
value: interval,
text: interval,
selected: interval === variable.state.value,
})),
// @ts-expect-error ?? how to fix this without adding the ts-expect-error
auto: variable.state.autoEnabled,
auto_min: variable.state.autoMinInterval,
auto_count: variable.state.autoStepCount,
});
} else if (sceneUtils.isTextBoxVariable(variable)) {
const current = {
text: variable.state.value,
value: variable.state.value,
};
variables.push({
...commonProperties,
current,
options: [{ ...current, selected: true }],
query: variable.state.value,
});
} else if (sceneUtils.isGroupByVariable(variable) && config.featureToggles.groupByVariable) {
// @ts-expect-error
const defaultVariableOption: VariableOption | undefined = variable.state.defaultValue
? {
value: variable.state.defaultValue.value,
text: variable.state.defaultValue.text,
}
: undefined;
variables.push({
...commonProperties,
datasource: variable.state.datasource,
// Only persist the statically defined options
options: variable.state.defaultOptions?.map((option) => ({
text: option.text,
value: String(option.value),
})),
current: {
// @ts-expect-error
text: variable.state.text,
// @ts-expect-error
value: variable.state.value,
},
defaultValue: defaultVariableOption,
...(variable.state.allowCustomValue !== undefined && { allowCustomValue: variable.state.allowCustomValue }),
});
} else if (sceneUtils.isAdHocVariable(variable)) {
const adhocVariable: VariableModel = {
...commonProperties,
datasource: variable.state.datasource,
// @ts-expect-error
baseFilters: variable.state.baseFilters || [],
filters: [...validateFiltersOrigin(variable.state.originFilters), ...variable.state.filters],
defaultKeys: variable.state.defaultKeys,
...(variable.state.allowCustomValue !== undefined && { allowCustomValue: variable.state.allowCustomValue }),
};
variables.push(adhocVariable);
} else if (sceneUtils.isSwitchVariable(variable)) {
variables.push({
...commonProperties,
current: {
value: variable.state.value,
text: variable.state.value,
},
options: [
{
value: variable.state.enabledValue,
text: variable.state.enabledValue,
},
{
value: variable.state.disabledValue,
text: variable.state.disabledValue,
},
],
});
} else if (variable.state.type === 'system') {
// Not persisted
} else {
throw new Error('Unsupported variable type');
}
}
// Remove some defaults
for (const variable of variables) {
if (variable.hide === OldVariableHide.dontHide) {
delete variable.hide;
}
if (!variable.skipUrlSync) {
delete variable.skipUrlSync;
}
if (variable.label === '') {
delete variable.label;
}
if (!variable.multi) {
delete variable.multi;
}
if (variable.sort === OldVariableSort.disabled) {
delete variable.sort;
}
}
return variables;
}
function variableValueOptionsToVariableOptions(varState: MultiValueVariable['state']): VariableOption[] {
return varState.options.map((o) => ({
value: String(o.value),
text: o.label,
selected: Array.isArray(varState.value) ? varState.value.includes(o.value) : varState.value === o.value,
...(o.properties && { properties: o.properties }),
}));
}
export function sceneVariablesSetToSchemaV2Variables(
set: SceneVariables,
keepQueryOptions?: boolean,
dsReferencesMapping?: DSReferencesMapping
): Array<
| QueryVariableKind
| TextVariableKind
| IntervalVariableKind
| DatasourceVariableKind
| CustomVariableKind
| ConstantVariableKind
| GroupByVariableKind
| AdhocVariableKind
| SwitchVariableKind
> {
let variables: Array<
| QueryVariableKind
| TextVariableKind
| IntervalVariableKind
| DatasourceVariableKind
| CustomVariableKind
| ConstantVariableKind
| GroupByVariableKind
| AdhocVariableKind
| SwitchVariableKind
> = [];
for (const variable of set.state.variables) {
const commonProperties = {
name: variable.state.name,
label: variable.state.label,
description: variable.state.description ?? undefined,
skipUrlSync: Boolean(variable.state.skipUrlSync),
hide: transformVariableHideToEnum(variable.state.hide) || defaultVariableHide(),
};
// current: VariableOption;
const currentVariableOption: VariableOption = {
// @ts-expect-error
value: variable.state.value,
// @ts-expect-error
text: variable.state.text,
};
let options: VariableOption[] = [];
// Query variable
if (sceneUtils.isQueryVariable(variable)) {
if (keepQueryOptions) {
options = variableValueOptionsToVariableOptions(variable.state);
}
const query = variable.state.query;
let dataQuery: DataQueryKind | string;
const datasource = getElementDatasource(set, variable, 'variable', undefined, dsReferencesMapping);
if (typeof query !== 'string') {
dataQuery = {
kind: 'DataQuery',
version: defaultDataQueryKind().version,
group: datasource?.type || getDataQueryKind(query),
...(datasource?.uid && {
datasource: {
name: datasource.uid,
},
}),
spec: query,
};
} else {
// Only include LEGACY_STRING_VALUE_KEY if query is a non-empty string
const spec: Record<string, string> = {};
if (query) {
spec[LEGACY_STRING_VALUE_KEY] = query;
}
dataQuery = {
kind: 'DataQuery',
version: defaultDataQueryKind().version,
group: datasource?.type || getDataQueryKind(query),
...(datasource?.uid && {
datasource: {
name: datasource.uid,
},
}),
spec,
};
}
const queryVariable: QueryVariableKind = {
kind: 'QueryVariable',
spec: {
...commonProperties,
current: currentVariableOption,
options,
query: dataQuery,
definition: variable.state.definition,
sort: transformSortVariableToEnum(variable.state.sort),
refresh: transformVariableRefreshToEnum(variable.state.refresh),
regex: variable.state.regex ?? '',
regexApplyTo: variable.state.regexApplyTo ?? 'value',
allValue: variable.state.allValue,
includeAll: variable.state.includeAll || false,
multi: variable.state.isMulti || false,
skipUrlSync: variable.state.skipUrlSync || false,
allowCustomValue: variable.state.allowCustomValue ?? true,
staticOptions: variable.state.staticOptions?.map((option) => ({
text: option.label,
value: String(option.value),
...(option.properties && { properties: option.properties }),
})),
staticOptionsOrder: variable.state.staticOptionsOrder,
},
};
variables.push(queryVariable);
// Custom variable
} else if (sceneUtils.isCustomVariable(variable)) {
const customVariable: CustomVariableKind = {
kind: 'CustomVariable',
spec: {
...commonProperties,
current: currentVariableOption,
options: [],
query: variable.state.query,
multi: variable.state.isMulti || false,
allValue: variable.state.allValue,
includeAll: variable.state.includeAll ?? false,
allowCustomValue: variable.state.allowCustomValue ?? true,
valuesFormat: variable.state.valuesFormat ?? 'csv',
},
};
variables.push(customVariable);
// Datasource variable
} else if (sceneUtils.isDataSourceVariable(variable)) {
const datasourceVariable: DatasourceVariableKind = {
kind: 'DatasourceVariable',
spec: {
...commonProperties,
current: currentVariableOption,
options: [],
regex: variable.state.regex ?? '',
refresh: 'onDashboardLoad',
pluginId: variable.state.pluginId ?? getDefaultDatasource().type,
multi: variable.state.isMulti || false,
includeAll: variable.state.includeAll || false,
allowCustomValue: variable.state.allowCustomValue ?? true,
},
};
if (variable.state.allValue !== undefined) {
datasourceVariable.spec.allValue = variable.state.allValue;
}
variables.push(datasourceVariable);
// Constant variable
} else if (sceneUtils.isConstantVariable(variable)) {
const value = variable.state.value ? String(variable.state.value) : '';
const constantVariable: ConstantVariableKind = {
kind: 'ConstantVariable',
spec: {
...commonProperties,
current: {
text: value,
value: value,
},
query: value,
},
};
variables.push(constantVariable);
// Interval variable
} else if (sceneUtils.isIntervalVariable(variable)) {
const intervals = getIntervalsQueryFromNewIntervalModel(variable.state.intervals);
const intervalVariable: IntervalVariableKind = {
kind: 'IntervalVariable',
spec: {
...commonProperties,
current: {
...currentVariableOption,
// Interval variable doesn't use text state
text: variable.state.value,
},
query: intervals,
refresh: defaultIntervalVariableSpec().refresh,
options: variable.state.intervals.map((interval) => ({
value: interval,
text: interval,
selected: interval === variable.state.value,
})),
auto: variable.state.autoEnabled ?? defaultIntervalVariableSpec().auto,
auto_min: variable.state.autoMinInterval ?? defaultIntervalVariableSpec().auto_min,
auto_count: variable.state.autoStepCount ?? defaultIntervalVariableSpec().auto_count,
},
};
variables.push(intervalVariable);
// Textbox variable
} else if (sceneUtils.isTextBoxVariable(variable)) {
const current = {
text: variable.state.value ?? '',
value: variable.state.value ?? '',
};
const textBoxVariable: TextVariableKind = {
kind: 'TextVariable',
spec: {
...commonProperties,
current,
query: variable.state.value ?? '',
},
};
variables.push(textBoxVariable);
// Groupby variable
} else if (sceneUtils.isGroupByVariable(variable) && config.featureToggles.groupByVariable) {
options = variableValueOptionsToVariableOptions(variable.state);
// @ts-expect-error
const defaultVariableOption: VariableOption | undefined = variable.state.defaultValue
? {
value: variable.state.defaultValue.value,
text: variable.state.defaultValue.text,
}
: undefined;
const ds = getDataSourceForQuery(
variable.state.datasource,
variable.state.datasource?.type || getDefaultDatasource().type!
);
const groupVariable: GroupByVariableKind = {
kind: 'GroupByVariable',
group: ds.type!,
datasource: {
name: ds.uid,
},
spec: {
...commonProperties,
// Only persist the statically defined options
options:
variable.state.defaultOptions?.map((option) => ({
text: option.text,
value: String(option.value),
})) || [],
current: currentVariableOption,
defaultValue: defaultVariableOption,
multi: variable.state.isMulti || false,
},
};
variables.push(groupVariable);
// Adhoc variable
} else if (sceneUtils.isAdHocVariable(variable)) {
const ds = getDataSourceForQuery(
variable.state.datasource,
variable.state.datasource?.type || getDefaultDatasource().type!
);
const adhocVariable: AdhocVariableKind = {
kind: 'AdhocVariable',
group: ds.type!,
datasource: {
name: ds.uid,
},
spec: {
...commonProperties,
name: variable.state.name,
baseFilters: validateFiltersOrigin(variable.state.baseFilters) || [],
filters: [
...validateFiltersOrigin(variable.state.originFilters),
...validateFiltersOrigin(variable.state.filters),
],
defaultKeys: variable.state.defaultKeys || [],
allowCustomValue: variable.state.allowCustomValue ?? true,
},
};
variables.push(adhocVariable);
// Switch variable
} else if (sceneUtils.isSwitchVariable(variable)) {
const switchVariable: SwitchVariableKind = {
kind: 'SwitchVariable',
spec: {
...commonProperties,
current: variable.state.value,
enabledValue: variable.state.enabledValue,
disabledValue: variable.state.disabledValue,
},
};
variables.push(switchVariable);
} else if (variable.state.type === 'system') {
// Do nothing
} else {
throw new Error('Unsupported variable type: ' + variable.state.type);
}
}
return variables;
}
export function validateFiltersOrigin(filters?: SceneAdHocFilterWithLabels[]): AdHocFilterWithLabels[] {
// Only keep dashboard originated filters in the schema
return filters?.filter((f): f is AdHocFilterWithLabels => !f.origin || f.origin === 'dashboard') || [];
}
export function isVariableEditable(variable: SceneVariable) {
return variable.state.type !== 'system';
}