diff --git a/public/app/angular/panel/AngularPanelReactWrapper.tsx b/public/app/angular/panel/AngularPanelReactWrapper.tsx index d676a431ef8..a994b5b0a20 100644 --- a/public/app/angular/panel/AngularPanelReactWrapper.tsx +++ b/public/app/angular/panel/AngularPanelReactWrapper.tsx @@ -37,7 +37,7 @@ export function getAngularPanelReactWrapper(plugin: PanelPlugin): ComponentType< // @ts-ignore panel: fakePanel, // @ts-ignore - dashboard: new DashboardModelCompatibilityWrapper(), + dashboard: getDashboardSrv().getCurrent(), size: { width: props.width, height: props.height }, queryRunner: queryRunner, }; diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 262d5af0f22..4ee82614482 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -1,9 +1,19 @@ import { CoreApp } from '@grafana/data'; import { sceneGraph, SceneGridItem, SceneGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardScene } from './DashboardScene'; describe('DashboardScene', () => { + describe('DashboardSrv.getCurrent compatibility', () => { + it('Should set to compatibility wrapper', () => { + const scene = buildTestScene(); + scene.activate(); + + expect(getDashboardSrv().getCurrent()?.uid).toBe('dash-1'); + }); + }); + describe('Editing and discarding', () => { describe('Given scene in edit mode', () => { let scene: DashboardScene; diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 2ccfde78141..30ed8897580 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -14,10 +14,12 @@ import { SceneObjectStateChangedEvent, sceneUtils, } from '@grafana/scenes'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardMeta } from 'app/types'; import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer'; import { SaveDashboardDrawer } from '../serialization/SaveDashboardDrawer'; +import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; import { findVizPanelByKey, forceRenderChildren, @@ -29,12 +31,21 @@ import { import { DashboardSceneUrlSync } from './DashboardSceneUrlSync'; export interface DashboardSceneState extends SceneObjectState { + /** The title */ title: string; + /** A uid when saved */ uid?: string; + /** @deprecated */ + id?: number | null; + /** Layout of panels */ body: SceneObject; + /** NavToolbar actions */ actions?: SceneObject[]; + /** Fixed row at the top of the canvas with for example variables and time range controls */ controls?: SceneObject[]; + /** True when editing */ isEditing?: boolean; + /** True when user made a change */ isDirty?: boolean; /** meta flags */ meta: DashboardMeta; @@ -84,11 +95,17 @@ export class DashboardScene extends SceneObjectBase { this.startTrackingChanges(); } + const oldDashboardWrapper = new DashboardModelCompatibilityWrapper(this); + + // @ts-expect-error + getDashboardSrv().setCurrent(oldDashboardWrapper); + // Deactivation logic return () => { window.__grafanaSceneContext = undefined; this.stopTrackingChanges(); this.stopUrlSync(); + oldDashboardWrapper.destroy(); }; } diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 48e7e46c1f7..2a634c2b5c5 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -22,6 +22,7 @@ export const NavToolbarActions = React.memo(({ dashboard }) => { if (uid) { toolbarActions.push( (({ dashboard }) => { toolbarActions.push( locationService.push(`/d/${uid}`)} 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 3e9a5012a51..8653a11f25a 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 @@ -308,6 +308,7 @@ exports[`transformSceneToSaveModel Given a simple scene Should transform back to "editable": true, "fiscalYearStartMonth": 1, "graphTooltip": 0, + "id": 1351, "links": [], "panels": [ { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 036aeaa44d2..5aa44c58f24 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -32,7 +32,6 @@ import { SceneDataLayerControls, AdHocFilterSet, } from '@grafana/scenes'; -import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardDTO } from 'app/types'; @@ -61,9 +60,6 @@ export function transformSaveModelToScene(rsp: DashboardDTO): DashboardScene { autoMigrateOldPanels: false, }); - // Setting for built-in annotations query to run - getDashboardSrv().setCurrent(oldModel); - return createDashboardSceneFromDashboardModel(oldModel); } @@ -215,6 +211,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) return new DashboardScene({ title: oldModel.title, uid: oldModel.uid, + id: oldModel.id, meta: oldModel.meta, body: new SceneGridLayout({ isLazy: true, diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index c3ad9af03a4..1cbd1a78ac0 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -92,6 +92,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa ...defaultDashboard, title: state.title, uid: state.uid, + id: state.id, time: { from: timeRange.from, to: timeRange.to, diff --git a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts new file mode 100644 index 00000000000..fa705c8ab74 --- /dev/null +++ b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts @@ -0,0 +1,85 @@ +import { TimeRangeUpdatedEvent } from '@grafana/runtime'; +import { behaviors, SceneGridItem, SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { DashboardCursorSync } from '@grafana/schema'; + +import { DashboardScene } from '../scene/DashboardScene'; + +import { DashboardModelCompatibilityWrapper } from './DashboardModelCompatibilityWrapper'; + +describe('DashboardModelCompatibilityWrapper', () => { + it('Provide basic prop and function of compatability', () => { + const { wrapper } = setup(); + + expect(wrapper.uid).toBe('dash-1'); + expect(wrapper.title).toBe('hello'); + + expect(wrapper.time.from).toBe('now-6h'); + }); + + it('Shared tooltip functions', () => { + const { scene, wrapper } = setup(); + expect(wrapper.sharedTooltipModeEnabled()).toBe(false); + expect(wrapper.sharedCrosshairModeOnly()).toBe(false); + + scene.setState({ $behaviors: [new behaviors.CursorSync({ sync: DashboardCursorSync.Crosshair })] }); + + expect(wrapper.sharedTooltipModeEnabled()).toBe(true); + expect(wrapper.sharedCrosshairModeOnly()).toBe(true); + }); + + it('Get timezone from time range', () => { + const { wrapper } = setup(); + expect(wrapper.getTimezone()).toBe('America/New_York'); + }); + + it('Should emit TimeRangeUpdatedEvent when time range change', () => { + const { scene, wrapper } = setup(); + let timeChanged = 0; + wrapper.events.subscribe(TimeRangeUpdatedEvent, () => timeChanged++); + + scene.state.$timeRange!.onRefresh(); + expect(timeChanged).toBe(1); + }); + + it('Can get fake panel with getPanelById', () => { + const { wrapper } = setup(); + + expect(wrapper.getPanelById(1)!.title).toBe('Panel A'); + expect(wrapper.getPanelById(2)!.title).toBe('Panel B'); + }); +}); + +function setup() { + const scene = new DashboardScene({ + title: 'hello', + uid: 'dash-1', + $timeRange: new SceneTimeRange({ + timeZone: 'America/New_York', + }), + body: new SceneGridLayout({ + children: [ + new SceneGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + }), + new SceneGridItem({ + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + }), + ], + }), + }); + + const wrapper = new DashboardModelCompatibilityWrapper(scene); + + return { scene, wrapper }; +} diff --git a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts index 33adcc149e2..6cce5418c78 100644 --- a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts +++ b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts @@ -1,17 +1,66 @@ -import { DashboardCursorSync, dateTimeFormat, DateTimeInput, EventBusSrv } from '@grafana/data'; -import { behaviors, sceneGraph } from '@grafana/scenes'; +import { Subscription } from 'rxjs'; + +import { AnnotationQuery, DashboardCursorSync, dateTimeFormat, DateTimeInput, EventBusSrv } from '@grafana/data'; +import { TimeRangeUpdatedEvent } from '@grafana/runtime'; +import { behaviors, SceneDataTransformer, sceneGraph, VizPanel } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; +import { findVizPanelByKey, getVizPanelKeyForPanelId } from './utils'; + /** * Will move this to make it the main way we remain somewhat compatible with getDashboardSrv().getCurrent */ export class DashboardModelCompatibilityWrapper { - events = new EventBusSrv(); - panelInitialized() {} + public events = new EventBusSrv(); + private _subs = new Subscription(); + + public constructor(private _scene: DashboardScene) { + const timeRange = sceneGraph.getTimeRange(_scene); + + this._subs.add( + timeRange.subscribeToState((state, prev) => { + if (state.value !== prev.value) { + this.events.publish(new TimeRangeUpdatedEvent(state.value)); + } + }) + ); + } + + public get id(): number | null { + return this._scene.state.id ?? null; + } + + public get uid() { + return this._scene.state.uid ?? null; + } + + public get title() { + return this._scene.state.title; + } + + public get meta() { + return this._scene.state.meta; + } + + public get time() { + const time = sceneGraph.getTimeRange(this._scene); + return { + from: time.state.from, + to: time.state.to, + }; + } + + /** + * Used from from timeseries migration handler to migrate time regions to dashboard annotations + */ + public get annotations(): { list: AnnotationQuery[] } { + console.error('Scenes DashboardModelCompatibilityWrapper.annotations not implemented (yet)'); + return { list: [] }; + } public getTimezone() { - const time = sceneGraph.getTimeRange(window.__grafanaSceneContext); + const time = sceneGraph.getTimeRange(this._scene); return time.getTimeZone(); } @@ -20,16 +69,14 @@ export class DashboardModelCompatibilityWrapper { } public sharedCrosshairModeOnly() { - return this._getSyncMode() > 1; + return this._getSyncMode() === 1; } private _getSyncMode() { - const dashboard = this.getDashboardScene(); - - if (dashboard.state.$behaviors) { - for (const behavior of dashboard.state.$behaviors) { + if (this._scene.state.$behaviors) { + for (const behavior of this._scene.state.$behaviors) { if (behavior instanceof behaviors.CursorSync) { - return behavior.state.sync > 0; + return behavior.state.sync; } } } @@ -37,14 +84,6 @@ export class DashboardModelCompatibilityWrapper { return DashboardCursorSync.Off; } - private getDashboardScene(): DashboardScene { - if (window.__grafanaSceneContext instanceof DashboardScene) { - return window.__grafanaSceneContext; - } - - throw new Error('Dashboard scene not found'); - } - public otherPanelInFullscreen(panel: unknown) { return false; } @@ -55,4 +94,62 @@ export class DashboardModelCompatibilityWrapper { timeZone: this.getTimezone(), }); } + + public getPanelById(id: number): PanelCompatibilityWrapper | null { + const vizPanel = findVizPanelByKey(this._scene, getVizPanelKeyForPanelId(id)); + if (vizPanel) { + return new PanelCompatibilityWrapper(vizPanel); + } + + return null; + } + + public removePanel(panel: PanelCompatibilityWrapper) { + // TODO + console.error('Scenes DashboardModelCompatibilityWrapper.removePanel not implemented (yet)'); + } + + public canEditAnnotations(dashboardUID?: string) { + // TOOD + return false; + } + + public panelInitialized() {} + + public destroy() { + this.events.removeAllListeners(); + this._subs.unsubscribe(); + } +} + +class PanelCompatibilityWrapper { + constructor(private _vizPanel: VizPanel) {} + + public get type() { + return this._vizPanel.state.pluginId; + } + + public get title() { + return this._vizPanel.state.title; + } + + public get transformations() { + if (this._vizPanel.state.$data instanceof SceneDataTransformer) { + return this._vizPanel.state.$data.state.transformations; + } + + return []; + } + + public refresh() { + console.error('Scenes PanelCompatibilityWrapper.refresh no implemented (yet)'); + } + + public render() { + console.error('Scenes PanelCompatibilityWrapper.render no implemented (yet)'); + } + + public getQueryRunner() { + console.error('Scenes PanelCompatibilityWrapper.getQueryRunner no implemented (yet)'); + } } diff --git a/public/app/plugins/panel/alertlist/AlertList.tsx b/public/app/plugins/panel/alertlist/AlertList.tsx index f0d8c893103..3ef78251866 100644 --- a/public/app/plugins/panel/alertlist/AlertList.tsx +++ b/public/app/plugins/panel/alertlist/AlertList.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import { useAsync } from 'react-use'; import { dateMath, dateTime, GrafanaTheme2, PanelProps } from '@grafana/data'; -import { getBackendSrv, getTemplateSrv } from '@grafana/runtime'; +import { getBackendSrv } from '@grafana/runtime'; import { Card, CustomScrollbar, Icon, useStyles2 } from '@grafana/ui'; import alertDef from 'app/features/alerting/state/alertDef'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; @@ -23,10 +23,9 @@ export function AlertList(props: PanelProps) { const params: any = { state: getStateFilter(props.options.stateFilter), }; - const panel = getDashboardSrv().getCurrent()?.getPanelById(props.id)!; if (props.options.alertName) { - params.query = getTemplateSrv().replace(props.options.alertName, panel.scopedVars); + params.query = props.replaceVariables(props.options.alertName); } if (props.options.folderId >= 0) {