Refactor: consistant plugin/meta usage (#16834)

This commit is contained in:
Ryan McKinley
2019-04-30 22:36:46 -07:00
committed by GitHub
parent fe20dde5db
commit 51a98565dc
16 changed files with 162 additions and 155 deletions

View File

@@ -1,6 +1,6 @@
import { ComponentClass } from 'react'; import { ComponentClass } from 'react';
import { TimeRange } from './time'; import { TimeRange } from './time';
import { PluginMeta } from './plugin'; import { PluginMeta, GrafanaPlugin } from './plugin';
import { TableData, TimeSeries, SeriesData, LoadingState } from './data'; import { TableData, TimeSeries, SeriesData, LoadingState } from './data';
import { PanelData } from './panel'; import { PanelData } from './panel';
@@ -8,11 +8,14 @@ export interface DataSourcePluginOptionsEditorProps<TOptions> {
options: TOptions; options: TOptions;
onOptionsChange: (options: TOptions) => void; onOptionsChange: (options: TOptions) => void;
} }
export class DataSourcePlugin<TOptions = {}, TQuery extends DataQuery = DataQuery> { export class DataSourcePlugin<TOptions = {}, TQuery extends DataQuery = DataQuery> extends GrafanaPlugin<
DataSourcePluginMeta
> {
DataSourceClass: DataSourceConstructor<TQuery>; DataSourceClass: DataSourceConstructor<TQuery>;
components: DataSourcePluginComponents<TOptions, TQuery>; components: DataSourcePluginComponents<TOptions, TQuery>;
constructor(DataSourceClass: DataSourceConstructor<TQuery>) { constructor(DataSourceClass: DataSourceConstructor<TQuery>) {
super();
this.DataSourceClass = DataSourceClass; this.DataSourceClass = DataSourceClass;
this.components = {}; this.components = {};
} }

View File

@@ -2,16 +2,13 @@ import { ComponentClass, ComponentType } from 'react';
import { LoadingState, SeriesData } from './data'; import { LoadingState, SeriesData } from './data';
import { TimeRange } from './time'; import { TimeRange } from './time';
import { ScopedVars, DataQueryRequest, DataQueryError, LegacyResponseData } from './datasource'; import { ScopedVars, DataQueryRequest, DataQueryError, LegacyResponseData } from './datasource';
import { PluginMeta } from './plugin'; import { PluginMeta, GrafanaPlugin } from './plugin';
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string; export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
export interface PanelPluginMeta extends PluginMeta { export interface PanelPluginMeta extends PluginMeta {
hideFromList?: boolean; hideFromList?: boolean;
sort: number; sort: number;
angularPlugin: AngularPanelPlugin | null;
panelPlugin: PanelPlugin | null;
hasBeenImported?: boolean;
// if length>0 the query tab will show up // if length>0 the query tab will show up
// Before 6.2 this could be table and/or series, but 6.2+ supports both transparently // Before 6.2 this could be table and/or series, but 6.2+ supports both transparently
@@ -72,14 +69,20 @@ export type PanelTypeChangedHandler<TOptions = any> = (
prevOptions: any prevOptions: any
) => Partial<TOptions>; ) => Partial<TOptions>;
export class PanelPlugin<TOptions = any> { export class PanelPlugin<TOptions = any> extends GrafanaPlugin<PanelPluginMeta> {
panel: ComponentType<PanelProps<TOptions>>; panel: ComponentType<PanelProps<TOptions>>;
editor?: ComponentClass<PanelEditorProps<TOptions>>; editor?: ComponentClass<PanelEditorProps<TOptions>>;
defaults?: TOptions; defaults?: TOptions;
onPanelMigration?: PanelMigrationHandler<TOptions>; onPanelMigration?: PanelMigrationHandler<TOptions>;
onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>; onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>;
/**
* Legacy angular ctrl. If this exists it will be used instead of the panel
*/
angularPanelCtrl?: any;
constructor(panel: ComponentType<PanelProps<TOptions>>) { constructor(panel: ComponentType<PanelProps<TOptions>>) {
super();
this.panel = panel; this.panel = panel;
} }
@@ -114,16 +117,6 @@ export class PanelPlugin<TOptions = any> {
} }
} }
export class AngularPanelPlugin {
components: {
PanelCtrl: any;
};
constructor(PanelCtrl: any) {
this.components = { PanelCtrl: PanelCtrl };
}
}
export interface PanelSize { export interface PanelSize {
width: number; width: number;
height: number; height: number;

View File

@@ -69,16 +69,20 @@ export interface PluginMetaInfo {
version: string; version: string;
} }
export class AppPlugin { export class GrafanaPlugin<T extends PluginMeta> {
meta: PluginMeta; // Meta is filled in by the plugin loading system
meta?: T;
// Soon this will also include common config options
}
export class AppPlugin extends GrafanaPlugin<PluginMeta> {
angular?: { angular?: {
ConfigCtrl?: any; ConfigCtrl?: any;
pages: { [component: string]: any }; pages: { [component: string]: any };
}; };
constructor(meta: PluginMeta, pluginExports: any) { setComponentsFromLegacyExports(pluginExports: any) {
this.meta = meta;
const legacy = { const legacy = {
ConfigCtrl: undefined, ConfigCtrl: undefined,
pages: {} as any, pages: {} as any,
@@ -89,7 +93,8 @@ export class AppPlugin {
this.angular = legacy; this.angular = legacy;
} }
if (meta.includes) { const { meta } = this;
if (meta && meta.includes) {
for (const include of meta.includes) { for (const include of meta.includes) {
const { type, component } = include; const { type, component } = include;
if (type === PluginIncludeType.page && component) { if (type === PluginIncludeType.page && component) {

View File

@@ -1,6 +1,5 @@
// Libraries // Libraries
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import config from 'app/core/config';
import classNames from 'classnames'; import classNames from 'classnames';
// Utils & Services // Utils & Services
@@ -9,7 +8,6 @@ import { importPanelPlugin } from 'app/features/plugins/plugin_loader';
// Components // Components
import { AddPanelWidget } from '../components/AddPanelWidget'; import { AddPanelWidget } from '../components/AddPanelWidget';
import { getPanelPluginNotFound } from './PanelPluginNotFound';
import { DashboardRow } from '../components/DashboardRow'; import { DashboardRow } from '../components/DashboardRow';
import { PanelChrome } from './PanelChrome'; import { PanelChrome } from './PanelChrome';
import { PanelEditor } from '../panel_editor/PanelEditor'; import { PanelEditor } from '../panel_editor/PanelEditor';
@@ -17,7 +15,7 @@ import { PanelResizer } from './PanelResizer';
// Types // Types
import { PanelModel, DashboardModel } from '../state'; import { PanelModel, DashboardModel } from '../state';
import { PanelPluginMeta, AngularPanelPlugin, PanelPlugin } from '@grafana/ui/src/types/panel'; import { PanelPluginMeta, PanelPlugin } from '@grafana/ui/src/types/panel';
import { AutoSizer } from 'react-virtualized'; import { AutoSizer } from 'react-virtualized';
export interface Props { export interface Props {
@@ -28,7 +26,7 @@ export interface Props {
} }
export interface State { export interface State {
plugin: PanelPluginMeta; plugin: PanelPlugin;
angularPanel: AngularComponent; angularPanel: AngularComponent;
} }
@@ -72,15 +70,12 @@ export class DashboardPanel extends PureComponent<Props, State> {
const { panel } = this.props; const { panel } = this.props;
// handle plugin loading & changing of plugin type // handle plugin loading & changing of plugin type
if (!this.state.plugin || this.state.plugin.id !== pluginId) { if (!this.state.plugin || this.state.plugin.meta.id !== pluginId) {
let plugin = config.panels[pluginId] || getPanelPluginNotFound(pluginId); const plugin = await importPanelPlugin(pluginId);
// unmount angular panel // unmount angular panel
this.cleanUpAngularPanel(); this.cleanUpAngularPanel();
// load the actual plugin code
plugin = await this.importPanelPluginModule(plugin);
if (panel.type !== pluginId) { if (panel.type !== pluginId) {
panel.changePlugin(plugin); panel.changePlugin(plugin);
} else { } else {
@@ -91,27 +86,6 @@ export class DashboardPanel extends PureComponent<Props, State> {
} }
} }
async importPanelPluginModule(plugin: PanelPluginMeta): Promise<PanelPluginMeta> {
if (plugin.hasBeenImported) {
return plugin;
}
try {
const importedPlugin = await importPanelPlugin(plugin.module);
if (importedPlugin instanceof AngularPanelPlugin) {
plugin.angularPlugin = importedPlugin as AngularPanelPlugin;
} else if (importedPlugin instanceof PanelPlugin) {
plugin.panelPlugin = importedPlugin as PanelPlugin;
}
} catch (e) {
plugin = getPanelPluginNotFound(plugin.id);
console.log('Failed to import plugin module', e);
}
plugin.hasBeenImported = true;
return plugin;
}
componentDidMount() { componentDidMount() {
this.loadPlugin(this.props.panel.type); this.loadPlugin(this.props.panel.type);
} }
@@ -186,7 +160,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
} }
// if we have not loaded plugin exports yet, wait // if we have not loaded plugin exports yet, wait
if (!plugin || !plugin.hasBeenImported) { if (!plugin) {
return null; return null;
} }
@@ -209,8 +183,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
onMouseLeave={this.onMouseLeave} onMouseLeave={this.onMouseLeave}
style={styles} style={styles}
> >
{plugin.panelPlugin && this.renderReactPanel()} {plugin.angularPanelCtrl ? this.renderAngularPanel() : this.renderReactPanel()}
{plugin.angularPlugin && this.renderAngularPanel()}
</div> </div>
)} )}
/> />

View File

@@ -16,7 +16,7 @@ import config from 'app/core/config';
// Types // Types
import { DashboardModel, PanelModel } from '../state'; import { DashboardModel, PanelModel } from '../state';
import { PanelPluginMeta, LoadingState, PanelData } from '@grafana/ui'; import { LoadingState, PanelData, PanelPlugin } from '@grafana/ui';
import { ScopedVars } from '@grafana/ui'; import { ScopedVars } from '@grafana/ui';
import templateSrv from 'app/features/templating/template_srv'; import templateSrv from 'app/features/templating/template_srv';
@@ -29,7 +29,7 @@ const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
export interface Props { export interface Props {
panel: PanelModel; panel: PanelModel;
dashboard: DashboardModel; dashboard: DashboardModel;
plugin: PanelPluginMeta; plugin: PanelPlugin;
isFullscreen: boolean; isFullscreen: boolean;
width: number; width: number;
height: number; height: number;
@@ -209,13 +209,13 @@ export class PanelChrome extends PureComponent<Props, State> {
} }
get wantsQueryExecution() { get wantsQueryExecution() {
return this.props.plugin.dataFormats.length > 0 && !this.hasPanelSnapshot; return this.props.plugin.meta.dataFormats.length > 0 && !this.hasPanelSnapshot;
} }
renderPanel(width: number, height: number): JSX.Element { renderPanel(width: number, height: number): JSX.Element {
const { panel, plugin } = this.props; const { panel, plugin } = this.props;
const { renderCounter, data, isFirstLoad } = this.state; const { renderCounter, data, isFirstLoad } = this.state;
const PanelComponent = plugin.panelPlugin.panel; const PanelComponent = plugin.panel;
// This is only done to increase a counter that is used by backend // This is only done to increase a counter that is used by backend
// image rendering (phantomjs/headless chrome) to know when to capture image // image rendering (phantomjs/headless chrome) to know when to capture image
@@ -236,7 +236,7 @@ export class PanelChrome extends PureComponent<Props, State> {
<PanelComponent <PanelComponent
data={data} data={data}
timeRange={data.request ? data.request.range : this.timeSrv.timeRange()} timeRange={data.request ? data.request.range : this.timeSrv.timeRange()}
options={panel.getOptions(plugin.panelPlugin.defaults)} options={panel.getOptions(plugin.defaults)}
width={width - 2 * config.theme.panelPadding.horizontal} width={width - 2 * config.theme.panelPadding.horizontal}
height={height - PANEL_HEADER_HEIGHT - config.theme.panelPadding.vertical} height={height - PANEL_HEADER_HEIGHT - config.theme.panelPadding.vertical}
renderCounter={renderCounter} renderCounter={renderCounter}

View File

@@ -7,14 +7,14 @@ import { AlertBox } from 'app/core/components/AlertBox/AlertBox';
// Types // Types
import { AppNotificationSeverity } from 'app/types'; import { AppNotificationSeverity } from 'app/types';
import { PanelPluginMeta, PanelProps, PanelPlugin, PluginType } from '@grafana/ui'; import { PanelProps, PanelPlugin, PluginType } from '@grafana/ui';
interface Props { interface Props {
pluginId: string; pluginId: string;
} }
class PanelPluginNotFound extends PureComponent<Props> { class PanelPluginNotFound extends PureComponent<Props> {
constructor(props) { constructor(props: Props) {
super(props); super(props);
} }
@@ -34,14 +34,15 @@ class PanelPluginNotFound extends PureComponent<Props> {
} }
} }
export function getPanelPluginNotFound(id: string): PanelPluginMeta { export function getPanelPluginNotFound(id: string): PanelPlugin {
const NotFound = class NotFound extends PureComponent<PanelProps> { const NotFound = class NotFound extends PureComponent<PanelProps> {
render() { render() {
return <PanelPluginNotFound pluginId={id} />; return <PanelPluginNotFound pluginId={id} />;
} }
}; };
return { const plugin = new PanelPlugin(NotFound);
plugin.meta = {
id: id, id: id,
name: id, name: id,
sort: 100, sort: 100,
@@ -63,7 +64,6 @@ export function getPanelPluginNotFound(id: string): PanelPluginMeta {
updated: '', updated: '',
version: '', version: '',
}, },
panelPlugin: new PanelPlugin(NotFound),
angularPlugin: null,
}; };
return plugin;
} }

View File

@@ -13,12 +13,12 @@ import { AngularComponent } from 'app/core/services/AngularLoader';
import { PanelModel } from '../state/PanelModel'; import { PanelModel } from '../state/PanelModel';
import { DashboardModel } from '../state/DashboardModel'; import { DashboardModel } from '../state/DashboardModel';
import { PanelPluginMeta, Tooltip } from '@grafana/ui'; import { Tooltip, PanelPlugin, PanelPluginMeta } from '@grafana/ui';
interface PanelEditorProps { interface PanelEditorProps {
panel: PanelModel; panel: PanelModel;
dashboard: DashboardModel; dashboard: DashboardModel;
plugin: PanelPluginMeta; plugin: PanelPlugin;
angularPanel?: AngularComponent; angularPanel?: AngularComponent;
onTypeChanged: (newType: PanelPluginMeta) => void; onTypeChanged: (newType: PanelPluginMeta) => void;
} }
@@ -55,7 +55,7 @@ const getPanelEditorTab = (tabId: PanelEditorTabIds): PanelEditorTab => {
}; };
export class PanelEditor extends PureComponent<PanelEditorProps> { export class PanelEditor extends PureComponent<PanelEditorProps> {
constructor(props) { constructor(props: PanelEditorProps) {
super(props); super(props);
} }
@@ -105,7 +105,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
]; ];
// handle panels that do not have queries tab // handle panels that do not have queries tab
if (plugin.dataFormats.length === 0) { if (plugin.meta.dataFormats.length === 0) {
// remove queries tab // remove queries tab
tabs.shift(); tabs.shift();
// switch tab // switch tab
@@ -114,7 +114,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
} }
} }
if (config.alertingEnabled && plugin.id === 'graph') { if (config.alertingEnabled && plugin.meta.id === 'graph') {
tabs.push(getPanelEditorTab(PanelEditorTabIds.Alert)); tabs.push(getPanelEditorTab(PanelEditorTabIds.Alert));
} }

View File

@@ -18,12 +18,12 @@ import { PanelModel } from '../state';
import { DashboardModel } from '../state'; import { DashboardModel } from '../state';
import { VizPickerSearch } from './VizPickerSearch'; import { VizPickerSearch } from './VizPickerSearch';
import PluginStateinfo from 'app/features/plugins/PluginStateInfo'; import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
import { PanelPluginMeta } from '@grafana/ui'; import { PanelPlugin, PanelPluginMeta } from '@grafana/ui';
interface Props { interface Props {
panel: PanelModel; panel: PanelModel;
dashboard: DashboardModel; dashboard: DashboardModel;
plugin: PanelPluginMeta; plugin: PanelPlugin;
angularPanel?: AngularComponent; angularPanel?: AngularComponent;
onTypeChanged: (newType: PanelPluginMeta) => void; onTypeChanged: (newType: PanelPluginMeta) => void;
updateLocation: typeof updateLocation; updateLocation: typeof updateLocation;
@@ -41,7 +41,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
element: HTMLElement; element: HTMLElement;
angularOptions: AngularComponent; angularOptions: AngularComponent;
constructor(props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@@ -54,7 +54,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
getReactPanelOptions = () => { getReactPanelOptions = () => {
const { panel, plugin } = this.props; const { panel, plugin } = this.props;
return panel.getOptions(plugin.panelPlugin.defaults); return panel.getOptions(plugin.defaults);
}; };
renderPanelOptions() { renderPanelOptions() {
@@ -64,12 +64,8 @@ export class VisualizationTab extends PureComponent<Props, State> {
return <div ref={element => (this.element = element)} />; return <div ref={element => (this.element = element)} />;
} }
if (plugin.panelPlugin) { if (plugin.editor) {
const PanelEditor = plugin.panelPlugin.editor; return <plugin.editor options={this.getReactPanelOptions()} onOptionsChange={this.onPanelOptionsChanged} />;
if (PanelEditor) {
return <PanelEditor options={this.getReactPanelOptions()} onOptionsChange={this.onPanelOptionsChanged} />;
}
} }
return <p>Visualization has no options</p>; return <p>Visualization has no options</p>;
@@ -176,11 +172,12 @@ export class VisualizationTab extends PureComponent<Props, State> {
renderToolbar = (): JSX.Element => { renderToolbar = (): JSX.Element => {
const { plugin } = this.props; const { plugin } = this.props;
const { isVizPickerOpen, searchQuery } = this.state; const { isVizPickerOpen, searchQuery } = this.state;
const { meta } = plugin;
if (isVizPickerOpen) { if (isVizPickerOpen) {
return ( return (
<VizPickerSearch <VizPickerSearch
plugin={plugin} plugin={meta}
searchQuery={searchQuery} searchQuery={searchQuery}
onChange={this.onSearchQueryChange} onChange={this.onSearchQueryChange}
onClose={this.onCloseVizPicker} onClose={this.onCloseVizPicker}
@@ -189,8 +186,8 @@ export class VisualizationTab extends PureComponent<Props, State> {
} else { } else {
return ( return (
<div className="toolbar__main" onClick={this.onOpenVizPicker}> <div className="toolbar__main" onClick={this.onOpenVizPicker}>
<img className="toolbar__main-image" src={plugin.info.logos.small} /> <img className="toolbar__main-image" src={meta.info.logos.small} />
<div className="toolbar__main-name">{plugin.name}</div> <div className="toolbar__main-name">{meta.name}</div>
<i className="fa fa-caret-down" /> <i className="fa fa-caret-down" />
</div> </div>
); );
@@ -198,14 +195,14 @@ export class VisualizationTab extends PureComponent<Props, State> {
}; };
onTypeChanged = (plugin: PanelPluginMeta) => { onTypeChanged = (plugin: PanelPluginMeta) => {
if (plugin.id === this.props.plugin.id) { if (plugin.id === this.props.plugin.meta.id) {
this.setState({ isVizPickerOpen: false }); this.setState({ isVizPickerOpen: false });
} else { } else {
this.props.onTypeChanged(plugin); this.props.onTypeChanged(plugin);
} }
}; };
renderHelp = () => <PluginHelp plugin={this.props.plugin} type="help" />; renderHelp = () => <PluginHelp plugin={this.props.plugin.meta} type="help" />;
setScrollTop = (event: React.MouseEvent<HTMLElement>) => { setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
@@ -215,6 +212,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
render() { render() {
const { plugin } = this.props; const { plugin } = this.props;
const { isVizPickerOpen, searchQuery, scrollTop } = this.state; const { isVizPickerOpen, searchQuery, scrollTop } = this.state;
const { meta } = plugin;
const pluginHelp: EditorToolbarView = { const pluginHelp: EditorToolbarView = {
heading: 'Help', heading: 'Help',
@@ -233,13 +231,13 @@ export class VisualizationTab extends PureComponent<Props, State> {
<> <>
<FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true} onExited={this.clearQuery}> <FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true} onExited={this.clearQuery}>
<VizTypePicker <VizTypePicker
current={plugin} current={meta}
onTypeChanged={this.onTypeChanged} onTypeChanged={this.onTypeChanged}
searchQuery={searchQuery} searchQuery={searchQuery}
onClose={this.onCloseVizPicker} onClose={this.onCloseVizPicker}
/> />
</FadeIn> </FadeIn>
<PluginStateinfo state={plugin.state} /> <PluginStateinfo state={meta.state} />
{this.renderPanelOptions()} {this.renderPanelOptions()}
</> </>
</EditorTabBody> </EditorTabBody>

View File

@@ -1,6 +1,5 @@
import { PanelModel } from './PanelModel'; import { PanelModel } from './PanelModel';
import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks'; import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
import { PanelPlugin, AngularPanelPlugin } from '@grafana/ui/src/types/panel';
class TablePanelCtrl {} class TablePanelCtrl {}
@@ -31,10 +30,13 @@ describe('PanelModel', () => {
}; };
model = new PanelModel(modelJson); model = new PanelModel(modelJson);
model.pluginLoaded( model.pluginLoaded(
getPanelPlugin({ getPanelPlugin(
{
id: 'table', id: 'table',
angularPlugin: new AngularPanelPlugin(TablePanelCtrl), },
}) null, // react
TablePanelCtrl // angular
)
); );
}); });
@@ -123,15 +125,10 @@ describe('PanelModel', () => {
describe('when changing to react panel', () => { describe('when changing to react panel', () => {
const onPanelTypeChanged = jest.fn(); const onPanelTypeChanged = jest.fn();
const reactPlugin = new PanelPlugin({} as any).setPanelChangeHandler(onPanelTypeChanged as any); const reactPlugin = getPanelPlugin({ id: 'react' }).setPanelChangeHandler(onPanelTypeChanged as any);
beforeEach(() => { beforeEach(() => {
model.changePlugin( model.changePlugin(reactPlugin);
getPanelPlugin({
id: 'react',
panelPlugin: reactPlugin,
})
);
}); });
it('should call react onPanelTypeChanged', () => { it('should call react onPanelTypeChanged', () => {

View File

@@ -6,7 +6,7 @@ import { Emitter } from 'app/core/utils/emitter';
import { getNextRefIdChar } from 'app/core/utils/query'; import { getNextRefIdChar } from 'app/core/utils/query';
// Types // Types
import { PanelPluginMeta, DataQuery, Threshold, ScopedVars, DataQueryResponseData } from '@grafana/ui'; import { DataQuery, Threshold, ScopedVars, DataQueryResponseData, PanelPlugin } from '@grafana/ui';
import config from 'app/core/config'; import config from 'app/core/config';
import { PanelQueryRunner } from './PanelQueryRunner'; import { PanelQueryRunner } from './PanelQueryRunner';
@@ -116,7 +116,7 @@ export class PanelModel {
cacheTimeout?: any; cacheTimeout?: any;
cachedPluginOptions?: any; cachedPluginOptions?: any;
legend?: { show: boolean }; legend?: { show: boolean };
plugin?: PanelPluginMeta; plugin?: PanelPlugin;
private queryRunner?: PanelQueryRunner; private queryRunner?: PanelQueryRunner;
constructor(model: any) { constructor(model: any) {
@@ -248,29 +248,25 @@ export class PanelModel {
}); });
} }
private getPluginVersion(plugin: PanelPluginMeta): string { pluginLoaded(plugin: PanelPlugin) {
return this.plugin && this.plugin.info.version ? this.plugin.info.version : config.buildInfo.version;
}
pluginLoaded(plugin: PanelPluginMeta) {
this.plugin = plugin; this.plugin = plugin;
if (plugin.panelPlugin && plugin.panelPlugin.onPanelMigration) { if (plugin.panel && plugin.onPanelMigration) {
const version = this.getPluginVersion(plugin); const version = getPluginVersion(plugin);
if (version !== this.pluginVersion) { if (version !== this.pluginVersion) {
this.options = plugin.panelPlugin.onPanelMigration(this); this.options = plugin.onPanelMigration(this);
this.pluginVersion = version; this.pluginVersion = version;
} }
} }
} }
changePlugin(newPlugin: PanelPluginMeta) { changePlugin(newPlugin: PanelPlugin) {
const pluginId = newPlugin.id; const pluginId = newPlugin.meta.id;
const oldOptions: any = this.getOptionsToRemember(); const oldOptions: any = this.getOptionsToRemember();
const oldPluginId = this.type; const oldPluginId = this.type;
// for angular panels we must remove all events and let angular panels do some cleanup // for angular panels we must remove all events and let angular panels do some cleanup
if (this.plugin.angularPlugin) { if (this.plugin.angularPanelCtrl) {
this.destroy(); this.destroy();
} }
@@ -291,18 +287,14 @@ export class PanelModel {
this.plugin = newPlugin; this.plugin = newPlugin;
// Let panel plugins inspect options from previous panel and keep any that it can use // Let panel plugins inspect options from previous panel and keep any that it can use
const reactPanel = newPlugin.panelPlugin; if (newPlugin.onPanelTypeChanged) {
if (reactPanel) {
if (reactPanel.onPanelTypeChanged) {
this.options = this.options || {}; this.options = this.options || {};
const old = oldOptions && oldOptions.options ? oldOptions.options : {}; const old = oldOptions && oldOptions.options ? oldOptions.options : {};
Object.assign(this.options, reactPanel.onPanelTypeChanged(this.options, oldPluginId, old)); Object.assign(this.options, newPlugin.onPanelTypeChanged(this.options, oldPluginId, old));
} }
if (reactPanel.onPanelMigration) { if (newPlugin.onPanelMigration) {
this.pluginVersion = this.getPluginVersion(newPlugin); this.pluginVersion = getPluginVersion(newPlugin);
}
} }
} }
@@ -341,3 +333,7 @@ export class PanelModel {
} }
} }
} }
function getPluginVersion(plugin: PanelPlugin): string {
return plugin && plugin.meta.info.version ? plugin.meta.info.version : config.buildInfo.version;
}

View File

@@ -63,7 +63,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
let importedPlugin: DataSourcePlugin; let importedPlugin: DataSourcePlugin;
try { try {
importedPlugin = await importDataSourcePlugin(dataSourceMeta.module); importedPlugin = await importDataSourcePlugin(dataSourceMeta);
} catch (e) { } catch (e) {
console.log('Failed to import plugin module', e); console.log('Failed to import plugin module', e);
} }

View File

@@ -1,4 +1,5 @@
import { PanelPluginMeta, PluginMeta, PluginType, PanelDataFormat } from '@grafana/ui'; import { PanelPluginMeta, PluginMeta, PluginType, PanelDataFormat, PanelPlugin, PanelProps } from '@grafana/ui';
import { ComponentType } from 'enzyme';
export const getMockPlugins = (amount: number): PluginMeta[] => { export const getMockPlugins = (amount: number): PluginMeta[] => {
const plugins = []; const plugins = [];
@@ -33,8 +34,14 @@ export const getMockPlugins = (amount: number): PluginMeta[] => {
return plugins; return plugins;
}; };
export const getPanelPlugin = (options: Partial<PanelPluginMeta>): PanelPluginMeta => { export const getPanelPlugin = (
return { options: Partial<PanelPluginMeta>,
reactPanel?: ComponentType<PanelProps>,
angularPanel?: any
): PanelPlugin => {
const plugin = new PanelPlugin(reactPanel);
plugin.angularPanelCtrl = angularPanel;
plugin.meta = {
id: options.id, id: options.id,
type: PluginType.panel, type: PluginType.panel,
name: options.id, name: options.id,
@@ -57,9 +64,8 @@ export const getPanelPlugin = (options: Partial<PanelPluginMeta>): PanelPluginMe
hideFromList: options.hideFromList === true, hideFromList: options.hideFromList === true,
module: '', module: '',
baseUrl: '', baseUrl: '',
panelPlugin: options.panelPlugin,
angularPlugin: options.angularPlugin,
}; };
return plugin;
}; };
export const getMockPlugin = () => { export const getMockPlugin = () => {

View File

@@ -53,7 +53,7 @@ export class DatasourceSrv {
const deferred = this.$q.defer(); const deferred = this.$q.defer();
importDataSourcePlugin(dsConfig.meta.module) importDataSourcePlugin(dsConfig.meta)
.then(dsPlugin => { .then(dsPlugin => {
// check if its in cache now // check if its in cache now
if (this.datasources[name]) { if (this.datasources[name]) {

View File

@@ -4,7 +4,7 @@ import _ from 'lodash';
import config from 'app/core/config'; import config from 'app/core/config';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import { AngularPanelPlugin, DataSourceApi } from '@grafana/ui/src/types'; import { DataSourceApi } from '@grafana/ui/src/types';
import { importPanelPlugin, importDataSourcePlugin, importAppPlugin } from './plugin_loader'; import { importPanelPlugin, importDataSourcePlugin, importAppPlugin } from './plugin_loader';
/** @ngInject */ /** @ngInject */
@@ -22,7 +22,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
}); });
} }
function relativeTemplateUrlToAbs(templateUrl, baseUrl) { function relativeTemplateUrlToAbs(templateUrl: string, baseUrl: string) {
if (!templateUrl) { if (!templateUrl) {
return undefined; return undefined;
} }
@@ -69,9 +69,8 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
}; };
const panelInfo = config.panels[scope.panel.type]; const panelInfo = config.panels[scope.panel.type];
return importPanelPlugin(panelInfo.module).then(panelPlugin => { return importPanelPlugin(panelInfo.id).then(panelPlugin => {
const angularPanelPlugin = panelPlugin as AngularPanelPlugin; const PanelCtrl = panelPlugin.angularPanelCtrl;
const PanelCtrl = angularPanelPlugin.components.PanelCtrl;
componentInfo.Component = PanelCtrl; componentInfo.Component = PanelCtrl;
if (!PanelCtrl || PanelCtrl.registered) { if (!PanelCtrl || PanelCtrl.registered) {
@@ -118,7 +117,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
} }
// Annotations // Annotations
case 'annotations-query-ctrl': { case 'annotations-query-ctrl': {
return importDataSourcePlugin(scope.ctrl.currentDatasource.meta.module).then(dsPlugin => { return importDataSourcePlugin(scope.ctrl.currentDatasource.meta).then(dsPlugin => {
return { return {
baseUrl: scope.ctrl.currentDatasource.meta.baseUrl, baseUrl: scope.ctrl.currentDatasource.meta.baseUrl,
name: 'annotations-query-ctrl-' + scope.ctrl.currentDatasource.meta.id, name: 'annotations-query-ctrl-' + scope.ctrl.currentDatasource.meta.id,
@@ -134,7 +133,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
// Datasource ConfigCtrl // Datasource ConfigCtrl
case 'datasource-config-ctrl': { case 'datasource-config-ctrl': {
const dsMeta = scope.ctrl.datasourceMeta; const dsMeta = scope.ctrl.datasourceMeta;
return importDataSourcePlugin(dsMeta.module).then(dsPlugin => { return importDataSourcePlugin(dsMeta).then(dsPlugin => {
scope.$watch( scope.$watch(
'ctrl.current', 'ctrl.current',
() => { () => {

View File

@@ -18,7 +18,7 @@ import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2'; import TimeSeries from 'app/core/time_series2';
import TableModel from 'app/core/table_model'; import TableModel from 'app/core/table_model';
import { coreModule, appEvents, contextSrv } from 'app/core/core'; import { coreModule, appEvents, contextSrv } from 'app/core/core';
import { DataSourcePlugin, AppPlugin, PanelPlugin, AngularPanelPlugin, PluginMeta } from '@grafana/ui/src/types'; import { DataSourcePlugin, AppPlugin, PanelPlugin, PluginMeta, DataSourcePluginMeta } from '@grafana/ui/src/types';
import * as datemath from 'app/core/utils/datemath'; import * as datemath from 'app/core/utils/datemath';
import * as fileExport from 'app/core/utils/file_export'; import * as fileExport from 'app/core/utils/file_export';
import * as flatten from 'app/core/utils/flatten'; import * as flatten from 'app/core/utils/flatten';
@@ -160,15 +160,18 @@ export function importPluginModule(path: string): Promise<any> {
return System.import(path); return System.import(path);
} }
export function importDataSourcePlugin(path: string): Promise<DataSourcePlugin<any>> { export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<DataSourcePlugin<any>> {
return importPluginModule(path).then(pluginExports => { return importPluginModule(meta.module).then(pluginExports => {
if (pluginExports.plugin) { if (pluginExports.plugin) {
return pluginExports.plugin as DataSourcePlugin<any>; const dsPlugin = pluginExports.plugin as DataSourcePlugin<any>;
dsPlugin.meta = meta;
return dsPlugin;
} }
if (pluginExports.Datasource) { if (pluginExports.Datasource) {
const dsPlugin = new DataSourcePlugin(pluginExports.Datasource); const dsPlugin = new DataSourcePlugin(pluginExports.Datasource);
dsPlugin.setComponentsFromLegacyExports(pluginExports); dsPlugin.setComponentsFromLegacyExports(pluginExports);
dsPlugin.meta = meta;
return dsPlugin; return dsPlugin;
} }
@@ -178,17 +181,49 @@ export function importDataSourcePlugin(path: string): Promise<DataSourcePlugin<a
export function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> { export function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
return importPluginModule(meta.module).then(pluginExports => { return importPluginModule(meta.module).then(pluginExports => {
return new AppPlugin(meta, pluginExports); const plugin = pluginExports.plugin ? (pluginExports.plugin as AppPlugin) : new AppPlugin();
plugin.meta = meta;
plugin.setComponentsFromLegacyExports(pluginExports);
return plugin;
}); });
} }
export function importPanelPlugin(path: string): Promise<AngularPanelPlugin | PanelPlugin> { import { getPanelPluginNotFound } from '../dashboard/dashgrid/PanelPluginNotFound';
return importPluginModule(path).then(pluginExports => {
interface PanelCache {
[key: string]: PanelPlugin;
}
const panelCache: PanelCache = {};
export function importPanelPlugin(id: string): Promise<PanelPlugin> {
const loaded = panelCache[id];
if (loaded) {
return Promise.resolve(loaded);
}
const meta = config.panels[id];
if (!meta) {
return Promise.resolve(getPanelPluginNotFound(id));
}
return importPluginModule(meta.module)
.then(pluginExports => {
if (pluginExports.plugin) { if (pluginExports.plugin) {
return pluginExports.plugin as PanelPlugin; return pluginExports.plugin as PanelPlugin;
} else { } else if (pluginExports.PanelCtrl) {
return new AngularPanelPlugin(pluginExports.PanelCtrl); const plugin = new PanelPlugin(null);
plugin.angularPanelCtrl = pluginExports.PanelCtrl;
return plugin;
} }
throw new Error('missing export: plugin or PanelCtrl');
})
.then(plugin => {
plugin.meta = meta;
return (panelCache[meta.id] = plugin);
})
.catch(err => {
// TODO, maybe a different error plugin
console.log('Error loading panel plugin', err);
return getPanelPluginNotFound(id);
}); });
} }

View File

@@ -3,9 +3,11 @@ import { importDataSourcePlugin } from './plugin_loader';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import DefaultVariableQueryEditor from '../templating/DefaultVariableQueryEditor'; import DefaultVariableQueryEditor from '../templating/DefaultVariableQueryEditor';
import { DataSourcePluginMeta } from '@grafana/ui';
import { TemplateSrv } from '../templating/template_srv';
async function loadComponent(module) { async function loadComponent(meta: DataSourcePluginMeta) {
const dsPlugin = await importDataSourcePlugin(module); const dsPlugin = await importDataSourcePlugin(meta);
if (dsPlugin.components.VariableQueryEditor) { if (dsPlugin.components.VariableQueryEditor) {
return dsPlugin.components.VariableQueryEditor; return dsPlugin.components.VariableQueryEditor;
} else { } else {
@@ -14,11 +16,11 @@ async function loadComponent(module) {
} }
/** @ngInject */ /** @ngInject */
function variableQueryEditorLoader(templateSrv) { function variableQueryEditorLoader(templateSrv: TemplateSrv) {
return { return {
restrict: 'E', restrict: 'E',
link: async (scope, elem) => { link: async (scope, elem) => {
const Component = await loadComponent(scope.currentDatasource.meta.module); const Component = await loadComponent(scope.currentDatasource.meta);
const props = { const props = {
datasource: scope.currentDatasource, datasource: scope.currentDatasource,
query: scope.current.query, query: scope.current.query,