diff --git a/public/app/angular/AngularApp.ts b/public/app/angular/AngularApp.ts index 37df54085e2..045f1d27985 100644 --- a/public/app/angular/AngularApp.ts +++ b/public/app/angular/AngularApp.ts @@ -14,12 +14,14 @@ import { config } from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { setAngularPanelReactWrapper } from 'app/features/plugins/importPanelPlugin'; import { buildImportMap } from 'app/features/plugins/loader/utils'; import * as sdk from 'app/plugins/sdk'; import { registerAngularDirectives } from './angular_wrappers'; import { initAngularRoutingBridge } from './bridgeReactAngularRouting'; import { monkeyPatchInjectorWithPreAssignedBindings } from './injectorMonkeyPatch'; +import { getAngularPanelReactWrapper } from './panel/AngularPanelReactWrapper'; import { promiseToDigest } from './promiseToDigest'; import { registerComponents } from './registerComponents'; @@ -56,6 +58,8 @@ export class AngularApp { init() { const app = angular.module('grafana', []); + setAngularPanelReactWrapper(getAngularPanelReactWrapper); + app.config([ '$controllerProvider', '$compileProvider', diff --git a/public/app/angular/panel/AngularPanelReactWrapper.tsx b/public/app/angular/panel/AngularPanelReactWrapper.tsx new file mode 100644 index 00000000000..d676a431ef8 --- /dev/null +++ b/public/app/angular/panel/AngularPanelReactWrapper.tsx @@ -0,0 +1,126 @@ +import React, { ComponentType, useEffect, useRef } from 'react'; +import { Observable, ReplaySubject } from 'rxjs'; + +import { EventBusSrv, PanelData, PanelPlugin, PanelProps, FieldConfigSource } from '@grafana/data'; +import { AngularComponent, getAngularLoader, RefreshEvent } from '@grafana/runtime'; +import { DashboardModelCompatibilityWrapper } from 'app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper'; +import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner'; +import { RenderEvent } from 'app/types/events'; + +interface AngularScopeProps { + panel: PanelModelCompatibilityWrapper; + dashboard: DashboardModelCompatibilityWrapper; + queryRunner: FakeQueryRunner; + size: { + height: number; + width: number; + }; +} + +export function getAngularPanelReactWrapper(plugin: PanelPlugin): ComponentType { + return function AngularWrapper(props: PanelProps) { + const divRef = useRef(null); + const angularState = useRef(); + const angularComponent = useRef(); + + useEffect(() => { + if (!divRef.current) { + return; + } + + const loader = getAngularLoader(); + const template = ''; + const queryRunner = new FakeQueryRunner(); + const fakePanel = new PanelModelCompatibilityWrapper(plugin, props, queryRunner); + + angularState.current = { + // @ts-ignore + panel: fakePanel, + // @ts-ignore + dashboard: new DashboardModelCompatibilityWrapper(), + size: { width: props.width, height: props.height }, + queryRunner: queryRunner, + }; + + angularComponent.current = loader.load(divRef.current, angularState.current, template); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Re-render angular panel when dimensions change + useEffect(() => { + if (!angularComponent.current) { + return; + } + + angularState.current!.size.height = props.height; + angularState.current!.size.width = props.width; + angularState.current!.panel.events.publish(new RenderEvent()); + }, [props.width, props.height]); + + // Pass new data to angular panel + useEffect(() => { + if (!angularState.current?.panel) { + return; + } + + angularState.current.queryRunner.forwardNewData(props.data); + }, [props.data]); + + return
; + }; +} + +class PanelModelCompatibilityWrapper { + id: number; + type: string; + title: string; + plugin: PanelPlugin; + events: EventBusSrv; + queryRunner: FakeQueryRunner; + fieldConfig: FieldConfigSource; + options: Record; + + constructor(plugin: PanelPlugin, props: PanelProps, queryRunner: FakeQueryRunner) { + // Assign legacy "root" level options + if (props.options.angularOptions) { + Object.assign(this, props.options.angularOptions); + } + + this.id = props.id; + this.type = plugin.meta.id; + this.title = props.title; + this.fieldConfig = props.fieldConfig; + this.options = props.options; + + this.plugin = plugin; + this.events = new EventBusSrv(); + this.queryRunner = queryRunner; + } + + refresh() { + this.events.publish(new RefreshEvent()); + } + + render() { + this.events.publish(new RenderEvent()); + } + + getQueryRunner() { + return this.queryRunner; + } +} + +class FakeQueryRunner { + private subject = new ReplaySubject(1); + + getData(options: GetDataOptions): Observable { + return this.subject; + } + + forwardNewData(data: PanelData) { + this.subject.next(data); + } + + run() {} +} diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 690a7bc735d..feb796f7b89 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -57,7 +57,7 @@ export interface DashboardLoaderState { export function transformSaveModelToScene(rsp: DashboardDTO): DashboardScene { // Just to have migrations run const oldModel = new DashboardModel(rsp.dashboard, rsp.meta, { - autoMigrateOldPanels: true, + autoMigrateOldPanels: false, }); // Setting for built-in annotations query to run diff --git a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts new file mode 100644 index 00000000000..33adcc149e2 --- /dev/null +++ b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts @@ -0,0 +1,58 @@ +import { DashboardCursorSync, dateTimeFormat, DateTimeInput, EventBusSrv } from '@grafana/data'; +import { behaviors, sceneGraph } from '@grafana/scenes'; + +import { DashboardScene } from '../scene/DashboardScene'; + +/** + * Will move this to make it the main way we remain somewhat compatible with getDashboardSrv().getCurrent + */ +export class DashboardModelCompatibilityWrapper { + events = new EventBusSrv(); + panelInitialized() {} + + public getTimezone() { + const time = sceneGraph.getTimeRange(window.__grafanaSceneContext); + return time.getTimeZone(); + } + + public sharedTooltipModeEnabled() { + return this._getSyncMode() > 0; + } + + public sharedCrosshairModeOnly() { + return this._getSyncMode() > 1; + } + + private _getSyncMode() { + const dashboard = this.getDashboardScene(); + + if (dashboard.state.$behaviors) { + for (const behavior of dashboard.state.$behaviors) { + if (behavior instanceof behaviors.CursorSync) { + return behavior.state.sync > 0; + } + } + } + + 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; + } + + public formatDate(date: DateTimeInput, format?: string) { + return dateTimeFormat(date, { + format, + timeZone: this.getTimezone(), + }); + } +} diff --git a/public/app/features/plugins/importPanelPlugin.ts b/public/app/features/plugins/importPanelPlugin.ts index 87e215e09f9..053090a1fc2 100644 --- a/public/app/features/plugins/importPanelPlugin.ts +++ b/public/app/features/plugins/importPanelPlugin.ts @@ -1,4 +1,6 @@ -import { PanelPlugin, PanelPluginMeta } from '@grafana/data'; +import { ComponentType } from 'react'; + +import { PanelPlugin, PanelPluginMeta, PanelProps } from '@grafana/data'; import config from 'app/core/config'; import { getPanelPluginLoadError } from '../panel/components/PanelPluginError'; @@ -70,9 +72,14 @@ function getPanelPlugin(meta: PanelPluginMeta): Promise { } throw new Error('missing export: plugin or PanelCtrl'); }) - .then((plugin) => { + .then((plugin: PanelPlugin) => { plugin.meta = meta; panelPluginCache[meta.id] = plugin; + + if (!plugin.panel && plugin.angularPanelCtrl) { + plugin.panel = getAngularPanelReactWrapper(plugin); + } + return plugin; }) .catch((err) => { @@ -81,3 +88,9 @@ function getPanelPlugin(meta: PanelPluginMeta): Promise { return getPanelPluginLoadError(meta, err); }); } + +let getAngularPanelReactWrapper = (plugin: PanelPlugin): ComponentType | null => null; + +export function setAngularPanelReactWrapper(wrapper: typeof getAngularPanelReactWrapper) { + getAngularPanelReactWrapper = wrapper; +}