From 8dfd918200ff8e0d750d905b04b860cdf9540941 Mon Sep 17 00:00:00 2001 From: Alexa V <239999+axelavargas@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:34:09 +0200 Subject: [PATCH] Dashboard: Use Interval variable in DashboardScene (#75836) Co-authored-by: Dominik Prokop --- .../transformSceneToSaveModel.test.ts.snap | 74 +++++++++++++- .../sceneVariablesSetToVariables.ts | 27 ++++- .../testfiles/dashboard_to_load1.json | 99 +++++++++++++++++++ .../transformSaveModelToScene.test.ts | 51 +++++++++- .../transformSaveModelToScene.ts | 25 ++++- .../transformSceneToSaveModel.test.ts | 6 +- .../features/dashboard-scene/utils/utils.ts | 55 ++++++++++- 7 files changed, 328 insertions(+), 9 deletions(-) diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap index 8653a11f25a..022e3016f89 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap @@ -62,7 +62,7 @@ exports[`transformSceneToSaveModel Annotations should transform annotations to s ] `; -exports[`transformSceneToSaveModel Given a scene with rows Should transform back to peristed model 1`] = ` +exports[`transformSceneToSaveModel Given a scene with rows Should transform back to persisted model 1`] = ` { "annotations": { "list": [ @@ -242,7 +242,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back } `; -exports[`transformSceneToSaveModel Given a simple scene Should transform back to peristed model 1`] = ` +exports[`transformSceneToSaveModel Given a simple scene with variables Should transform back to persisted model 1`] = ` { "annotations": { "list": [ @@ -448,6 +448,76 @@ exports[`transformSceneToSaveModel Given a simple scene Should transform back to "tags": [], "templating": { "list": [ + { + "auto": true, + "auto_count": 30, + "auto_min": "10s", + "current": { + "text": "1m", + "value": "1m", + }, + "hide": 2, + "name": "intervalVar", + "query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "refresh": 2, + "type": "interval", + }, + { + "current": { + "text": [ + "a", + ], + "value": [ + "a", + ], + }, + "includeAll": true, + "multi": true, + "name": "customVar", + "options": [], + "query": "a, b, c", + "type": "custom", + }, + { + "current": { + "text": "gdev-testdata", + "value": "PD8C576611E62080A", + }, + "includeAll": false, + "name": "dsVar", + "options": [], + "query": "grafana-testdata-datasource", + "refresh": 1, + "regex": "", + "type": "datasource", + }, + { + "current": { + "text": "A", + "value": "A", + }, + "includeAll": false, + "name": "query0", + "options": [], + "query": { + "query": "*", + "refId": "StandardVariableQuery", + }, + "refresh": 1, + "regex": "", + "type": "query", + }, + { + "current": { + "text": "test", + "value": "test", + }, + "hide": 2, + "name": "constant", + "query": "test", + "skipUrlSync": true, + "type": "constant", + }, { "datasource": { "type": "prometheus", diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts index 17e69a1ac4b..5063c6fd594 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts @@ -1,6 +1,15 @@ -import { SceneVariableSet, QueryVariable, CustomVariable, DataSourceVariable, ConstantVariable } from '@grafana/scenes'; +import { + SceneVariableSet, + QueryVariable, + CustomVariable, + DataSourceVariable, + ConstantVariable, + IntervalVariable, +} from '@grafana/scenes'; import { VariableModel, VariableHide, VariableRefresh, VariableSort } from '@grafana/schema'; +import { getIntervalsQueryFromNewIntervalModel } from '../utils/utils'; + export function sceneVariablesSetToVariables(set: SceneVariableSet) { const variables: VariableModel[] = []; for (const variable of set.state.variables) { @@ -78,6 +87,22 @@ export function sceneVariablesSetToVariables(set: SceneVariableSet) { query: variable.state.value, hide: VariableHide.hideVariable, }); + } else if (variable instanceof IntervalVariable) { + const intervals = getIntervalsQueryFromNewIntervalModel(variable.state.intervals); + variables.push({ + ...commonProperties, + current: { + text: variable.state.value, + value: variable.state.value, + }, + query: intervals, + hide: VariableHide.hideVariable, + refresh: variable.state.refresh, + // @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 { throw new Error('Unsupported variable type'); } diff --git a/public/app/features/dashboard-scene/serialization/testfiles/dashboard_to_load1.json b/public/app/features/dashboard-scene/serialization/testfiles/dashboard_to_load1.json index f5e7476efda..2032a958cfd 100644 --- a/public/app/features/dashboard-scene/serialization/testfiles/dashboard_to_load1.json +++ b/public/app/features/dashboard-scene/serialization/testfiles/dashboard_to_load1.json @@ -202,6 +202,105 @@ "name": "Filters", "skipUrlSync": false, "type": "adhoc" + }, + { + "auto": true, + "auto_count": 30, + "auto_min": "10s", + "current": { + "selected": false, + "text": "1m", + "value": "1m" + }, + "hide": 0, + "name": "intervalVar", + "query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "queryValue": "", + "refresh": 2, + "skipUrlSync": false, + "type": "interval" + }, + { + "current": { + "selected": true, + "text": ["a"], + "value": ["a"] + }, + "hide": 0, + "includeAll": true, + "multi": true, + "name": "customVar", + "options": [ + { + "selected": false, + "text": "All", + "value": "$__all" + }, + { + "selected": true, + "text": "a", + "value": "a" + }, + { + "selected": false, + "text": "b", + "value": "b" + }, + { + "selected": false, + "text": "c", + "value": "c" + } + ], + "query": "a, b, c", + "skipUrlSync": false, + "type": "custom" + }, + { + "current": { + "selected": false, + "text": "gdev-testdata", + "value": "PD8C576611E62080A" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "dsVar", + "options": [], + "query": "grafana-testdata-datasource", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "selected": false, + "text": "A", + "value": "A" + }, + "definition": "*", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "query0", + "options": [], + "query": { + "query": "*", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "hide": 2, + "name": "constant", + "query": "test", + "skipUrlSync": false, + "type": "constant" } ] }, diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index 17cc17ea60f..7660265d0e4 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -637,7 +637,56 @@ describe('transformSaveModelToScene', () => { }); }); - it.each(['interval', 'textbox', 'system'])('should throw for unsupported (yet) variables', (type) => { + it('should migrate interval variable', () => { + const variable = { + name: 'intervalVar', + label: 'Interval Label', + type: 'interval' as VariableType, + 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: '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.each(['textbox', 'system'])('should throw for unsupported (yet) variables', (type) => { const variable = { name: 'query0', type: type as VariableType, diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 5aa44c58f24..825342252c0 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -3,6 +3,7 @@ import { ConstantVariableModel, CustomVariableModel, DataSourceVariableModel, + IntervalVariableModel, QueryVariableModel, VariableModel, } from '@grafana/data'; @@ -19,6 +20,7 @@ import { DataSourceVariable, QueryVariable, ConstantVariable, + IntervalVariable, SceneRefreshPicker, SceneGridItem, SceneObject, @@ -44,7 +46,11 @@ import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; import { createPanelDataProvider } from '../utils/createPanelDataProvider'; -import { getVizPanelKeyForPanelId } from '../utils/utils'; +import { + getCurrentValueForOldIntervalModel, + getIntervalsFromOldIntervalModel, + getVizPanelKeyForPanelId, +} from '../utils/utils'; import { getAngularPanelMigrationHandler } from './angularMigration'; @@ -292,6 +298,21 @@ export function createSceneVariableFromVariableModel(variable: VariableModel): S isMulti: variable.multi, hide: variable.hide, }); + } else if (isIntervalVariable(variable)) { + const intervals = getIntervalsFromOldIntervalModel(variable); + const currentInterval = getCurrentValueForOldIntervalModel(variable, intervals); + return new IntervalVariable({ + ...commonProperties, + value: currentInterval, + description: variable.description, + intervals: intervals, + autoEnabled: variable.auto, + autoStepCount: variable.auto_count, + autoMinInterval: variable.auto_min, + refresh: variable.refresh, + skipUrlSync: variable.skipUrlSync, + hide: variable.hide, + }); } else if (isConstantVariable(variable)) { return new ConstantVariable({ ...commonProperties, @@ -323,6 +344,7 @@ export function buildGridItemForLibPanel(panel: PanelModel) { height: panel.gridPos.h, }); } + export function buildGridItemForPanel(panel: PanelModel): SceneGridItemLike { const vizPanelState: VizPanelState = { key: getVizPanelKeyForPanelId(panel.id), @@ -382,4 +404,5 @@ const isCustomVariable = (v: VariableModel): v is CustomVariableModel => v.type const isQueryVariable = (v: VariableModel): v is QueryVariableModel => v.type === 'query'; const isDataSourceVariable = (v: VariableModel): v is DataSourceVariableModel => v.type === 'datasource'; const isConstantVariable = (v: VariableModel): v is ConstantVariableModel => v.type === 'constant'; +const isIntervalVariable = (v: VariableModel): v is IntervalVariableModel => v.type === 'interval'; const isAdhocVariable = (v: VariableModel): v is AdHocVariableModel => v.type === 'adhoc'; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index 04163e6fbcb..5328a8a30d2 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -138,8 +138,8 @@ jest.mock('@grafana/runtime', () => ({ }, })); describe('transformSceneToSaveModel', () => { - describe('Given a simple scene', () => { - it('Should transform back to peristed model', () => { + describe('Given a simple scene with variables', () => { + it('Should transform back to persisted model', () => { const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); const saveModel = transformSceneToSaveModel(scene); @@ -148,7 +148,7 @@ describe('transformSceneToSaveModel', () => { }); describe('Given a scene with rows', () => { - it('Should transform back to peristed model', () => { + it('Should transform back to persisted model', () => { const scene = transformSaveModelToScene({ dashboard: repeatingRowsAndPanelsDashboardJson as any, meta: {} }); const saveModel = transformSceneToSaveModel(scene); const row2: RowPanel = saveModel.panels![2] as RowPanel; diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 604f7d354b5..b28c27c8834 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -1,4 +1,4 @@ -import { UrlQueryMap, urlUtil } from '@grafana/data'; +import { IntervalVariableModel, UrlQueryMap, urlUtil } from '@grafana/data'; import { config, locationSearchToObject } from '@grafana/runtime'; import { MultiValueVariable, @@ -8,6 +8,7 @@ import { SceneQueryRunner, VizPanel, } from '@grafana/scenes'; +import { initialIntervalVariableModelState } from 'app/features/variables/interval/reducer'; import { DashboardScene } from '../scene/DashboardScene'; @@ -150,6 +151,58 @@ export function getMultiVariableValues(variable: MultiValueVariable) { }; } +// Transform old interval model to new interval model from scenes +export function getIntervalsFromOldIntervalModel(variable: IntervalVariableModel): string[] { + // separate intervals by quotes either single or double + const matchIntervals = variable.query.match(/(["'])(.*?)\1|\w+/g); + + // If no intervals are found in query, return the initial state of the interval reducer. + if (!matchIntervals) { + return initialIntervalVariableModelState.query?.split(',') ?? []; + } + const uniqueIntervals = new Set(); + + // when options are defined in variable.query + const intervals = matchIntervals.reduce((uniqueIntervals: Set, text: string) => { + // Remove surrounding quotes from the interval value. + const intervalValue = text.replace(/["']+/g, ''); + + // Skip intervals that start with "$__auto_interval_",scenes will handle them. + if (intervalValue.startsWith('$__auto_interval_')) { + return uniqueIntervals; + } + + // Add the interval if it's not already in the Set. + uniqueIntervals.add(intervalValue); + return uniqueIntervals; + }, uniqueIntervals); + + return Array.from(intervals); +} + +// Transform new interval scene model to old interval core model +export function getIntervalsQueryFromNewIntervalModel(intervals: string[]): string { + const variableQuery = Array.isArray(intervals) ? intervals.join(',') : ''; + return variableQuery; +} + +export function getCurrentValueForOldIntervalModel(variable: IntervalVariableModel, intervals: string[]): string { + const selectedInterval = Array.isArray(variable.current.value) ? variable.current.value[0] : variable.current.value; + + // If the interval is the old auto format, return the new auto interval from scenes. + if (selectedInterval.startsWith('$__auto_interval_')) { + return '$__auto'; + } + + // Check if the selected interval is valid. + if (intervals.includes(selectedInterval)) { + return selectedInterval; + } + + // If the selected interval is not valid, return the first valid interval. + return intervals[0]; +} + export function getQueryRunnerFor(sceneObject: SceneObject | undefined): SceneQueryRunner | undefined { if (!sceneObject) { return undefined;