diff --git a/public/app/features/dashboard-scene/scene/DashboardDataLayerSet.tsx b/public/app/features/dashboard-scene/scene/DashboardDataLayerSet.tsx new file mode 100644 index 00000000000..453e58b81b1 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/DashboardDataLayerSet.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import { + SceneDataLayerProviderState, + SceneDataLayerProvider, + SceneDataLayerSetBase, + SceneComponentProps, +} from '@grafana/scenes'; + +import { AlertStatesDataLayer } from './AlertStatesDataLayer'; + +export interface DashboardDataLayerSetState extends SceneDataLayerProviderState { + alertStatesLayer?: AlertStatesDataLayer; + annotationLayers: SceneDataLayerProvider[]; +} + +export class DashboardDataLayerSet + extends SceneDataLayerSetBase + implements SceneDataLayerProvider +{ + public constructor(state: Partial) { + super({ + ...state, + name: state.name ?? 'Data layers', + annotationLayers: state.annotationLayers ?? [], + }); + + this.addActivationHandler(() => this._onActivate()); + } + + private _onActivate() { + this._subs.add( + this.subscribeToState((newState, oldState) => { + if (newState.annotationLayers !== oldState.annotationLayers) { + this.querySub?.unsubscribe(); + this.subscribeToAllLayers(this.getAllLayers()); + } + }) + ); + + this.subscribeToAllLayers(this.getAllLayers()); + + return () => { + this.querySub?.unsubscribe(); + }; + } + + public addAnnotationLayer(layer: SceneDataLayerProvider) { + this.setState({ annotationLayers: [...this.state.annotationLayers, layer] }); + } + + private getAllLayers() { + const layers = [...this.state.annotationLayers]; + + if (this.state.alertStatesLayer) { + layers.push(this.state.alertStatesLayer); + } + + return layers; + } + + public static Component = ({ model }: SceneComponentProps) => { + const { annotationLayers } = model.useState(); + + return ( + <> + {annotationLayers.map((layer) => ( + + ))} + + ); + }; +} diff --git a/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts b/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts index 516ff3080b2..579f74d2a55 100644 --- a/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts +++ b/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts @@ -1,9 +1,10 @@ import { AnnotationChangeEvent, AnnotationEventUIModel, CoreApp, DataFrame } from '@grafana/data'; -import { AdHocFiltersVariable, dataLayers, SceneDataLayerSet, sceneGraph, sceneUtils, VizPanel } from '@grafana/scenes'; +import { AdHocFiltersVariable, dataLayers, sceneGraph, sceneUtils, VizPanel } from '@grafana/scenes'; import { DataSourceRef } from '@grafana/schema'; import { AdHocFilterItem, PanelContext } from '@grafana/ui'; import { deleteAnnotation, saveAnnotation, updateAnnotation } from 'app/features/annotations/api'; +import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils'; import { DashboardScene } from './DashboardScene'; @@ -128,13 +129,13 @@ export function setDashboardPanelContext(vizPanel: VizPanel, context: PanelConte } function getBuiltInAnnotationsLayer(scene: DashboardScene): dataLayers.AnnotationsDataLayer | undefined { + const set = dashboardSceneGraph.getDataLayers(scene); // When there is no builtin annotations query we disable the ability to add annotations - if (scene.state.$data instanceof SceneDataLayerSet) { - for (const layer of scene.state.$data.state.layers) { - if (layer instanceof dataLayers.AnnotationsDataLayer) { - if (layer.state.isEnabled && layer.state.query.builtIn) { - return layer; - } + + for (const layer of set.state.annotationLayers) { + if (layer instanceof dataLayers.AnnotationsDataLayer) { + if (layer.state.isEnabled && layer.state.query.builtIn) { + return layer; } } } diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index 3bc76a2d975..47d9033ae77 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -20,7 +20,6 @@ import { GroupByVariable, QueryVariable, SceneDataLayerControls, - SceneDataLayerSet, SceneDataTransformer, SceneGridLayout, SceneGridRow, @@ -42,6 +41,7 @@ import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard import { DashboardDataDTO } from 'app/types'; import { AddLibraryPanelWidget } from '../scene/AddLibraryPanelWidget'; +import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardGridItem } from '../scene/DashboardGridItem'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { PanelTimeRange } from '../scene/PanelTimeRange'; @@ -1230,26 +1230,26 @@ describe('transformSaveModelToScene', () => { it('Should build correct scene model', () => { const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); - expect(scene.state.$data).toBeInstanceOf(SceneDataLayerSet); + expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet); expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls); - const dataLayers = scene.state.$data as SceneDataLayerSet; - expect(dataLayers.state.layers).toHaveLength(4); - expect(dataLayers.state.layers[0].state.name).toBe('Annotations & Alerts'); - expect(dataLayers.state.layers[0].state.isEnabled).toBe(true); - expect(dataLayers.state.layers[0].state.isHidden).toBe(false); + const dataLayers = scene.state.$data as DashboardDataLayerSet; + expect(dataLayers.state.annotationLayers).toHaveLength(4); + expect(dataLayers.state.annotationLayers[0].state.name).toBe('Annotations & Alerts'); + expect(dataLayers.state.annotationLayers[0].state.isEnabled).toBe(true); + expect(dataLayers.state.annotationLayers[0].state.isHidden).toBe(false); - expect(dataLayers.state.layers[1].state.name).toBe('Enabled'); - expect(dataLayers.state.layers[1].state.isEnabled).toBe(true); - expect(dataLayers.state.layers[1].state.isHidden).toBe(false); + expect(dataLayers.state.annotationLayers[1].state.name).toBe('Enabled'); + expect(dataLayers.state.annotationLayers[1].state.isEnabled).toBe(true); + expect(dataLayers.state.annotationLayers[1].state.isHidden).toBe(false); - expect(dataLayers.state.layers[2].state.name).toBe('Disabled'); - expect(dataLayers.state.layers[2].state.isEnabled).toBe(false); - expect(dataLayers.state.layers[2].state.isHidden).toBe(false); + expect(dataLayers.state.annotationLayers[2].state.name).toBe('Disabled'); + expect(dataLayers.state.annotationLayers[2].state.isEnabled).toBe(false); + expect(dataLayers.state.annotationLayers[2].state.isHidden).toBe(false); - expect(dataLayers.state.layers[3].state.name).toBe('Hidden'); - expect(dataLayers.state.layers[3].state.isEnabled).toBe(true); - expect(dataLayers.state.layers[3].state.isHidden).toBe(true); + expect(dataLayers.state.annotationLayers[3].state.name).toBe('Hidden'); + expect(dataLayers.state.annotationLayers[3].state.isEnabled).toBe(true); + expect(dataLayers.state.annotationLayers[3].state.isHidden).toBe(true); }); }); @@ -1258,12 +1258,11 @@ describe('transformSaveModelToScene', () => { config.unifiedAlertingEnabled = true; const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); - expect(scene.state.$data).toBeInstanceOf(SceneDataLayerSet); + expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet); expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls); - const dataLayers = scene.state.$data as SceneDataLayerSet; - expect(dataLayers.state.layers).toHaveLength(5); - expect(dataLayers.state.layers[4].state.name).toBe('Alert States'); + const dataLayers = scene.state.$data as DashboardDataLayerSet; + expect(dataLayers.state.alertStatesLayer).toBeDefined(); }); it('Should add alert states data layer if any panel has a legacy alert defined', () => { @@ -1272,12 +1271,11 @@ describe('transformSaveModelToScene', () => { dashboard.panels![0].alert = {}; const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); - expect(scene.state.$data).toBeInstanceOf(SceneDataLayerSet); + expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet); expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls); - const dataLayers = scene.state.$data as SceneDataLayerSet; - expect(dataLayers.state.layers).toHaveLength(5); - expect(dataLayers.state.layers[4].state.name).toBe('Alert States'); + const dataLayers = scene.state.$data as DashboardDataLayerSet; + expect(dataLayers.state.alertStatesLayer).toBeDefined(); }); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 60553ea9188..b92c7a847dc 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -20,7 +20,6 @@ import { behaviors, VizPanelState, SceneGridItemLike, - SceneDataLayerSet, SceneDataLayerProvider, SceneDataLayerControls, TextBoxVariable, @@ -37,6 +36,7 @@ import { AddLibraryPanelWidget } from '../scene/AddLibraryPanelWidget'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; +import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem'; import { registerDashboardMacro } from '../scene/DashboardMacro'; import { DashboardScene } from '../scene/DashboardScene'; @@ -216,8 +216,9 @@ function createRowFromPanelModel(row: PanelModel, content: SceneGridItemLike[]): } export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) { - let variables: SceneVariableSet | undefined = undefined; - let layers: SceneDataLayerProvider[] = []; + let variables: SceneVariableSet | undefined; + let annotationLayers: SceneDataLayerProvider[] = []; + let alertStatesLayer: AlertStatesDataLayer | undefined; if (oldModel.templating?.list?.length) { const variableObjects = oldModel.templating.list @@ -244,7 +245,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) } if (oldModel.annotations?.list?.length && !oldModel.isSnapshot()) { - layers = oldModel.annotations?.list.map((a) => { + annotationLayers = oldModel.annotations?.list.map((a) => { // Each annotation query is an individual data layer return new DashboardAnnotationsDataLayer({ key: `annotations-${a.name}`, @@ -264,12 +265,10 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) } if (shouldUseAlertStatesLayer) { - layers.push( - new AlertStatesDataLayer({ - key: 'alert-states', - name: 'Alert States', - }) - ); + alertStatesLayer = new AlertStatesDataLayer({ + key: 'alert-states', + name: 'Alert States', + }); } const dashboardScene = new DashboardScene({ @@ -307,12 +306,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) trackIfIsEmpty, new behaviors.LiveNowTimer(oldModel.liveNow), ], - $data: - layers.length > 0 - ? new SceneDataLayerSet({ - layers, - }) - : undefined, + $data: new DashboardDataLayerSet({ annotationLayers, alertStatesLayer }), controls: new DashboardControls({ variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()], timePicker: new SceneTimePicker({}), diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index a70db28790a..2f1d5772cf0 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -16,13 +16,14 @@ import { } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { getPluginLinkExtensions, setPluginImportUtils } from '@grafana/runtime'; -import { MultiValueVariable, SceneDataLayerSet, SceneGridLayout, SceneGridRow, VizPanel } from '@grafana/scenes'; +import { MultiValueVariable, SceneGridLayout, SceneGridRow, VizPanel } from '@grafana/scenes'; import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema'; import { PanelModel } from 'app/features/dashboard/state'; import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; import { reduceTransformRegistryItem } from 'app/features/transformers/editors/ReduceTransformerEditor'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; +import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardGridItem } from '../scene/DashboardGridItem'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; @@ -402,10 +403,11 @@ describe('transformSceneToSaveModel', () => { expect(saveModel.annotations?.list?.length).toBe(4); expect(saveModel.annotations?.list).toMatchSnapshot(); }); + it('should transform annotations to save model after state changes', () => { const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); - const layers = (scene.state.$data as SceneDataLayerSet)?.state.layers; + const layers = (scene.state.$data as DashboardDataLayerSet)?.state.annotationLayers; const enabledLayer = layers[1]; const hiddenLayer = layers[3]; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 28b460d41db..386d3df6e3c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -10,7 +10,6 @@ import { SceneDataTransformer, SceneVariableSet, LocalValueVariable, - SceneDataLayerSet, } from '@grafana/scenes'; import { AnnotationQuery, @@ -33,6 +32,7 @@ import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/Dashboard import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types'; import { AddLibraryPanelWidget } from '../scene/AddLibraryPanelWidget'; +import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; @@ -77,10 +77,9 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa } let annotations: AnnotationQuery[] = []; - if (data instanceof SceneDataLayerSet) { - const layers = data.state.layers; - annotations = dataLayersToAnnotations(layers); + if (data instanceof DashboardDataLayerSet) { + annotations = dataLayersToAnnotations(data.state.annotationLayers); } if (variablesSet instanceof SceneVariableSet) { diff --git a/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx b/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx index 5d2e3a78187..105f747a867 100644 --- a/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx @@ -1,10 +1,10 @@ import { map, of } from 'rxjs'; import { AnnotationQuery, DataQueryRequest, DataSourceApi, LoadingState, PanelData } from '@grafana/data'; -import { SceneDataLayerSet, SceneGridLayout, SceneTimeRange, dataLayers } from '@grafana/scenes'; +import { SceneGridLayout, SceneTimeRange, dataLayers } from '@grafana/scenes'; -import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; +import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardScene } from '../scene/DashboardScene'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { activateFullSceneTree } from '../utils/test-utils'; @@ -48,40 +48,26 @@ jest.mock('@grafana/runtime', () => ({ describe('AnnotationsEditView', () => { describe('Dashboard annotations state', () => { let annotationsView: AnnotationsEditView; - let dashboardScene: DashboardScene; beforeEach(async () => { const result = await buildTestScene(); annotationsView = result.annotationsView; - dashboardScene = result.dashboard; }); it('should return the correct urlKey', () => { expect(annotationsView.getUrlKey()).toBe('annotations'); }); - it('should return the annotations length', () => { - expect(annotationsView.getAnnotationsLength()).toBe(1); - }); - - it('should return 0 if no annotations', () => { - dashboardScene.setState({ - $data: new SceneDataLayerSet({ layers: [] }), - }); - - expect(annotationsView.getAnnotationsLength()).toBe(0); - }); - it('should add a new annotation and group it with the other annotations', () => { const dataLayers = dashboardSceneGraph.getDataLayers(annotationsView.getDashboard()); - expect(dataLayers?.state.layers.length).toBe(2); + expect(dataLayers?.state.annotationLayers.length).toBe(1); annotationsView.onNew(); - expect(dataLayers?.state.layers.length).toBe(3); - expect(dataLayers?.state.layers[1].state.name).toBe(newAnnotationName); - expect(dataLayers?.state.layers[1].isActive).toBe(true); + expect(dataLayers?.state.annotationLayers.length).toBe(2); + expect(dataLayers?.state.annotationLayers[1].state.name).toBe(newAnnotationName); + expect(dataLayers?.state.annotationLayers[1].isActive).toBe(true); }); it('should move an annotation up one position', () => { @@ -89,13 +75,13 @@ describe('AnnotationsEditView', () => { annotationsView.onNew(); - expect(dataLayers?.state.layers.length).toBe(3); - expect(dataLayers?.state.layers[0].state.name).toBe('test'); + expect(dataLayers?.state.annotationLayers.length).toBe(2); + expect(dataLayers?.state.annotationLayers[0].state.name).toBe('test'); annotationsView.onMove(1, MoveDirection.UP); - expect(dataLayers?.state.layers.length).toBe(3); - expect(dataLayers?.state.layers[0].state.name).toBe(newAnnotationName); + expect(dataLayers?.state.annotationLayers.length).toBe(2); + expect(dataLayers?.state.annotationLayers[0].state.name).toBe(newAnnotationName); }); it('should move an annotation down one position', () => { @@ -103,33 +89,32 @@ describe('AnnotationsEditView', () => { annotationsView.onNew(); - expect(dataLayers?.state.layers.length).toBe(3); - expect(dataLayers?.state.layers[0].state.name).toBe('test'); + expect(dataLayers?.state.annotationLayers.length).toBe(2); + expect(dataLayers?.state.annotationLayers[0].state.name).toBe('test'); annotationsView.onMove(0, MoveDirection.DOWN); - expect(dataLayers?.state.layers.length).toBe(3); - expect(dataLayers?.state.layers[0].state.name).toBe(newAnnotationName); + expect(dataLayers?.state.annotationLayers.length).toBe(2); + expect(dataLayers?.state.annotationLayers[0].state.name).toBe(newAnnotationName); }); it('should delete annotation at index', () => { const dataLayers = dashboardSceneGraph.getDataLayers(annotationsView.getDashboard()); - expect(dataLayers?.state.layers.length).toBe(2); + expect(dataLayers?.state.annotationLayers.length).toBe(1); annotationsView.onDelete(0); - expect(dataLayers?.state.layers.length).toBe(1); - expect(dataLayers?.state.layers[0].state.name).toBe('Alert States'); + expect(dataLayers?.state.annotationLayers.length).toBe(0); }); it('should update an annotation at index', () => { const dataLayers = dashboardSceneGraph.getDataLayers(annotationsView.getDashboard()); - expect(dataLayers?.state.layers[0].state.name).toBe('test'); + expect(dataLayers?.state.annotationLayers[0].state.name).toBe('test'); const annotation: AnnotationQuery = { - ...(dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query, + ...(dataLayers?.state.annotationLayers[0] as dataLayers.AnnotationsDataLayer).state.query, }; annotation.name = 'new name'; @@ -138,12 +123,16 @@ describe('AnnotationsEditView', () => { annotation.iconColor = 'blue'; annotationsView.onUpdate(annotation, 0); - expect(dataLayers?.state.layers.length).toBe(2); - expect(dataLayers?.state.layers[0].state.name).toBe('new name'); - expect((dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query.name).toBe('new name'); - expect((dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query.hide).toBe(true); - expect((dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query.enable).toBe(false); - expect((dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query.iconColor).toBe('blue'); + expect(dataLayers?.state.annotationLayers.length).toBe(1); + expect(dataLayers?.state.annotationLayers[0].state.name).toBe('new name'); + expect((dataLayers?.state.annotationLayers[0] as dataLayers.AnnotationsDataLayer).state.query.name).toBe( + 'new name' + ); + expect((dataLayers?.state.annotationLayers[0] as dataLayers.AnnotationsDataLayer).state.query.hide).toBe(true); + expect((dataLayers?.state.annotationLayers[0] as dataLayers.AnnotationsDataLayer).state.query.enable).toBe(false); + expect((dataLayers?.state.annotationLayers[0] as dataLayers.AnnotationsDataLayer).state.query.iconColor).toBe( + 'blue' + ); }); }); }); @@ -158,8 +147,8 @@ async function buildTestScene() { meta: { canEdit: true, }, - $data: new SceneDataLayerSet({ - layers: [ + $data: new DashboardDataLayerSet({ + annotationLayers: [ new DashboardAnnotationsDataLayer({ key: `annotations-test`, query: { @@ -175,10 +164,6 @@ async function buildTestScene() { isEnabled: true, isHidden: false, }), - new AlertStatesDataLayer({ - key: 'alert-states', - name: 'Alert States', - }), ], }), body: new SceneGridLayout({ diff --git a/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx b/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx index e93d1bd3e74..ded0738d759 100644 --- a/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx @@ -40,7 +40,7 @@ export class AnnotationsEditView extends SceneObjectBase layer instanceof DashboardAnnotationsDataLayer).length; - } - public getDashboard(): DashboardScene { return this._dashboard; } @@ -76,18 +70,9 @@ export class AnnotationsEditView extends SceneObjectBase { @@ -100,25 +85,21 @@ export class AnnotationsEditView extends SceneObjectBase { const data = dashboardSceneGraph.getDataLayers(this._dashboard); + const layers = [...data.state.annotationLayers]; - const layers = [...data.state.layers]; const [layer] = layers.splice(idx, 1); layers.splice(idx + direction, 0, layer); - data.setState({ - layers, - }); + data.setState({ annotationLayers: layers }); }; public onDelete = (idx: number) => { const data = dashboardSceneGraph.getDataLayers(this._dashboard); + const layers = [...data.state.annotationLayers]; - const layers = [...data.state.layers]; layers.splice(idx, 1); - data.setState({ - layers, - }); + data.setState({ annotationLayers: layers }); }; public onUpdate = (annotation: AnnotationQuery, editIndex: number) => { @@ -139,14 +120,14 @@ export class AnnotationsEditView extends SceneObjectBase) { const dashboard = model.getDashboard(); - const { layers } = dashboardSceneGraph.getDataLayers(dashboard).useState(); + const { annotationLayers } = dashboardSceneGraph.getDataLayers(dashboard).useState(); const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); const { editIndex } = model.useState(); const panels = dashboardSceneGraph.getVizPanels(dashboard); - const annotations: AnnotationQuery[] = dataLayersToAnnotations(layers); + const annotations: AnnotationQuery[] = dataLayersToAnnotations(annotationLayers); - if (editIndex != null && editIndex < model.getAnnotationsLength()) { + if (editIndex != null && editIndex < annotationLayers.length) { return ( ({ table: css({ width: '100%', - overflowX: 'scroll', + overflowX: 'auto', }), }); diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts index 0b7d6e7ca89..e9cd110226d 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts @@ -1,17 +1,9 @@ -import { - SceneDataLayerSet, - SceneGridLayout, - SceneGridRow, - SceneQueryRunner, - SceneTimeRange, - VizPanel, - behaviors, -} from '@grafana/scenes'; +import { SceneGridLayout, SceneGridRow, SceneQueryRunner, SceneTimeRange, VizPanel, behaviors } from '@grafana/scenes'; import { DashboardCursorSync } from '@grafana/schema'; -import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; +import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; @@ -75,8 +67,8 @@ describe('dashboardSceneGraph', () => { it('should return the scene data layers', () => { const dataLayers = dashboardSceneGraph.getDataLayers(scene); - expect(dataLayers).toBeInstanceOf(SceneDataLayerSet); - expect(dataLayers?.state.layers.length).toBe(2); + expect(dataLayers).toBeInstanceOf(DashboardDataLayerSet); + expect(dataLayers?.state.annotationLayers.length).toBe(1); }); it('should throw if there are no scene data layers', () => { @@ -84,7 +76,7 @@ describe('dashboardSceneGraph', () => { $data: undefined, }); - expect(() => dashboardSceneGraph.getDataLayers(scene)).toThrow('SceneDataLayerSet not found'); + expect(() => dashboardSceneGraph.getDataLayers(scene)).toThrow('DashboardDataLayerSet not found'); }); }); @@ -241,8 +233,8 @@ function buildTestScene(overrides?: Partial) { sync: DashboardCursorSync.Crosshair, }), ], - $data: new SceneDataLayerSet({ - layers: [ + $data: new DashboardDataLayerSet({ + annotationLayers: [ new DashboardAnnotationsDataLayer({ key: `annotation`, query: { @@ -255,10 +247,6 @@ function buildTestScene(overrides?: Partial) { isEnabled: true, isHidden: false, }), - new AlertStatesDataLayer({ - key: 'alert-states', - name: 'Alert States', - }), ], }), body: new SceneGridLayout({ diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index 119e2d522a6..5c85b33a284 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -1,5 +1,6 @@ -import { VizPanel, SceneGridRow, SceneDataLayerSet, sceneGraph, SceneGridLayout, behaviors } from '@grafana/scenes'; +import { VizPanel, SceneGridRow, sceneGraph, SceneGridLayout, behaviors } from '@grafana/scenes'; +import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; @@ -53,11 +54,11 @@ function getVizPanels(scene: DashboardScene): VizPanel[] { return panels; } -function getDataLayers(scene: DashboardScene): SceneDataLayerSet { +function getDataLayers(scene: DashboardScene): DashboardDataLayerSet { const data = sceneGraph.getData(scene); - if (!(data instanceof SceneDataLayerSet)) { - throw new Error('SceneDataLayerSet not found'); + if (!(data instanceof DashboardDataLayerSet)) { + throw new Error('DashboardDataLayerSet not found'); } return data; diff --git a/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap b/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap index 272f0ec96d0..3827c5e8889 100644 --- a/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap +++ b/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap @@ -552,6 +552,37 @@ exports[`Graph Migrations time regions should migrate 1`] = ` } `; +exports[`Graph Migrations time regions should migrate in scenes dashboard 1`] = ` +{ + "alert": undefined, + "datasource": { + "type": "datasource", + "uid": "gdev-testdata", + }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "points", + "spanNulls": false, + }, + }, + "overrides": [], + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + }, + "tooltip": { + "mode": "single", + "sort": "none", + }, + }, +} +`; + exports[`Graph Migrations transforms should preserve "constant" transform 1`] = ` { "defaults": { diff --git a/public/app/plugins/panel/timeseries/migrations.test.ts b/public/app/plugins/panel/timeseries/migrations.test.ts index 48d08f11329..2b86f89ba7f 100644 --- a/public/app/plugins/panel/timeseries/migrations.test.ts +++ b/public/app/plugins/panel/timeseries/migrations.test.ts @@ -5,6 +5,9 @@ import { TooltipDisplayMode, SortOrder } from '@grafana/schema'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel, PanelModel as PanelModelState } from 'app/features/dashboard/state'; import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures'; +import { dataLayersToAnnotations } from 'app/features/dashboard-scene/serialization/dataLayersToAnnotations'; +import { transformSaveModelToScene } from 'app/features/dashboard-scene/serialization/transformSaveModelToScene'; +import { dashboardSceneGraph } from 'app/features/dashboard-scene/utils/dashboardSceneGraph'; import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types'; import { graphPanelChangedHandler } from './migrations'; @@ -125,6 +128,42 @@ describe('Graph Migrations', () => { ).toHaveLength(1); expect(panel).toMatchSnapshot(); }); + + test('should migrate in scenes dashboard', () => { + const old = { + angular: { + timeRegions: [ + { + colorMode: 'red', + fill: true, + fillColor: 'rgba(234, 112, 112, 0.12)', + fromDayOfWeek: 1, + line: true, + lineColor: 'rgba(237, 46, 24, 0.60)', + op: 'time', + }, + ], + }, + }; + + const panel = { datasource: { type: 'datasource', uid: 'gdev-testdata' } } as PanelModel; + + dashboard.panels.push(new PanelModelState(panel)); + + const scene = transformSaveModelToScene({ dashboard, meta: {} }); + window.__grafanaSceneContext = scene; + + panel.options = graphPanelChangedHandler(panel, 'graph', old, prevFieldConfig); + + const layers = dashboardSceneGraph.getDataLayers(scene).state.annotationLayers; + const annotations = dataLayersToAnnotations(layers); + + expect(annotations).toHaveLength(2); // built-in + time region + expect( + annotations.filter((annotation) => annotation.target?.queryType === GrafanaQueryType.TimeRegions) + ).toHaveLength(1); + expect(panel).toMatchSnapshot(); + }); }); describe('legend', () => { diff --git a/public/app/plugins/panel/timeseries/migrations.ts b/public/app/plugins/panel/timeseries/migrations.ts index 7acc221794f..fd67bfb9436 100644 --- a/public/app/plugins/panel/timeseries/migrations.ts +++ b/public/app/plugins/panel/timeseries/migrations.ts @@ -37,6 +37,9 @@ import { import { TimeRegionConfig } from 'app/core/utils/timeRegions'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { DashboardAnnotationsDataLayer } from 'app/features/dashboard-scene/scene/DashboardAnnotationsDataLayer'; +import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; +import { dashboardSceneGraph } from 'app/features/dashboard-scene/utils/dashboardSceneGraph'; import { GrafanaQuery, GrafanaQueryType } from 'app/plugins/datasource/grafana/types'; import { defaultGraphConfig } from './config'; @@ -61,17 +64,8 @@ export const graphPanelChangedHandler: PanelTypeChangedHandler = ( panel: panel, }); - const dashboard = getDashboardSrv().getCurrent(); - if (dashboard && annotations?.length > 0) { - dashboard.annotations.list = [...dashboard.annotations.list, ...annotations]; - - // Trigger a full dashboard refresh when annotations change - if (dashboardRefreshDebouncer == null) { - dashboardRefreshDebouncer = setTimeout(() => { - dashboardRefreshDebouncer = null; - getTimeSrv().refreshTimeModel(); - }); - } + if (annotations?.length > 0) { + addAnnotationsToDashboard(annotations); } panel.fieldConfig = fieldConfig; // Mutates the incoming panel @@ -400,7 +394,7 @@ export function graphToTimeseriesOptions(angular: any): { // timeRegions migration if (angular.timeRegions?.length) { let regions = angular.timeRegions.map((old: GraphTimeRegionConfig, idx: number) => ({ - name: `T${idx + 1}`, + name: `T${idx}`, color: old.colorMode !== 'custom' ? old.colorMode : old.fillColor, line: old.line, fill: old.fill, @@ -423,7 +417,7 @@ export function graphToTimeseriesOptions(angular: any): { ids: [angular.panel.id], }, iconColor: region.fillColor ?? (region as any).color, - name: `T${idx + 1}`, + name: `Time region for panel ${angular.panel.title}${idx > 0 ? ` ${idx}` : ''}`, target: { queryType: GrafanaQueryType.TimeRegions, refId: 'Anno', @@ -751,3 +745,40 @@ function getStackingFromOverrides(value: Boolean | string) { group: isString(value) ? value : defaultGroupName, }; } + +function addAnnotationsToDashboard(annotations: AnnotationQuery[]) { + const scene = window.__grafanaSceneContext; + + if (scene instanceof DashboardScene) { + const dataLayers = dashboardSceneGraph.getDataLayers(scene); + const annotationLayers = [...dataLayers.state.annotationLayers]; + + for (let annotation of annotations) { + const newAnnotation = new DashboardAnnotationsDataLayer({ + key: `annotations-${annotation.name}`, + query: annotation, + name: annotation.name, + isEnabled: annotation.enable, + isHidden: annotation.hide, + }); + + annotationLayers.push(newAnnotation); + } + + dataLayers.setState({ annotationLayers }); + return; + } + + const dashboard = getDashboardSrv().getCurrent(); + if (dashboard) { + dashboard.annotations.list = [...dashboard.annotations.list, ...annotations]; + + // Trigger a full dashboard refresh when annotations change + if (dashboardRefreshDebouncer == null) { + dashboardRefreshDebouncer = setTimeout(() => { + dashboardRefreshDebouncer = null; + getTimeSrv().refreshTimeModel(); + }); + } + } +}