mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Panels: Refactoring how panel plugins sets hooks and components, #16166
This commit is contained in:
@@ -24,12 +24,12 @@ export interface PanelEditorProps<T = any> {
|
||||
/**
|
||||
* Called when a panel is first loaded with existing options
|
||||
*/
|
||||
export type PanelMigrationHook<TOptions = any> = (exiting: any, oldVersion?: string) => Partial<TOptions>;
|
||||
export type PanelMigrationHandler<TOptions = any> = (exiting: any, oldVersion?: string) => Partial<TOptions>;
|
||||
|
||||
/**
|
||||
* Called before a panel is initalized
|
||||
*/
|
||||
export type PanelTypeChangedHook<TOptions = any> = (
|
||||
export type PanelTypeChangedHandler<TOptions = any> = (
|
||||
options: Partial<TOptions>,
|
||||
prevPluginId: string,
|
||||
prevOptions?: any
|
||||
@@ -39,6 +39,22 @@ export class ReactPanelPlugin<TOptions = any> {
|
||||
panel: ComponentClass<PanelProps<TOptions>>;
|
||||
editor?: ComponentClass<PanelEditorProps<TOptions>>;
|
||||
defaults?: TOptions;
|
||||
onPanelMigration?: PanelMigrationHandler<TOptions>;
|
||||
onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>;
|
||||
|
||||
constructor(panel: ComponentClass<PanelProps<TOptions>>) {
|
||||
this.panel = panel;
|
||||
}
|
||||
|
||||
setEditor(editor: ComponentClass<PanelEditorProps<TOptions>>) {
|
||||
this.editor = editor;
|
||||
return this;
|
||||
}
|
||||
|
||||
setDefaults(defaults: TOptions) {
|
||||
this.defaults = defaults;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called before the panel first loads if
|
||||
@@ -46,17 +62,18 @@ export class ReactPanelPlugin<TOptions = any> {
|
||||
*
|
||||
* This is a good place to support any changes to the options model
|
||||
*/
|
||||
onPanelMigration?: PanelMigrationHook<TOptions>;
|
||||
setMigrationHandler(handler: PanelMigrationHandler) {
|
||||
this.onPanelMigration = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when the visualization was changed. This
|
||||
* passes in the options that were used in the previous visualization
|
||||
*/
|
||||
onPanelTypeChanged?: PanelTypeChangedHook<TOptions>;
|
||||
|
||||
constructor(panel: ComponentClass<PanelProps<TOptions>>, defaults?: TOptions) {
|
||||
this.panel = panel;
|
||||
this.defaults = defaults;
|
||||
setPanelChangeHandler(handler: PanelTypeChangedHandler) {
|
||||
this.onPanelTypeChanged = handler;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ export interface PluginExports {
|
||||
|
||||
// Panel plugin
|
||||
PanelCtrl?: any;
|
||||
reactPanel: ReactPanelPlugin;
|
||||
reactPanel?: ReactPanelPlugin;
|
||||
}
|
||||
|
||||
export interface PluginMeta {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import config from 'app/core/config';
|
||||
import classNames from 'classnames';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||
import { importPluginModule } from 'app/features/plugins/plugin_loader';
|
||||
@@ -15,7 +14,6 @@ import { PanelEditor } from '../panel_editor/PanelEditor';
|
||||
import { PanelModel, DashboardModel } from '../state';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
import { PanelResizer } from './PanelResizer';
|
||||
import { PanelTypeChangedHook } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
@@ -72,9 +70,6 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
if (!this.state.plugin || this.state.plugin.id !== pluginId) {
|
||||
let plugin = config.panels[pluginId] || getPanelPluginNotFound(pluginId);
|
||||
|
||||
// remember if this is from an angular panel
|
||||
const fromAngularPanel = this.state.angularPanel != null;
|
||||
|
||||
// unmount angular panel
|
||||
this.cleanUpAngularPanel();
|
||||
|
||||
@@ -87,26 +82,11 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
if (panel.type !== pluginId) {
|
||||
if (fromAngularPanel) {
|
||||
// for angular panels only we need to remove all events and let angular panels do some cleanup
|
||||
panel.destroy();
|
||||
|
||||
this.props.panel.changeType(pluginId);
|
||||
} else {
|
||||
let hook: PanelTypeChangedHook | null = null;
|
||||
if (plugin.exports.reactPanel) {
|
||||
hook = plugin.exports.reactPanel.onPanelTypeChanged;
|
||||
}
|
||||
panel.changeType(pluginId, hook);
|
||||
}
|
||||
} else if (plugin.exports && plugin.exports.reactPanel && panel.options) {
|
||||
const pluginVersion = get(plugin, 'info.version') || config.buildInfo.version;
|
||||
const hook = plugin.exports.reactPanel.onPanelMigration;
|
||||
if (hook && panel.pluginVersion !== pluginVersion) {
|
||||
panel.options = hook(panel.options, panel.pluginVersion);
|
||||
panel.pluginVersion = pluginVersion;
|
||||
}
|
||||
panel.changePlugin(plugin);
|
||||
} else {
|
||||
panel.pluginLoaded(plugin);
|
||||
}
|
||||
|
||||
this.setState({ plugin, angularPanel: null });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PanelModel } from './PanelModel';
|
||||
import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
|
||||
|
||||
describe('PanelModel', () => {
|
||||
describe('when creating new panel model', () => {
|
||||
@@ -76,7 +77,7 @@ describe('PanelModel', () => {
|
||||
|
||||
describe('when changing panel type', () => {
|
||||
beforeEach(() => {
|
||||
model.changeType('graph');
|
||||
model.changePlugin(getPanelPlugin({ id: 'graph', exports: {} }));
|
||||
model.alert = { id: 2 };
|
||||
});
|
||||
|
||||
@@ -85,12 +86,12 @@ describe('PanelModel', () => {
|
||||
});
|
||||
|
||||
it('should restore table properties when changing back', () => {
|
||||
model.changeType('table');
|
||||
model.changePlugin(getPanelPlugin({ id: 'table', exports: {} }));
|
||||
expect(model.showColumns).toBe(true);
|
||||
});
|
||||
|
||||
it('should remove alert rule when changing type that does not support it', () => {
|
||||
model.changeType('table');
|
||||
model.changePlugin(getPanelPlugin({ id: 'table', exports: {} }));
|
||||
expect(model.alert).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,8 @@ import { Emitter } from 'app/core/utils/emitter';
|
||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||
|
||||
// Types
|
||||
import { DataQuery, TimeSeries, Threshold, ScopedVars, PanelTypeChangedHook } from '@grafana/ui';
|
||||
import { TableData } from '@grafana/ui/src';
|
||||
import { DataQuery, TimeSeries, Threshold, ScopedVars, TableData } from '@grafana/ui';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
|
||||
export interface GridPos {
|
||||
x: number;
|
||||
@@ -23,6 +23,7 @@ const notPersistedProperties: { [str: string]: boolean } = {
|
||||
isEditing: true,
|
||||
hasRefreshed: true,
|
||||
cachedPluginOptions: true,
|
||||
plugin: true,
|
||||
};
|
||||
|
||||
// For angular panels we need to clean up properties when changing type
|
||||
@@ -112,6 +113,7 @@ export class PanelModel {
|
||||
cacheTimeout?: any;
|
||||
cachedPluginOptions?: any;
|
||||
legend?: { show: boolean };
|
||||
plugin?: PanelPlugin;
|
||||
|
||||
constructor(model: any) {
|
||||
this.events = new Emitter();
|
||||
@@ -242,11 +244,27 @@ export class PanelModel {
|
||||
});
|
||||
}
|
||||
|
||||
changeType(pluginId: string, hook?: PanelTypeChangedHook) {
|
||||
pluginLoaded(plugin: PanelPlugin) {
|
||||
this.plugin = plugin;
|
||||
|
||||
const { reactPanel } = plugin.exports;
|
||||
|
||||
if (reactPanel && reactPanel.onPanelMigration) {
|
||||
this.options = reactPanel.onPanelMigration(this.options, this.pluginVersion);
|
||||
this.pluginVersion = plugin.info ? plugin.info.version : '1.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
changePlugin(newPlugin: PanelPlugin) {
|
||||
const pluginId = newPlugin.id;
|
||||
const oldOptions: any = this.getOptionsToRemember();
|
||||
const oldPluginId = this.type;
|
||||
const reactPanel = newPlugin.exports.reactPanel;
|
||||
|
||||
this.type = pluginId;
|
||||
// for angular panels we must remove all events and let angular panels do some cleanup
|
||||
if (!reactPanel) {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
// remove panel type specific options
|
||||
for (const key of _.keys(this)) {
|
||||
@@ -260,12 +278,16 @@ export class PanelModel {
|
||||
this.cachedPluginOptions[oldPluginId] = oldOptions;
|
||||
this.restorePanelOptions(pluginId);
|
||||
|
||||
// switch
|
||||
this.type = pluginId;
|
||||
this.plugin = newPlugin;
|
||||
|
||||
// Callback that can validate and migrate any existing settings
|
||||
if (hook) {
|
||||
const onPanelTypeChanged = reactPanel ? reactPanel.onPanelTypeChanged : null;
|
||||
if (onPanelTypeChanged) {
|
||||
this.options = this.options || {};
|
||||
const old = oldOptions ? oldOptions.options : null;
|
||||
|
||||
Object.assign(this.options, hook(this.options, oldPluginId, old));
|
||||
Object.assign(this.options, onPanelTypeChanged(this.options, oldPluginId, old));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export const getMockPlugins = (amount: number): Plugin[] => {
|
||||
return plugins;
|
||||
};
|
||||
|
||||
export const getPanelPlugin = (options: { id: string; sort?: number; hideFromList?: boolean }): PanelPlugin => {
|
||||
export const getPanelPlugin = (options: Partial<PanelPlugin>): PanelPlugin => {
|
||||
return {
|
||||
id: options.id,
|
||||
name: options.id,
|
||||
@@ -56,6 +56,7 @@ export const getPanelPlugin = (options: { id: string; sort?: number; hideFromLis
|
||||
hideFromList: options.hideFromList === true,
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
exports: options.exports,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BarGaugePanelEditor } from './BarGaugePanelEditor';
|
||||
import { BarGaugeOptions, defaults } from './types';
|
||||
import { singleStatBaseOptionsCheck } from '../singlestat2/module';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel, defaults);
|
||||
|
||||
reactPanel.editor = BarGaugePanelEditor;
|
||||
reactPanel.onPanelTypeChanged = singleStatBaseOptionsCheck;
|
||||
export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel)
|
||||
.setDefaults(defaults)
|
||||
.setEditor(BarGaugePanelEditor)
|
||||
.setPanelChangeHandler(singleStatBaseOptionsCheck);
|
||||
|
||||
@@ -5,8 +5,8 @@ import { GaugePanel } from './GaugePanel';
|
||||
import { GaugeOptions, defaults } from './types';
|
||||
import { singleStatBaseOptionsCheck, singleStatMigrationCheck } from '../singlestat2/module';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel, defaults);
|
||||
|
||||
reactPanel.editor = GaugePanelEditor;
|
||||
reactPanel.onPanelTypeChanged = singleStatBaseOptionsCheck;
|
||||
reactPanel.onPanelMigration = singleStatMigrationCheck;
|
||||
export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel)
|
||||
.setDefaults(defaults)
|
||||
.setEditor(GaugePanelEditor)
|
||||
.setPanelChangeHandler(singleStatBaseOptionsCheck)
|
||||
.setMigrationHandler(singleStatMigrationCheck);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { ReactPanelPlugin } from '@grafana/ui';
|
||||
|
||||
import { GraphPanelEditor } from './GraphPanelEditor';
|
||||
import { GraphPanel } from './GraphPanel';
|
||||
import { Options, defaults } from './types';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<Options>(GraphPanel, defaults);
|
||||
|
||||
reactPanel.editor = GraphPanelEditor;
|
||||
export const reactPanel = new ReactPanelPlugin<Options>(GraphPanel).setDefaults(defaults).setEditor(GraphPanelEditor);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PieChartOptions } from './types';
|
||||
import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor';
|
||||
import { SingleStatValueOptions } from '../singlestat2/types';
|
||||
|
||||
export default class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChartOptions>> {
|
||||
export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChartOptions>> {
|
||||
onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
|
||||
this.props.onOptionsChange({
|
||||
...this.props.options,
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { ReactPanelPlugin } from '@grafana/ui';
|
||||
|
||||
import PieChartPanelEditor from './PieChartPanelEditor';
|
||||
import { PieChartPanelEditor } from './PieChartPanelEditor';
|
||||
import { PieChartPanel } from './PieChartPanel';
|
||||
import { PieChartOptions, defaults } from './types';
|
||||
import { singleStatBaseOptionsCheck } from '../singlestat2/module';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<PieChartOptions>(PieChartPanel, defaults);
|
||||
|
||||
reactPanel.editor = PieChartPanelEditor;
|
||||
reactPanel.onPanelTypeChanged = singleStatBaseOptionsCheck;
|
||||
export const reactPanel = new ReactPanelPlugin<PieChartOptions>(PieChartPanel)
|
||||
.setDefaults(defaults)
|
||||
.setEditor(PieChartPanelEditor);
|
||||
|
||||
@@ -35,8 +35,8 @@ export const singleStatMigrationCheck = (exiting: any, oldVersion?: string) => {
|
||||
return options;
|
||||
};
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<SingleStatOptions>(SingleStatPanel, defaults);
|
||||
|
||||
reactPanel.editor = SingleStatEditor;
|
||||
reactPanel.onPanelTypeChanged = singleStatBaseOptionsCheck;
|
||||
reactPanel.onPanelMigration = singleStatMigrationCheck;
|
||||
export const reactPanel = new ReactPanelPlugin<SingleStatOptions>(SingleStatPanel)
|
||||
.setDefaults(defaults)
|
||||
.setEditor(SingleStatEditor)
|
||||
.setPanelChangeHandler(singleStatMigrationCheck)
|
||||
.setMigrationHandler(singleStatMigrationCheck);
|
||||
|
||||
@@ -4,5 +4,4 @@ import { TablePanelEditor } from './TablePanelEditor';
|
||||
import { TablePanel } from './TablePanel';
|
||||
import { Options, defaults } from './types';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<Options>(TablePanel, defaults);
|
||||
reactPanel.editor = TablePanelEditor;
|
||||
export const reactPanel = new ReactPanelPlugin<Options>(TablePanel).setDefaults(defaults).setEditor(TablePanelEditor);
|
||||
|
||||
@@ -4,12 +4,12 @@ import { TextPanelEditor } from './TextPanelEditor';
|
||||
import { TextPanel } from './TextPanel';
|
||||
import { TextOptions, defaults } from './types';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<TextOptions>(TextPanel, defaults);
|
||||
|
||||
reactPanel.editor = TextPanelEditor;
|
||||
reactPanel.onPanelTypeChanged = (options: TextOptions, prevPluginId: string, prevOptions: any) => {
|
||||
if (prevPluginId === 'text') {
|
||||
return prevOptions as TextOptions;
|
||||
}
|
||||
return options;
|
||||
};
|
||||
export const reactPanel = new ReactPanelPlugin<TextOptions>(TextPanel)
|
||||
.setDefaults(defaults)
|
||||
.setEditor(TextPanelEditor)
|
||||
.setPanelChangeHandler((options: TextOptions, prevPluginId: string, prevOptions: any) => {
|
||||
if (prevPluginId === 'text') {
|
||||
return prevOptions as TextOptions;
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user