Panels: Fixes crashing issue when migrating angular panels (#58232)

This commit is contained in:
Torkel Ödegaard
2022-11-08 16:26:02 +01:00
committed by GitHub
parent 25f79ef2b9
commit 50a197014f
7 changed files with 18 additions and 67 deletions

View File

@@ -66,10 +66,9 @@ export function updateDuplicateLibraryPanels(
panel.configRev++; panel.configRev++;
if (pluginChanged) { if (pluginChanged) {
const cleanUpKey = panel.key;
panel.generateNewKey(); panel.generateNewKey();
dispatch(panelModelAndPluginReady({ key: panel.key, plugin: panel.plugin!, cleanUpKey })); dispatch(panelModelAndPluginReady({ key: panel.key, plugin: panel.plugin! }));
} }
// Resend last query result on source panel query runner // Resend last query result on source panel query runner
@@ -129,10 +128,9 @@ export function exitPanelEditor(): ThunkResult<void> {
if (panelTypeChanged) { if (panelTypeChanged) {
// Loaded plugin is not included in the persisted properties so is not handled by restoreModel // Loaded plugin is not included in the persisted properties so is not handled by restoreModel
sourcePanel.plugin = panel.plugin; sourcePanel.plugin = panel.plugin;
const cleanUpKey = sourcePanel.key;
sourcePanel.generateNewKey(); sourcePanel.generateNewKey();
await dispatch(panelModelAndPluginReady({ key: sourcePanel.key, plugin: panel.plugin!, cleanUpKey })); await dispatch(panelModelAndPluginReady({ key: sourcePanel.key, plugin: panel.plugin! }));
} }
// Resend last query result on source panel query runner // Resend last query result on source panel query runner

View File

@@ -5,7 +5,7 @@ import { DashboardMeta } from 'app/types';
import { DashboardModel } from '../state'; import { DashboardModel } from '../state';
import { DashboardGridUnconnected as DashboardGrid, Props } from './DashboardGrid'; import { DashboardGrid, Props } from './DashboardGrid';
jest.mock('app/features/dashboard/dashgrid/LazyLoader', () => { jest.mock('app/features/dashboard/dashgrid/LazyLoader', () => {
const LazyLoader: React.FC = ({ children }) => { const LazyLoader: React.FC = ({ children }) => {
@@ -58,7 +58,6 @@ describe('DashboardGrid', () => {
editPanel: null, editPanel: null,
viewPanel: null, viewPanel: null,
dashboard: getTestDashboard(), dashboard: getTestDashboard(),
cleanAndRemoveMany: jest.fn,
}; };
expect(() => render(<DashboardGrid {...props} />)).not.toThrow(); expect(() => render(<DashboardGrid {...props} />)).not.toThrow();
}); });

View File

@@ -1,13 +1,11 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React, { PureComponent, CSSProperties } from 'react'; import React, { PureComponent, CSSProperties } from 'react';
import ReactGridLayout, { ItemCallback } from 'react-grid-layout'; import ReactGridLayout, { ItemCallback } from 'react-grid-layout';
import { connect, ConnectedProps } from 'react-redux';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
import { cleanAndRemoveMany } from 'app/features/panel/state/actions';
import { DashboardPanelsChangedEvent } from 'app/types/events'; import { DashboardPanelsChangedEvent } from 'app/types/events';
import { AddPanelWidget } from '../components/AddPanelWidget'; import { AddPanelWidget } from '../components/AddPanelWidget';
@@ -17,7 +15,7 @@ import { GridPos } from '../state/PanelModel';
import { DashboardPanel } from './DashboardPanel'; import { DashboardPanel } from './DashboardPanel';
export interface OwnProps { export interface Props {
dashboard: DashboardModel; dashboard: DashboardModel;
editPanel: PanelModel | null; editPanel: PanelModel | null;
viewPanel: PanelModel | null; viewPanel: PanelModel | null;
@@ -27,15 +25,7 @@ export interface State {
isLayoutInitialized: boolean; isLayoutInitialized: boolean;
} }
const mapDispatchToProps = { export class DashboardGrid extends PureComponent<Props, State> {
cleanAndRemoveMany,
};
const connector = connect(null, mapDispatchToProps);
export type Props = OwnProps & ConnectedProps<typeof connector>;
export class DashboardGridUnconnected extends PureComponent<Props, State> {
private panelMap: { [key: string]: PanelModel } = {}; private panelMap: { [key: string]: PanelModel } = {};
private eventSubs = new Subscription(); private eventSubs = new Subscription();
private windowHeight = 1200; private windowHeight = 1200;
@@ -59,7 +49,6 @@ export class DashboardGridUnconnected extends PureComponent<Props, State> {
componentWillUnmount() { componentWillUnmount() {
this.eventSubs.unsubscribe(); this.eventSubs.unsubscribe();
this.props.cleanAndRemoveMany(Object.keys(this.panelMap));
} }
buildLayout() { buildLayout() {
@@ -323,5 +312,3 @@ function translateGridHeightToScreenHeight(gridHeight: number): number {
} }
GrafanaGridItem.displayName = 'GridItemWithDimensions'; GrafanaGridItem.displayName = 'GridItemWithDimensions';
export const DashboardGrid = connector(DashboardGridUnconnected);

View File

@@ -102,6 +102,9 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
componentWillUnmount() { componentWillUnmount() {
this.subs.unsubscribe(); this.subs.unsubscribe();
if (this.props.angularComponent) {
this.props.angularComponent?.destroy();
}
} }
componentDidUpdate(prevProps: Props, prevState: State) { componentDidUpdate(prevProps: Props, prevState: State) {

View File

@@ -3,6 +3,7 @@ import { getBackendSrv } from '@grafana/runtime';
import { notifyApp } from 'app/core/actions'; import { notifyApp } from 'app/core/actions';
import { createSuccessNotification } from 'app/core/copy/appNotification'; import { createSuccessNotification } from 'app/core/copy/appNotification';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { removeAllPanels } from 'app/features/panel/state/reducers';
import { updateTimeZoneForSession, updateWeekStartForSession } from 'app/features/profile/state/reducers'; import { updateTimeZoneForSession, updateWeekStartForSession } from 'app/features/profile/state/reducers';
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel, ThunkResult } from 'app/types'; import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel, ThunkResult } from 'app/types';
@@ -125,6 +126,7 @@ export const cleanUpDashboardAndVariables = (): ThunkResult<void> => (dispatch,
getTimeSrv().stopAutoRefresh(); getTimeSrv().stopAutoRefresh();
dispatch(cleanUpDashboard()); dispatch(cleanUpDashboard());
dispatch(removeAllPanels());
dashboardWatcher.leave(); dashboardWatcher.leave();
}; };

View File

@@ -8,13 +8,7 @@ import { loadPanelPlugin } from 'app/features/plugins/admin/state/actions';
import { ThunkResult } from 'app/types'; import { ThunkResult } from 'app/types';
import { PanelOptionsChangedEvent, PanelQueriesChangedEvent } from 'app/types/events'; import { PanelOptionsChangedEvent, PanelQueriesChangedEvent } from 'app/types/events';
import { import { changePanelKey, panelModelAndPluginReady, removePanel } from './reducers';
changePanelKey,
cleanUpAngularComponent,
panelModelAndPluginReady,
removePanel,
removePanels,
} from './reducers';
export function initPanelState(panel: PanelModel): ThunkResult<void> { export function initPanelState(panel: PanelModel): ThunkResult<void> {
return async (dispatch, getStore) => { return async (dispatch, getStore) => {
@@ -45,23 +39,11 @@ export function initPanelState(panel: PanelModel): ThunkResult<void> {
} }
export function cleanUpPanelState(panelKey: string): ThunkResult<void> { export function cleanUpPanelState(panelKey: string): ThunkResult<void> {
return (dispatch, getStore) => { return (dispatch) => {
const store = getStore().panels;
cleanUpAngularComponent(store[panelKey]);
dispatch(removePanel({ key: panelKey })); dispatch(removePanel({ key: panelKey }));
}; };
} }
export function cleanAndRemoveMany(panelKeys: string[]): ThunkResult<void> {
return (dispatch, getStore) => {
const store = getStore().panels;
for (const key of panelKeys) {
cleanUpAngularComponent(store[key]);
}
dispatch(removePanels({ keys: panelKeys }));
};
}
export interface ChangePanelPluginAndOptionsArgs { export interface ChangePanelPluginAndOptionsArgs {
panel: PanelModel; panel: PanelModel;
pluginId: string; pluginId: string;
@@ -89,8 +71,6 @@ export function changePanelPlugin({
plugin = await dispatch(loadPanelPlugin(pluginId)); plugin = await dispatch(loadPanelPlugin(pluginId));
} }
let cleanUpKey = panel.key;
if (panel.type !== pluginId) { if (panel.type !== pluginId) {
panel.changePlugin(plugin); panel.changePlugin(plugin);
} }
@@ -110,7 +90,7 @@ export function changePanelPlugin({
panel.generateNewKey(); panel.generateNewKey();
dispatch(panelModelAndPluginReady({ key: panel.key, plugin, cleanUpKey })); dispatch(panelModelAndPluginReady({ key: panel.key, plugin }));
}; };
} }
@@ -139,12 +119,10 @@ export function changeToLibraryPanel(panel: PanelModel, libraryPanel: LibraryEle
plugin = await dispatch(loadPanelPlugin(newPluginId)); plugin = await dispatch(loadPanelPlugin(newPluginId));
} }
const oldKey = panel.key;
panel.pluginLoaded(plugin); panel.pluginLoaded(plugin);
panel.generateNewKey(); panel.generateNewKey();
await dispatch(panelModelAndPluginReady({ key: panel.key, plugin, cleanUpKey: oldKey })); await dispatch(panelModelAndPluginReady({ key: panel.key, plugin }));
} else { } else {
// Even if the plugin is the same, we want to change the key // Even if the plugin is the same, we want to change the key
// to force a rerender // to force a rerender

View File

@@ -1,4 +1,4 @@
import { createSlice, Draft, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { PanelPlugin } from '@grafana/data'; import { PanelPlugin } from '@grafana/data';
import { AngularComponent } from '@grafana/runtime'; import { AngularComponent } from '@grafana/runtime';
@@ -18,11 +18,6 @@ const panelsSlice = createSlice({
initialState, initialState,
reducers: { reducers: {
panelModelAndPluginReady: (state, action: PayloadAction<PanelModelAndPluginReadyPayload>) => { panelModelAndPluginReady: (state, action: PayloadAction<PanelModelAndPluginReadyPayload>) => {
if (action.payload.cleanUpKey) {
cleanUpAngularComponent(state[action.payload.cleanUpKey]);
delete state[action.payload.cleanUpKey];
}
state[action.payload.key] = { state[action.payload.key] = {
plugin: action.payload.plugin, plugin: action.payload.plugin,
}; };
@@ -34,33 +29,22 @@ const panelsSlice = createSlice({
removePanel: (state, action: PayloadAction<{ key: string }>) => { removePanel: (state, action: PayloadAction<{ key: string }>) => {
delete state[action.payload.key]; delete state[action.payload.key];
}, },
removePanels: (state, action: PayloadAction<{ keys: string[] }>) => { removeAllPanels: (state) => {
for (const key of action.payload.keys) { Object.keys(state).forEach((key) => delete state[key]);
delete state[key];
}
}, },
setPanelInstanceState: (state, action: PayloadAction<SetPanelInstanceStatePayload>) => { setPanelInstanceState: (state, action: PayloadAction<SetPanelInstanceStatePayload>) => {
state[action.payload.key].instanceState = action.payload.value; state[action.payload.key].instanceState = action.payload.value;
}, },
setPanelAngularComponent: (state, action: PayloadAction<SetPanelAngularComponentPayload>) => { setPanelAngularComponent: (state, action: PayloadAction<SetPanelAngularComponentPayload>) => {
const panelState = state[action.payload.key]; const panelState = state[action.payload.key];
cleanUpAngularComponent(panelState);
panelState.angularComponent = action.payload.angularComponent; panelState.angularComponent = action.payload.angularComponent;
}, },
}, },
}); });
export function cleanUpAngularComponent(panelState?: Draft<PanelState>) {
if (panelState?.angularComponent) {
panelState.angularComponent.destroy();
}
}
export interface PanelModelAndPluginReadyPayload { export interface PanelModelAndPluginReadyPayload {
key: string; key: string;
plugin: PanelPlugin; plugin: PanelPlugin;
/** Used to cleanup previous state when we change key (used when switching panel plugin) */
cleanUpKey?: string;
} }
export interface SetPanelAngularComponentPayload { export interface SetPanelAngularComponentPayload {
@@ -79,7 +63,7 @@ export const {
setPanelInstanceState, setPanelInstanceState,
changePanelKey, changePanelKey,
removePanel, removePanel,
removePanels, removeAllPanels,
} = panelsSlice.actions; } = panelsSlice.actions;
export const panelsReducer = panelsSlice.reducer; export const panelsReducer = panelsSlice.reducer;