From d18cdae3e2c56c26351efb263c174ebe838cc84e Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Wed, 27 Nov 2024 14:35:37 +0100 Subject: [PATCH] Schema V2 to DashboardScene: Settings + Panels (#96074) * Transform Save mode to Scene: Settings * Transform Save mode to Scene: Panels --------- Co-authored-by: Dominik Prokop Co-authored-by: alexandra vargas Co-authored-by: Haris Rozajac --- .../dashboard/v2alpha0/dashboard.gen.ts | 26 +- .../dashboard/v2alpha0/dashboard.schema.cue | 11 +- .../src/schema/dashboard/v2alpha0/examples.ts | 355 ++++++++-- .../transformSaveModelSchemaV2ToScene.test.ts | 213 ++++++ .../transformSaveModelSchemaV2ToScene.ts | 631 ++++++++++++++++++ .../transformSaveModelToScene.ts | 2 +- .../serialization/transformToV1TypesUtils.ts | 131 ++++ .../features/dashboard/api/dashboard_api.ts | 2 +- 8 files changed, 1284 insertions(+), 87 deletions(-) create mode 100644 public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts create mode 100644 public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts create mode 100644 public/app/features/dashboard-scene/serialization/transformToV1TypesUtils.ts diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.gen.ts index 23dfe83a255..0695c70cf03 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.gen.ts @@ -124,6 +124,14 @@ export interface DataSourceRef { export const defaultDataSourceRef = (): DataSourceRef => ({ }); +export enum DataTopic { + AlertStates = "alertStates", + Annotations = "annotations", + Series = "series", +} + +export const defaultDataTopic = (): DataTopic => (DataTopic.AlertStates); + // Transformations allow to manipulate data returned by a query before the system applies a visualization. // Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, // use the output of one transformation as the input to another transformation, etc. @@ -135,8 +143,7 @@ export interface DataTransformerConfig { // Optional frame matcher. When missing it will be applied to all results filter?: MatcherConfig; // Where to pull DataFrames from as input to transformation - // replaced with common.DataTopic - topic?: "series" | "annotations" | "alertStates"; + topic?: DataTopic; // Options to be passed to the transformer // Valid options depend on the transformer id options: any; @@ -247,7 +254,7 @@ export const defaultMatcherConfig = (): MatcherConfig => ({ }); export interface Threshold { - value: number | null; + value: number; color: string; } @@ -283,13 +290,13 @@ export const defaultValueMapping = (): ValueMapping => (defaultValueMap()); // `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. // `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A. export enum MappingType { - Value = "value", - Range = "range", - Regex = "regex", - Special = "special", + ValueToText = "value", + RangeToText = "range", + RegexToText = "regex", + SpecialValue = "special", } -export const defaultMappingType = (): MappingType => (MappingType.Value); +export const defaultMappingType = (): MappingType => (MappingType.ValueToText); // Maps text values to a color or different display text and color. // For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. @@ -376,7 +383,7 @@ export enum SpecialValueMatch { False = "false", Null = "null", NotANumber = "nan", - NullNan = "null+nan", + NullAndNaN = "null+nan", Empty = "empty", } @@ -542,6 +549,7 @@ export interface QueryOptionsSpec { queryCachingTTL?: number; interval?: string; cacheTimeout?: string; + hideTimeOverride?: boolean; } export const defaultQueryOptionsSpec = (): QueryOptionsSpec => ({ diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index e7922f46eae..026c42663db 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -102,6 +102,8 @@ DataSourceRef: { uid?: string } +DataTopic: "alertStates" | "annotations" | "series" + // Transformations allow to manipulate data returned by a query before the system applies a visualization. // Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, // use the output of one transformation as the input to another transformation, etc. @@ -113,7 +115,7 @@ DataTransformerConfig: { // Optional frame matcher. When missing it will be applied to all results filter?: MatcherConfig // Where to pull DataFrames from as input to transformation - topic?: "series" | "annotations" | "alertStates" // replaced with common.DataTopic + topic?: DataTopic // Options to be passed to the transformer // Valid options depend on the transformer id options: _ @@ -217,7 +219,7 @@ MatcherConfig: { } Threshold: { - value: number | null + value: number color: string } @@ -235,7 +237,7 @@ ValueMapping: ValueMap | RangeMap | RegexMap | SpecialValueMap // `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. // `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. // `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A. -MappingType: "value" | "range" | "regex" | "special" +MappingType: "value" | "range" | "regex" | "special" @cog(kind="enum",memberNames="ValueToText|RangeToText|RegexToText|SpecialValue") // Maps text values to a color or different display text and color. // For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. @@ -287,7 +289,7 @@ SpecialValueMap: { } // Special value types supported by the `SpecialValueMap` -SpecialValueMatch: "true" | "false" | "null" | "nan" | "null+nan" | "empty" +SpecialValueMatch: "true" | "false" | "null" | "nan" | "null+nan" | "empty" @cog(kind="enum",memberNames="True|False|Null|NaN|NullAndNaN|Empty") // Result used as replacement with text and color when the value matches ValueMappingResult: { @@ -383,6 +385,7 @@ QueryOptionsSpec: { queryCachingTTL?: int interval?: string cacheTimeout?: string + hideTimeOverride?: bool } DataQueryKind: { diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts index 5b98aaf0854..e1dd3ec8cb9 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts @@ -1,36 +1,38 @@ -import { DashboardCursorSync, DashboardV2Spec } from './dashboard.gen'; +import { + DashboardCursorSync, + DashboardLinkType, + DashboardV2Spec, + VariableHide, + VariableRefresh, + VariableSort, +} from './dashboard.gen'; export const handyTestingSchema: DashboardV2Spec = { - id: 1, - title: 'Default Dashboard', - description: 'This is a default dashboard', - cursorSync: DashboardCursorSync.Off, - liveNow: false, - preload: false, + title: 'Test Dashboard', + description: 'Test Description', editable: true, - links: [], - tags: [], - schemaVersion: 39, + preload: true, + schemaVersion: 40, + tags: ['tag1', 'tag2'], + liveNow: true, + cursorSync: DashboardCursorSync.Crosshair, timeSettings: { - timezone: 'browser', - from: 'now-6h', - to: 'now', - autoRefresh: '10s', - autoRefreshIntervals: ['10s', '1m', '5m', '15m', '30m', '1h', '6h', '12h', '1d'], - quickRanges: ['now/d', 'now/w', 'now/M', 'now/y'], - hideTimepicker: false, - weekStart: 'sunday', + autoRefresh: '5s', + autoRefreshIntervals: ['5s', '10s', '30s'], fiscalYearStartMonth: 1, + from: 'now-1h', + hideTimepicker: false, + nowDelay: '1m', + quickRanges: [], + timezone: 'UTC', + to: 'now', + weekStart: 'monday', }, - + annotations: [], elements: { - timeSeriesTest: { + 'test-panel-uid': { kind: 'Panel', spec: { - title: 'Time Series Test', - description: 'This is a test panel', - uid: 'timeSeriesTest', - links: [], data: { kind: 'QueryGroup', spec: { @@ -38,44 +40,63 @@ export const handyTestingSchema: DashboardV2Spec = { { kind: 'PanelQuery', spec: { + refId: 'A', + datasource: { + type: 'prometheus', + uid: 'datasource1', + }, query: { kind: 'prometheus', spec: { - query: 'up', + expr: 'test-query', }, }, - datasource: { uid: 'gdev-prometheus', type: 'prometheus' }, hidden: false, - refId: 'A', }, }, ], + queryOptions: { + timeFrom: '1h', + maxDataPoints: 100, + timeShift: '1h', + queryCachingTTL: 60, + interval: '1m', + cacheTimeout: '1m', + hideTimeOverride: false, + }, transformations: [ { kind: 'limit', spec: { - id: 'limit', // id is competing w/ kind + id: 'limit', + disabled: false, + filter: { + id: 'byValue', + options: { + reducer: 'sum', + }, + }, options: { limit: 10, }, }, }, ], - queryOptions: { - maxDataPoints: 100, - cacheTimeout: '1m', - }, }, }, + description: 'Test Description', + links: [], + title: 'Test Panel', + uid: 'test-panel-uid', vizConfig: { kind: 'timeseries', spec: { - pluginVersion: '11.0.0', - options: {}, fieldConfig: { defaults: {}, overrides: [], }, + options: {}, + pluginVersion: '7.0.0', }, }, }, @@ -88,62 +109,252 @@ export const handyTestingSchema: DashboardV2Spec = { { kind: 'GridLayoutItem', spec: { - element: { kind: 'ElementReference', name: 'timeSeriesTest' }, + element: { + kind: 'ElementReference', + name: 'test-panel-uid', + }, + height: 0, + width: 0, x: 0, y: 0, - width: 12, - height: 6, }, }, ], }, }, - variables: [], - annotations: [ + links: [ { - kind: 'AnnotationQuery', + asDropdown: false, + icon: '', + includeVars: false, + keepTime: false, + tags: [], + targetBlank: false, + title: 'Test Link', + tooltip: '', + type: DashboardLinkType.Dashboards, + url: 'http://test.com', + }, + ], + + variables: [ + { + kind: 'QueryVariable', spec: { - datasource: { type: 'datasource', uid: 'grafana' }, - query: { - kind: 'grafana', - spec: { - queryType: 'timeRegions', - matchAny: false, - timeRegion: { - from: '12:27', - fromDayOfWeek: 2, - timezone: 'browser', - to: '11:30', - toDayOfWeek: 2, - }, - }, + allValue: '*', + current: { + text: 'text1', + value: 'value1', }, - enable: true, - filter: { - ids: [], + datasource: { + type: 'prometheus', + uid: 'datasource1', }, - hide: false, - iconColor: 'blue', - name: 'Grafana annotations', + definition: 'definition1', + description: 'A query variable', + hide: VariableHide.DontHide, + includeAll: true, + label: 'Query Variable', + multi: true, + name: 'queryVar', + options: [], + query: 'query1', + refresh: VariableRefresh.OnDashboardLoad, + regex: 'regex1', + skipUrlSync: false, + sort: VariableSort.Disabled, }, }, { - kind: 'AnnotationQuery', + kind: 'CustomVariable', spec: { - datasource: { uid: 'gdev-prometheus', type: 'prometheus' }, - query: { - kind: 'prometheus', - spec: { - query: 'up', + allValue: 'All', + current: { + text: 'option1', + value: 'option1', + }, + description: 'A custom variable', + hide: VariableHide.DontHide, + includeAll: true, + label: 'Custom Variable', + multi: true, + name: 'customVar', + options: [ + { + selected: true, + text: 'option1', + value: 'option1', }, + { + selected: false, + text: 'option2', + value: 'option2', + }, + ], + query: 'option1, option2', + skipUrlSync: false, + }, + }, + { + kind: 'DatasourceVariable', + spec: { + allValue: undefined, + current: { + text: 'text1', + value: 'value1', }, - enable: true, - filter: { - ids: [], + defaultOptionEnabled: true, + description: 'A datasource variable', + hide: VariableHide.DontHide, + includeAll: false, + label: 'Datasource Variable', + multi: false, + name: 'datasourceVar', + options: [], + pluginId: 'datasource1', + refresh: VariableRefresh.OnDashboardLoad, + regex: 'regex1', + skipUrlSync: false, + }, + }, + { + kind: 'ConstantVariable', + spec: { + current: { + text: 'value4', + value: 'value4', }, - hide: false, - iconColor: 'red', - name: 'Prometheus annotations', + description: 'A constant variable', + hide: VariableHide.DontHide, + label: 'Constant Variable', + name: 'constantVar', + query: 'value4', + skipUrlSync: true, + }, + }, + { + kind: 'IntervalVariable', + spec: { + auto: false, + auto_count: 10, + auto_min: '1m', + current: { + text: '1m', + value: '1m', + }, + description: 'An interval variable', + hide: VariableHide.DontHide, + label: 'Interval Variable', + name: 'intervalVar', + options: [ + { + selected: true, + text: '1m', + value: '1m', + }, + { + selected: false, + text: '5m', + value: '5m', + }, + { + selected: false, + text: '10m', + value: '10m', + }, + ], + query: '1m,5m,10m', + refresh: VariableRefresh.OnDashboardLoad, + skipUrlSync: false, + }, + }, + { + kind: 'TextVariable', + spec: { + current: { + text: 'value6', + value: 'value6', + }, + description: 'A text variable', + hide: VariableHide.DontHide, + label: 'Text Variable', + name: 'textVar', + query: 'value6', + skipUrlSync: false, + }, + }, + { + kind: 'GroupByVariable', + spec: { + current: { + text: 'text7', + value: 'value7', + }, + datasource: { + type: 'prometheus', + uid: 'datasource2', + }, + description: 'A group by variable', + hide: VariableHide.DontHide, + includeAll: false, + label: 'Group By Variable', + multi: false, + name: 'groupByVar', + options: [ + { + text: 'option1', + value: 'option1', + }, + { + text: 'option2', + value: 'option2', + }, + ], + skipUrlSync: false, + }, + }, + { + kind: 'AdhocVariable', + spec: { + baseFilters: [ + { + condition: 'AND', + key: 'key1', + operator: '=', + value: 'value1', + }, + { + condition: 'OR', + key: 'key2', + operator: '=', + value: 'value2', + }, + ], + datasource: { + type: 'prometheus', + uid: 'datasource3', + }, + defaultKeys: [ + { + expandable: true, + group: 'defaultGroup1', + text: 'defaultKey1', + value: 'defaultKey1', + }, + ], + description: 'An adhoc variable', + filters: [ + { + condition: 'AND', + key: 'key3', + operator: '=', + value: 'value3', + }, + ], + hide: VariableHide.DontHide, + label: 'Adhoc Variable', + name: 'adhocVar', + skipUrlSync: false, }, }, ], diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts new file mode 100644 index 00000000000..28d0ceaa7f0 --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -0,0 +1,213 @@ +import { cloneDeep } from 'lodash'; + +import { config } from '@grafana/runtime'; +import { behaviors, sceneGraph, SceneQueryRunner } from '@grafana/scenes'; +import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen'; +import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/examples'; +import { DashboardWithAccessInfo } from 'app/features/dashboard/api/dashboard_api'; +import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; + +import { DashboardLayoutManager } from '../scene/types'; +import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; +import { getQueryRunnerFor } from '../utils/utils'; + +import { transformSaveModelSchemaV2ToScene } from './transformSaveModelSchemaV2ToScene'; +import { transformCursorSynctoEnum } from './transformToV2TypesUtils'; + +const defaultDashboard: DashboardWithAccessInfo = { + kind: 'DashboardWithAccessInfo', + metadata: { + name: 'dashboard-uid', + namespace: 'default', + labels: {}, + resourceVersion: '', + creationTimestamp: '', + }, + spec: handyTestingSchema, + access: {}, + apiVersion: 'v2', +}; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getDataSourceSrv: () => ({ + getInstanceSettings: jest.fn(), + }), +})); + +describe('transformSaveModelSchemaV2ToScene', () => { + beforeAll(() => { + config.featureToggles.groupByVariable = true; + }); + + afterAll(() => { + config.featureToggles.groupByVariable = false; + }); + + it('should initialize the DashboardScene with the model state', () => { + const scene = transformSaveModelSchemaV2ToScene(defaultDashboard); + const dashboardControls = scene.state.controls!; + const dash = defaultDashboard.spec; + + expect(scene.state.uid).toEqual(defaultDashboard.metadata.name); + expect(scene.state.title).toEqual(dash.title); + expect(scene.state.description).toEqual(dash.description); + expect(scene.state.editable).toEqual(dash.editable); + expect(scene.state.preload).toEqual(true); + expect(scene.state.version).toEqual(dash.schemaVersion); + expect(scene.state.tags).toEqual(dash.tags); + + const liveNow = scene.state.$behaviors?.find((b) => b instanceof behaviors.LiveNowTimer); + expect(liveNow?.state.enabled).toEqual(dash.liveNow); + + const cursorSync = scene.state.$behaviors?.find((b) => b instanceof behaviors.CursorSync); + expect(transformCursorSynctoEnum(cursorSync?.state.sync)).toEqual(dash.cursorSync); + + // Dashboard links + expect(scene.state.links).toHaveLength(dash.links.length); + expect(scene.state.links![0].title).toBe(dash.links[0].title); + + // Time settings + const time = dash.timeSettings; + const refreshPicker = dashboardSceneGraph.getRefreshPicker(scene)!; + const timeRange = sceneGraph.getTimeRange(scene)!; + + // Time settings + expect(refreshPicker.state.refresh).toEqual(time.autoRefresh); + expect(refreshPicker.state.intervals).toEqual(time.autoRefreshIntervals); + expect(timeRange?.state.fiscalYearStartMonth).toEqual(dash.timeSettings.fiscalYearStartMonth); + expect(timeRange?.state.value.raw).toEqual({ from: dash.timeSettings.from, to: dash.timeSettings.to }); + expect(dashboardControls.state.hideTimeControls).toEqual(dash.timeSettings.hideTimepicker); + expect(timeRange?.state.UNSAFE_nowDelay).toEqual(dash.timeSettings.nowDelay); + expect(timeRange?.state.timeZone).toEqual(dash.timeSettings.timezone); + expect(timeRange?.state.weekStart).toEqual(dash.timeSettings.weekStart); + expect(dashboardControls).toBeDefined(); + expect(dashboardControls.state.refreshPicker.state.intervals).toEqual(time.autoRefreshIntervals); + expect(dashboardControls.state.hideTimeControls).toBe(time.hideTimepicker); + + // TODO: Variables + // expect(scene.state?.$variables?.state.variables).toHaveLength(dash.variables.length); + // expect(scene.state?.$variables?.getByName(dash.variables[0].spec.name)).toBeInstanceOf(QueryVariable); + // expect(scene.state?.$variables?.getByName(dash.variables[1].spec.name)).toBeInstanceOf(TextBoxVariable); ... + + // TODO: Annotations + // expect(scene.state.annotations).toHaveLength(dash.annotations.length); + // expect(scene.state.annotations[0].text).toBe(dash.annotations[0].text); ... + + // To be implemented + // expect(timePicker.state.ranges).toEqual(dash.timeSettings.quickRanges); + + // VizPanel + const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); + expect(vizPanels).toHaveLength(1); + const vizPanel = vizPanels[0]; + expect(vizPanel.state.title).toBe(dash.elements['test-panel-uid'].spec.title); + expect(vizPanel.state.description).toBe(dash.elements['test-panel-uid'].spec.description); + expect(vizPanel.state.pluginId).toBe(dash.elements['test-panel-uid'].spec.vizConfig.kind); + expect(vizPanel.state.pluginVersion).toBe(dash.elements['test-panel-uid'].spec.vizConfig.spec.pluginVersion); + expect(vizPanel.state.options).toEqual(dash.elements['test-panel-uid'].spec.vizConfig.spec.options); + expect(vizPanel.state.fieldConfig).toEqual(dash.elements['test-panel-uid'].spec.vizConfig.spec.fieldConfig); + + // FIXME: There is an error of data being undefined + // expect(vizPanel.state.$data).toBeInstanceOf(SceneDataTransformer); + // const dataTransformer = vizPanel.state.$data as SceneDataTransformer; + // expect(dataTransformer.state.transformations).toEqual([{ id: 'transform1', options: {} }]); + + // expect(dataTransformer.state.$data).toBeInstanceOf(SceneQueryRunner); + const queryRunner = getQueryRunnerFor(vizPanel); + expect(queryRunner).toBeInstanceOf(SceneQueryRunner); + expect(queryRunner?.state.datasource).toBeUndefined(); + // expect(queryRunner.state.queries).toEqual([{ query: 'test-query', datasource: { uid: 'datasource1', type: 'prometheus' } }]); + // expect(queryRunner.state.maxDataPoints).toBe(100); + // expect(queryRunner.state.cacheTimeout).toBe('1m'); + // expect(queryRunner.state.queryCachingTTL).toBe(60); + // expect(queryRunner.state.minInterval).toBe('1m'); + // expect(queryRunner.state.dataLayerFilter?.panelId).toBe(1); + + // FIXME: Fix the key incompatibility since panel is not numeric anymore + // expect(vizPanel.state.key).toBe(dash.elements['test-panel-uid'].spec.uid); + + // FIXME: Tests for layout + }); + + it('should set panel ds if it is mixed DS', () => { + const dashboard = cloneDeep(defaultDashboard); + dashboard.spec.elements['test-panel-uid'].spec.data.spec.queries.push({ + kind: 'PanelQuery', + spec: { + refId: 'A', + datasource: { + type: 'graphite', + uid: 'datasource1', + }, + hidden: false, + query: { + kind: 'prometheus', + spec: { + expr: 'test-query', + }, + }, + }, + }); + + const scene = transformSaveModelSchemaV2ToScene(dashboard); + + const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); + expect(vizPanels.length).toBe(1); + expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.type).toBe('mixed'); + expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.uid).toBe(MIXED_DATASOURCE_NAME); + }); + + it('should set panel ds as undefined if it is not mixed DS', () => { + const dashboard = cloneDeep(defaultDashboard); + dashboard.spec.elements['test-panel-uid'].spec.data.spec.queries.push({ + kind: 'PanelQuery', + spec: { + refId: 'A', + datasource: { + type: 'prometheus', + uid: 'datasource1', + }, + hidden: false, + query: { + kind: 'prometheus', + spec: { + expr: 'test-query', + }, + }, + }, + }); + + const scene = transformSaveModelSchemaV2ToScene(dashboard); + + const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); + expect(vizPanels.length).toBe(1); + expect(getQueryRunnerFor(vizPanels[0])?.state.datasource).toBeUndefined(); + }); + + // Skipping the test because the schema doesn't accept the ds to be undefined. + // In future PR, we will mark it as optional so, this test should pass and the runtime code should be updated. + it.skip('should set panel ds as undefined if it is not mixed DS', () => { + const dashboard = cloneDeep(defaultDashboard); + dashboard.spec.elements['test-panel-uid'].spec.data.spec.queries.push({ + kind: 'PanelQuery', + // @ts-expect-error TODO: When marking DS as optional, this should be fixed + spec: { + refId: 'A', + hidden: false, + query: { + kind: 'prometheus', + spec: { + expr: 'test-query', + }, + }, + }, + }); + + const scene = transformSaveModelSchemaV2ToScene(dashboard); + + const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); + expect(vizPanels.length).toBe(1); + expect(getQueryRunnerFor(vizPanels[0])?.state.datasource).toBeUndefined(); + }); +}); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts new file mode 100644 index 00000000000..db5b251c020 --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -0,0 +1,631 @@ +import { uniqueId } from 'lodash'; + +import { config, getDataSourceSrv } from '@grafana/runtime'; +import { + AdHocFiltersVariable, + behaviors, + ConstantVariable, + CustomVariable, + DataSourceVariable, + GroupByVariable, + IntervalVariable, + QueryVariable, + SceneDataLayerControls, + SceneDataProvider, + SceneDataQuery, + SceneDataTransformer, + SceneGridItemLike, + SceneGridLayout, + SceneObject, + SceneQueryRunner, + SceneRefreshPicker, + SceneTimePicker, + SceneTimeRange, + SceneVariable, + SceneVariableSet, + TextBoxVariable, + VariableValueSelectors, + VizPanel, + VizPanelMenu, + VizPanelState, +} from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema/dist/esm/index.gen'; +import { + AdhocVariableKind, + ConstantVariableKind, + CustomVariableKind, + DashboardV2Spec, + DatasourceVariableKind, + defaultAdhocVariableKind, + defaultConstantVariableKind, + defaultCustomVariableKind, + defaultDatasourceVariableKind, + defaultGroupByVariableKind, + defaultIntervalVariableKind, + defaultQueryVariableKind, + defaultTextVariableKind, + GroupByVariableKind, + IntervalVariableKind, + PanelKind, + PanelQueryKind, + QueryVariableKind, + TextVariableKind, +} from '@grafana/schema/src/schema/dashboard/v2alpha0/dashboard.gen'; +import { DashboardWithAccessInfo } from 'app/features/dashboard/api/dashboard_api'; +import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; + +import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; +import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; +import { DashboardControls } from '../scene/DashboardControls'; +import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; +import { DashboardDatasourceBehaviour } from '../scene/DashboardDatasourceBehaviour'; +import { registerDashboardMacro } from '../scene/DashboardMacro'; +import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior'; +import { DashboardScene } from '../scene/DashboardScene'; +import { DashboardScopesFacade } from '../scene/DashboardScopesFacade'; +import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; +import { PanelNotices } from '../scene/PanelNotices'; +import { PanelTimeRange } from '../scene/PanelTimeRange'; +import { AngularDeprecation } from '../scene/angular/AngularDeprecation'; +import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; +import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; +import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState'; +import { getDashboardSceneFor, getIntervalsFromQueryString } from '../utils/utils'; + +import { SnapshotVariable } from './custom-variables/SnapshotVariable'; +import { registerPanelInteractionsReporter } from './transformSaveModelToScene'; +import { + transformCursorSyncV2ToV1, + transformSortVariableToEnumV1, + transformValueMappingsToV1, + transformVariableHideToEnumV1, + transformVariableRefreshToEnumV1, +} from './transformToV1TypesUtils'; + +const DEFAULT_DATASOURCE = 'default'; + +type TypedVariableModelv2 = + | QueryVariableKind + | TextVariableKind + | ConstantVariableKind + | DatasourceVariableKind + | IntervalVariableKind + | CustomVariableKind + | GroupByVariableKind + | AdhocVariableKind; + +export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo): DashboardScene { + const { spec: dashboard, metadata } = dto; + + const annotationLayers = dashboard.annotations.map((annotation) => { + return new DashboardAnnotationsDataLayer({ + key: uniqueId('annotations-'), + query: annotation.spec, + name: annotation.spec.name, + isEnabled: Boolean(annotation.spec.enable), + isHidden: Boolean(annotation.spec.hide), + }); + }); + + const dashboardScene = new DashboardScene({ + description: dashboard.description, + editable: dashboard.editable, + preload: dashboard.preload, + id: dashboard.id, + isDirty: false, + links: dashboard.links, + // TODO: Combine access and metadata to compose the V1 meta object + meta: {}, + tags: dashboard.tags, + title: dashboard.title, + uid: metadata.name, + version: dashboard.schemaVersion, + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + isLazy: dashboard.preload ? false : true, + children: createSceneGridLayoutForItems(dashboard), + $behaviors: [trackIfEmpty], + }), + }), + $timeRange: new SceneTimeRange({ + from: dashboard.timeSettings.from, + to: dashboard.timeSettings.to, + fiscalYearStartMonth: dashboard.timeSettings.fiscalYearStartMonth, + timeZone: dashboard.timeSettings.timezone, + weekStart: dashboard.timeSettings.weekStart, + UNSAFE_nowDelay: dashboard.timeSettings.nowDelay, + }), + $variables: getVariables(dashboard), + $behaviors: [ + new behaviors.CursorSync({ + sync: transformCursorSyncV2ToV1(dashboard.cursorSync), + }), + new behaviors.SceneQueryController(), + registerDashboardMacro, + registerPanelInteractionsReporter, + new behaviors.LiveNowTimer({ enabled: dashboard.liveNow }), + preserveDashboardSceneStateInLocalStorage, + addPanelsOnLoadBehavior, + new DashboardScopesFacade({ + reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange, + uid: dashboard.id?.toString(), + }), + new DashboardReloadBehavior({ + reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange, + uid: dashboard.id?.toString(), + version: 1, + }), + ], + $data: new DashboardDataLayerSet({ + annotationLayers, + }), + controls: new DashboardControls({ + variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()], + timePicker: new SceneTimePicker({}), + refreshPicker: new SceneRefreshPicker({ + refresh: dashboard.timeSettings.autoRefresh, + intervals: dashboard.timeSettings.autoRefreshIntervals, + withText: true, + }), + hideTimeControls: dashboard.timeSettings.hideTimepicker, + }), + }); + + return dashboardScene; +} + +function createSceneGridLayoutForItems(dashboard: DashboardV2Spec): SceneGridItemLike[] { + const gridElements = dashboard.layout.spec.items; + + return gridElements.map((element) => { + if (element.kind === 'GridLayoutItem') { + const panel = dashboard.elements[element.spec.element.name]; + + if (!panel) { + throw new Error(`Panel with uid ${element.spec.element.name} not found in the dashboard elements`); + } + + if (panel.kind === 'Panel') { + const vizPanel = buildVizPanel(panel); + + return new DashboardGridItem({ + key: `grid-item-${panel.spec.uid}`, + x: element.spec.x, + y: element.spec.y, + width: element.spec.width, + height: element.spec.height, + itemHeight: element.spec.height, + body: vizPanel, + }); + } else { + throw new Error(`Unknown element kind: ${element.kind}`); + } + } else { + throw new Error(`Unknown layout element kind: ${element.kind}`); + } + }); +} + +function buildVizPanel(panel: PanelKind): VizPanel { + const titleItems: SceneObject[] = []; + + if (config.featureToggles.angularDeprecationUI) { + titleItems.push(new AngularDeprecation()); + } + + // FIXME: Links in a panel are DashboardLinks not DataLinks + // titleItems.push( + // new VizPanelLinks({ + // rawLinks: panel.spec.links, + // menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }), + // }) + // ); + + titleItems.push(new PanelNotices()); + + const queryOptions = panel.spec.data.spec.queryOptions; + const timeOverrideShown = (queryOptions.timeFrom || queryOptions.timeShift) && !queryOptions.hideTimeOverride; + + const vizPanelState: VizPanelState = { + key: panel.spec.uid, + title: panel.spec.title, + description: panel.spec.description, + pluginId: panel.spec.vizConfig.kind, + options: panel.spec.vizConfig.spec.options, + fieldConfig: transformValueMappingsToV1(panel.spec.vizConfig.spec.fieldConfig), + pluginVersion: panel.spec.vizConfig.spec.pluginVersion, + // FIXME: Transparent is not added to the schema yet + // displayMode: panel.spec.transparent ? 'transparent' : undefined, + hoverHeader: !panel.spec.title && !timeOverrideShown, + hoverHeaderOffset: 0, + $data: createPanelDataProvider(panel), + titleItems, + $behaviors: [], + extendPanelContext: setDashboardPanelContext, + // _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel), //FIXME: Angular Migration + }; + + // FIXME: Library Panel + // if (panel.spec.libraryPanel) { + // vizPanelState.$behaviors!.push( + // new LibraryPanelBehavior({ uid: panel.spec.libraryPanel.uid, name: panel.spec.libraryPanel.name }) + // ); + // vizPanelState.pluginId = LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID; + // vizPanelState.$data = undefined; + // } + + if (!config.publicDashboardAccessToken) { + vizPanelState.menu = new VizPanelMenu({ + $behaviors: [panelMenuBehavior], + }); + } + + if (queryOptions.timeFrom || queryOptions.timeShift) { + vizPanelState.$timeRange = new PanelTimeRange({ + timeFrom: queryOptions.timeFrom, + timeShift: queryOptions.timeShift, + hideTimeOverride: queryOptions.hideTimeOverride, + }); + } + + return new VizPanel(vizPanelState); +} + +function trackIfEmpty(grid: SceneGridLayout) { + getDashboardSceneFor(grid).setState({ isEmpty: grid.state.children.length === 0 }); + + const sub = grid.subscribeToState((n, p) => { + if (n.children.length !== p.children.length || n.children !== p.children) { + getDashboardSceneFor(grid).setState({ isEmpty: n.children.length === 0 }); + } + }); + + return () => { + sub.unsubscribe(); + }; +} + +function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined { + if (!panel.spec.data?.spec.queries?.length) { + return undefined; + } + + let datasource: DataSourceRef | undefined = undefined; + let isMixedDatasource = false; + + panel.spec.data.spec.queries.forEach((query) => { + if (!datasource) { + datasource = query.spec.datasource; + } else if (datasource.uid !== query.spec.datasource.uid || datasource.type !== query.spec.datasource.type) { + isMixedDatasource = true; + } + }); + + return isMixedDatasource ? { type: 'mixed', uid: MIXED_DATASOURCE_NAME } : undefined; +} + +function panelQueryKindToSceneQuery(query: PanelQueryKind): SceneDataQuery { + return { + refId: query.spec.refId, + datasource: query.spec.datasource, + hide: query.spec.hidden, + ...query.spec.query.spec, + }; +} + +export function createPanelDataProvider(panelKind: PanelKind): SceneDataProvider | undefined { + const panel = panelKind.spec; + const targets = panel.data?.spec.queries ?? []; + // Skip setting query runner for panels without queries + if (!targets?.length) { + return undefined; + } + + // Skip setting query runner for panel plugins with skipDataQuery + if (config.panels[panel.vizConfig.kind]?.skipDataQuery) { + return undefined; + } + + let dataProvider: SceneDataProvider | undefined = undefined; + const datasource = getPanelDataSource(panelKind); + + dataProvider = new SceneQueryRunner({ + datasource, + queries: targets.map(panelQueryKindToSceneQuery), + maxDataPoints: panel.data.spec.queryOptions.maxDataPoints ?? undefined, + maxDataPointsFromWidth: true, + cacheTimeout: panel.data.spec.queryOptions.cacheTimeout, + queryCachingTTL: panel.data.spec.queryOptions.queryCachingTTL, + minInterval: panel.data.spec.queryOptions.interval ?? undefined, + dataLayerFilter: { + // FIXME: This is asking for a number as panel ID but here the uid of a panel is string + panelId: Number.isNaN(parseInt(panel.uid, 10)) ? 0 : parseInt(panel.uid, 10), + }, + $behaviors: [new DashboardDatasourceBehaviour({})], + }); + + // Wrap inner data provider in a data transformer + return new SceneDataTransformer({ + $data: dataProvider, + transformations: panel.data.spec.transformations.map((transformation) => transformation.spec), + }); +} + +function getVariables(dashboard: DashboardV2Spec): SceneVariableSet | undefined { + let variables: SceneVariableSet | undefined; + + if (dashboard.variables.length) { + if (false) { + // FIXME: isSnapshot is not added to the schema yet + //if (dashboard.meta?.isSnapshot) { + // in the old model we use .meta.isSnapshot but meta is not persisted + // variables = createVariablesForSnapshot(dashboard); + } else { + variables = createVariablesForDashboard(dashboard); + } + } else { + // Create empty variable set + variables = new SceneVariableSet({ + variables: [], + }); + } + + return variables; +} + +function createVariablesForDashboard(dashboard: DashboardV2Spec) { + const variableObjects = dashboard.variables + .map((v) => { + try { + return createSceneVariableFromVariableModel(v); + } catch (err) { + console.error(err); + return null; + } + }) + // TODO: Remove filter + // Added temporarily to allow skipping non-compatible variables + .filter((v): v is SceneVariable => Boolean(v)); + + return new SceneVariableSet({ + variables: variableObjects, + }); +} + +function createSceneVariableFromVariableModel(variable: TypedVariableModelv2): SceneVariable { + const commonProperties = { + name: variable.spec.name, + label: variable.spec.label, + description: variable.spec.description, + }; + if (variable.kind === defaultAdhocVariableKind().kind) { + return new AdHocFiltersVariable({ + ...commonProperties, + description: variable.spec.description, + skipUrlSync: variable.spec.skipUrlSync, + hide: transformVariableHideToEnumV1(variable.spec.hide), + datasource: variable.spec.datasource, + applyMode: 'auto', + filters: variable.spec.filters ?? [], + baseFilters: variable.spec.baseFilters ?? [], + defaultKeys: variable.spec.defaultKeys, + useQueriesAsFilterForOptions: true, + layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined, + supportsMultiValueOperators: Boolean( + getDataSourceSrv().getInstanceSettings(variable.spec.datasource)?.meta.multiValueFilterOperators + ), + }); + } + if (variable.kind === defaultCustomVariableKind().kind) { + return new CustomVariable({ + ...commonProperties, + value: variable.spec.current?.value ?? '', + text: variable.spec.current?.text ?? '', + + query: variable.spec.query, + isMulti: variable.spec.multi, + allValue: variable.spec.allValue || undefined, + includeAll: variable.spec.includeAll, + defaultToAll: Boolean(variable.spec.includeAll), + skipUrlSync: variable.spec.skipUrlSync, + hide: transformVariableHideToEnumV1(variable.spec.hide), + }); + } else if (variable.kind === defaultQueryVariableKind().kind) { + return new QueryVariable({ + ...commonProperties, + value: variable.spec.current?.value ?? '', + text: variable.spec.current?.text ?? '', + query: getDataQueryForVariable(variable), + datasource: variable.spec.datasource, + sort: transformSortVariableToEnumV1(variable.spec.sort), + refresh: transformVariableRefreshToEnumV1(variable.spec.refresh), + regex: variable.spec.regex, + allValue: variable.spec.allValue || undefined, + includeAll: variable.spec.includeAll, + defaultToAll: Boolean(variable.spec.includeAll), + isMulti: variable.spec.multi, + skipUrlSync: variable.spec.skipUrlSync, + hide: transformVariableHideToEnumV1(variable.spec.hide), + definition: variable.spec.definition, + }); + } else if (variable.kind === defaultDatasourceVariableKind().kind) { + return new DataSourceVariable({ + ...commonProperties, + value: variable.spec.current?.value ?? '', + text: variable.spec.current?.text ?? '', + regex: variable.spec.regex, + pluginId: variable.spec.pluginId, + allValue: variable.spec.allValue || undefined, + includeAll: variable.spec.includeAll, + defaultToAll: Boolean(variable.spec.includeAll), + skipUrlSync: variable.spec.skipUrlSync, + isMulti: variable.spec.multi, + hide: transformVariableHideToEnumV1(variable.spec.hide), + defaultOptionEnabled: + variable.spec.current?.value === DEFAULT_DATASOURCE && variable.spec.current?.text === 'default', + }); + } else if (variable.kind === defaultIntervalVariableKind().kind) { + const intervals = getIntervalsFromQueryString(variable.spec.query); + const currentInterval = getCurrentValueForOldIntervalModel(variable, intervals); + return new IntervalVariable({ + ...commonProperties, + value: currentInterval, + intervals: intervals, + autoEnabled: variable.spec.auto, + autoStepCount: variable.spec.auto_count, + autoMinInterval: variable.spec.auto_min, + refresh: transformVariableRefreshToEnumV1(variable.spec.refresh), + skipUrlSync: variable.spec.skipUrlSync, + hide: transformVariableHideToEnumV1(variable.spec.hide), + }); + } else if (variable.kind === defaultConstantVariableKind().kind) { + return new ConstantVariable({ + ...commonProperties, + value: variable.spec.query, + skipUrlSync: variable.spec.skipUrlSync, + hide: transformVariableHideToEnumV1(variable.spec.hide), + }); + } else if (variable.kind === defaultTextVariableKind().kind) { + let val; + if (!variable?.spec.current?.value) { + val = variable.spec.query; + } else { + if (typeof variable.spec.current.value === 'string') { + val = variable.spec.current.value; + } else { + val = variable.spec.current.value[0]; + } + } + + return new TextBoxVariable({ + ...commonProperties, + value: val, + skipUrlSync: variable.spec.skipUrlSync, + hide: transformVariableHideToEnumV1(variable.spec.hide), + }); + } else if (config.featureToggles.groupByVariable && variable.kind === defaultGroupByVariableKind().kind) { + return new GroupByVariable({ + ...commonProperties, + datasource: variable.spec.datasource, + value: variable.spec.current?.value || [], + text: variable.spec.current?.text || [], + skipUrlSync: variable.spec.skipUrlSync, + hide: transformVariableHideToEnumV1(variable.spec.hide), + // @ts-expect-error + defaultOptions: variable.options, + }); + } else { + throw new Error(`Scenes: Unsupported variable type ${variable.kind}`); + } +} + +function getDataQueryForVariable(variable: QueryVariableKind) { + return typeof variable.spec.query !== 'string' + ? { + ...variable.spec.query.spec, + refId: variable.spec.query.spec.refId ?? 'A', + } + : (variable.spec.query ?? ''); +} + +export function getCurrentValueForOldIntervalModel(variable: IntervalVariableKind, intervals: string[]): string { + const selectedInterval = Array.isArray(variable.spec.current.value) + ? variable.spec.current.value[0] + : variable.spec.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 createVariablesForSnapshot(dashboard: DashboardV2Spec): SceneVariableSet { + const variableObjects = dashboard.variables + .map((v) => { + try { + // for adhoc we are using the AdHocFiltersVariable from scenes becuase of its complexity + if (v.kind === 'AdhocVariable') { + return new AdHocFiltersVariable({ + name: v.spec.name, + label: v.spec.label, + readOnly: true, + description: v.spec.description, + skipUrlSync: v.spec.skipUrlSync, + hide: transformVariableHideToEnumV1(v.spec.hide), + datasource: v.spec.datasource, + applyMode: 'auto', + filters: v.spec.filters ?? [], + baseFilters: v.spec.baseFilters ?? [], + defaultKeys: v.spec.defaultKeys, + useQueriesAsFilterForOptions: true, + layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined, + supportsMultiValueOperators: Boolean( + getDataSourceSrv().getInstanceSettings(v.spec.datasource)?.meta.multiValueFilterOperators + ), + }); + } + // for other variable types we are using the SnapshotVariable + return createSnapshotVariable(v); + } catch (err) { + console.error(err); + return null; + } + }) + // TODO: Remove filter + // Added temporarily to allow skipping non-compatible variables + .filter((v): v is SceneVariable => Boolean(v)); + + return new SceneVariableSet({ + variables: variableObjects, + }); +} + +/** Snapshots variables are read-only and should not be updated */ +export function createSnapshotVariable(variable: TypedVariableModelv2): SceneVariable { + let snapshotVariable: SnapshotVariable; + let current: { value: string | string[]; text: string | string[] }; + if (variable.kind === 'IntervalVariable') { + const intervals = getIntervalsFromQueryString(variable.spec.query); + const currentInterval = getCurrentValueForOldIntervalModel(variable, intervals); + snapshotVariable = new SnapshotVariable({ + name: variable.spec.name, + label: variable.spec.label, + description: variable.spec.description, + value: currentInterval, + text: currentInterval, + hide: transformVariableHideToEnumV1(variable.spec.hide), + }); + return snapshotVariable; + } + + if (variable.kind === 'ConstantVariable' || variable.kind === 'AdhocVariable') { + current = { + value: '', + text: '', + }; + } else { + current = { + value: variable.spec.current?.value ?? '', + text: variable.spec.current?.text ?? '', + }; + } + + snapshotVariable = new SnapshotVariable({ + name: variable.spec.name, + label: variable.spec.label, + description: variable.spec.description, + value: current?.value ?? '', + text: current?.text ?? '', + hide: transformVariableHideToEnumV1(variable.spec.hide), + }); + return snapshotVariable; +} diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 8f776253450..a63312a9f81 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -359,7 +359,7 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem { }); } -function registerPanelInteractionsReporter(scene: DashboardScene) { +export function registerPanelInteractionsReporter(scene: DashboardScene) { // Subscriptions set with subscribeToEvent are automatically unsubscribed when the scene deactivated scene.subscribeToEvent(UserActionEvent, (e) => { const { interaction } = e.payload; diff --git a/public/app/features/dashboard-scene/serialization/transformToV1TypesUtils.ts b/public/app/features/dashboard-scene/serialization/transformToV1TypesUtils.ts new file mode 100644 index 00000000000..26a9c5cd8bb --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/transformToV1TypesUtils.ts @@ -0,0 +1,131 @@ +import { FieldConfigSource as FieldConfigSourceV1, SpecialValueMatch as SpecialValueMatchV1 } from '@grafana/data'; +import { + VariableHide as VariableHideV1, + VariableRefresh as VariableRefreshV1, + VariableSort as VariableSortV1, + DashboardCursorSync as DashboardCursorSyncV1, + defaultDashboardCursorSync, +} from '@grafana/schema'; +import { + DashboardCursorSync, + MappingType, + VariableHide, + VariableRefresh, + VariableSort, + FieldConfigSource, + SpecialValueMatch, +} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen'; + +export function transformVariableRefreshToEnumV1(refresh?: VariableRefresh): VariableRefreshV1 { + switch (refresh) { + case VariableRefresh.Never: + return VariableRefreshV1.never; + case VariableRefresh.OnDashboardLoad: + return VariableRefreshV1.onDashboardLoad; + case VariableRefresh.OnTimeRangeChanged: + return VariableRefreshV1.onTimeRangeChanged; + default: + return VariableRefreshV1.never; + } +} + +export function transformVariableHideToEnumV1(hide?: VariableHide): VariableHideV1 { + switch (hide) { + case VariableHide.DontHide: + return VariableHideV1.dontHide; + case VariableHide.HideLabel: + return VariableHideV1.hideLabel; + case VariableHide.HideVariable: + return VariableHideV1.hideVariable; + default: + return VariableHideV1.dontHide; + } +} + +export function transformSortVariableToEnumV1(sort?: VariableSort): VariableSortV1 { + switch (sort) { + case VariableSort.Disabled: + return VariableSortV1.disabled; + case VariableSort.NumericalAsc: + return VariableSortV1.numericalAsc; + case VariableSort.NumericalDesc: + return VariableSortV1.numericalDesc; + case VariableSort.AlphabeticalAsc: + return VariableSortV1.alphabeticalAsc; + case VariableSort.AlphabeticalDesc: + return VariableSortV1.alphabeticalDesc; + default: + return VariableSortV1.disabled; + } +} + +export function transformCursorSyncV2ToV1(cursorSync: DashboardCursorSync): DashboardCursorSyncV1 { + switch (cursorSync) { + case DashboardCursorSync.Crosshair: + return DashboardCursorSyncV1.Crosshair; + case DashboardCursorSync.Tooltip: + return DashboardCursorSyncV1.Tooltip; + case DashboardCursorSync.Off: + return DashboardCursorSyncV1.Off; + default: + return defaultDashboardCursorSync; + } +} + +function transformSpecialValueMatchToV1(match: SpecialValueMatch): SpecialValueMatchV1 { + switch (match) { + case SpecialValueMatch.True: + return SpecialValueMatchV1.True; + case SpecialValueMatch.False: + return SpecialValueMatchV1.False; + case SpecialValueMatch.Null: + return SpecialValueMatchV1.Null; + case SpecialValueMatch.NotANumber: + return SpecialValueMatchV1.NaN; + case SpecialValueMatch.NullAndNaN: + return SpecialValueMatchV1.NullAndNaN; + case SpecialValueMatch.Empty: + return SpecialValueMatchV1.Empty; + default: + throw new Error(`Unknown match type: ${match}`); + } +} + +export function transformValueMappingsToV1(fieldConfig: FieldConfigSource): FieldConfigSourceV1 { + return { + ...fieldConfig, + defaults: { + ...fieldConfig.defaults, + mappings: fieldConfig.defaults.mappings?.map((mapping) => { + switch (mapping.type) { + case 'value': + return { + ...mapping, + type: MappingType.ValueToText, + }; + case 'range': + return { + ...mapping, + type: MappingType.RangeToText, + }; + case 'regex': + return { + ...mapping, + type: MappingType.RegexToText, + }; + case 'special': + return { + ...mapping, + options: { + ...mapping.options, + match: transformSpecialValueMatchToV1(mapping.options.match), + }, + type: MappingType.SpecialValue, + }; + default: + return mapping; + } + }), + }, + }; +} diff --git a/public/app/features/dashboard/api/dashboard_api.ts b/public/app/features/dashboard/api/dashboard_api.ts index 8d54bb66a0e..22988facd18 100644 --- a/public/app/features/dashboard/api/dashboard_api.ts +++ b/public/app/features/dashboard/api/dashboard_api.ts @@ -46,7 +46,7 @@ class LegacyDashboardAPI implements DashboardAPI { } } -interface DashboardWithAccessInfo extends Resource { +export interface DashboardWithAccessInfo extends Resource { access: Object; // TODO... }