mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PanelState: Introduce a new separate redux panel state not keyed by panel.id (#40302)
* Initial pass to move panel state to it's own, and make it by key not panel.id * Progress * Not making much progress, having panel.key be mutable is causing a lot of issues * Think this is starting to work * Began fixing tests * Add selector * Bug fixes and changes to cleanup, and fixing all flicking when switching library panels * Removed console.log * fixes after merge * fixing tests * fixing tests * Added new test for changePlugin thunk
This commit is contained in:
parent
3d9e2d8c82
commit
d62ca1283c
@ -41,7 +41,7 @@ import { configureStore } from './store/configureStore';
|
||||
import { AppWrapper } from './AppWrapper';
|
||||
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
|
||||
import { AngularApp } from './angular/AngularApp';
|
||||
import { PanelRenderer } from './features/panel/PanelRenderer';
|
||||
import { PanelRenderer } from './features/panel/components/PanelRenderer';
|
||||
import { QueryRunner } from './features/query/state/QueryRunner';
|
||||
import { getTimeSrv } from './features/dashboard/services/TimeSrv';
|
||||
import { getVariablesUrlParams } from './features/variables/getAllVariableValuesForUrl';
|
||||
|
@ -15,6 +15,7 @@ import organizationReducers from 'app/features/org/state/reducers';
|
||||
import ldapReducers from 'app/features/admin/state/reducers';
|
||||
import templatingReducers from 'app/features/variables/state/reducers';
|
||||
import importDashboardReducers from 'app/features/manage-dashboards/state/reducers';
|
||||
import panelsReducers from 'app/features/panel/state/reducers';
|
||||
|
||||
const rootReducers = {
|
||||
...sharedReducers,
|
||||
@ -32,6 +33,7 @@ const rootReducers = {
|
||||
...ldapReducers,
|
||||
...templatingReducers,
|
||||
...importDashboardReducers,
|
||||
...panelsReducers,
|
||||
};
|
||||
|
||||
const addedReducers = {};
|
||||
|
@ -108,7 +108,7 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise<strin
|
||||
};
|
||||
}
|
||||
|
||||
const exploreState = JSON.stringify({ ...state, originPanelId: panel.getSavedId() });
|
||||
const exploreState = JSON.stringify({ ...state, originPanelId: panel.id });
|
||||
url = urlUtil.renderUrl('/explore', { left: exploreState });
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ import { AppNotificationSeverity, StoreState } from 'app/types';
|
||||
import { PanelNotSupported } from '../dashboard/components/PanelEditor/PanelNotSupported';
|
||||
import { AlertState } from '../../plugins/datasource/alertmanager/types';
|
||||
import { EventBusSrv } from '@grafana/data';
|
||||
import { getPanelStateForModel } from 'app/features/panel/state/selectors';
|
||||
|
||||
interface AngularPanelController {
|
||||
_enableAlert: () => void;
|
||||
@ -197,11 +198,7 @@ class UnConnectedAlertTab extends PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} icon="history" title="State history" onDismiss={onDismiss} onClickBackdrop={onDismiss}>
|
||||
<StateHistory
|
||||
dashboard={dashboard}
|
||||
panelId={panel.editSourceId ?? panel.id}
|
||||
onRefresh={() => this.panelCtrl?.refresh()}
|
||||
/>
|
||||
<StateHistory dashboard={dashboard} panelId={panel.id} onRefresh={() => this.panelCtrl?.refresh()} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@ -263,7 +260,7 @@ class UnConnectedAlertTab extends PureComponent<Props, State> {
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
|
||||
return {
|
||||
angularPanelComponent: state.dashboard.panels[props.panel.id].angularComponent,
|
||||
angularPanelComponent: getPanelStateForModel(state, props.panel)?.angularComponent,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -39,7 +39,7 @@ export class TestRuleResult extends PureComponent<Props, State> {
|
||||
|
||||
// now replace panel to get current edits
|
||||
model.panels = model.panels.map((dashPanel) => {
|
||||
return dashPanel.id === panel.editSourceId ? panel.getSaveModel() : dashPanel;
|
||||
return dashPanel.id === panel.id ? panel.getSaveModel() : dashPanel;
|
||||
});
|
||||
|
||||
const payload = { dashboard: model, panelId: panel.id };
|
||||
|
@ -139,10 +139,11 @@ const dashboard = {
|
||||
folderTitle: 'super folder',
|
||||
},
|
||||
} as DashboardModel;
|
||||
|
||||
const panel = ({
|
||||
datasource: dataSources.prometheus.uid,
|
||||
title: 'mypanel',
|
||||
editSourceId: 34,
|
||||
id: 34,
|
||||
targets: [
|
||||
{
|
||||
expr: 'sum(some_metric [$__interval])) by (app)',
|
||||
@ -273,11 +274,11 @@ describe('PanelAlertTabContent', () => {
|
||||
|
||||
expect(mocks.api.fetchRulerRules).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, {
|
||||
dashboardUID: dashboard.uid,
|
||||
panelId: panel.editSourceId,
|
||||
panelId: panel.id,
|
||||
});
|
||||
expect(mocks.api.fetchRules).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, {
|
||||
dashboardUID: dashboard.uid,
|
||||
panelId: panel.editSourceId,
|
||||
panelId: panel.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -37,13 +37,13 @@ export function usePanelCombinedRules({ dashboard, panel, poll = false }: Option
|
||||
dispatch(
|
||||
fetchPromRulesAction({
|
||||
rulesSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
filter: { dashboardUID: dashboard.uid, panelId: panel.editSourceId },
|
||||
filter: { dashboardUID: dashboard.uid, panelId: panel.id },
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
fetchRulerRulesAction({
|
||||
rulesSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
filter: { dashboardUID: dashboard.uid, panelId: panel.editSourceId },
|
||||
filter: { dashboardUID: dashboard.uid, panelId: panel.id },
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -55,7 +55,7 @@ export function usePanelCombinedRules({ dashboard, panel, poll = false }: Option
|
||||
};
|
||||
}
|
||||
return () => {};
|
||||
}, [dispatch, poll, panel.editSourceId, dashboard.uid]);
|
||||
}, [dispatch, poll, panel.id, dashboard.uid]);
|
||||
|
||||
const loading = promRuleRequest.loading || rulerRuleRequest.loading;
|
||||
const errors = [promRuleRequest.error, rulerRuleRequest.error].filter(
|
||||
@ -73,7 +73,7 @@ export function usePanelCombinedRules({ dashboard, panel, poll = false }: Option
|
||||
.filter(
|
||||
(rule) =>
|
||||
rule.annotations[Annotation.dashboardUID] === dashboard.uid &&
|
||||
rule.annotations[Annotation.panelID] === String(panel.editSourceId)
|
||||
rule.annotations[Annotation.panelID] === String(panel.id)
|
||||
),
|
||||
[combinedNamespaces, dashboard, panel]
|
||||
);
|
||||
|
@ -281,7 +281,7 @@ export const panelToRuleFormValues = async (
|
||||
dashboard: DashboardModel
|
||||
): Promise<Partial<RuleFormValues> | undefined> => {
|
||||
const { targets } = panel;
|
||||
if (!panel.editSourceId || !dashboard.uid) {
|
||||
if (!panel.id || !dashboard.uid) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -324,7 +324,7 @@ export const panelToRuleFormValues = async (
|
||||
},
|
||||
{
|
||||
key: Annotation.panelID,
|
||||
value: String(panel.editSourceId),
|
||||
value: String(panel.id),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -19,7 +19,7 @@ export class EventEditorCtrl {
|
||||
constructor() {}
|
||||
|
||||
$onInit() {
|
||||
this.event.panelId = this.panelCtrl.panel.editSourceId ?? this.panelCtrl.panel.id; // set correct id if in panel edit
|
||||
this.event.panelId = this.panelCtrl.panel.id; // set correct id if in panel edit
|
||||
this.event.dashboardId = this.panelCtrl.dashboard.id;
|
||||
|
||||
// Annotations query returns time as Unix timestamp in milliseconds
|
||||
|
@ -10,6 +10,7 @@ import { InspectContent } from './InspectContent';
|
||||
import { useDatasourceMetadata, useInspectTabs } from './hooks';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { InspectTab } from 'app/features/inspector/types';
|
||||
import { getPanelStateForModel } from 'app/features/panel/state/selectors';
|
||||
|
||||
interface OwnProps {
|
||||
dashboard: DashboardModel;
|
||||
@ -63,7 +64,7 @@ const PanelInspectorUnconnected: React.FC<Props> = ({ panel, dashboard, plugin }
|
||||
};
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
|
||||
const panelState = state.dashboard.panels[props.panel.id];
|
||||
const panelState = getPanelStateForModel(state, props.panel);
|
||||
if (!panelState) {
|
||||
return { plugin: null };
|
||||
}
|
||||
|
@ -8,10 +8,11 @@ import { AngularComponent, getAngularLoader } from '@grafana/runtime';
|
||||
// Types
|
||||
import { PanelModel, DashboardModel } from '../../state';
|
||||
import { PanelPlugin, PanelPluginMeta } from '@grafana/data';
|
||||
import { changePanelPlugin } from '../../state/actions';
|
||||
import { changePanelPlugin } from 'app/features/panel/state/actions';
|
||||
import { StoreState } from 'app/types';
|
||||
import { getSectionOpenState, saveSectionOpenState } from './state/utils';
|
||||
import { PanelCtrl } from 'app/features/panel/panel_ctrl';
|
||||
import { getPanelStateForModel } from 'app/features/panel/state/selectors';
|
||||
|
||||
interface OwnProps {
|
||||
panel: PanelModel;
|
||||
@ -20,7 +21,7 @@ interface OwnProps {
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState, props: OwnProps) => ({
|
||||
angularPanelComponent: state.dashboard.panels[props.panel.id].angularComponent,
|
||||
angularPanelComponent: getPanelStateForModel(state, props.panel)?.angularComponent,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = { changePanelPlugin };
|
||||
@ -41,7 +42,10 @@ export class AngularPanelOptionsUnconnected extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (this.props.plugin !== prevProps.plugin) {
|
||||
if (
|
||||
this.props.plugin !== prevProps.plugin ||
|
||||
this.props.angularPanelComponent !== prevProps.angularPanelComponent
|
||||
) {
|
||||
this.cleanUpAngularOptions();
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,6 @@ import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
||||
import { toggleTableView } from './state/reducers';
|
||||
|
||||
import { getPanelEditorTabs } from './state/selectors';
|
||||
import { getPanelStateById } from '../../state/selectors';
|
||||
import { getVariables } from 'app/features/variables/state/selectors';
|
||||
|
||||
import { StoreState } from 'app/types';
|
||||
@ -55,6 +54,7 @@ import {
|
||||
import { notifyApp } from '../../../../core/actions';
|
||||
import { PanelEditorTableView } from './PanelEditorTableView';
|
||||
import { PanelModelWithLibraryPanel } from 'app/features/library-panels/types';
|
||||
import { getPanelStateForModel } from 'app/features/panel/state/selectors';
|
||||
|
||||
interface OwnProps {
|
||||
dashboard: DashboardModel;
|
||||
@ -64,12 +64,12 @@ interface OwnProps {
|
||||
|
||||
const mapStateToProps = (state: StoreState) => {
|
||||
const panel = state.panelEditor.getPanel();
|
||||
const { plugin, instanceState } = getPanelStateById(state.dashboard, panel.id);
|
||||
const panelState = getPanelStateForModel(state, panel);
|
||||
|
||||
return {
|
||||
plugin: plugin,
|
||||
panel,
|
||||
instanceState,
|
||||
plugin: panelState?.plugin,
|
||||
instanceState: panelState?.instanceState,
|
||||
initDone: state.panelEditor.initDone,
|
||||
uiState: state.panelEditor.ui,
|
||||
tableViewEnabled: state.panelEditor.tableViewEnabled,
|
||||
@ -244,8 +244,10 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<div className={styles.centeringContainer} style={{ width, height }}>
|
||||
<div style={panelSize} data-panelid={panel.editSourceId}>
|
||||
<div style={panelSize} data-panelid={panel.id}>
|
||||
<DashboardPanel
|
||||
key={panel.key}
|
||||
stateKey={panel.key}
|
||||
dashboard={dashboard}
|
||||
panel={panel}
|
||||
isEditing={true}
|
||||
@ -253,6 +255,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
isInView={true}
|
||||
width={panelSize.width}
|
||||
height={panelSize.height}
|
||||
skipStateCleanUp={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { PanelChrome } from '@grafana/ui';
|
||||
import { PanelRenderer } from 'app/features/panel/PanelRenderer';
|
||||
import { PanelRenderer } from 'app/features/panel/components/PanelRenderer';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { PanelModel, DashboardModel } from '../../state';
|
||||
import { usePanelLatestData } from './usePanelLatestData';
|
||||
|
@ -2,7 +2,7 @@ import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme, PanelPluginMeta, SelectableValue } from '@grafana/data';
|
||||
import { Button, CustomScrollbar, Icon, Input, RadioButtonGroup, useStyles } from '@grafana/ui';
|
||||
import { changePanelPlugin } from '../../state/actions';
|
||||
import { changePanelPlugin } from '../../../panel/state/actions';
|
||||
import { PanelModel } from '../../state/PanelModel';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { filterPluginList, getAllPanelPluginMeta, VizTypePicker } from '../VizTypePicker/VizTypePicker';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { thunkTester } from '../../../../../../test/core/thunk/thunkTester';
|
||||
import { closeEditor, initialState, PanelEditorState } from './reducers';
|
||||
import { exitPanelEditor, initPanelEditor, skipPanelUpdate } from './actions';
|
||||
import { cleanUpEditPanel, panelModelAndPluginReady } from '../../../state/reducers';
|
||||
import { cleanUpPanelState, panelModelAndPluginReady } from 'app/features/panel/state/reducers';
|
||||
import { DashboardModel, PanelModel } from '../../../state';
|
||||
import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks';
|
||||
|
||||
@ -14,15 +14,20 @@ describe('panelEditor actions', () => {
|
||||
const sourcePanel = new PanelModel({ id: 12, type: 'graph' });
|
||||
|
||||
const dispatchedActions = await thunkTester({
|
||||
panelEditorNew: { ...initialState },
|
||||
panelEditor: { ...initialState },
|
||||
plugins: {
|
||||
panels: {},
|
||||
},
|
||||
})
|
||||
.givenThunk(initPanelEditor)
|
||||
.whenThunkIsDispatched(sourcePanel, dashboard);
|
||||
|
||||
expect(dispatchedActions.length).toBe(1);
|
||||
expect(dispatchedActions[0].payload.sourcePanel).toBe(sourcePanel);
|
||||
expect(dispatchedActions[0].payload.panel).not.toBe(sourcePanel);
|
||||
expect(dispatchedActions[0].payload.panel.id).not.toBe(sourcePanel.id);
|
||||
expect(dispatchedActions.length).toBe(2);
|
||||
expect(dispatchedActions[0].type).toBe(panelModelAndPluginReady.type);
|
||||
|
||||
expect(dispatchedActions[1].payload.sourcePanel).toBe(sourcePanel);
|
||||
expect(dispatchedActions[1].payload.panel).not.toBe(sourcePanel);
|
||||
expect(dispatchedActions[1].payload.panel.id).toBe(sourcePanel.id);
|
||||
});
|
||||
});
|
||||
|
||||
@ -52,8 +57,8 @@ describe('panelEditor actions', () => {
|
||||
.whenThunkIsDispatched();
|
||||
|
||||
expect(dispatchedActions.length).toBe(2);
|
||||
expect(dispatchedActions[0].type).toBe(closeEditor.type);
|
||||
expect(dispatchedActions[1].type).toBe(cleanUpEditPanel.type);
|
||||
expect(dispatchedActions[0].type).toBe(cleanUpPanelState.type);
|
||||
expect(dispatchedActions[1].type).toBe(closeEditor.type);
|
||||
expect(sourcePanel.getOptions()).toEqual({ prop: true });
|
||||
expect(sourcePanel.id).toEqual(12);
|
||||
});
|
||||
@ -140,7 +145,7 @@ describe('panelEditor actions', () => {
|
||||
describe('when called with a panel that is the same as the modified panel', () => {
|
||||
it('then it should return true', () => {
|
||||
const meta: any = {};
|
||||
const modified: any = { editSourceId: 14, libraryPanel: { uid: '123', name: 'Name', meta, version: 1 } };
|
||||
const modified: any = { id: 14, libraryPanel: { uid: '123', name: 'Name', meta, version: 1 } };
|
||||
const panel: any = { id: 14, libraryPanel: { uid: '123', name: 'Name', meta, version: 1 } };
|
||||
|
||||
expect(skipPanelUpdate(modified, panel)).toEqual(true);
|
||||
|
@ -8,14 +8,17 @@ import {
|
||||
setPanelEditorUIState,
|
||||
updateEditorInitState,
|
||||
} from './reducers';
|
||||
import { cleanUpEditPanel, panelModelAndPluginReady } from '../../../state/reducers';
|
||||
import { cleanUpPanelState, panelModelAndPluginReady } from 'app/features/panel/state/reducers';
|
||||
import store from 'app/core/store';
|
||||
import { pick } from 'lodash';
|
||||
import { initPanelState } from 'app/features/panel/state/actions';
|
||||
|
||||
export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardModel): ThunkResult<void> {
|
||||
return (dispatch) => {
|
||||
return async (dispatch) => {
|
||||
const panel = dashboard.initEditPanel(sourcePanel);
|
||||
|
||||
await dispatch(initPanelState(panel));
|
||||
|
||||
dispatch(
|
||||
updateEditorInitState({
|
||||
panel,
|
||||
@ -60,7 +63,9 @@ export function updateDuplicateLibraryPanels(
|
||||
panel.configRev++;
|
||||
|
||||
if (pluginChanged) {
|
||||
dispatch(panelModelAndPluginReady({ panelId: panel.id, plugin: panel.plugin! }));
|
||||
panel.generateNewKey();
|
||||
|
||||
dispatch(panelModelAndPluginReady({ key: panel.key, plugin: panel.plugin! }));
|
||||
}
|
||||
|
||||
// Resend last query result on source panel query runner
|
||||
@ -85,7 +90,7 @@ export function skipPanelUpdate(modifiedPanel: PanelModel, panelToUpdate: PanelM
|
||||
}
|
||||
|
||||
// don't update the modifiedPanel twice
|
||||
if (panelToUpdate.id && panelToUpdate.id === modifiedPanel.editSourceId) {
|
||||
if (panelToUpdate.id && panelToUpdate.id === modifiedPanel.id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -101,27 +106,28 @@ export function exitPanelEditor(): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const dashboard = getStore().dashboard.getModel();
|
||||
const { getPanel, getSourcePanel, shouldDiscardChanges } = getStore().panelEditor;
|
||||
const panel = getPanel();
|
||||
|
||||
if (dashboard) {
|
||||
dashboard.exitPanelEditor();
|
||||
}
|
||||
|
||||
if (!shouldDiscardChanges) {
|
||||
const panel = getPanel();
|
||||
const modifiedSaveModel = panel.getSaveModel();
|
||||
const sourcePanel = getSourcePanel();
|
||||
const panelTypeChanged = sourcePanel.type !== panel.type;
|
||||
|
||||
dispatch(updateDuplicateLibraryPanels(panel, dashboard));
|
||||
|
||||
// restore the source panel ID before we update source panel
|
||||
modifiedSaveModel.id = sourcePanel.id;
|
||||
|
||||
sourcePanel.restoreModel(modifiedSaveModel);
|
||||
sourcePanel.configRev++; // force check the configs
|
||||
|
||||
// Loaded plugin is not included in the persisted properties
|
||||
// So is not handled by restoreModel
|
||||
sourcePanel.plugin = panel.plugin;
|
||||
|
||||
if (panelTypeChanged) {
|
||||
await dispatch(panelModelAndPluginReady({ panelId: sourcePanel.id, plugin: panel.plugin! }));
|
||||
// Loaded plugin is not included in the persisted properties so is not handled by restoreModel
|
||||
sourcePanel.plugin = panel.plugin;
|
||||
sourcePanel.generateNewKey();
|
||||
|
||||
await dispatch(panelModelAndPluginReady({ key: sourcePanel.key, plugin: panel.plugin! }));
|
||||
}
|
||||
|
||||
// Resend last query result on source panel query runner
|
||||
@ -132,12 +138,8 @@ export function exitPanelEditor(): ThunkResult<void> {
|
||||
}, 20);
|
||||
}
|
||||
|
||||
if (dashboard) {
|
||||
dashboard.exitPanelEditor();
|
||||
}
|
||||
|
||||
dispatch(cleanUpPanelState({ key: panel.key }));
|
||||
dispatch(closeEditor());
|
||||
dispatch(cleanUpEditPanel());
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,12 @@ jest.mock('app/features/dashboard/components/DashboardSettings/GeneralSettings',
|
||||
return { GeneralSettings };
|
||||
});
|
||||
|
||||
jest.mock('app/features/query/components/QueryGroup', () => {
|
||||
return {
|
||||
QueryGroup: () => null,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('app/core/core', () => ({
|
||||
appEvents: {
|
||||
subscribe: () => {
|
||||
|
@ -94,6 +94,7 @@ export class SoloPanelPage extends Component<Props, State> {
|
||||
}
|
||||
return (
|
||||
<DashboardPanel
|
||||
stateKey={panel.key}
|
||||
width={width}
|
||||
height={height}
|
||||
dashboard={dashboard}
|
||||
|
@ -179,7 +179,7 @@ export class DashboardGrid extends PureComponent<Props, State> {
|
||||
isViewing={panel.isViewing}
|
||||
>
|
||||
{(width: number, height: number) => {
|
||||
return this.renderPanel(panel, width, height, panel.key);
|
||||
return this.renderPanel(panel, width, height);
|
||||
}}
|
||||
</GrafanaGridItem>
|
||||
);
|
||||
@ -188,18 +188,19 @@ export class DashboardGrid extends PureComponent<Props, State> {
|
||||
return panelElements;
|
||||
}
|
||||
|
||||
renderPanel(panel: PanelModel, width: any, height: any, itemKey: string) {
|
||||
renderPanel(panel: PanelModel, width: any, height: any) {
|
||||
if (panel.type === 'row') {
|
||||
return <DashboardRow key={itemKey} panel={panel} dashboard={this.props.dashboard} />;
|
||||
return <DashboardRow key={panel.key} panel={panel} dashboard={this.props.dashboard} />;
|
||||
}
|
||||
|
||||
if (panel.type === 'add-panel') {
|
||||
return <AddPanelWidget key={itemKey} panel={panel} dashboard={this.props.dashboard} />;
|
||||
return <AddPanelWidget key={panel.key} panel={panel} dashboard={this.props.dashboard} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardPanel
|
||||
key={itemKey}
|
||||
key={panel.key}
|
||||
stateKey={panel.key}
|
||||
panel={panel}
|
||||
dashboard={this.props.dashboard}
|
||||
isEditing={panel.isEditing}
|
||||
|
@ -1,27 +1,23 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
// Components
|
||||
import { PanelChrome } from './PanelChrome';
|
||||
import { PanelChromeAngular } from './PanelChromeAngular';
|
||||
|
||||
// Actions
|
||||
import { initDashboardPanel } from '../state/actions';
|
||||
|
||||
// Types
|
||||
import { DashboardModel, PanelModel } from '../state';
|
||||
import { StoreState } from 'app/types';
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { initPanelState } from '../../panel/state/actions';
|
||||
import { cleanUpPanelState } from '../../panel/state/reducers';
|
||||
|
||||
export interface OwnProps {
|
||||
panel: PanelModel;
|
||||
stateKey: string;
|
||||
dashboard: DashboardModel;
|
||||
isEditing: boolean;
|
||||
isViewing: boolean;
|
||||
isInView: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
skipStateCleanUp?: boolean;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
@ -29,7 +25,7 @@ export interface State {
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState, props: OwnProps) => {
|
||||
const panelState = state.dashboard.panels[props.panel.id];
|
||||
const panelState = state.panels[props.stateKey];
|
||||
if (!panelState) {
|
||||
return { plugin: null };
|
||||
}
|
||||
@ -41,7 +37,8 @@ const mapStateToProps = (state: StoreState, props: OwnProps) => {
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
initDashboardPanel,
|
||||
initPanelState,
|
||||
cleanUpPanelState,
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
@ -60,7 +57,16 @@ export class DashboardPanelUnconnected extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.initDashboardPanel(this.props.panel);
|
||||
if (!this.props.plugin) {
|
||||
this.props.initPanelState(this.props.panel);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Most of the time an unmount should result in cleanup but in PanelEdit it should not
|
||||
if (!this.props.skipStateCleanUp) {
|
||||
this.props.cleanUpPanelState({ key: this.props.stateKey });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
@ -37,7 +37,7 @@ import { deleteAnnotation, saveAnnotation, updateAnnotation } from '../../annota
|
||||
import { getDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner';
|
||||
import { liveTimer } from './liveTimer';
|
||||
import { isSoloRoute } from '../../../routes/utils';
|
||||
import { setPanelInstanceState } from '../state/reducers';
|
||||
import { setPanelInstanceState } from '../../panel/state/reducers';
|
||||
import { store } from 'app/store/store';
|
||||
|
||||
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
||||
@ -63,7 +63,7 @@ export interface State {
|
||||
liveTime?: TimeRange;
|
||||
}
|
||||
|
||||
export class PanelChrome extends Component<Props, State> {
|
||||
export class PanelChrome extends PureComponent<Props, State> {
|
||||
private readonly timeSrv: TimeSrv = getTimeSrv();
|
||||
private subs = new Subscription();
|
||||
private eventFilter: EventFilterOptions = { onlyLocal: true };
|
||||
@ -103,7 +103,7 @@ export class PanelChrome extends Component<Props, State> {
|
||||
});
|
||||
|
||||
// Set redux panel state so panel options can get notified
|
||||
store.dispatch(setPanelInstanceState({ panelId: this.props.panel.id, value }));
|
||||
store.dispatch(setPanelInstanceState({ key: this.props.panel.key, value }));
|
||||
};
|
||||
|
||||
getPanelContextApp() {
|
||||
@ -221,19 +221,6 @@ export class PanelChrome extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(prevProps: Props, prevState: State) {
|
||||
const { plugin, panel } = this.props;
|
||||
|
||||
// If plugin changed we need to process fieldOverrides again
|
||||
// We do this by asking panel query runner to resend last result
|
||||
if (prevProps.plugin !== plugin) {
|
||||
panel.getQueryRunner().resendLastResult();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Updates the response with information from the stream
|
||||
// The next is outside a react synthetic event so setState is not batched
|
||||
// So in this context we can only do a single call to setState
|
||||
|
@ -8,12 +8,13 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { PanelHeader } from './PanelHeader/PanelHeader';
|
||||
import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
|
||||
import { setPanelAngularComponent } from '../state/reducers';
|
||||
import { setPanelAngularComponent } from 'app/features/panel/state/reducers';
|
||||
import config from 'app/core/config';
|
||||
import { DashboardModel, PanelModel } from '../state';
|
||||
import { StoreState } from 'app/types';
|
||||
import { PANEL_BORDER } from 'app/core/constants';
|
||||
import { isSoloRoute } from '../../../routes/utils';
|
||||
import { getPanelStateForModel } from 'app/features/panel/state/selectors';
|
||||
|
||||
interface OwnProps {
|
||||
panel: PanelModel;
|
||||
@ -27,7 +28,7 @@ interface OwnProps {
|
||||
}
|
||||
|
||||
interface ConnectedProps {
|
||||
angularComponent?: AngularComponent | null;
|
||||
angularComponent?: AngularComponent;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
@ -98,7 +99,6 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.cleanUpAngularPanel();
|
||||
this.subs.unsubscribe();
|
||||
}
|
||||
|
||||
@ -106,7 +106,6 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
||||
const { plugin, height, width, panel } = this.props;
|
||||
|
||||
if (prevProps.plugin !== plugin) {
|
||||
this.cleanUpAngularPanel();
|
||||
this.loadAngularPanel();
|
||||
}
|
||||
|
||||
@ -154,21 +153,11 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
setPanelAngularComponent({
|
||||
panelId: panel.id,
|
||||
key: panel.key,
|
||||
angularComponent: loader.load(this.element, this.scopeProps, template),
|
||||
});
|
||||
}
|
||||
|
||||
cleanUpAngularPanel() {
|
||||
const { angularComponent, setPanelAngularComponent, panel } = this.props;
|
||||
|
||||
if (angularComponent) {
|
||||
angularComponent.destroy();
|
||||
}
|
||||
|
||||
setPanelAngularComponent({ panelId: panel.id, angularComponent: null });
|
||||
}
|
||||
|
||||
hasOverlayHeader() {
|
||||
const { panel } = this.props;
|
||||
const { data } = this.state;
|
||||
@ -226,7 +215,7 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
|
||||
return {
|
||||
angularComponent: state.dashboard.panels[props.panel.id].angularComponent,
|
||||
angularComponent: getPanelStateForModel(state, props.panel)?.angularComponent,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { PanelMenuItem } from '@grafana/data';
|
||||
import { DashboardModel, PanelModel } from '../../state';
|
||||
import { StoreState } from '../../../../types';
|
||||
import { getPanelMenu } from '../../utils/getPanelMenu';
|
||||
import { getPanelStateForModel } from 'app/features/panel/state/selectors';
|
||||
|
||||
interface PanelHeaderMenuProviderApi {
|
||||
items: PanelMenuItem[];
|
||||
@ -18,9 +19,8 @@ interface Props {
|
||||
|
||||
export const PanelHeaderMenuProvider: FC<Props> = ({ panel, dashboard, children }) => {
|
||||
const [items, setItems] = useState<PanelMenuItem[]>([]);
|
||||
const angularComponent = useSelector(
|
||||
(state: StoreState) => state.dashboard.panels[panel.id]?.angularComponent || null
|
||||
);
|
||||
const angularComponent = useSelector((state: StoreState) => getPanelStateForModel(state, panel)?.angularComponent);
|
||||
|
||||
useEffect(() => {
|
||||
setItems(getPanelMenu(dashboard, panel, angularComponent));
|
||||
}, [dashboard, panel, angularComponent, setItems]);
|
||||
|
@ -285,11 +285,8 @@ export class DashboardModel {
|
||||
.map((panel: PanelModel) => {
|
||||
// If we save while editing we should include the panel in edit mode instead of the
|
||||
// unmodified source panel
|
||||
if (this.panelInEdit && this.panelInEdit.editSourceId === panel.id) {
|
||||
const saveModel = this.panelInEdit.getSaveModel();
|
||||
// while editing a panel we modify its id, need to restore it here
|
||||
saveModel.id = this.panelInEdit.editSourceId;
|
||||
return saveModel;
|
||||
if (this.panelInEdit && this.panelInEdit.id === panel.id) {
|
||||
return this.panelInEdit.getSaveModel();
|
||||
}
|
||||
|
||||
return panel.getSaveModel();
|
||||
|
@ -265,7 +265,6 @@ describe('PanelModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
model.editSourceId = 1001;
|
||||
model.fieldConfig.defaults.decimals = 3;
|
||||
model.fieldConfig.defaults.custom = {
|
||||
customProp: true,
|
||||
@ -289,10 +288,6 @@ describe('PanelModel', () => {
|
||||
model.alert = { id: 2 };
|
||||
});
|
||||
|
||||
it('should keep editSourceId', () => {
|
||||
expect(model.editSourceId).toBe(1001);
|
||||
});
|
||||
|
||||
it('should keep maxDataPoints', () => {
|
||||
expect(model.maxDataPoints).toBe(100);
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
// Libraries
|
||||
import { cloneDeep, defaultsDeep, isArray, isEqual, keys } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
// Utils
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||
@ -20,7 +21,6 @@ import {
|
||||
PanelModel as IPanelModel,
|
||||
DatasourceRef,
|
||||
} from '@grafana/data';
|
||||
import { EDIT_PANEL_ID } from 'app/core/constants';
|
||||
import config from 'app/core/config';
|
||||
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
|
||||
import {
|
||||
@ -61,7 +61,6 @@ const notPersistedProperties: { [str: string]: boolean } = {
|
||||
plugin: true,
|
||||
queryRunner: true,
|
||||
replaceVariables: true,
|
||||
editSourceId: true,
|
||||
configRev: true,
|
||||
getDisplayTitle: true,
|
||||
dataSupport: true,
|
||||
@ -104,13 +103,13 @@ const mustKeepProps: { [str: string]: boolean } = {
|
||||
queryRunner: true,
|
||||
transformations: true,
|
||||
fieldConfig: true,
|
||||
editSourceId: true,
|
||||
maxDataPoints: true,
|
||||
interval: true,
|
||||
replaceVariables: true,
|
||||
libraryPanel: true,
|
||||
getDisplayTitle: true,
|
||||
configRev: true,
|
||||
key: true,
|
||||
};
|
||||
|
||||
const defaults: any = {
|
||||
@ -130,7 +129,6 @@ const defaults: any = {
|
||||
export class PanelModel implements DataConfigSource, IPanelModel {
|
||||
/* persisted id, used in URL to identify a panel */
|
||||
id!: number;
|
||||
editSourceId?: number;
|
||||
gridPos!: GridPos;
|
||||
type!: string;
|
||||
title!: string;
|
||||
@ -178,7 +176,11 @@ export class PanelModel implements DataConfigSource, IPanelModel {
|
||||
cachedPluginOptions: Record<string, PanelOptionsCache> = {};
|
||||
legend?: { show: boolean; sort?: string; sortDesc?: boolean };
|
||||
plugin?: PanelPlugin;
|
||||
key: string; // unique in dashboard, changes will force a react reload
|
||||
/**
|
||||
* Unique in application state, this is used as redux key for panel and for redux panel state
|
||||
* Change will cause unmount and re-init of panel
|
||||
*/
|
||||
key: string;
|
||||
|
||||
/**
|
||||
* The PanelModel event bus only used for internal and legacy angular support.
|
||||
@ -192,7 +194,7 @@ export class PanelModel implements DataConfigSource, IPanelModel {
|
||||
this.events = new EventBusSrv();
|
||||
this.restoreModel(model);
|
||||
this.replaceVariables = this.replaceVariables.bind(this);
|
||||
this.key = this.id ? `${this.id}` : `panel-${Math.floor(Math.random() * 100000)}`;
|
||||
this.key = uuidv4();
|
||||
}
|
||||
|
||||
/** Given a persistened PanelModel restores property values */
|
||||
@ -230,6 +232,10 @@ export class PanelModel implements DataConfigSource, IPanelModel {
|
||||
this.ensureQueryIds();
|
||||
}
|
||||
|
||||
generateNewKey() {
|
||||
this.key = uuidv4();
|
||||
}
|
||||
|
||||
ensureQueryIds() {
|
||||
if (this.targets && isArray(this.targets)) {
|
||||
for (const query of this.targets) {
|
||||
@ -303,7 +309,7 @@ export class PanelModel implements DataConfigSource, IPanelModel {
|
||||
this.getQueryRunner().run({
|
||||
datasource: this.datasource,
|
||||
queries: this.targets,
|
||||
panelId: this.editSourceId || this.id,
|
||||
panelId: this.id,
|
||||
dashboardId: dashboardId,
|
||||
timezone: dashboardTimezone,
|
||||
timeRange: timeData.timeRange,
|
||||
@ -475,12 +481,9 @@ export class PanelModel implements DataConfigSource, IPanelModel {
|
||||
getEditClone() {
|
||||
const sourceModel = this.getSaveModel();
|
||||
|
||||
// Temporary id for the clone, restored later in redux action when changes are saved
|
||||
sourceModel.id = EDIT_PANEL_ID;
|
||||
sourceModel.editSourceId = this.id;
|
||||
|
||||
const clone = new PanelModel(sourceModel);
|
||||
clone.isEditing = true;
|
||||
|
||||
const sourceQueryRunner = this.getQueryRunner();
|
||||
|
||||
// Copy last query result
|
||||
@ -590,14 +593,6 @@ export class PanelModel implements DataConfigSource, IPanelModel {
|
||||
this.getQueryRunner().resendLastResult();
|
||||
}
|
||||
|
||||
/*
|
||||
* Panel have a different id while in edit mode (to more easily be able to discard changes)
|
||||
* Use this to always get the underlying source id
|
||||
* */
|
||||
getSavedId(): number {
|
||||
return this.editSourceId ?? this.id;
|
||||
}
|
||||
|
||||
/*
|
||||
* This is the title used when displaying the title in the UI so it will include any interpolated variables.
|
||||
* If you need the raw title without interpolation use title property instead.
|
||||
|
@ -3,20 +3,12 @@ import { getBackendSrv } from '@grafana/runtime';
|
||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||
// Actions
|
||||
import { loadPluginDashboards } from '../../plugins/state/actions';
|
||||
import {
|
||||
cleanUpDashboard,
|
||||
loadDashboardPermissions,
|
||||
panelModelAndPluginReady,
|
||||
setPanelAngularComponent,
|
||||
} from './reducers';
|
||||
import { cleanUpDashboard, loadDashboardPermissions } from './reducers';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { loadPanelPlugin } from 'app/features/plugins/state/actions';
|
||||
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
||||
// Types
|
||||
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel, ThunkResult } from 'app/types';
|
||||
import { PanelModel } from './PanelModel';
|
||||
import { cancelVariables } from '../../variables/state/actions';
|
||||
import { getPanelPluginNotFound } from '../dashgrid/PanelPluginError';
|
||||
import { getTimeSrv } from '../services/TimeSrv';
|
||||
import { TimeZone } from '@grafana/data';
|
||||
|
||||
@ -121,55 +113,6 @@ export function removeDashboard(uri: string): ThunkResult<void> {
|
||||
};
|
||||
}
|
||||
|
||||
export function initDashboardPanel(panel: PanelModel): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
let pluginToLoad = panel.type;
|
||||
let plugin = getStore().plugins.panels[pluginToLoad];
|
||||
|
||||
if (!plugin) {
|
||||
try {
|
||||
plugin = await dispatch(loadPanelPlugin(pluginToLoad));
|
||||
} catch (e) {
|
||||
// When plugin not found
|
||||
plugin = getPanelPluginNotFound(pluginToLoad, pluginToLoad === 'row');
|
||||
}
|
||||
}
|
||||
|
||||
if (!panel.plugin) {
|
||||
panel.pluginLoaded(plugin);
|
||||
}
|
||||
|
||||
dispatch(panelModelAndPluginReady({ panelId: panel.id, plugin }));
|
||||
};
|
||||
}
|
||||
|
||||
export function changePanelPlugin(panel: PanelModel, pluginId: string): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
// ignore action is no change
|
||||
if (panel.type === pluginId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const store = getStore();
|
||||
let plugin = store.plugins.panels[pluginId];
|
||||
|
||||
if (!plugin) {
|
||||
plugin = await dispatch(loadPanelPlugin(pluginId));
|
||||
}
|
||||
|
||||
// clean up angular component (scope / ctrl state)
|
||||
const angularComponent = store.dashboard.panels[panel.id].angularComponent;
|
||||
if (angularComponent) {
|
||||
angularComponent.destroy();
|
||||
dispatch(setPanelAngularComponent({ panelId: panel.id, angularComponent: null }));
|
||||
}
|
||||
|
||||
panel.changePlugin(plugin);
|
||||
|
||||
dispatch(panelModelAndPluginReady({ panelId: panel.id, plugin }));
|
||||
};
|
||||
}
|
||||
|
||||
export const cleanUpDashboardAndVariables = (): ThunkResult<void> => (dispatch, getStore) => {
|
||||
const store = getStore();
|
||||
const dashboard = store.dashboard.getModel();
|
||||
|
@ -51,11 +51,6 @@ 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', () => {
|
||||
|
@ -1,14 +1,12 @@
|
||||
import { createSlice, PayloadAction, Draft } from '@reduxjs/toolkit';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import {
|
||||
DashboardAclDTO,
|
||||
DashboardInitError,
|
||||
DashboardInitPhase,
|
||||
DashboardState,
|
||||
PanelState,
|
||||
QueriesToUpdateOnDashboardLoad,
|
||||
} from 'app/types';
|
||||
import { AngularComponent } from '@grafana/runtime';
|
||||
import { EDIT_PANEL_ID } from 'app/core/constants';
|
||||
import { processAclItems } from 'app/core/utils/acl';
|
||||
import { panelEditorReducer } from '../components/PanelEditor/state/reducers';
|
||||
import { DashboardModel } from './DashboardModel';
|
||||
@ -21,7 +19,6 @@ export const initialState: DashboardState = {
|
||||
getModel: () => null,
|
||||
permissions: [],
|
||||
modifiedQueries: null,
|
||||
panels: {},
|
||||
initError: null,
|
||||
};
|
||||
|
||||
@ -45,12 +42,6 @@ const dashbardSlice = createSlice({
|
||||
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;
|
||||
@ -60,7 +51,6 @@ const dashbardSlice = createSlice({
|
||||
};
|
||||
},
|
||||
cleanUpDashboard: (state) => {
|
||||
state.panels = {};
|
||||
state.initPhase = DashboardInitPhase.NotStarted;
|
||||
state.isInitSlow = false;
|
||||
state.initError = null;
|
||||
@ -72,32 +62,12 @@ const dashbardSlice = createSlice({
|
||||
clearDashboardQueriesToUpdateOnLoad: (state) => {
|
||||
state.modifiedQueries = null;
|
||||
},
|
||||
panelModelAndPluginReady: (state, action: PayloadAction<PanelModelAndPluginReadyPayload>) => {
|
||||
updatePanelState(state, action.payload.panelId, { plugin: action.payload.plugin });
|
||||
},
|
||||
cleanUpEditPanel: (state) => {
|
||||
delete state.panels[EDIT_PANEL_ID];
|
||||
},
|
||||
setPanelInstanceState: (state, action: PayloadAction<SetPanelInstanceStatePayload>) => {
|
||||
updatePanelState(state, action.payload.panelId, { instanceState: action.payload.value });
|
||||
},
|
||||
setPanelAngularComponent: (state, action: PayloadAction<SetPanelAngularComponentPayload>) => {
|
||||
updatePanelState(state, action.payload.panelId, { angularComponent: action.payload.angularComponent });
|
||||
},
|
||||
addPanel: (state, action: PayloadAction<PanelModel>) => {
|
||||
state.panels[action.payload.id] = { pluginId: action.payload.type };
|
||||
//state.panels[action.payload.id] = { pluginId: action.payload.type };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function updatePanelState(state: Draft<DashboardState>, panelId: number, ps: Partial<PanelState>) {
|
||||
if (!state.panels[panelId]) {
|
||||
state.panels[panelId] = ps as PanelState;
|
||||
} else {
|
||||
Object.assign(state.panels[panelId], ps);
|
||||
}
|
||||
}
|
||||
|
||||
export interface PanelModelAndPluginReadyPayload {
|
||||
panelId: number;
|
||||
plugin: PanelPlugin;
|
||||
@ -123,11 +93,7 @@ export const {
|
||||
cleanUpDashboard,
|
||||
setDashboardQueriesToUpdateOnLoad,
|
||||
clearDashboardQueriesToUpdateOnLoad,
|
||||
panelModelAndPluginReady,
|
||||
addPanel,
|
||||
cleanUpEditPanel,
|
||||
setPanelAngularComponent,
|
||||
setPanelInstanceState,
|
||||
} = dashbardSlice.actions;
|
||||
|
||||
export const dashboardReducer = dashbardSlice.reducer;
|
||||
|
@ -1,14 +1,6 @@
|
||||
import { DashboardState, PanelState, StoreState } from 'app/types';
|
||||
import { StoreState } from 'app/types';
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { getPanelPluginNotFound } from '../dashgrid/PanelPluginError';
|
||||
|
||||
export function getPanelStateById(state: DashboardState, panelId: number): PanelState {
|
||||
if (!panelId) {
|
||||
return {} as PanelState;
|
||||
}
|
||||
|
||||
return state.panels[panelId] ?? ({} as PanelState);
|
||||
}
|
||||
import { getPanelPluginNotFound } from '../../panel/components/PanelPluginError';
|
||||
|
||||
export const getPanelPluginWithFallback = (panelType: string) => (state: StoreState): PanelPlugin => {
|
||||
const plugin = state.plugins.panels[panelType];
|
||||
|
@ -6,7 +6,7 @@ import { LibraryElementDTO } from '../../types';
|
||||
import { PanelTypeCard } from 'app/features/dashboard/components/VizTypePicker/PanelTypeCard';
|
||||
import { DeleteLibraryPanelModal } from '../DeleteLibraryPanelModal/DeleteLibraryPanelModal';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { getPanelPluginNotFound } from 'app/features/dashboard/dashgrid/PanelPluginError';
|
||||
import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPluginError';
|
||||
|
||||
export interface LibraryPanelCardProps {
|
||||
libraryPanel: LibraryElementDTO;
|
||||
|
@ -7,10 +7,8 @@ import { Button, useStyles2, VerticalGroup } from '@grafana/ui';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { AddLibraryPanelModal } from '../AddLibraryPanelModal/AddLibraryPanelModal';
|
||||
import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView';
|
||||
import { PanelDirectiveReadyEvent, PanelOptionsChangedEvent, PanelQueriesChangedEvent } from 'app/types/events';
|
||||
import { LibraryElementDTO } from '../../types';
|
||||
import { toPanelModelLibraryPanel } from '../../utils';
|
||||
import { changePanelPlugin } from 'app/features/dashboard/state/actions';
|
||||
import { changeToLibraryPanel } from 'app/features/panel/state/actions';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { ChangeLibraryPanelModal } from '../ChangeLibraryPanelModal/ChangeLibraryPanelModal';
|
||||
import { PanelTypeFilter } from '../../../../core/components/PanelTypeFilter/PanelTypeFilter';
|
||||
@ -38,29 +36,10 @@ export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
|
||||
if (!changeToPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
setChangeToPanel(undefined);
|
||||
|
||||
const panelTypeChanged = panel.type !== changeToPanel.model.type;
|
||||
|
||||
if (panelTypeChanged) {
|
||||
await dispatch(changePanelPlugin(panel, changeToPanel.model.type));
|
||||
}
|
||||
|
||||
panel.restoreModel({
|
||||
...changeToPanel.model,
|
||||
gridPos: panel.gridPos,
|
||||
id: panel.id,
|
||||
libraryPanel: toPanelModelLibraryPanel(changeToPanel),
|
||||
});
|
||||
|
||||
panel.configRev = 0;
|
||||
panel.refresh();
|
||||
const unsubscribeEvent = panel.events.subscribe(PanelDirectiveReadyEvent, () => {
|
||||
panel.refresh();
|
||||
unsubscribeEvent.unsubscribe();
|
||||
});
|
||||
panel.events.publish(PanelQueriesChangedEvent);
|
||||
panel.events.publish(PanelOptionsChangedEvent);
|
||||
dispatch(changeToLibraryPanel(panel, changeToPanel));
|
||||
};
|
||||
|
||||
const onAddToPanelLibrary = () => {
|
||||
|
@ -3,10 +3,12 @@ import { applyFieldOverrides, FieldConfigSource, getTimeZone, PanelData, PanelPl
|
||||
import { PanelRendererProps } from '@grafana/runtime';
|
||||
import { appEvents } from 'app/core/core';
|
||||
import { useAsync } from 'react-use';
|
||||
import { getPanelOptionsWithDefaults, OptionDefaults } from '../dashboard/state/getPanelOptionsWithDefaults';
|
||||
import { importPanelPlugin } from '../plugins/plugin_loader';
|
||||
import { getPanelOptionsWithDefaults, OptionDefaults } from '../../dashboard/state/getPanelOptionsWithDefaults';
|
||||
import { importPanelPlugin } from '../../plugins/importPanelPlugin';
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
|
||||
const defaultFieldConfig = { defaults: {}, overrides: [] };
|
||||
|
||||
export function PanelRenderer<P extends object = any, F extends object = any>(props: PanelRendererProps<P, F>) {
|
||||
const {
|
||||
pluginId,
|
39
public/app/features/panel/state/actions.test.ts
Normal file
39
public/app/features/panel/state/actions.test.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { thunkTester } from '../../../../test/core/thunk/thunkTester';
|
||||
import { changePanelPlugin } from './actions';
|
||||
import { panelModelAndPluginReady } from './reducers';
|
||||
import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks';
|
||||
|
||||
jest.mock('app/features/plugins/importPanelPlugin', () => {
|
||||
return {
|
||||
importPanelPlugin: function () {
|
||||
return Promise.resolve(
|
||||
getPanelPlugin({
|
||||
id: 'table',
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('panel state actions', () => {
|
||||
describe('changePanelPlugin', () => {
|
||||
it('Should load plugin and call changePlugin', async () => {
|
||||
const sourcePanel = new PanelModel({ id: 12, type: 'graph' });
|
||||
|
||||
const dispatchedActions = await thunkTester({
|
||||
plugins: {
|
||||
panels: {},
|
||||
},
|
||||
panels: {},
|
||||
})
|
||||
.givenThunk(changePanelPlugin)
|
||||
.whenThunkIsDispatched(sourcePanel, 'table');
|
||||
|
||||
expect(dispatchedActions.length).toBe(2);
|
||||
expect(dispatchedActions[0].type).toBe('plugins/loadPanelPlugin/fulfilled');
|
||||
expect(dispatchedActions[1].type).toBe(panelModelAndPluginReady.type);
|
||||
expect(sourcePanel.type).toBe('table');
|
||||
});
|
||||
});
|
||||
});
|
94
public/app/features/panel/state/actions.ts
Normal file
94
public/app/features/panel/state/actions.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPluginError';
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import { loadPanelPlugin } from 'app/features/plugins/state/actions';
|
||||
import { ThunkResult } from 'app/types';
|
||||
import { panelModelAndPluginReady } from './reducers';
|
||||
import { LibraryElementDTO } from 'app/features/library-panels/types';
|
||||
import { toPanelModelLibraryPanel } from 'app/features/library-panels/utils';
|
||||
import { PanelOptionsChangedEvent, PanelQueriesChangedEvent } from 'app/types/events';
|
||||
|
||||
export function initPanelState(panel: PanelModel): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
let pluginToLoad = panel.type;
|
||||
let plugin = getStore().plugins.panels[pluginToLoad];
|
||||
|
||||
if (!plugin) {
|
||||
try {
|
||||
plugin = await dispatch(loadPanelPlugin(pluginToLoad));
|
||||
} catch (e) {
|
||||
// When plugin not found
|
||||
plugin = getPanelPluginNotFound(pluginToLoad, pluginToLoad === 'row');
|
||||
}
|
||||
}
|
||||
|
||||
if (!panel.plugin) {
|
||||
panel.pluginLoaded(plugin);
|
||||
}
|
||||
|
||||
dispatch(panelModelAndPluginReady({ key: panel.key, plugin }));
|
||||
};
|
||||
}
|
||||
|
||||
export function changePanelPlugin(panel: PanelModel, pluginId: string): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
// ignore action is no change
|
||||
if (panel.type === pluginId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const store = getStore();
|
||||
let plugin = store.plugins.panels[pluginId];
|
||||
|
||||
if (!plugin) {
|
||||
plugin = await dispatch(loadPanelPlugin(pluginId));
|
||||
}
|
||||
|
||||
const oldKey = panel.key;
|
||||
|
||||
panel.changePlugin(plugin);
|
||||
panel.generateNewKey();
|
||||
|
||||
dispatch(panelModelAndPluginReady({ key: panel.key, plugin, cleanUpKey: oldKey }));
|
||||
};
|
||||
}
|
||||
|
||||
export function changeToLibraryPanel(panel: PanelModel, libraryPanel: LibraryElementDTO): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const newPluginId = libraryPanel.model.type;
|
||||
const oldType = panel.type;
|
||||
|
||||
// Update model but preserve gridPos & id
|
||||
panel.restoreModel({
|
||||
...libraryPanel.model,
|
||||
gridPos: panel.gridPos,
|
||||
id: panel.id,
|
||||
libraryPanel: toPanelModelLibraryPanel(libraryPanel.model),
|
||||
});
|
||||
|
||||
// a new library panel usually means new queries, clear any current result
|
||||
panel.getQueryRunner().clearLastResult();
|
||||
|
||||
// Handle plugin change
|
||||
if (oldType !== newPluginId) {
|
||||
const store = getStore();
|
||||
let plugin = store.plugins.panels[newPluginId];
|
||||
|
||||
if (!plugin) {
|
||||
plugin = await dispatch(loadPanelPlugin(newPluginId));
|
||||
}
|
||||
|
||||
const oldKey = panel.key;
|
||||
|
||||
panel.pluginLoaded(plugin);
|
||||
panel.generateNewKey();
|
||||
|
||||
await dispatch(panelModelAndPluginReady({ key: panel.key, plugin, cleanUpKey: oldKey }));
|
||||
}
|
||||
|
||||
panel.configRev = 0;
|
||||
panel.refresh();
|
||||
|
||||
panel.events.publish(PanelQueriesChangedEvent);
|
||||
panel.events.publish(PanelOptionsChangedEvent);
|
||||
};
|
||||
}
|
78
public/app/features/panel/state/reducers.ts
Normal file
78
public/app/features/panel/state/reducers.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { createSlice, Draft, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { AngularComponent } from '@grafana/runtime';
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
|
||||
export type RootPanelsState = Record<string, PanelState>;
|
||||
|
||||
export interface PanelState {
|
||||
plugin?: PanelPlugin;
|
||||
angularComponent?: AngularComponent;
|
||||
instanceState?: any | null;
|
||||
}
|
||||
|
||||
export const initialState: RootPanelsState = {};
|
||||
|
||||
const panelsSlice = createSlice({
|
||||
name: 'panels',
|
||||
initialState,
|
||||
reducers: {
|
||||
panelModelAndPluginReady: (state, action: PayloadAction<PanelModelAndPluginReadyPayload>) => {
|
||||
if (action.payload.cleanUpKey) {
|
||||
cleanUpAngularComponent(state[action.payload.cleanUpKey]);
|
||||
delete state[action.payload.cleanUpKey];
|
||||
}
|
||||
|
||||
state[action.payload.key] = {
|
||||
plugin: action.payload.plugin,
|
||||
};
|
||||
},
|
||||
cleanUpPanelState: (state, action: PayloadAction<{ key: string }>) => {
|
||||
cleanUpAngularComponent(state[action.payload.key]);
|
||||
delete state[action.payload.key];
|
||||
},
|
||||
setPanelInstanceState: (state, action: PayloadAction<SetPanelInstanceStatePayload>) => {
|
||||
state[action.payload.key].instanceState = action.payload.value;
|
||||
},
|
||||
setPanelAngularComponent: (state, action: PayloadAction<SetPanelAngularComponentPayload>) => {
|
||||
const panelState = state[action.payload.key];
|
||||
cleanUpAngularComponent(panelState);
|
||||
panelState.angularComponent = action.payload.angularComponent;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function cleanUpAngularComponent(panelState?: Draft<PanelState>) {
|
||||
if (panelState?.angularComponent) {
|
||||
panelState.angularComponent.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export interface PanelModelAndPluginReadyPayload {
|
||||
key: string;
|
||||
plugin: PanelPlugin;
|
||||
/** Used to cleanup previous state when we change key (used when switching panel plugin) */
|
||||
cleanUpKey?: string;
|
||||
}
|
||||
|
||||
export interface SetPanelAngularComponentPayload {
|
||||
key: string;
|
||||
angularComponent: AngularComponent;
|
||||
}
|
||||
|
||||
export interface SetPanelInstanceStatePayload {
|
||||
key: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export const {
|
||||
panelModelAndPluginReady,
|
||||
setPanelAngularComponent,
|
||||
setPanelInstanceState,
|
||||
cleanUpPanelState,
|
||||
} = panelsSlice.actions;
|
||||
|
||||
export const panelsReducer = panelsSlice.reducer;
|
||||
|
||||
export default {
|
||||
panels: panelsReducer,
|
||||
};
|
7
public/app/features/panel/state/selectors.ts
Normal file
7
public/app/features/panel/state/selectors.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { StoreState } from 'app/types';
|
||||
import { PanelState } from './reducers';
|
||||
|
||||
export function getPanelStateForModel(state: StoreState, model: PanelModel): PanelState | undefined {
|
||||
return state.panels[model.key];
|
||||
}
|
@ -24,7 +24,8 @@ import { Alert, LinkButton, PluginSignatureBadge, Tooltip, Badge, useStyles2, Ic
|
||||
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { getPluginSettings } from './PluginSettingsCache';
|
||||
import { importAppPlugin, importDataSourcePlugin, importPanelPluginFromMeta } from './plugin_loader';
|
||||
import { importAppPlugin, importDataSourcePlugin } from './plugin_loader';
|
||||
import { importPanelPluginFromMeta } from './importPanelPlugin';
|
||||
import { getNotFoundNav } from 'app/core/nav_model_srv';
|
||||
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||
import { AppConfigCtrlWrapper } from './wrappers/AppConfigWrapper';
|
||||
|
@ -2,7 +2,7 @@ import { createAsyncThunk, Update } from '@reduxjs/toolkit';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { StoreState, ThunkResult } from 'app/types';
|
||||
import { importPanelPlugin } from 'app/features/plugins/plugin_loader';
|
||||
import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin';
|
||||
import {
|
||||
getRemotePlugins,
|
||||
getPluginErrors,
|
||||
|
53
public/app/features/plugins/importPanelPlugin.ts
Normal file
53
public/app/features/plugins/importPanelPlugin.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import config from 'app/core/config';
|
||||
import * as grafanaData from '@grafana/data';
|
||||
import { getPanelPluginLoadError } from '../panel/components/PanelPluginError';
|
||||
import { importPluginModule } from './plugin_loader';
|
||||
|
||||
interface PanelCache {
|
||||
[key: string]: Promise<grafanaData.PanelPlugin>;
|
||||
}
|
||||
const panelCache: PanelCache = {};
|
||||
|
||||
export function importPanelPlugin(id: string): Promise<grafanaData.PanelPlugin> {
|
||||
const loaded = panelCache[id];
|
||||
if (loaded) {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
const meta = config.panels[id];
|
||||
|
||||
if (!meta) {
|
||||
throw new Error(`Plugin ${id} not found`);
|
||||
}
|
||||
|
||||
panelCache[id] = getPanelPlugin(meta);
|
||||
|
||||
return panelCache[id];
|
||||
}
|
||||
|
||||
export function importPanelPluginFromMeta(meta: grafanaData.PanelPluginMeta): Promise<grafanaData.PanelPlugin> {
|
||||
return getPanelPlugin(meta);
|
||||
}
|
||||
|
||||
function getPanelPlugin(meta: grafanaData.PanelPluginMeta): Promise<grafanaData.PanelPlugin> {
|
||||
return importPluginModule(meta.module)
|
||||
.then((pluginExports) => {
|
||||
if (pluginExports.plugin) {
|
||||
return pluginExports.plugin as grafanaData.PanelPlugin;
|
||||
} else if (pluginExports.PanelCtrl) {
|
||||
const plugin = new grafanaData.PanelPlugin(null);
|
||||
plugin.angularPanelCtrl = pluginExports.PanelCtrl;
|
||||
return plugin;
|
||||
}
|
||||
throw new Error('missing export: plugin or PanelCtrl');
|
||||
})
|
||||
.then((plugin) => {
|
||||
plugin.meta = meta;
|
||||
return plugin;
|
||||
})
|
||||
.catch((err) => {
|
||||
// TODO, maybe a different error plugin
|
||||
console.warn('Error loading panel plugin: ' + meta.id, err);
|
||||
return getPanelPluginLoadError(meta, err);
|
||||
});
|
||||
}
|
@ -5,7 +5,8 @@ import config from 'app/core/config';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
import { DataSourceApi, PanelEvents } from '@grafana/data';
|
||||
import { importPanelPlugin, importDataSourcePlugin, importAppPlugin } from './plugin_loader';
|
||||
import { importDataSourcePlugin, importAppPlugin } from './plugin_loader';
|
||||
import { importPanelPlugin } from './importPanelPlugin';
|
||||
import DatasourceSrv from './datasource_srv';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
|
||||
|
@ -33,6 +33,7 @@ import * as emotion from '@emotion/css';
|
||||
import * as grafanaData from '@grafana/data';
|
||||
import * as grafanaUIraw from '@grafana/ui';
|
||||
import * as grafanaRuntime from '@grafana/runtime';
|
||||
import { GenericDataSourcePlugin } from '../datasources/settings/PluginSettings';
|
||||
|
||||
// Help the 6.4 to 6.5 migration
|
||||
// The base classes were moved from @grafana/ui to @grafana/data
|
||||
@ -218,55 +219,3 @@ export function importAppPlugin(meta: grafanaData.PluginMeta): Promise<grafanaDa
|
||||
return plugin;
|
||||
});
|
||||
}
|
||||
|
||||
import { getPanelPluginLoadError } from '../dashboard/dashgrid/PanelPluginError';
|
||||
import { GenericDataSourcePlugin } from '../datasources/settings/PluginSettings';
|
||||
|
||||
interface PanelCache {
|
||||
[key: string]: Promise<grafanaData.PanelPlugin>;
|
||||
}
|
||||
const panelCache: PanelCache = {};
|
||||
|
||||
export function importPanelPlugin(id: string): Promise<grafanaData.PanelPlugin> {
|
||||
const loaded = panelCache[id];
|
||||
if (loaded) {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
const meta = config.panels[id];
|
||||
|
||||
if (!meta) {
|
||||
throw new Error(`Plugin ${id} not found`);
|
||||
}
|
||||
|
||||
panelCache[id] = getPanelPlugin(meta);
|
||||
|
||||
return panelCache[id];
|
||||
}
|
||||
|
||||
export function importPanelPluginFromMeta(meta: grafanaData.PanelPluginMeta): Promise<grafanaData.PanelPlugin> {
|
||||
return getPanelPlugin(meta);
|
||||
}
|
||||
|
||||
function getPanelPlugin(meta: grafanaData.PanelPluginMeta): Promise<grafanaData.PanelPlugin> {
|
||||
return importPluginModule(meta.module)
|
||||
.then((pluginExports) => {
|
||||
if (pluginExports.plugin) {
|
||||
return pluginExports.plugin as grafanaData.PanelPlugin;
|
||||
} else if (pluginExports.PanelCtrl) {
|
||||
const plugin = new grafanaData.PanelPlugin(null);
|
||||
plugin.angularPanelCtrl = pluginExports.PanelCtrl;
|
||||
return plugin;
|
||||
}
|
||||
throw new Error('missing export: plugin or PanelCtrl');
|
||||
})
|
||||
.then((plugin) => {
|
||||
plugin.meta = meta;
|
||||
return plugin;
|
||||
})
|
||||
.catch((err) => {
|
||||
// TODO, maybe a different error plugin
|
||||
console.warn('Error loading panel plugin: ' + meta.id, err);
|
||||
return getPanelPluginLoadError(meta, err);
|
||||
});
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { getBackendSrv } from '@grafana/runtime';
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { ThunkResult } from 'app/types';
|
||||
import { config } from 'app/core/config';
|
||||
import { importPanelPlugin } from 'app/features/plugins/plugin_loader';
|
||||
import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin';
|
||||
import {
|
||||
loadPanelPlugin as loadPanelPluginNew,
|
||||
loadPluginDashboards as loadPluginDashboardsNew,
|
||||
|
@ -257,8 +257,7 @@ export class PanelQueryRunner {
|
||||
|
||||
if (dataSupport.alertStates || dataSupport.annotations) {
|
||||
const panel = (this.dataConfigSource as unknown) as PanelModel;
|
||||
const id = panel.editSourceId ?? panel.id;
|
||||
panelData = mergePanelAndDashData(observable, getDashboardQueryRunner().getResult(id));
|
||||
panelData = mergePanelAndDashData(observable, getDashboardQueryRunner().getResult(panel.id));
|
||||
}
|
||||
|
||||
this.subscription = panelData.subscribe({
|
||||
@ -292,6 +291,12 @@ export class PanelQueryRunner {
|
||||
}
|
||||
};
|
||||
|
||||
clearLastResult() {
|
||||
this.lastResult = undefined;
|
||||
// A new subject is also needed since it's a replay subject that remembers/sends last value
|
||||
this.subject = new ReplaySubject(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the panel is closed
|
||||
*/
|
||||
|
@ -19,7 +19,7 @@ import { DataWarning, GraphFieldConfig, GraphPanelOptions } from './types';
|
||||
import { auto } from 'angular';
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { getDataTimeRange } from './utils';
|
||||
import { changePanelPlugin } from 'app/features/dashboard/state/actions';
|
||||
import { changePanelPlugin } from 'app/features/panel/state/actions';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { ThresholdMapper } from 'app/features/alerting/state/ThresholdMapper';
|
||||
import { appEvents } from '../../../core/core';
|
||||
|
@ -3,10 +3,9 @@ import { loadPluginCss } from '@grafana/runtime';
|
||||
import { PanelCtrl as PanelCtrlES6 } from 'app/features/panel/panel_ctrl';
|
||||
import { MetricsPanelCtrl as MetricsPanelCtrlES6 } from 'app/features/panel/metrics_panel_ctrl';
|
||||
import { QueryCtrl as QueryCtrlES6 } from 'app/features/panel/query_ctrl';
|
||||
import { alertTab } from 'app/features/alerting/AlertTabCtrl';
|
||||
|
||||
const PanelCtrl = makeClassES5Compatible(PanelCtrlES6);
|
||||
const MetricsPanelCtrl = makeClassES5Compatible(MetricsPanelCtrlES6);
|
||||
const QueryCtrl = makeClassES5Compatible(QueryCtrlES6);
|
||||
|
||||
export { PanelCtrl, MetricsPanelCtrl, QueryCtrl, alertTab, loadPluginCss };
|
||||
export { PanelCtrl, MetricsPanelCtrl, QueryCtrl, loadPluginCss };
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { DashboardAcl } from './acl';
|
||||
import { DataQuery, PanelPlugin } from '@grafana/data';
|
||||
import { DataQuery } from '@grafana/data';
|
||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { AngularComponent } from '@grafana/runtime';
|
||||
|
||||
export interface DashboardDTO {
|
||||
redirectUri?: string;
|
||||
@ -75,13 +74,6 @@ export interface QueriesToUpdateOnDashboardLoad {
|
||||
queries: DataQuery[];
|
||||
}
|
||||
|
||||
export interface PanelState {
|
||||
pluginId: string;
|
||||
plugin?: PanelPlugin;
|
||||
angularComponent?: AngularComponent | null;
|
||||
instanceState?: any | null;
|
||||
}
|
||||
|
||||
export interface DashboardState {
|
||||
getModel: GetMutableDashboardModelFn;
|
||||
initPhase: DashboardInitPhase;
|
||||
@ -89,5 +81,4 @@ export interface DashboardState {
|
||||
initError: DashboardInitError | null;
|
||||
permissions: DashboardAcl[];
|
||||
modifiedQueries: QueriesToUpdateOnDashboardLoad | null;
|
||||
panels: { [id: string]: PanelState };
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user