diff --git a/.betterer.results b/.betterer.results index 0055fe606a3..f2e94bfa6cf 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1794,12 +1794,15 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], "public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/package.json b/package.json index c443e2185be..6675780ae2d 100644 --- a/package.json +++ b/package.json @@ -251,7 +251,7 @@ "@grafana/lezer-traceql": "0.0.6", "@grafana/monaco-logql": "^0.0.7", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "^1.3.1", + "@grafana/scenes": "^1.3.3", "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", "@kusto/monaco-kusto": "^7.4.0", diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx index d769273a84e..debaf3a0cbf 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx @@ -13,6 +13,16 @@ import { setupLoadDashboardMock } from '../utils/test-utils'; import { DashboardScenePage, Props } from './DashboardScenePage'; +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getDataSourceSrv: () => { + return { + get: jest.fn().mockResolvedValue({}), + getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }), + }; + }, +})); + function setup() { const context = getGrafanaContextMock(); const props: Props = { diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index e3bc9c76c8c..78faff65731 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -1,5 +1,6 @@ import { StateManagerBase } from 'app/core/services/StateManagerBase'; import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { buildPanelEditScene, PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardScene } from '../scene/DashboardScene'; @@ -66,6 +67,7 @@ export class DashboardScenePageStateManager extends StateManagerBase implements SceneDataProvider { private _querySub: Unsubscribable | undefined; private _sourceDataDeactivationHandler?: SceneDeactivationHandler; + private _results = new ReplaySubject(); constructor(state: ShareQueryDataProviderState) { super(state); @@ -40,6 +44,10 @@ export class ShareQueryDataProvider extends SceneObjectBase { + return this._results; + } + private _subscribeToSource() { const { query } = this.state; @@ -78,7 +86,18 @@ export class ShareQueryDataProvider extends SceneObjectBase this.setState({ data: state.data })); + this._querySub = sourceData.subscribeToState((state) => { + this._results.next({ + origin: this, + data: state.data || { + state: LoadingState.Done, + series: [], + timeRange: getDefaultTimeRange(), + }, + }); + + this.setState({ data: state.data }); + }); // Copy the initial state this.setState({ data: sourceData.state.data }); diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap index 332723bfc4f..099d379fb0b 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap @@ -1,7 +1,85 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`transformSceneToSaveModel Annotations should transform annotations to save model 1`] = ` +[ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana", + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard", + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata", + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "Enabled", + "target": { + "lines": 4, + "refId": "Anno", + "scenarioId": "annotations", + }, + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata", + }, + "enable": false, + "hide": false, + "iconColor": "yellow", + "name": "Disabled", + "target": { + "lines": 5, + "refId": "Anno", + "scenarioId": "annotations", + }, + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata", + }, + "enable": true, + "hide": true, + "iconColor": "dark-purple", + "name": "Hidden", + "target": { + "lines": 6, + "refId": "Anno", + "scenarioId": "annotations", + }, + }, +] +`; + exports[`transformSceneToSaveModel Given a scene with rows Should transform back to peristed model 1`] = ` { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --", + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard", + }, + ], + }, "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, @@ -120,6 +198,67 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back exports[`transformSceneToSaveModel Given a simple scene Should transform back to peristed model 1`] = ` { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana", + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard", + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata", + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "Enabled", + "target": { + "lines": 4, + "refId": "Anno", + "scenarioId": "annotations", + }, + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata", + }, + "enable": false, + "hide": false, + "iconColor": "yellow", + "name": "Disabled", + "target": { + "lines": 5, + "refId": "Anno", + "scenarioId": "annotations", + }, + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata", + }, + "enable": true, + "hide": true, + "iconColor": "dark-purple", + "name": "Hidden", + "target": { + "lines": 6, + "refId": "Anno", + "scenarioId": "annotations", + }, + }, + ], + }, "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, diff --git a/public/app/features/dashboard-scene/serialization/testfiles/dashboard_to_load1.json b/public/app/features/dashboard-scene/serialization/testfiles/dashboard_to_load1.json index 0127c5df5f8..9933ebfd7b5 100644 --- a/public/app/features/dashboard-scene/serialization/testfiles/dashboard_to_load1.json +++ b/public/app/features/dashboard-scene/serialization/testfiles/dashboard_to_load1.json @@ -8,10 +8,53 @@ "uid": "grafana" }, "enable": true, - "hide": true, + "hide": false, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "enable": true, + "iconColor": "red", + "name": "Enabled", + "target": { + "lines": 4, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "enable": false, + "iconColor": "yellow", + "name": "Disabled", + "target": { + "lines": 5, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "enable": true, + "hide": true, + "iconColor": "dark-purple", + "name": "Hidden", + "target": { + "lines": 6, + "refId": "Anno", + "scenarioId": "annotations" + } } ] }, diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index fc107fadff5..9fc21ef4e40 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -5,6 +5,8 @@ import { CustomVariable, DataSourceVariable, QueryVariable, + SceneDataLayerControls, + SceneDataLayers, SceneDataTransformer, SceneGridItem, SceneGridLayout, @@ -23,6 +25,7 @@ import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { ShareQueryDataProvider } from '../scene/ShareQueryDataProvider'; +import dashboard_to_load1 from './testfiles/dashboard_to_load1.json'; import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json'; import { createDashboardSceneFromDashboardModel, @@ -637,6 +640,33 @@ describe('transformSaveModelToScene', () => { expect(lastRow.state.isCollapsed).toBe(true); }); }); + + describe('Annotation queries', () => { + it('Should build correct scene model', () => { + const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); + + expect(scene.state.$data).toBeInstanceOf(SceneDataLayers); + expect(scene.state.controls![0]).toBeInstanceOf(SceneDataLayerControls); + + const dataLayers = scene.state.$data as SceneDataLayers; + 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); + + 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.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.layers[3].state.name).toBe('Hidden'); + expect(dataLayers.state.layers[3].state.isEnabled).toBe(true); + expect(dataLayers.state.layers[3].state.isHidden).toBe(true); + }); + }); }); function buildGridItemForTest(saveModel: Partial): { gridItem: SceneGridItem; vizPanel: VizPanel } { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 17c789c123e..f494c0a8e50 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -26,7 +26,12 @@ import { behaviors, VizPanelState, SceneGridItemLike, + SceneDataLayers, + dataLayers, + SceneDataLayerProvider, + SceneDataLayerControls, } from '@grafana/scenes'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardDTO } from 'app/types'; @@ -51,6 +56,9 @@ export function transformSaveModelToScene(rsp: DashboardDTO): DashboardScene { autoMigrateOldPanels: true, }); + // Setting for built-in annotations query to run + getDashboardSrv().setCurrent(oldModel); + return createDashboardSceneFromDashboardModel(oldModel); } @@ -148,6 +156,7 @@ function createRowFromPanelModel(row: PanelModel, content: SceneGridItemLike[]): export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) { let variables: SceneVariableSet | undefined = undefined; + let layers: SceneDataLayerProvider[] = []; if (oldModel.templating?.list?.length) { const variableObjects = oldModel.templating.list @@ -168,7 +177,20 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) }); } + if (oldModel.annotations?.list?.length) { + layers = oldModel.annotations?.list.map((a) => { + // Each annotation query is an individual data layer + return new dataLayers.AnnotationsDataLayer({ + query: a, + name: a.name, + isEnabled: Boolean(a.enable), + isHidden: Boolean(a.hide), + }); + }); + } + const controls: SceneObject[] = [ + new SceneDataLayerControls(), new VariableValueSelectors({}), new SceneControlsSpacer(), new SceneTimePicker({}), @@ -193,6 +215,12 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) sync: oldModel.graphTooltip, }), ], + $data: + layers.length > 0 + ? new SceneDataLayers({ + layers, + }) + : undefined, controls: controls, }); } diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index 774bc109778..3cc466b03c2 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -1,4 +1,11 @@ -import { MultiValueVariable, SceneGridItemLike, SceneGridLayout, SceneGridRow, SceneVariable } from '@grafana/scenes'; +import { + MultiValueVariable, + SceneDataLayers, + SceneGridItemLike, + SceneGridLayout, + SceneGridRow, + SceneVariable, +} from '@grafana/scenes'; import { Panel, RowPanel } from '@grafana/schema'; import { PanelModel } from 'app/features/dashboard/state'; @@ -94,6 +101,36 @@ describe('transformSceneToSaveModel', () => { expect(saveModel.gridPos?.h).toBe(8); }); }); + + describe('Annotations', () => { + it('should transform annotations to save model', () => { + const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); + const saveModel = transformSceneToSaveModel(scene); + + 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 SceneDataLayers)?.state.layers; + const enabledLayer = layers[1]; + const hiddenLayer = layers[3]; + + enabledLayer.setState({ + isEnabled: false, + }); + hiddenLayer.setState({ + isHidden: false, + }); + + const saveModel = transformSceneToSaveModel(scene); + + expect(saveModel.annotations?.list?.length).toBe(4); + expect(saveModel.annotations?.list?.[1].enable).toEqual(false); + expect(saveModel.annotations?.list?.[3].hide).toEqual(false); + }); + }); }); export function buildGridItemFromPanelSchema(panel: Partial): SceneGridItemLike { diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 6ef94c84f37..5b5c821bfb3 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -1,5 +1,14 @@ -import { SceneGridItem, SceneGridItemLike, SceneGridLayout, SceneGridRow, VizPanel } from '@grafana/scenes'; -import { Dashboard, defaultDashboard, FieldConfigSource, Panel, RowPanel } from '@grafana/schema'; +import { + SceneDataLayers, + SceneGridItem, + SceneGridItemLike, + SceneGridLayout, + SceneGridRow, + VizPanel, + dataLayers, + SceneDataLayerProvider, +} from '@grafana/scenes'; +import { AnnotationQuery, Dashboard, defaultDashboard, FieldConfigSource, Panel, RowPanel } from '@grafana/schema'; import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object'; import { DashboardScene } from '../scene/DashboardScene'; @@ -11,6 +20,7 @@ import { getPanelIdForVizPanel } from '../utils/utils'; export function transformSceneToSaveModel(scene: DashboardScene): Dashboard { const state = scene.state; const timeRange = state.$timeRange!.state; + const data = state.$data; const body = state.body; const panels: Panel[] = []; @@ -30,6 +40,13 @@ export function transformSceneToSaveModel(scene: DashboardScene): Dashboard { } } + let annotations: AnnotationQuery[] = []; + if (data instanceof SceneDataLayers) { + const layers = data.state.layers; + + annotations = dataLayersToAnnotations(layers); + } + const dashboard: Dashboard = { ...defaultDashboard, title: state.title, @@ -39,6 +56,9 @@ export function transformSceneToSaveModel(scene: DashboardScene): Dashboard { to: timeRange.to, }, panels, + annotations: { + list: annotations, + }, }; return sortedDeepCloneWithoutNulls(dashboard); @@ -141,3 +161,20 @@ export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array