mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 15:42:13 +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 { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
|
import { setAngularPanelReactWrapper } from 'app/features/plugins/importPanelPlugin';
|
||||||
import { buildImportMap } from 'app/features/plugins/loader/utils';
|
import { buildImportMap } from 'app/features/plugins/loader/utils';
|
||||||
import * as sdk from 'app/plugins/sdk';
|
import * as sdk from 'app/plugins/sdk';
|
||||||
|
|
||||||
import { registerAngularDirectives } from './angular_wrappers';
|
import { registerAngularDirectives } from './angular_wrappers';
|
||||||
import { initAngularRoutingBridge } from './bridgeReactAngularRouting';
|
import { initAngularRoutingBridge } from './bridgeReactAngularRouting';
|
||||||
import { monkeyPatchInjectorWithPreAssignedBindings } from './injectorMonkeyPatch';
|
import { monkeyPatchInjectorWithPreAssignedBindings } from './injectorMonkeyPatch';
|
||||||
|
import { getAngularPanelReactWrapper } from './panel/AngularPanelReactWrapper';
|
||||||
import { promiseToDigest } from './promiseToDigest';
|
import { promiseToDigest } from './promiseToDigest';
|
||||||
import { registerComponents } from './registerComponents';
|
import { registerComponents } from './registerComponents';
|
||||||
|
|
||||||
@ -56,6 +58,8 @@ export class AngularApp {
|
|||||||
init() {
|
init() {
|
||||||
const app = angular.module('grafana', []);
|
const app = angular.module('grafana', []);
|
||||||
|
|
||||||
|
setAngularPanelReactWrapper(getAngularPanelReactWrapper);
|
||||||
|
|
||||||
app.config([
|
app.config([
|
||||||
'$controllerProvider',
|
'$controllerProvider',
|
||||||
'$compileProvider',
|
'$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 {
|
export function transformSaveModelToScene(rsp: DashboardDTO): DashboardScene {
|
||||||
// Just to have migrations run
|
// Just to have migrations run
|
||||||
const oldModel = new DashboardModel(rsp.dashboard, rsp.meta, {
|
const oldModel = new DashboardModel(rsp.dashboard, rsp.meta, {
|
||||||
autoMigrateOldPanels: true,
|
autoMigrateOldPanels: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setting for built-in annotations query to run
|
// 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 config from 'app/core/config';
|
||||||
|
|
||||||
import { getPanelPluginLoadError } from '../panel/components/PanelPluginError';
|
import { getPanelPluginLoadError } from '../panel/components/PanelPluginError';
|
||||||
@ -70,9 +72,14 @@ function getPanelPlugin(meta: PanelPluginMeta): Promise<PanelPlugin> {
|
|||||||
}
|
}
|
||||||
throw new Error('missing export: plugin or PanelCtrl');
|
throw new Error('missing export: plugin or PanelCtrl');
|
||||||
})
|
})
|
||||||
.then((plugin) => {
|
.then((plugin: PanelPlugin) => {
|
||||||
plugin.meta = meta;
|
plugin.meta = meta;
|
||||||
panelPluginCache[meta.id] = plugin;
|
panelPluginCache[meta.id] = plugin;
|
||||||
|
|
||||||
|
if (!plugin.panel && plugin.angularPanelCtrl) {
|
||||||
|
plugin.panel = getAngularPanelReactWrapper(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
return plugin;
|
return plugin;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -81,3 +88,9 @@ function getPanelPlugin(meta: PanelPluginMeta): Promise<PanelPlugin> {
|
|||||||
return getPanelPluginLoadError(meta, err);
|
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