DashboardScene: Support for Angular panels (#76072)

* Could work

* it's working

* remove comment

* Update comment

* Make options work

* Progress

* Update panel class name

* Update public/app/angular/panel/AngularPanelReactWrapper.tsx

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>

* Fixes

---------

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Torkel Ödegaard
2023-10-11 14:27:20 +02:00
committed by GitHub
parent d17f25d8c4
commit e46e66313d
5 changed files with 204 additions and 3 deletions

View File

@ -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',

View File

@ -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<PanelProps> {
return function AngularWrapper(props: PanelProps) {
const divRef = useRef<HTMLDivElement>(null);
const angularState = useRef<AngularScopeProps | undefined>();
const angularComponent = useRef<AngularComponent | undefined>();
useEffect(() => {
if (!divRef.current) {
return;
}
const loader = getAngularLoader();
const template = '<plugin-component type="panel" class="panel-height-helper"></plugin-component>';
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 <div ref={divRef} className="panel-height-helper" />;
};
}
class PanelModelCompatibilityWrapper {
id: number;
type: string;
title: string;
plugin: PanelPlugin;
events: EventBusSrv;
queryRunner: FakeQueryRunner;
fieldConfig: FieldConfigSource;
options: Record<string, unknown>;
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<PanelData>(1);
getData(options: GetDataOptions): Observable<PanelData> {
return this.subject;
}
forwardNewData(data: PanelData) {
this.subject.next(data);
}
run() {}
}

View File

@ -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

View File

@ -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(),
});
}
}

View File

@ -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<PanelPlugin> {
}
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<PanelPlugin> {
return getPanelPluginLoadError(meta, err);
});
}
let getAngularPanelReactWrapper = (plugin: PanelPlugin): ComponentType<PanelProps> | null => null;
export function setAngularPanelReactWrapper(wrapper: typeof getAngularPanelReactWrapper) {
getAngularPanelReactWrapper = wrapper;
}