mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboard: Move some plugin & panel state to redux (#22052)
* WIP: dashboard panel redux * Progress * Progress * Changing plugin type * Progress * Updated * Progess * Fixed timing issue * Updated * Fixed unit tests * Fixed issue in dashboard page * Updated test
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { AddPanelWidget, Props } from './AddPanelWidget';
|
||||
import { AddPanelWidgetUnconnected as AddPanelWidget, Props } from './AddPanelWidget';
|
||||
import { DashboardModel, PanelModel } from '../../state';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
dashboard: {} as DashboardModel,
|
||||
panel: {} as PanelModel,
|
||||
addPanelToDashboard: jest.fn() as any,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
@@ -3,28 +3,36 @@ import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { LocationUpdate } from '@grafana/runtime';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
import { connect, MapDispatchToProps } from 'react-redux';
|
||||
// Utils
|
||||
import config from 'app/core/config';
|
||||
import store from 'app/core/store';
|
||||
// Store
|
||||
import { store as reduxStore } from 'app/store/store';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { addPanelToDashboard } from 'app/features/dashboard/state/reducers';
|
||||
// Types
|
||||
import { DashboardModel, PanelModel } from '../../state';
|
||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
|
||||
export type PanelPluginInfo = { id: any; defaults: { gridPos: { w: any; h: any }; title: any } };
|
||||
|
||||
export interface Props {
|
||||
export interface OwnProps {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
}
|
||||
|
||||
export interface DispatchProps {
|
||||
addPanelToDashboard: typeof addPanelToDashboard;
|
||||
}
|
||||
|
||||
export type Props = OwnProps & DispatchProps;
|
||||
|
||||
export interface State {
|
||||
copiedPanelPlugins: any[];
|
||||
}
|
||||
|
||||
export class AddPanelWidget extends React.Component<Props, State> {
|
||||
export class AddPanelWidgetUnconnected extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.handleCloseAddPanel = this.handleCloseAddPanel.bind(this);
|
||||
@@ -188,3 +196,7 @@ export class AddPanelWidget extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { addPanelToDashboard };
|
||||
|
||||
export const AddPanelWidget = connect(null, mapDispatchToProps)(AddPanelWidgetUnconnected);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
mockToolkitActionCreatorWithoutPayload,
|
||||
ToolkitActionCreatorWithoutPayloadMockType,
|
||||
} from 'test/core/redux/mocks';
|
||||
import { DashboardInitPhase, DashboardRouteInfo, MutableDashboard } from 'app/types';
|
||||
import { DashboardInitPhase, DashboardRouteInfo } from 'app/types';
|
||||
import { notifyApp, updateLocation } from 'app/core/actions';
|
||||
|
||||
jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
|
||||
@@ -215,7 +215,7 @@ describe('DashboardPage', () => {
|
||||
it('should set scrollTop to 0', () => {
|
||||
expect(ctx.wrapper).not.toBe(null);
|
||||
expect(ctx.wrapper?.state()).not.toBe(null);
|
||||
expect(ctx.wrapper?.state().scrollTop).toBe(0);
|
||||
expect(ctx.wrapper?.state().updateScrollTop).toBe(0);
|
||||
});
|
||||
|
||||
it('should add panel widget to dashboard panels', () => {
|
||||
@@ -272,7 +272,7 @@ describe('DashboardPage', () => {
|
||||
},
|
||||
},
|
||||
dashboard: {
|
||||
getModel: () => null as MutableDashboard,
|
||||
getModel: () => null as DashboardModel,
|
||||
},
|
||||
} as any);
|
||||
|
||||
@@ -290,7 +290,7 @@ describe('DashboardPage', () => {
|
||||
},
|
||||
},
|
||||
dashboard: {
|
||||
getModel: () => null as MutableDashboard,
|
||||
getModel: () => null as DashboardModel,
|
||||
},
|
||||
} as any);
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ export interface State {
|
||||
isFullscreen: boolean;
|
||||
fullscreenPanel: PanelModel | null;
|
||||
scrollTop: number;
|
||||
updateScrollTop: number;
|
||||
updateScrollTop?: number;
|
||||
rememberScrollTop: number;
|
||||
showLoadingState: boolean;
|
||||
}
|
||||
@@ -75,7 +75,6 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
showLoadingState: false,
|
||||
fullscreenPanel: null,
|
||||
scrollTop: 0,
|
||||
updateScrollTop: null,
|
||||
rememberScrollTop: 0,
|
||||
};
|
||||
|
||||
@@ -127,19 +126,19 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
|
||||
// Sync url state with model
|
||||
if (urlFullscreen !== dashboard.meta.fullscreen || urlEdit !== dashboard.meta.isEditing) {
|
||||
if (!isNaN(parseInt(urlPanelId, 10))) {
|
||||
this.onEnterFullscreen();
|
||||
if (urlPanelId && !isNaN(parseInt(urlPanelId, 10))) {
|
||||
this.onEnterFullscreen(dashboard, urlPanelId);
|
||||
} else {
|
||||
this.onLeaveFullscreen();
|
||||
this.onLeaveFullscreen(dashboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEnterFullscreen() {
|
||||
const { dashboard, urlEdit, urlFullscreen, urlPanelId } = this.props;
|
||||
|
||||
const panelId = parseInt(urlPanelId, 10);
|
||||
onEnterFullscreen(dashboard: DashboardModel, urlPanelId: string) {
|
||||
const { urlEdit, urlFullscreen } = this.props;
|
||||
|
||||
const panelId = parseInt(urlPanelId!, 10);
|
||||
dashboard;
|
||||
// need to expand parent row if this panel is inside a row
|
||||
dashboard.expandParentRowFor(panelId);
|
||||
|
||||
@@ -148,7 +147,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
if (panel) {
|
||||
dashboard.setViewMode(panel, urlFullscreen, urlEdit);
|
||||
this.setState({
|
||||
isEditing: urlEdit && dashboard.meta.canEdit,
|
||||
isEditing: urlEdit && dashboard.meta.canEdit === true,
|
||||
isFullscreen: urlFullscreen,
|
||||
fullscreenPanel: panel,
|
||||
rememberScrollTop: this.state.scrollTop,
|
||||
@@ -159,9 +158,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
onLeaveFullscreen() {
|
||||
const { dashboard } = this.props;
|
||||
|
||||
onLeaveFullscreen(dashboard: DashboardModel) {
|
||||
if (this.state.fullscreenPanel) {
|
||||
dashboard.setViewMode(this.state.fullscreenPanel, false, false);
|
||||
}
|
||||
@@ -181,7 +178,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
|
||||
triggerPanelsRendering() {
|
||||
try {
|
||||
this.props.dashboard.render();
|
||||
this.props.dashboard!.render();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.props.notifyApp(createErrorNotification(`Panel rendering error`, err));
|
||||
@@ -226,7 +223,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
});
|
||||
|
||||
// scroll to top after adding panel
|
||||
this.setState({ scrollTop: 0 });
|
||||
this.setState({ updateScrollTop: 0 });
|
||||
};
|
||||
|
||||
renderSlowInitState() {
|
||||
|
||||
@@ -113,7 +113,6 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
||||
autoHideTimeout={200}
|
||||
className="custom-scrollbar--page"
|
||||
hideTracksWhenNotNeeded={false}
|
||||
scrollTop={null}
|
||||
setScrollTop={[Function]}
|
||||
updateAfterMountMs={500}
|
||||
>
|
||||
@@ -449,7 +448,6 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
autoHideTimeout={200}
|
||||
className="custom-scrollbar--page"
|
||||
hideTracksWhenNotNeeded={false}
|
||||
scrollTop={null}
|
||||
setScrollTop={[Function]}
|
||||
updateAfterMountMs={500}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,10 @@ import classNames from 'classnames';
|
||||
// @ts-ignore
|
||||
import sizeMe from 'react-sizeme';
|
||||
|
||||
// Components
|
||||
import { AddPanelWidget } from '../components/AddPanelWidget';
|
||||
import { DashboardRow } from '../components/DashboardRow';
|
||||
|
||||
// Types
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
import { DashboardPanel } from './DashboardPanel';
|
||||
@@ -102,6 +106,7 @@ export class DashboardGrid extends PureComponent<Props> {
|
||||
|
||||
componentDidMount() {
|
||||
const { dashboard } = this.props;
|
||||
|
||||
dashboard.on(panelAdded, this.triggerForceUpdate);
|
||||
dashboard.on(panelRemoved, this.triggerForceUpdate);
|
||||
dashboard.on(CoreEvents.repeatsProcessed, this.triggerForceUpdate);
|
||||
@@ -173,7 +178,6 @@ export class DashboardGrid extends PureComponent<Props> {
|
||||
for (const panel of this.props.dashboard.panels) {
|
||||
panel.resizeDone();
|
||||
}
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onViewModeChanged = () => {
|
||||
@@ -241,10 +245,12 @@ export class DashboardGrid extends PureComponent<Props> {
|
||||
|
||||
renderPanels() {
|
||||
const panelElements = [];
|
||||
|
||||
for (const panel of this.props.dashboard.panels) {
|
||||
const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.fullscreen });
|
||||
const id = panel.id.toString();
|
||||
panel.isInView = this.isInView(panel);
|
||||
|
||||
panelElements.push(
|
||||
<div
|
||||
key={id}
|
||||
@@ -254,13 +260,7 @@ export class DashboardGrid extends PureComponent<Props> {
|
||||
this.panelRef[id] = elem;
|
||||
}}
|
||||
>
|
||||
<DashboardPanel
|
||||
panel={panel}
|
||||
dashboard={this.props.dashboard}
|
||||
isEditing={panel.isEditing}
|
||||
isFullscreen={panel.fullscreen}
|
||||
isInView={panel.isInView}
|
||||
/>
|
||||
{this.renderPanel(panel)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -268,6 +268,26 @@ export class DashboardGrid extends PureComponent<Props> {
|
||||
return panelElements;
|
||||
}
|
||||
|
||||
renderPanel(panel: PanelModel) {
|
||||
if (panel.type === 'row') {
|
||||
return <DashboardRow panel={panel} dashboard={this.props.dashboard} />;
|
||||
}
|
||||
|
||||
if (panel.type === 'add-panel') {
|
||||
return <AddPanelWidget panel={panel} dashboard={this.props.dashboard} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardPanel
|
||||
panel={panel}
|
||||
dashboard={this.props.dashboard}
|
||||
isEditing={panel.isEditing}
|
||||
isFullscreen={panel.fullscreen}
|
||||
isInView={panel.isInView}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard, isFullscreen } = this.props;
|
||||
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
// Utils & Services
|
||||
import { importPanelPlugin } from 'app/features/plugins/plugin_loader';
|
||||
import { connect, MapStateToProps, MapDispatchToProps } from 'react-redux';
|
||||
|
||||
// Components
|
||||
import { AddPanelWidget } from '../components/AddPanelWidget';
|
||||
import { DashboardRow } from '../components/DashboardRow';
|
||||
import { PanelChrome } from './PanelChrome';
|
||||
import { PanelEditor } from '../panel_editor/PanelEditor';
|
||||
import { PanelResizer } from './PanelResizer';
|
||||
import { PanelChromeAngular } from './PanelChromeAngular';
|
||||
|
||||
// Actions
|
||||
import { initDashboardPanel } from '../state/actions';
|
||||
|
||||
// Types
|
||||
import { PanelModel, DashboardModel } from '../state';
|
||||
import { PanelPluginMeta, PanelPlugin } from '@grafana/data';
|
||||
import { StoreState } from 'app/types';
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
|
||||
export interface Props {
|
||||
export interface OwnProps {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
isEditing: boolean;
|
||||
@@ -27,12 +27,21 @@ export interface Props {
|
||||
isInView: boolean;
|
||||
}
|
||||
|
||||
export interface ConnectedProps {
|
||||
plugin?: PanelPlugin;
|
||||
}
|
||||
|
||||
export interface DispatchProps {
|
||||
initDashboardPanel: typeof initDashboardPanel;
|
||||
}
|
||||
|
||||
export type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
|
||||
export interface State {
|
||||
plugin: PanelPlugin;
|
||||
isLazy: boolean;
|
||||
}
|
||||
|
||||
export class DashboardPanel extends PureComponent<Props, State> {
|
||||
export class DashboardPanelUnconnected extends PureComponent<Props, State> {
|
||||
element: HTMLElement;
|
||||
specialPanels: { [key: string]: Function } = {};
|
||||
|
||||
@@ -40,56 +49,15 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
plugin: null,
|
||||
isLazy: !props.isInView,
|
||||
};
|
||||
|
||||
this.specialPanels['row'] = this.renderRow.bind(this);
|
||||
this.specialPanels['add-panel'] = this.renderAddPanel.bind(this);
|
||||
}
|
||||
|
||||
isSpecial(pluginId: string) {
|
||||
return this.specialPanels[pluginId];
|
||||
}
|
||||
|
||||
renderRow() {
|
||||
return <DashboardRow panel={this.props.panel} dashboard={this.props.dashboard} />;
|
||||
}
|
||||
|
||||
renderAddPanel() {
|
||||
return <AddPanelWidget panel={this.props.panel} dashboard={this.props.dashboard} />;
|
||||
}
|
||||
|
||||
onPluginTypeChange = (plugin: PanelPluginMeta) => {
|
||||
this.loadPlugin(plugin.id);
|
||||
};
|
||||
|
||||
async loadPlugin(pluginId: string) {
|
||||
if (this.isSpecial(pluginId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { panel } = this.props;
|
||||
|
||||
// handle plugin loading & changing of plugin type
|
||||
if (!this.state.plugin || this.state.plugin.meta.id !== pluginId) {
|
||||
const plugin = await importPanelPlugin(pluginId);
|
||||
|
||||
if (panel.type !== pluginId) {
|
||||
panel.changePlugin(plugin);
|
||||
} else {
|
||||
panel.pluginLoaded(plugin);
|
||||
}
|
||||
|
||||
this.setState({ plugin });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadPlugin(this.props.panel.type);
|
||||
this.props.initDashboardPanel(this.props.panel);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
componentDidUpdate() {
|
||||
if (this.state.isLazy && this.props.isInView) {
|
||||
this.setState({ isLazy: false });
|
||||
}
|
||||
@@ -104,8 +72,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
renderPanel() {
|
||||
const { dashboard, panel, isFullscreen, isInView, isInEditMode } = this.props;
|
||||
const { plugin } = this.state;
|
||||
const { dashboard, panel, isFullscreen, isInView, isInEditMode, plugin } = this.props;
|
||||
|
||||
return (
|
||||
<AutoSizer>
|
||||
@@ -146,12 +113,8 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { panel, dashboard, isFullscreen, isEditing } = this.props;
|
||||
const { plugin, isLazy } = this.state;
|
||||
|
||||
if (this.isSpecial(panel.type)) {
|
||||
return this.specialPanels[panel.type]();
|
||||
}
|
||||
const { panel, dashboard, isFullscreen, isEditing, plugin } = this.props;
|
||||
const { isLazy } = this.state;
|
||||
|
||||
// if we have not loaded plugin exports yet, wait
|
||||
if (!plugin) {
|
||||
@@ -190,15 +153,18 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{panel.isEditing && (
|
||||
<PanelEditor
|
||||
panel={panel}
|
||||
plugin={plugin}
|
||||
dashboard={dashboard}
|
||||
onPluginTypeChange={this.onPluginTypeChange}
|
||||
/>
|
||||
)}
|
||||
{panel.isEditing && <PanelEditor panel={panel} plugin={plugin} dashboard={dashboard} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
|
||||
return {
|
||||
plugin: state.plugins.panels[props.panel.type],
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { initDashboardPanel };
|
||||
|
||||
export const DashboardPanel = connect(mapStateToProps, mapDispatchToProps)(DashboardPanelUnconnected);
|
||||
|
||||
@@ -115,8 +115,6 @@ export class PanelChromeAngular extends PureComponent<Props, State> {
|
||||
this.cleanUpAngularPanel();
|
||||
this.loadAngularPanel();
|
||||
}
|
||||
|
||||
this.loadAngularPanel();
|
||||
}
|
||||
|
||||
loadAngularPanel() {
|
||||
|
||||
@@ -49,7 +49,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
id="panel-1"
|
||||
key="1"
|
||||
>
|
||||
<DashboardPanel
|
||||
<Component
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
@@ -292,7 +292,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
id="panel-2"
|
||||
key="2"
|
||||
>
|
||||
<DashboardPanel
|
||||
<Component
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
@@ -535,7 +535,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
id="panel-3"
|
||||
key="3"
|
||||
>
|
||||
<DashboardPanel
|
||||
<Component
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
@@ -778,7 +778,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
id="panel-4"
|
||||
key="4"
|
||||
>
|
||||
<DashboardPanel
|
||||
<Component
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import { PanelPlugin, PanelPluginMeta } from '@grafana/data';
|
||||
@@ -16,18 +15,19 @@ import { DashboardModel } from '../state/DashboardModel';
|
||||
import { StoreState } from '../../../types';
|
||||
import { panelEditorCleanUp, PanelEditorTab, PanelEditorTabIds } from './state/reducers';
|
||||
import { changePanelEditorTab, refreshPanelEditor } from './state/actions';
|
||||
import { changePanelPlugin } from '../state/actions';
|
||||
import { getActiveTabAndTabs } from './state/selectors';
|
||||
|
||||
interface PanelEditorProps {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
plugin: PanelPlugin;
|
||||
onPluginTypeChange: (newType: PanelPluginMeta) => void;
|
||||
activeTab: PanelEditorTabIds;
|
||||
tabs: PanelEditorTab[];
|
||||
refreshPanelEditor: typeof refreshPanelEditor;
|
||||
panelEditorCleanUp: typeof panelEditorCleanUp;
|
||||
changePanelEditorTab: typeof changePanelEditorTab;
|
||||
changePanelPlugin: typeof changePanelPlugin;
|
||||
}
|
||||
|
||||
class UnConnectedPanelEditor extends PureComponent<PanelEditorProps> {
|
||||
@@ -63,9 +63,7 @@ class UnConnectedPanelEditor extends PureComponent<PanelEditorProps> {
|
||||
};
|
||||
|
||||
onPluginTypeChange = (newType: PanelPluginMeta) => {
|
||||
const { onPluginTypeChange } = this.props;
|
||||
onPluginTypeChange(newType);
|
||||
|
||||
this.props.changePanelPlugin(this.props.panel, newType.id);
|
||||
this.refreshFromState(newType);
|
||||
};
|
||||
|
||||
@@ -108,11 +106,10 @@ class UnConnectedPanelEditor extends PureComponent<PanelEditorProps> {
|
||||
}
|
||||
}
|
||||
|
||||
export const mapStateToProps = (state: StoreState) => getActiveTabAndTabs(state.location, state.panelEditor);
|
||||
const mapStateToProps = (state: StoreState) => getActiveTabAndTabs(state.location, state.panelEditor);
|
||||
const mapDispatchToProps = { refreshPanelEditor, panelEditorCleanUp, changePanelEditorTab, changePanelPlugin };
|
||||
|
||||
const mapDispatchToProps = { refreshPanelEditor, panelEditorCleanUp, changePanelEditorTab };
|
||||
|
||||
export const PanelEditor = hot(module)(connect(mapStateToProps, mapDispatchToProps)(UnConnectedPanelEditor));
|
||||
export const PanelEditor = connect(mapStateToProps, mapDispatchToProps)(UnConnectedPanelEditor);
|
||||
|
||||
interface TabItemParams {
|
||||
tab: PanelEditorTab;
|
||||
|
||||
@@ -92,6 +92,7 @@ const defaults: any = {
|
||||
};
|
||||
|
||||
export class PanelModel {
|
||||
/* persisted id, used in URL to identify a panel */
|
||||
id: number;
|
||||
gridPos: GridPos;
|
||||
type: string;
|
||||
@@ -280,6 +281,7 @@ export class PanelModel {
|
||||
|
||||
if (plugin.panel && plugin.onPanelMigration) {
|
||||
const version = getPluginVersion(plugin);
|
||||
|
||||
if (version !== this.pluginVersion) {
|
||||
this.options = plugin.onPanelMigration(this);
|
||||
this.pluginVersion = version;
|
||||
|
||||
@@ -3,10 +3,12 @@ import { getBackendSrv } from '@grafana/runtime';
|
||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||
// Actions
|
||||
import { loadPluginDashboards } from '../../plugins/state/actions';
|
||||
import { loadDashboardPermissions } from './reducers';
|
||||
import { loadDashboardPermissions, dashboardPanelTypeChanged } from './reducers';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { loadPanelPlugin } from 'app/features/plugins/state/actions';
|
||||
// Types
|
||||
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel, ThunkResult } from 'app/types';
|
||||
import { PanelModel } from './PanelModel';
|
||||
|
||||
export function getDashboardPermissions(id: number): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
@@ -108,3 +110,35 @@ export function removeDashboard(uri: string): ThunkResult<void> {
|
||||
dispatch(loadPluginDashboards());
|
||||
};
|
||||
}
|
||||
|
||||
export function initDashboardPanel(panel: PanelModel): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
let plugin = getStore().plugins.panels[panel.type];
|
||||
|
||||
if (!plugin) {
|
||||
plugin = await dispatch(loadPanelPlugin(panel.type));
|
||||
}
|
||||
|
||||
if (!panel.plugin) {
|
||||
panel.pluginLoaded(plugin);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function changePanelPlugin(panel: PanelModel, pluginId: string): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
// ignore action is no change
|
||||
if (panel.type === pluginId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let plugin = getStore().plugins.panels[pluginId];
|
||||
|
||||
if (!plugin) {
|
||||
plugin = await dispatch(loadPanelPlugin(pluginId));
|
||||
}
|
||||
|
||||
panel.changePlugin(plugin);
|
||||
dispatch(dashboardPanelTypeChanged({ panelId: panel.id, pluginId }));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,7 +32,15 @@ describe('dashboard reducer', () => {
|
||||
beforeEach(() => {
|
||||
state = dashboardReducer(initialState, dashboardInitFetching());
|
||||
state = dashboardReducer(state, dashboardInitSlow());
|
||||
state = dashboardReducer(state, dashboardInitCompleted(new DashboardModel({ title: 'My dashboard' })));
|
||||
state = dashboardReducer(
|
||||
state,
|
||||
dashboardInitCompleted(
|
||||
new DashboardModel({
|
||||
title: 'My dashboard',
|
||||
panels: [{ id: 1 }, { id: 2 }],
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should set model', async () => {
|
||||
@@ -42,6 +50,11 @@ describe('dashboard reducer', () => {
|
||||
it('should set reset isInitSlow', async () => {
|
||||
expect(state.isInitSlow).toBe(false);
|
||||
});
|
||||
|
||||
it('should create panel state', async () => {
|
||||
expect(state.panels['1']).toBeDefined();
|
||||
expect(state.panels['2']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dashboardInitFailed', () => {
|
||||
|
||||
@@ -3,13 +3,13 @@ import {
|
||||
DashboardInitPhase,
|
||||
DashboardState,
|
||||
DashboardAclDTO,
|
||||
MutableDashboard,
|
||||
DashboardInitError,
|
||||
QueriesToUpdateOnDashboardLoad,
|
||||
} from 'app/types';
|
||||
import { processAclItems } from 'app/core/utils/acl';
|
||||
import { panelEditorReducer } from '../panel_editor/state/reducers';
|
||||
import { DashboardModel } from './DashboardModel';
|
||||
import { PanelModel } from './PanelModel';
|
||||
|
||||
export const initialState: DashboardState = {
|
||||
initPhase: DashboardInitPhase.NotStarted,
|
||||
@@ -17,6 +17,8 @@ export const initialState: DashboardState = {
|
||||
getModel: () => null,
|
||||
permissions: [],
|
||||
modifiedQueries: null,
|
||||
panels: {},
|
||||
initError: null,
|
||||
};
|
||||
|
||||
const dashbardSlice = createSlice({
|
||||
@@ -35,10 +37,16 @@ const dashbardSlice = createSlice({
|
||||
dashboardInitSlow: (state, action: PayloadAction) => {
|
||||
state.isInitSlow = true;
|
||||
},
|
||||
dashboardInitCompleted: (state, action: PayloadAction<MutableDashboard>) => {
|
||||
dashboardInitCompleted: (state, action: PayloadAction<DashboardModel>) => {
|
||||
state.getModel = () => action.payload;
|
||||
state.initPhase = DashboardInitPhase.Completed;
|
||||
state.isInitSlow = false;
|
||||
|
||||
for (const panel of action.payload.panels) {
|
||||
state.panels[panel.id] = {
|
||||
pluginId: panel.type,
|
||||
};
|
||||
}
|
||||
},
|
||||
dashboardInitFailed: (state, action: PayloadAction<DashboardInitError>) => {
|
||||
state.initPhase = DashboardInitPhase.Failed;
|
||||
@@ -49,7 +57,7 @@ const dashbardSlice = createSlice({
|
||||
},
|
||||
cleanUpDashboard: (state, action: PayloadAction) => {
|
||||
if (state.getModel()) {
|
||||
state.getModel().destroy();
|
||||
state.getModel()!.destroy();
|
||||
state.getModel = () => null;
|
||||
}
|
||||
|
||||
@@ -63,9 +71,22 @@ const dashbardSlice = createSlice({
|
||||
clearDashboardQueriesToUpdateOnLoad: (state, action: PayloadAction) => {
|
||||
state.modifiedQueries = null;
|
||||
},
|
||||
dashboardPanelTypeChanged: (state, action: PayloadAction<DashboardPanelTypeChangedPayload>) => {
|
||||
state.panels[action.payload.panelId] = { pluginId: action.payload.pluginId };
|
||||
},
|
||||
addPanelToDashboard: (state, action: PayloadAction<AddPanelPayload>) => {},
|
||||
},
|
||||
});
|
||||
|
||||
export interface DashboardPanelTypeChangedPayload {
|
||||
panelId: number;
|
||||
pluginId: string;
|
||||
}
|
||||
|
||||
export interface AddPanelPayload {
|
||||
panel: PanelModel;
|
||||
}
|
||||
|
||||
export const {
|
||||
loadDashboardPermissions,
|
||||
dashboardInitFetching,
|
||||
@@ -76,6 +97,8 @@ export const {
|
||||
cleanUpDashboard,
|
||||
setDashboardQueriesToUpdateOnLoad,
|
||||
clearDashboardQueriesToUpdateOnLoad,
|
||||
dashboardPanelTypeChanged,
|
||||
addPanelToDashboard,
|
||||
} = dashbardSlice.actions;
|
||||
|
||||
export const dashboardReducer = dashbardSlice.reducer;
|
||||
|
||||
@@ -225,21 +225,24 @@ import { getPanelPluginNotFound, getPanelPluginLoadError } from '../dashboard/da
|
||||
import { GenericDataSourcePlugin } from '../datasources/settings/PluginSettings';
|
||||
|
||||
interface PanelCache {
|
||||
[key: string]: PanelPlugin;
|
||||
[key: string]: Promise<PanelPlugin>;
|
||||
}
|
||||
const panelCache: PanelCache = {};
|
||||
|
||||
export function importPanelPlugin(id: string): Promise<PanelPlugin> {
|
||||
const loaded = panelCache[id];
|
||||
|
||||
if (loaded) {
|
||||
return Promise.resolve(loaded);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
const meta = config.panels[id];
|
||||
|
||||
if (!meta) {
|
||||
return Promise.resolve(getPanelPluginNotFound(id));
|
||||
}
|
||||
|
||||
return importPluginModule(meta.module)
|
||||
panelCache[id] = importPluginModule(meta.module)
|
||||
.then(pluginExports => {
|
||||
if (pluginExports.plugin) {
|
||||
return pluginExports.plugin as PanelPlugin;
|
||||
@@ -252,11 +255,13 @@ export function importPanelPlugin(id: string): Promise<PanelPlugin> {
|
||||
})
|
||||
.then(plugin => {
|
||||
plugin.meta = meta;
|
||||
return (panelCache[meta.id] = plugin);
|
||||
return plugin;
|
||||
})
|
||||
.catch(err => {
|
||||
// TODO, maybe a different error plugin
|
||||
console.warn('Error loading panel plugin: ' + id, err);
|
||||
return getPanelPluginLoadError(meta, err);
|
||||
});
|
||||
|
||||
return panelCache[id];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { ThunkResult } from 'app/types';
|
||||
import { pluginDashboardsLoad, pluginDashboardsLoaded, pluginsLoaded } from './reducers';
|
||||
import { pluginDashboardsLoad, pluginDashboardsLoaded, pluginsLoaded, panelPluginLoaded } from './reducers';
|
||||
import { importPanelPlugin } from 'app/features/plugins/plugin_loader';
|
||||
|
||||
export function loadPlugins(): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
@@ -18,3 +19,20 @@ export function loadPluginDashboards(): ThunkResult<void> {
|
||||
dispatch(pluginDashboardsLoaded(response));
|
||||
};
|
||||
}
|
||||
|
||||
export function loadPanelPlugin(pluginId: string): ThunkResult<Promise<PanelPlugin>> {
|
||||
return async (dispatch, getStore) => {
|
||||
let plugin = getStore().plugins.panels[pluginId];
|
||||
|
||||
if (!plugin) {
|
||||
plugin = await importPanelPlugin(pluginId);
|
||||
|
||||
// second check to protect against raise condition
|
||||
if (!getStore().plugins.panels[pluginId]) {
|
||||
dispatch(panelPluginLoaded(plugin));
|
||||
}
|
||||
}
|
||||
|
||||
return plugin;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { PluginMeta } from '@grafana/data';
|
||||
import { PluginMeta, PanelPlugin } from '@grafana/data';
|
||||
import { PluginsState } from 'app/types';
|
||||
import { LayoutMode, LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector';
|
||||
import { PluginDashboard } from '../../../types/plugins';
|
||||
@@ -11,26 +11,33 @@ export const initialState: PluginsState = {
|
||||
hasFetched: false,
|
||||
dashboards: [],
|
||||
isLoadingPluginDashboards: false,
|
||||
panels: {},
|
||||
};
|
||||
|
||||
const pluginsSlice = createSlice({
|
||||
name: 'plugins',
|
||||
initialState,
|
||||
reducers: {
|
||||
pluginsLoaded: (state, action: PayloadAction<PluginMeta[]>): PluginsState => {
|
||||
return { ...state, hasFetched: true, plugins: action.payload };
|
||||
pluginsLoaded: (state, action: PayloadAction<PluginMeta[]>) => {
|
||||
state.hasFetched = true;
|
||||
state.plugins = action.payload;
|
||||
},
|
||||
setPluginsSearchQuery: (state, action: PayloadAction<string>): PluginsState => {
|
||||
return { ...state, searchQuery: action.payload };
|
||||
setPluginsSearchQuery: (state, action: PayloadAction<string>) => {
|
||||
state.searchQuery = action.payload;
|
||||
},
|
||||
setPluginsLayoutMode: (state, action: PayloadAction<LayoutMode>): PluginsState => {
|
||||
return { ...state, layoutMode: action.payload };
|
||||
setPluginsLayoutMode: (state, action: PayloadAction<LayoutMode>) => {
|
||||
state.layoutMode = action.payload;
|
||||
},
|
||||
pluginDashboardsLoad: (state, action: PayloadAction<undefined>): PluginsState => {
|
||||
return { ...state, dashboards: [], isLoadingPluginDashboards: true };
|
||||
pluginDashboardsLoad: (state, action: PayloadAction<undefined>) => {
|
||||
state.isLoadingPluginDashboards = true;
|
||||
state.dashboards = [];
|
||||
},
|
||||
pluginDashboardsLoaded: (state, action: PayloadAction<PluginDashboard[]>): PluginsState => {
|
||||
return { ...state, dashboards: action.payload, isLoadingPluginDashboards: false };
|
||||
pluginDashboardsLoaded: (state, action: PayloadAction<PluginDashboard[]>) => {
|
||||
state.isLoadingPluginDashboards = false;
|
||||
state.dashboards = action.payload;
|
||||
},
|
||||
panelPluginLoaded: (state, action: PayloadAction<PanelPlugin>) => {
|
||||
state.panels[action.payload.meta!.id] = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -41,6 +48,7 @@ export const {
|
||||
pluginDashboardsLoaded,
|
||||
setPluginsLayoutMode,
|
||||
setPluginsSearchQuery,
|
||||
panelPluginLoaded,
|
||||
} = pluginsSlice.actions;
|
||||
|
||||
export const pluginsReducer = pluginsSlice.reducer;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { DashboardAcl } from './acl';
|
||||
import { DataQuery } from '@grafana/data';
|
||||
|
||||
export interface MutableDashboard {
|
||||
title: string;
|
||||
meta: DashboardMeta;
|
||||
destroy: () => void;
|
||||
}
|
||||
import { AngularComponent } from '@grafana/runtime';
|
||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
|
||||
export interface DashboardDTO {
|
||||
redirectUri?: string;
|
||||
@@ -65,18 +61,24 @@ export interface DashboardInitError {
|
||||
|
||||
export const KIOSK_MODE_TV = 'tv';
|
||||
export type KioskUrlValue = 'tv' | '1' | true;
|
||||
export type GetMutableDashboardModelFn = () => MutableDashboard | null;
|
||||
export type GetMutableDashboardModelFn = () => DashboardModel | null;
|
||||
|
||||
export interface QueriesToUpdateOnDashboardLoad {
|
||||
panelId: number;
|
||||
queries: DataQuery[];
|
||||
}
|
||||
|
||||
export interface PanelState {
|
||||
pluginId: string;
|
||||
angularPanel?: AngularComponent;
|
||||
}
|
||||
|
||||
export interface DashboardState {
|
||||
getModel: GetMutableDashboardModelFn;
|
||||
initPhase: DashboardInitPhase;
|
||||
isInitSlow: boolean;
|
||||
initError?: DashboardInitError;
|
||||
initError: DashboardInitError | null;
|
||||
permissions: DashboardAcl[] | null;
|
||||
modifiedQueries: QueriesToUpdateOnDashboardLoad | null;
|
||||
panels: { [id: string]: PanelState };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PluginMeta } from '@grafana/data';
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
|
||||
export interface PluginDashboard {
|
||||
dashboardId: number;
|
||||
@@ -16,6 +17,10 @@ export interface PluginDashboard {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface PanelPluginsIndex {
|
||||
[id: string]: PanelPlugin;
|
||||
}
|
||||
|
||||
export interface PluginsState {
|
||||
plugins: PluginMeta[];
|
||||
searchQuery: string;
|
||||
@@ -23,6 +28,7 @@ export interface PluginsState {
|
||||
hasFetched: boolean;
|
||||
dashboards: PluginDashboard[];
|
||||
isLoadingPluginDashboards: boolean;
|
||||
panels: PanelPluginsIndex;
|
||||
}
|
||||
|
||||
export interface VariableQueryProps {
|
||||
|
||||
Reference in New Issue
Block a user