mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 18:42:27 +08:00
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:
@ -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',
|
||||
|
126
public/app/angular/panel/AngularPanelReactWrapper.tsx
Normal file
126
public/app/angular/panel/AngularPanelReactWrapper.tsx
Normal 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() {}
|
||||
}
|
@ -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
|
||||
|
@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user