diff --git a/.betterer.results b/.betterer.results index f7274be5286..f62f4482efd 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2279,6 +2279,9 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/dashboard-scene/v2schema/test-helpers.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/dashboard/api/dashboard_api.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts index 801deb57bc9..b9744d3f00a 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts @@ -22,7 +22,91 @@ export const handyTestingSchema: DashboardV2Spec = { to: 'now', weekStart: 'monday', }, - annotations: [], + annotations: [ + { + kind: 'AnnotationQuery', + spec: { + builtIn: true, + query: { + kind: 'prometheus', + spec: { + expr: 'test-query', + }, + }, + datasource: { + type: 'prometheus', + uid: 'uid', + }, + filter: { ids: [] }, + enable: true, + hide: false, + iconColor: 'rgba(0, 211, 255, 1)', + name: 'Annotations & Alerts', + }, + }, + { + kind: 'AnnotationQuery', + spec: { + datasource: { + type: 'grafana-testdata-datasource', + uid: 'uid', + }, + enable: true, + iconColor: 'red', + name: 'Enabled', + query: { + kind: 'grafana-testdata-datasource', + spec: { + lines: 4, + refId: 'Anno', + scenarioId: 'annotations', + }, + }, + filter: { ids: [] }, + hide: true, + }, + }, + { + kind: 'AnnotationQuery', + spec: { + datasource: { + type: 'grafana-testdata-datasource', + uid: 'uid', + }, + filter: { ids: [] }, + enable: false, + iconColor: 'yellow', + name: 'Disabled', + query: { + kind: 'grafana-testdata-datasource', + spec: { lines: 5, refId: 'Anno', scenarioId: 'annotations' }, + }, + hide: false, + }, + }, + { + kind: 'AnnotationQuery', + spec: { + datasource: { + type: 'grafana-testdata-datasource', + uid: 'uid', + }, + filter: { ids: [] }, + enable: true, + hide: true, + iconColor: 'dark-purple', + name: 'Hidden', + query: { + kind: 'grafana-testdata-datasource', + spec: { + lines: 6, + refId: 'Anno', + scenarioId: 'annotations', + }, + }, + }, + }, + ], elements: { 'test-panel-uid': { kind: 'Panel', diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index 777dbd3c675..96025a0c76d 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -1,15 +1,38 @@ 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 { + behaviors, + ConstantVariable, + CustomVariable, + DataSourceVariable, + IntervalVariable, + QueryVariable, + TextBoxVariable, + sceneGraph, + GroupByVariable, + AdHocFiltersVariable, +} from '@grafana/scenes'; +import { + AdhocVariableKind, + ConstantVariableKind, + CustomVariableKind, + DashboardV2Spec, + DatasourceVariableKind, + GroupByVariableKind, + IntervalVariableKind, + QueryVariableKind, + TextVariableKind, +} 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 { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardLayoutManager } from '../scene/types'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getQueryRunnerFor } from '../utils/utils'; +import { validateVariable, validateVizPanel } from '../v2schema/test-helpers'; import { transformSaveModelSchemaV2ToScene } from './transformSaveModelSchemaV2ToScene'; import { transformCursorSynctoEnum } from './transformToV2TypesUtils'; @@ -85,14 +108,96 @@ describe('transformSaveModelSchemaV2ToScene', () => { 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); ... + // Variables + const variables = scene.state?.$variables; + expect(variables?.state.variables).toHaveLength(dash.variables.length); + validateVariable({ + sceneVariable: variables?.state.variables[0], + variableKind: dash.variables[0] as QueryVariableKind, + scene: scene, + dashSpec: dash, + sceneVariableClass: QueryVariable, + index: 0, + }); + validateVariable({ + sceneVariable: variables?.state.variables[1], + variableKind: dash.variables[1] as CustomVariableKind, + scene: scene, + dashSpec: dash, + sceneVariableClass: CustomVariable, + index: 1, + }); + validateVariable({ + sceneVariable: variables?.state.variables[2], + variableKind: dash.variables[2] as DatasourceVariableKind, + scene: scene, + dashSpec: dash, + sceneVariableClass: DataSourceVariable, + index: 2, + }); + validateVariable({ + sceneVariable: variables?.state.variables[3], + variableKind: dash.variables[3] as ConstantVariableKind, + scene: scene, + dashSpec: dash, + sceneVariableClass: ConstantVariable, + index: 3, + }); + validateVariable({ + sceneVariable: variables?.state.variables[4], + variableKind: dash.variables[4] as IntervalVariableKind, + scene: scene, + dashSpec: dash, + sceneVariableClass: IntervalVariable, + index: 4, + }); + validateVariable({ + sceneVariable: variables?.state.variables[5], + variableKind: dash.variables[5] as TextVariableKind, + scene: scene, + dashSpec: dash, + sceneVariableClass: TextBoxVariable, + index: 5, + }); + validateVariable({ + sceneVariable: variables?.state.variables[6], + variableKind: dash.variables[6] as GroupByVariableKind, + scene: scene, + dashSpec: dash, + sceneVariableClass: GroupByVariable, + index: 6, + }); + validateVariable({ + sceneVariable: variables?.state.variables[7], + variableKind: dash.variables[7] as AdhocVariableKind, + scene: scene, + dashSpec: dash, + sceneVariableClass: AdHocFiltersVariable, + index: 7, + }); - // TODO: Annotations - // expect(scene.state.annotations).toHaveLength(dash.annotations.length); - // expect(scene.state.annotations[0].text).toBe(dash.annotations[0].text); ... + // Annotations + expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet); + const dataLayers = scene.state.$data as DashboardDataLayerSet; + expect(dataLayers.state.annotationLayers).toHaveLength(dash.annotations.length); + expect(dataLayers.state.annotationLayers[0].state.name).toBe(dash.annotations[0].spec.name); + expect(dataLayers.state.annotationLayers[0].state.isEnabled).toBe(dash.annotations[0].spec.enable); + expect(dataLayers.state.annotationLayers[0].state.isHidden).toBe(dash.annotations[0].spec.hide); + + // Enabled + expect(dataLayers.state.annotationLayers[1].state.name).toBe(dash.annotations[1].spec.name); + expect(dataLayers.state.annotationLayers[1].state.isEnabled).toBe(dash.annotations[1].spec.enable); + expect(dataLayers.state.annotationLayers[1].state.isHidden).toBe(dash.annotations[1].spec.hide); + + // Disabled + expect(dataLayers.state.annotationLayers[2].state.name).toBe(dash.annotations[2].spec.name); + expect(dataLayers.state.annotationLayers[2].state.isEnabled).toBe(dash.annotations[2].spec.enable); + expect(dataLayers.state.annotationLayers[2].state.isHidden).toBe(dash.annotations[2].spec.hide); + + // Hidden + expect(dataLayers.state.annotationLayers[3].state.name).toBe(dash.annotations[3].spec.name); + expect(dataLayers.state.annotationLayers[3].state.isEnabled).toBe(dash.annotations[3].spec.enable); + expect(dataLayers.state.annotationLayers[3].state.isHidden).toBe(dash.annotations[3].spec.hide); // To be implemented // expect(timePicker.state.ranges).toEqual(dash.timeSettings.quickRanges); @@ -101,31 +206,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { 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); + validateVizPanel(vizPanel, dash); // FIXME: Tests for layout }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 22a1ef865d0..5657a813cf7 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -85,7 +85,7 @@ import { const DEFAULT_DATASOURCE = 'default'; -type TypedVariableModelv2 = +export type TypedVariableModelV2 = | QueryVariableKind | TextVariableKind | ConstantVariableKind @@ -396,7 +396,7 @@ function createVariablesForDashboard(dashboard: DashboardV2Spec) { }); } -function createSceneVariableFromVariableModel(variable: TypedVariableModelv2): SceneVariable { +function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): SceneVariable { const commonProperties = { name: variable.spec.name, label: variable.spec.label, @@ -593,7 +593,7 @@ export function createVariablesForSnapshot(dashboard: DashboardV2Spec): SceneVar } /** Snapshots variables are read-only and should not be updated */ -export function createSnapshotVariable(variable: TypedVariableModelv2): SceneVariable { +export function createSnapshotVariable(variable: TypedVariableModelV2): SceneVariable { let snapshotVariable: SnapshotVariable; let current: { value: string | string[]; text: string | string[] }; if (variable.kind === 'IntervalVariable') { diff --git a/public/app/features/dashboard-scene/v2schema/test-helpers.ts b/public/app/features/dashboard-scene/v2schema/test-helpers.ts new file mode 100644 index 00000000000..8c514af6f27 --- /dev/null +++ b/public/app/features/dashboard-scene/v2schema/test-helpers.ts @@ -0,0 +1,88 @@ +import { + AdHocFiltersVariable, + DataSourceVariable, + GroupByVariable, + QueryVariable, + SceneDataTransformer, + SceneQueryRunner, + SceneVariable, + SceneVariableState, + VizPanel, +} from '@grafana/scenes'; +import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { TypedVariableModelV2 } from '../serialization/transformSaveModelSchemaV2ToScene'; +import { getQueryRunnerFor } from '../utils/utils'; + +type SceneVariableConstructor> = new ( + initialState: Partial +) => V; + +interface VariableValidation> { + sceneVariable: SceneVariable | undefined; + variableKind: T; + scene: DashboardScene; + dashSpec: DashboardV2Spec; + sceneVariableClass: SceneVariableConstructor; + index: number; +} + +export function validateVariable< + T extends TypedVariableModelV2, + S extends SceneVariableState, + V extends SceneVariable, +>({ sceneVariable, variableKind, scene, dashSpec, sceneVariableClass, index }: VariableValidation) { + if (variableKind.kind === 'AdhocVariable' && sceneVariable instanceof AdHocFiltersVariable) { + expect(sceneVariable).toBeInstanceOf(AdHocFiltersVariable); + expect(scene.state?.$variables?.getByName(dashSpec.variables[index].spec.name)?.getValue()).toBe( + `${variableKind.spec.filters[0].key}="${variableKind.spec.filters[0].value}"` + ); + expect(sceneVariable?.state.datasource).toEqual(variableKind.spec.datasource); + } else if (variableKind.kind !== 'AdhocVariable') { + expect(sceneVariable).toBeInstanceOf(sceneVariableClass); + expect(scene.state?.$variables?.getByName(dashSpec.variables[index].spec.name)?.getValue()).toBe( + variableKind.spec.current.value + ); + } + if (sceneVariable instanceof DataSourceVariable && variableKind.kind === 'DatasourceVariable') { + expect(sceneVariable?.state.pluginId).toBe(variableKind.spec.pluginId); + } + if (sceneVariable instanceof QueryVariable && variableKind.kind === 'QueryVariable') { + expect(sceneVariable?.state.datasource).toBe(variableKind.spec.datasource); + expect(sceneVariable?.state.query).toBe(variableKind.spec.query); + } + if (sceneVariable instanceof GroupByVariable && variableKind.kind === 'CustomVariable') { + expect(sceneVariable?.state.datasource).toBe(variableKind.spec.query); + } +} + +export function validateVizPanel(vizPanel: VizPanel, dash: DashboardV2Spec) { + 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); + expect(vizPanel.state.key).toBe(dash.elements['test-panel-uid'].spec.uid); + + expect(vizPanel.state.$data).toBeInstanceOf(SceneDataTransformer); + const dataTransformer = vizPanel.state.$data as SceneDataTransformer; + expect(dataTransformer.state.transformations[0]).toEqual( + dash.elements['test-panel-uid'].spec.data.spec.transformations[0].spec + ); + + expect(dataTransformer.state.$data).toBeInstanceOf(SceneQueryRunner); + const queryRunner = getQueryRunnerFor(vizPanel)!; + expect(queryRunner).toBeInstanceOf(SceneQueryRunner); + expect(queryRunner.state.queries).toEqual([ + { datasource: { type: 'prometheus', uid: 'datasource1' }, expr: 'test-query', hide: false, refId: 'A' }, + ]); + expect(queryRunner.state.maxDataPoints).toBe(100); + expect(queryRunner.state.cacheTimeout).toBe('1m'); + expect(queryRunner.state.queryCachingTTL).toBe(60); + expect(queryRunner.state.minInterval).toBe('1m'); + // FIXME: This is asking for a number as panel ID but here the uid of a panel is string + // will be fixed once scenes package is updated to support string panel ID + // expect(queryRunner.state.dataLayerFilter?.panelId).toBe(0); +}