mirror of https://github.com/grafana/grafana.git
DashboardPage: Refactor state to fix state timing bugs and reduce unnecessary re-renders (#36460)
* DashboardPage: Refactoring state handling to improve performance and fix bugs with state out of sync * Fixed exit panel editor timing issues * New tests in RTL * Updated comment * Removed unused imports
This commit is contained in:
parent
96a51561a3
commit
a67eaf6b62
|
|
@ -11,7 +11,7 @@ import * as H from 'history';
|
||||||
import { SaveLibraryPanelModal } from 'app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal';
|
import { SaveLibraryPanelModal } from 'app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal';
|
||||||
import { PanelModelWithLibraryPanel } from 'app/features/library-panels/types';
|
import { PanelModelWithLibraryPanel } from 'app/features/library-panels/types';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { discardPanelChanges } from '../PanelEditor/state/actions';
|
import { discardPanelChanges, exitPanelEditor } from '../PanelEditor/state/actions';
|
||||||
import { DashboardSavedEvent } from 'app/types/events';
|
import { DashboardSavedEvent } from 'app/types/events';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
|
@ -77,6 +77,11 @@ export const DashboardPrompt = React.memo(({ dashboard }: Props) => {
|
||||||
|
|
||||||
// Are we still on the same dashboard?
|
// Are we still on the same dashboard?
|
||||||
if (originalPath === location.pathname || !original) {
|
if (originalPath === location.pathname || !original) {
|
||||||
|
// This is here due to timing reasons we want the exit panel editor state changes to happen before router update
|
||||||
|
if (panelInEdit && !search.has('editPanel')) {
|
||||||
|
dispatch(exitPanelEditor());
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPane
|
||||||
import { SaveDashboardModalProxy } from '../SaveDashboard/SaveDashboardModalProxy';
|
import { SaveDashboardModalProxy } from '../SaveDashboard/SaveDashboardModalProxy';
|
||||||
import { DashboardPanel } from '../../dashgrid/DashboardPanel';
|
import { DashboardPanel } from '../../dashgrid/DashboardPanel';
|
||||||
|
|
||||||
import { discardPanelChanges, initPanelEditor, panelEditorCleanUp, updatePanelEditorUIState } from './state/actions';
|
import { discardPanelChanges, initPanelEditor, updatePanelEditorUIState } from './state/actions';
|
||||||
|
|
||||||
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
||||||
import { toggleTableView } from './state/reducers';
|
import { toggleTableView } from './state/reducers';
|
||||||
|
|
@ -78,7 +78,6 @@ const mapStateToProps = (state: StoreState) => {
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
initPanelEditor,
|
initPanelEditor,
|
||||||
panelEditorCleanUp,
|
|
||||||
discardPanelChanges,
|
discardPanelChanges,
|
||||||
updatePanelEditorUIState,
|
updatePanelEditorUIState,
|
||||||
updateTimeZoneForSession,
|
updateTimeZoneForSession,
|
||||||
|
|
@ -115,7 +114,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.props.panelEditorCleanUp();
|
// redux action exitPanelEditor is called on location change from DashboardPrompt
|
||||||
this.eventSubs?.unsubscribe();
|
this.eventSubs?.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { thunkTester } from '../../../../../../test/core/thunk/thunkTester';
|
import { thunkTester } from '../../../../../../test/core/thunk/thunkTester';
|
||||||
import { closeCompleted, initialState, PanelEditorState } from './reducers';
|
import { closeCompleted, initialState, PanelEditorState } from './reducers';
|
||||||
import { initPanelEditor, panelEditorCleanUp } from './actions';
|
import { initPanelEditor, exitPanelEditor } from './actions';
|
||||||
import { cleanUpEditPanel, panelModelAndPluginReady } from '../../../state/reducers';
|
import { cleanUpEditPanel, panelModelAndPluginReady } from '../../../state/reducers';
|
||||||
import { DashboardModel, PanelModel } from '../../../state';
|
import { DashboardModel, PanelModel } from '../../../state';
|
||||||
import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks';
|
import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks';
|
||||||
|
|
@ -48,7 +48,7 @@ describe('panelEditor actions', () => {
|
||||||
getModel: () => dashboard,
|
getModel: () => dashboard,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.givenThunk(panelEditorCleanUp)
|
.givenThunk(exitPanelEditor)
|
||||||
.whenThunkIsDispatched();
|
.whenThunkIsDispatched();
|
||||||
|
|
||||||
expect(dispatchedActions.length).toBe(2);
|
expect(dispatchedActions.length).toBe(2);
|
||||||
|
|
@ -83,7 +83,7 @@ describe('panelEditor actions', () => {
|
||||||
getModel: () => dashboard,
|
getModel: () => dashboard,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.givenThunk(panelEditorCleanUp)
|
.givenThunk(exitPanelEditor)
|
||||||
.whenThunkIsDispatched();
|
.whenThunkIsDispatched();
|
||||||
|
|
||||||
expect(dispatchedActions.length).toBe(3);
|
expect(dispatchedActions.length).toBe(3);
|
||||||
|
|
@ -118,7 +118,7 @@ describe('panelEditor actions', () => {
|
||||||
getModel: () => dashboard,
|
getModel: () => dashboard,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.givenThunk(panelEditorCleanUp)
|
.givenThunk(exitPanelEditor)
|
||||||
.whenThunkIsDispatched();
|
.whenThunkIsDispatched();
|
||||||
|
|
||||||
expect(dispatchedActions.length).toBe(2);
|
expect(dispatchedActions.length).toBe(2);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import {
|
||||||
import { cleanUpEditPanel, panelModelAndPluginReady } from '../../../state/reducers';
|
import { cleanUpEditPanel, panelModelAndPluginReady } from '../../../state/reducers';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import { locationService } from '@grafana/runtime';
|
|
||||||
|
|
||||||
export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardModel): ThunkResult<void> {
|
export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardModel): ThunkResult<void> {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
|
|
@ -34,12 +33,6 @@ export function discardPanelChanges(): ThunkResult<void> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exitPanelEditor(): ThunkResult<void> {
|
|
||||||
return async (dispatch, getStore) => {
|
|
||||||
locationService.partial({ editPanel: null, tab: null });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateDuplicateLibraryPanels(modifiedPanel: PanelModel, dashboard: DashboardModel | null, dispatch: any) {
|
function updateDuplicateLibraryPanels(modifiedPanel: PanelModel, dashboard: DashboardModel | null, dispatch: any) {
|
||||||
if (modifiedPanel.libraryPanel?.uid === undefined || !dashboard) {
|
if (modifiedPanel.libraryPanel?.uid === undefined || !dashboard) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -74,8 +67,8 @@ function updateDuplicateLibraryPanels(modifiedPanel: PanelModel, dashboard: Dash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function panelEditorCleanUp(): ThunkResult<void> {
|
export function exitPanelEditor(): ThunkResult<void> {
|
||||||
return (dispatch, getStore) => {
|
return async (dispatch, getStore) => {
|
||||||
const dashboard = getStore().dashboard.getModel();
|
const dashboard = getStore().dashboard.getModel();
|
||||||
const { getPanel, getSourcePanel, shouldDiscardChanges } = getStore().panelEditor;
|
const { getPanel, getSourcePanel, shouldDiscardChanges } = getStore().panelEditor;
|
||||||
|
|
||||||
|
|
@ -98,7 +91,7 @@ export function panelEditorCleanUp(): ThunkResult<void> {
|
||||||
sourcePanel.plugin = panel.plugin;
|
sourcePanel.plugin = panel.plugin;
|
||||||
|
|
||||||
if (panelTypeChanged) {
|
if (panelTypeChanged) {
|
||||||
dispatch(panelModelAndPluginReady({ panelId: sourcePanel.id, plugin: panel.plugin! }));
|
await dispatch(panelModelAndPluginReady({ panelId: sourcePanel.id, plugin: panel.plugin! }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resend last query result on source panel query runner
|
// Resend last query result on source panel query runner
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,43 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { Provider } from 'react-redux';
|
||||||
import { UnthemedDashboardPage, mapStateToProps, Props, State } from './DashboardPage';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { UnthemedDashboardPage, Props } from './DashboardPage';
|
||||||
|
import { Router } from 'react-router-dom';
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
import { DashboardModel } from '../state';
|
import { DashboardModel } from '../state';
|
||||||
|
import { configureStore } from '../../../store/configureStore';
|
||||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||||
import { DashboardInitPhase, DashboardRoutes } from 'app/types';
|
import { DashboardInitPhase, DashboardRoutes } from 'app/types';
|
||||||
import { notifyApp } from 'app/core/actions';
|
import { notifyApp } from 'app/core/actions';
|
||||||
import { cleanUpDashboardAndVariables } from '../state/actions';
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||||
import { createTheme } from '@grafana/data';
|
import { createTheme } from '@grafana/data';
|
||||||
|
|
||||||
jest.mock('app/features/dashboard/components/DashboardSettings/GeneralSettings', () => ({}));
|
jest.mock('app/features/dashboard/components/DashboardSettings/GeneralSettings', () => {
|
||||||
|
class GeneralSettings extends React.Component<{}, {}> {
|
||||||
|
render() {
|
||||||
|
return <>general settings</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { GeneralSettings };
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('app/core/core', () => ({
|
||||||
|
appEvents: {
|
||||||
|
subscribe: () => {
|
||||||
|
return { unsubscribe: () => {} };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
interface ScenarioContext {
|
interface ScenarioContext {
|
||||||
cleanUpDashboardAndVariablesMock: typeof cleanUpDashboardAndVariables;
|
|
||||||
dashboard?: DashboardModel | null;
|
dashboard?: DashboardModel | null;
|
||||||
setDashboardProp: (overrides?: any, metaOverrides?: any) => void;
|
container?: HTMLElement;
|
||||||
wrapper?: ShallowWrapper<Props, State, UnthemedDashboardPage>;
|
|
||||||
mount: (propOverrides?: Partial<Props>) => void;
|
mount: (propOverrides?: Partial<Props>) => void;
|
||||||
|
unmount: () => void;
|
||||||
|
props: Props;
|
||||||
|
rerender: (propOverrides?: Partial<Props>) => void;
|
||||||
setup: (fn: () => void) => void;
|
setup: (fn: () => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,8 +48,8 @@ function getTestDashboard(overrides?: any, metaOverrides?: any): DashboardModel
|
||||||
panels: [
|
panels: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
type: 'graph',
|
type: 'timeseries',
|
||||||
title: 'My graph',
|
title: 'My panel title',
|
||||||
gridPos: { x: 0, y: 0, w: 1, h: 1 },
|
gridPos: { x: 0, y: 0, w: 1, h: 1 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -46,15 +66,11 @@ function dashboardPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
|
||||||
let setupFn: () => void;
|
let setupFn: () => void;
|
||||||
|
|
||||||
const ctx: ScenarioContext = {
|
const ctx: ScenarioContext = {
|
||||||
cleanUpDashboardAndVariablesMock: jest.fn(),
|
|
||||||
setup: (fn) => {
|
setup: (fn) => {
|
||||||
setupFn = fn;
|
setupFn = fn;
|
||||||
},
|
},
|
||||||
setDashboardProp: (overrides?: any, metaOverrides?: any) => {
|
|
||||||
ctx.dashboard = getTestDashboard(overrides, metaOverrides);
|
|
||||||
ctx.wrapper?.setProps({ dashboard: ctx.dashboard });
|
|
||||||
},
|
|
||||||
mount: (propOverrides?: Partial<Props>) => {
|
mount: (propOverrides?: Partial<Props>) => {
|
||||||
|
const store = configureStore();
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
...getRouteComponentProps({
|
...getRouteComponentProps({
|
||||||
match: { params: { slug: 'my-dash', uid: '11' } } as any,
|
match: { params: { slug: 'my-dash', uid: '11' } } as any,
|
||||||
|
|
@ -64,7 +80,7 @@ function dashboardPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
|
||||||
isInitSlow: false,
|
isInitSlow: false,
|
||||||
initDashboard: jest.fn(),
|
initDashboard: jest.fn(),
|
||||||
notifyApp: mockToolkitActionCreator(notifyApp),
|
notifyApp: mockToolkitActionCreator(notifyApp),
|
||||||
cleanUpDashboardAndVariables: ctx.cleanUpDashboardAndVariablesMock,
|
cleanUpDashboardAndVariables: jest.fn(),
|
||||||
cancelVariables: jest.fn(),
|
cancelVariables: jest.fn(),
|
||||||
templateVarsChangedInUrl: jest.fn(),
|
templateVarsChangedInUrl: jest.fn(),
|
||||||
dashboard: null,
|
dashboard: null,
|
||||||
|
|
@ -73,9 +89,36 @@ function dashboardPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
ctx.props = props;
|
||||||
ctx.dashboard = props.dashboard;
|
ctx.dashboard = props.dashboard;
|
||||||
ctx.wrapper = shallow(<UnthemedDashboardPage {...props} />);
|
|
||||||
|
const { container, rerender, unmount } = render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<Router history={locationService.getHistory()}>
|
||||||
|
<UnthemedDashboardPage {...props} />
|
||||||
|
</Router>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.container = container;
|
||||||
|
|
||||||
|
ctx.rerender = (newProps?: Partial<Props>) => {
|
||||||
|
Object.assign(props, newProps);
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<Provider store={store}>
|
||||||
|
<Router history={locationService.getHistory()}>
|
||||||
|
<UnthemedDashboardPage {...props} />
|
||||||
|
</Router>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.unmount = unmount;
|
||||||
},
|
},
|
||||||
|
props: {} as Props,
|
||||||
|
rerender: () => {},
|
||||||
|
unmount: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -92,226 +135,140 @@ describe('DashboardPage', () => {
|
||||||
ctx.mount();
|
ctx.mount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should render nothing', () => {
|
it('Should call initDashboard on mount', () => {
|
||||||
expect(ctx.wrapper).toMatchSnapshot();
|
expect(ctx.props.initDashboard).toBeCalledWith({
|
||||||
|
fixUrl: true,
|
||||||
|
routeName: 'normal-dashboard',
|
||||||
|
urlSlug: 'my-dash',
|
||||||
|
urlUid: '11',
|
||||||
|
});
|
||||||
|
expect(ctx.container).toBeEmptyDOMElement();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
dashboardPageScenario('Dashboard is fetching slowly', (ctx) => {
|
dashboardPageScenario('Given dashboard slow loading state', (ctx) => {
|
||||||
ctx.setup(() => {
|
ctx.setup(() => {
|
||||||
ctx.mount();
|
ctx.mount();
|
||||||
ctx.wrapper?.setProps({
|
ctx.rerender({ isInitSlow: true });
|
||||||
isInitSlow: true,
|
});
|
||||||
initPhase: DashboardInitPhase.Fetching,
|
|
||||||
|
it('Should show spinner', () => {
|
||||||
|
expect(screen.getByText('Cancel loading dashboard')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should render slow init state', () => {
|
dashboardPageScenario('Given a simple dashboard', (ctx) => {
|
||||||
expect(ctx.wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
dashboardPageScenario('Dashboard init completed ', (ctx) => {
|
|
||||||
ctx.setup(() => {
|
ctx.setup(() => {
|
||||||
ctx.mount();
|
ctx.mount();
|
||||||
ctx.setDashboardProp();
|
ctx.rerender({ dashboard: getTestDashboard() });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should render panels', () => {
|
||||||
|
expect(screen.getByText('My panel title')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should update title', () => {
|
it('Should update title', () => {
|
||||||
expect(document.title).toBe('My dashboard - Grafana');
|
expect(document.title).toBe('My dashboard - Grafana');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should render dashboard grid', () => {
|
|
||||||
expect(ctx.wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
dashboardPageScenario('When user goes into panel edit', (ctx) => {
|
dashboardPageScenario('When going into view mode', (ctx) => {
|
||||||
ctx.setup(() => {
|
ctx.setup(() => {
|
||||||
ctx.mount();
|
ctx.mount({
|
||||||
ctx.setDashboardProp();
|
dashboard: getTestDashboard(),
|
||||||
ctx.wrapper?.setProps({
|
queryParams: { viewPanel: '1' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should render panel in view mode', () => {
|
||||||
|
expect(ctx.dashboard?.panelInView).toBeDefined();
|
||||||
|
expect(ctx.dashboard?.panels[0].isViewing).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should reset state when leaving', () => {
|
||||||
|
ctx.rerender({ queryParams: {} });
|
||||||
|
|
||||||
|
expect(ctx.dashboard?.panelInView).toBeUndefined();
|
||||||
|
expect(ctx.dashboard?.panels[0].isViewing).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dashboardPageScenario('When going into edit mode', (ctx) => {
|
||||||
|
ctx.setup(() => {
|
||||||
|
ctx.mount({
|
||||||
|
dashboard: getTestDashboard(),
|
||||||
queryParams: { editPanel: '1' },
|
queryParams: { editPanel: '1' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should update component state to fullscreen and edit', () => {
|
it('Should render panel in edit mode', () => {
|
||||||
const state = ctx.wrapper?.state();
|
expect(ctx.dashboard?.panelInEdit).toBeDefined();
|
||||||
expect(state).not.toBe(null);
|
|
||||||
expect(state?.editPanel).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
dashboardPageScenario('When user goes into panel edit but has no edit permissions', (ctx) => {
|
it('Should render panel editor', () => {
|
||||||
ctx.setup(() => {
|
expect(screen.getByTitle('Apply changes and go back to dashboard')).toBeInTheDocument();
|
||||||
ctx.mount();
|
|
||||||
ctx.setDashboardProp({}, { canEdit: false });
|
|
||||||
ctx.wrapper?.setProps({
|
|
||||||
queryParams: { editPanel: '1' },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should update component state to fullscreen and edit', () => {
|
it('Should reset state when leaving', () => {
|
||||||
const state = ctx.wrapper?.state();
|
ctx.rerender({ queryParams: {} });
|
||||||
expect(state?.editPanel).toBe(null);
|
expect(screen.queryByTitle('Apply changes and go back to dashboard')).not.toBeInTheDocument();
|
||||||
});
|
|
||||||
});
|
|
||||||
dashboardPageScenario('When user goes back to dashboard from edit panel', (ctx) => {
|
|
||||||
ctx.setup(() => {
|
|
||||||
ctx.mount();
|
|
||||||
ctx.setDashboardProp();
|
|
||||||
ctx.wrapper?.setState({ scrollTop: 100 });
|
|
||||||
ctx.wrapper?.setProps({
|
|
||||||
queryParams: { editPanel: '1' },
|
|
||||||
});
|
|
||||||
ctx.wrapper?.setProps({
|
|
||||||
queryParams: {},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should update model state normal state', () => {
|
|
||||||
expect(ctx.dashboard).toBeDefined();
|
|
||||||
// @ts-ignore typescript doesn't understand that dashboard must be defined to reach the row below
|
|
||||||
expect(ctx.dashboard.panelInEdit).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should update component state to normal and restore scrollTop', () => {
|
|
||||||
const state = ctx.wrapper?.state();
|
|
||||||
expect(ctx.wrapper).not.toBe(null);
|
|
||||||
expect(state).not.toBe(null);
|
|
||||||
expect(state?.editPanel).toBe(null);
|
|
||||||
expect(state?.scrollTop).toBe(100);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
dashboardPageScenario('When dashboard has editview url state', (ctx) => {
|
|
||||||
ctx.setup(() => {
|
|
||||||
ctx.mount();
|
|
||||||
ctx.setDashboardProp();
|
|
||||||
ctx.wrapper?.setProps({
|
|
||||||
queryParams: { editview: 'settings' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render settings view', () => {
|
|
||||||
expect(ctx.wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
dashboardPageScenario('When adding panel', (ctx) => {
|
|
||||||
ctx.setup(() => {
|
|
||||||
ctx.mount();
|
|
||||||
ctx.setDashboardProp();
|
|
||||||
ctx.wrapper?.setState({ scrollTop: 100 });
|
|
||||||
ctx.wrapper?.instance().onAddPanel();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set scrollTop to 0', () => {
|
|
||||||
expect(ctx.wrapper).not.toBe(null);
|
|
||||||
expect(ctx.wrapper?.state()).not.toBe(null);
|
|
||||||
expect(ctx.wrapper?.state().updateScrollTop).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add panel widget to dashboard panels', () => {
|
|
||||||
expect(ctx.dashboard).not.toBe(null);
|
|
||||||
expect(ctx.dashboard?.panels[0].type).toBe('add-panel');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
dashboardPageScenario('Given panel with id 0', (ctx) => {
|
|
||||||
ctx.setup(() => {
|
|
||||||
ctx.mount();
|
|
||||||
ctx.setDashboardProp({
|
|
||||||
panels: [{ id: 0, type: 'graph' }],
|
|
||||||
schemaVersion: 17,
|
|
||||||
});
|
|
||||||
ctx.wrapper?.setProps({
|
|
||||||
queryParams: { editPanel: '0' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should go into edit mode', () => {
|
|
||||||
const state = ctx.wrapper?.state();
|
|
||||||
expect(ctx.wrapper).not.toBe(null);
|
|
||||||
expect(state).not.toBe(null);
|
|
||||||
expect(state?.editPanel).not.toBe(null);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
dashboardPageScenario('When dashboard unmounts', (ctx) => {
|
dashboardPageScenario('When dashboard unmounts', (ctx) => {
|
||||||
ctx.setup(() => {
|
ctx.setup(() => {
|
||||||
ctx.mount();
|
ctx.mount();
|
||||||
ctx.setDashboardProp({
|
ctx.rerender({ dashboard: getTestDashboard() });
|
||||||
panels: [{ id: 0, type: 'graph' }],
|
ctx.unmount();
|
||||||
schemaVersion: 17,
|
|
||||||
});
|
|
||||||
ctx.wrapper?.unmount();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should call clean up action', () => {
|
it('Should call close action', () => {
|
||||||
expect(ctx.cleanUpDashboardAndVariablesMock).toHaveBeenCalledTimes(1);
|
expect(ctx.props.cleanUpDashboardAndVariables).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
dashboardPageScenario('Kiosk mode none', (ctx) => {
|
dashboardPageScenario('When dashboard changes', (ctx) => {
|
||||||
ctx.setup(() => {
|
ctx.setup(() => {
|
||||||
ctx.mount({
|
ctx.mount();
|
||||||
queryParams: {},
|
ctx.rerender({ dashboard: getTestDashboard() });
|
||||||
});
|
ctx.rerender({
|
||||||
ctx.setDashboardProp({
|
match: {
|
||||||
panels: [{ id: 0, type: 'graph' }],
|
params: { uid: 'new-uid' },
|
||||||
schemaVersion: 17,
|
} as any,
|
||||||
|
dashboard: getTestDashboard({ title: 'Another dashboard' }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render dashboard navigation ', () => {
|
it('Should call clean up action and init', () => {
|
||||||
expect(ctx.wrapper?.find(`[aria-label="${selectors.pages.Dashboard.DashNav.nav}"]`)).toHaveLength(1);
|
expect(ctx.props.cleanUpDashboardAndVariables).toHaveBeenCalledTimes(1);
|
||||||
expect(ctx.wrapper?.find(`[aria-label="${selectors.pages.Dashboard.SubMenu.submenu}"]`)).toHaveLength(1);
|
expect(ctx.props.initDashboard).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
dashboardPageScenario('Kiosk mode tv', (ctx) => {
|
dashboardPageScenario('No kiosk mode tv', (ctx) => {
|
||||||
ctx.setup(() => {
|
ctx.setup(() => {
|
||||||
ctx.mount({
|
ctx.mount({ dashboard: getTestDashboard() });
|
||||||
queryParams: { kiosk: 'tv' },
|
ctx.rerender({ dashboard: ctx.dashboard });
|
||||||
});
|
});
|
||||||
ctx.setDashboardProp({
|
|
||||||
panels: [{ id: 0, type: 'graph' }],
|
it('should render dashboard page toolbar and submenu', () => {
|
||||||
schemaVersion: 17,
|
expect(screen.queryAllByLabelText(selectors.pages.Dashboard.DashNav.nav)).toHaveLength(1);
|
||||||
|
expect(screen.queryAllByLabelText(selectors.pages.Dashboard.SubMenu.submenu)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render dashboard navigation ', () => {
|
dashboardPageScenario('When in full kiosk mode', (ctx) => {
|
||||||
expect(ctx.wrapper?.find(`[aria-label="${selectors.pages.Dashboard.DashNav.nav}"]`)).toHaveLength(1);
|
|
||||||
expect(ctx.wrapper?.find(`[aria-label="${selectors.pages.Dashboard.SubMenu.submenu}"]`)).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
dashboardPageScenario('Kiosk mode full', (ctx) => {
|
|
||||||
ctx.setup(() => {
|
ctx.setup(() => {
|
||||||
ctx.mount({
|
ctx.mount({
|
||||||
queryParams: { kiosk: true },
|
queryParams: { kiosk: true },
|
||||||
|
dashboard: getTestDashboard(),
|
||||||
});
|
});
|
||||||
ctx.setDashboardProp({
|
ctx.rerender({ dashboard: ctx.dashboard });
|
||||||
panels: [{ id: 0, type: 'graph' }],
|
|
||||||
schemaVersion: 17,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render dashboard navigation and submenu', () => {
|
it('should not render page toolbar and submenu', () => {
|
||||||
expect(ctx.wrapper?.find(`[aria-label="${selectors.pages.Dashboard.DashNav.nav}"]`)).toHaveLength(0);
|
expect(screen.queryAllByLabelText(selectors.pages.Dashboard.DashNav.nav)).toHaveLength(0);
|
||||||
expect(ctx.wrapper?.find(`[aria-label="${selectors.pages.Dashboard.SubMenu.submenu}"]`)).toHaveLength(0);
|
expect(screen.queryAllByLabelText(selectors.pages.Dashboard.SubMenu.submenu)).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mapStateToProps', () => {
|
|
||||||
const props = mapStateToProps({
|
|
||||||
panelEditor: {},
|
|
||||||
dashboard: {
|
|
||||||
getModel: () => ({} as DashboardModel),
|
|
||||||
},
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
expect(props.dashboard).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
|
|
@ -30,6 +29,7 @@ import { GrafanaTheme2, UrlQueryValue } from '@grafana/data';
|
||||||
import { DashboardLoading } from '../components/DashboardLoading/DashboardLoading';
|
import { DashboardLoading } from '../components/DashboardLoading/DashboardLoading';
|
||||||
import { DashboardFailed } from '../components/DashboardLoading/DashboardFailed';
|
import { DashboardFailed } from '../components/DashboardLoading/DashboardFailed';
|
||||||
import { DashboardPrompt } from '../components/DashboardPrompt/DashboardPrompt';
|
import { DashboardPrompt } from '../components/DashboardPrompt/DashboardPrompt';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
export interface DashboardPageRouteParams {
|
export interface DashboardPageRouteParams {
|
||||||
uid?: string;
|
uid?: string;
|
||||||
|
|
@ -72,6 +72,8 @@ export interface State {
|
||||||
updateScrollTop?: number;
|
updateScrollTop?: number;
|
||||||
rememberScrollTop: number;
|
rememberScrollTop: number;
|
||||||
showLoadingState: boolean;
|
showLoadingState: boolean;
|
||||||
|
panelNotFound: boolean;
|
||||||
|
editPanelAccessDenied: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||||
|
|
@ -85,6 +87,8 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||||
showLoadingState: false,
|
showLoadingState: false,
|
||||||
scrollTop: 0,
|
scrollTop: 0,
|
||||||
rememberScrollTop: 0,
|
rememberScrollTop: 0,
|
||||||
|
panelNotFound: false,
|
||||||
|
editPanelAccessDenied: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,7 +103,6 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||||
|
|
||||||
closeDashboard() {
|
closeDashboard() {
|
||||||
this.props.cleanUpDashboardAndVariables();
|
this.props.cleanUpDashboardAndVariables();
|
||||||
this.setPanelFullscreenClass(false);
|
|
||||||
this.setState(this.getCleanState());
|
this.setState(this.getCleanState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,10 +123,8 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||||
const { dashboard, match, queryParams, templateVarsChangedInUrl } = this.props;
|
const { dashboard, match, templateVarsChangedInUrl } = this.props;
|
||||||
const { editPanel, viewPanel } = this.state;
|
|
||||||
|
|
||||||
const routeReloadCounter = (this.props.history.location.state as any)?.routeReloadCounter;
|
const routeReloadCounter = (this.props.history.location.state as any)?.routeReloadCounter;
|
||||||
|
|
||||||
if (!dashboard) {
|
if (!dashboard) {
|
||||||
|
|
@ -163,83 +164,88 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlEditPanelId = queryParams.editPanel;
|
|
||||||
const urlViewPanelId = queryParams.viewPanel;
|
|
||||||
|
|
||||||
// entering edit mode
|
// entering edit mode
|
||||||
if (!editPanel && urlEditPanelId) {
|
if (this.state.editPanel && !prevState.editPanel) {
|
||||||
dashboardWatcher.setEditingState(true);
|
dashboardWatcher.setEditingState(true);
|
||||||
|
|
||||||
this.getPanelByIdFromUrlParam(urlEditPanelId, (panel) => {
|
|
||||||
// if no edit permission show error
|
|
||||||
if (!dashboard.canEditPanel(panel)) {
|
|
||||||
this.props.notifyApp(createErrorNotification('Permission to edit panel denied'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ editPanel: panel });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// leaving edit mode
|
// leaving edit mode
|
||||||
if (editPanel && !urlEditPanelId) {
|
if (!this.state.editPanel && prevState.editPanel) {
|
||||||
dashboardWatcher.setEditingState(false);
|
dashboardWatcher.setEditingState(false);
|
||||||
this.setState({ editPanel: null });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// entering view mode
|
if (this.state.editPanelAccessDenied) {
|
||||||
if (!viewPanel && urlViewPanelId) {
|
this.props.notifyApp(createErrorNotification('Permission to edit panel denied'));
|
||||||
this.getPanelByIdFromUrlParam(urlViewPanelId, (panel) => {
|
locationService.partial({ editPanel: null });
|
||||||
this.setPanelFullscreenClass(true);
|
|
||||||
dashboard.initViewPanel(panel);
|
|
||||||
this.setState({
|
|
||||||
viewPanel: panel,
|
|
||||||
rememberScrollTop: this.state.scrollTop,
|
|
||||||
updateScrollTop: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// leaving view mode
|
if (this.state.panelNotFound) {
|
||||||
if (viewPanel && !urlViewPanelId) {
|
this.props.notifyApp(createErrorNotification(`Panel not found`));
|
||||||
this.setPanelFullscreenClass(false);
|
|
||||||
dashboard.exitViewPanel(viewPanel);
|
|
||||||
this.setState(
|
|
||||||
{ viewPanel: null, updateScrollTop: this.state.rememberScrollTop },
|
|
||||||
this.triggerPanelsRendering.bind(this)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getPanelByIdFromUrlParam(urlPanelId: string, callback: (panel: PanelModel) => void) {
|
|
||||||
const { dashboard } = this.props;
|
|
||||||
|
|
||||||
const panelId = parseInt(urlPanelId!, 10);
|
|
||||||
dashboard!.expandParentRowFor(panelId);
|
|
||||||
const panel = dashboard!.getPanelById(panelId);
|
|
||||||
|
|
||||||
if (!panel) {
|
|
||||||
// Panel not found
|
|
||||||
this.props.notifyApp(createErrorNotification(`Panel with ID ${urlPanelId} not found`));
|
|
||||||
// Clear url state
|
|
||||||
locationService.partial({ editPanel: null, viewPanel: null });
|
locationService.partial({ editPanel: null, viewPanel: null });
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(panel);
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerPanelsRendering() {
|
|
||||||
try {
|
|
||||||
this.props.dashboard!.render();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.props.notifyApp(createErrorNotification(`Panel rendering error`, err));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setPanelFullscreenClass(isFullscreen: boolean) {
|
static getDerivedStateFromProps(props: Props, state: State) {
|
||||||
$('body').toggleClass('panel-in-fullscreen', isFullscreen);
|
const { dashboard, queryParams } = props;
|
||||||
|
|
||||||
|
const urlEditPanelId = queryParams.editPanel;
|
||||||
|
const urlViewPanelId = queryParams.viewPanel;
|
||||||
|
|
||||||
|
if (!dashboard) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entering edit mode
|
||||||
|
if (!state.editPanel && urlEditPanelId) {
|
||||||
|
const panel = dashboard.getPanelByUrlId(urlEditPanelId);
|
||||||
|
if (!panel) {
|
||||||
|
return { ...state, panelNotFound: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dashboard.canEditPanel(panel)) {
|
||||||
|
return { ...state, editPanel: panel };
|
||||||
|
} else {
|
||||||
|
return { ...state, editPanelAccessDenied: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Leaving edit mode
|
||||||
|
else if (state.editPanel && !urlEditPanelId) {
|
||||||
|
return { ...state, editPanel: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entering view mode
|
||||||
|
if (!state.viewPanel && urlViewPanelId) {
|
||||||
|
const panel = dashboard.getPanelByUrlId(urlViewPanelId);
|
||||||
|
if (!panel) {
|
||||||
|
return { ...state, panelNotFound: urlEditPanelId };
|
||||||
|
}
|
||||||
|
|
||||||
|
// This mutable state feels wrong to have in getDerivedStateFromProps
|
||||||
|
// Should move this state out of dashboard in the future
|
||||||
|
dashboard.initViewPanel(panel);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
viewPanel: panel,
|
||||||
|
rememberScrollTop: state.scrollTop,
|
||||||
|
updateScrollTop: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Leaving view mode
|
||||||
|
else if (state.viewPanel && !urlViewPanelId) {
|
||||||
|
// This mutable state feels wrong to have in getDerivedStateFromProps
|
||||||
|
// Should move this state out of dashboard in the future
|
||||||
|
dashboard.exitViewPanel(state.viewPanel);
|
||||||
|
|
||||||
|
return { ...state, viewPanel: null, updateScrollTop: state.rememberScrollTop };
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we removed url edit state, clear any panel not found state
|
||||||
|
if (state.panelNotFound || (state.editPanelAccessDenied && !urlEditPanelId)) {
|
||||||
|
return { ...state, panelNotFound: false, editPanelAccessDenied: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
setScrollTop = ({ scrollTop }: ScrollbarPosition): void => {
|
setScrollTop = ({ scrollTop }: ScrollbarPosition): void => {
|
||||||
|
|
@ -288,7 +294,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dashboard, isInitSlow, initError, isPanelEditorOpen, queryParams, theme } = this.props;
|
const { dashboard, isInitSlow, initError, queryParams, theme } = this.props;
|
||||||
const { editPanel, viewPanel, scrollTop, updateScrollTop } = this.state;
|
const { editPanel, viewPanel, scrollTop, updateScrollTop } = this.state;
|
||||||
const kioskMode = getKioskMode(queryParams.kiosk);
|
const kioskMode = getKioskMode(queryParams.kiosk);
|
||||||
const styles = getStyles(theme, kioskMode);
|
const styles = getStyles(theme, kioskMode);
|
||||||
|
|
@ -304,9 +310,12 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||||
// Only trigger render when the scroll has moved by 25
|
// Only trigger render when the scroll has moved by 25
|
||||||
const approximateScrollTop = Math.round(scrollTop / 25) * 25;
|
const approximateScrollTop = Math.round(scrollTop / 25) * 25;
|
||||||
const inspectPanel = this.getInspectPanel();
|
const inspectPanel = this.getInspectPanel();
|
||||||
|
const containerClassNames = classnames(styles.dashboardContainer, {
|
||||||
|
'panel-in-fullscreen': viewPanel,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.dashboardContainer}>
|
<div className={containerClassNames}>
|
||||||
{kioskMode !== KioskMode.Full && (
|
{kioskMode !== KioskMode.Full && (
|
||||||
<div aria-label={selectors.pages.Dashboard.DashNav.nav}>
|
<div aria-label={selectors.pages.Dashboard.DashNav.nav}>
|
||||||
<DashNav
|
<DashNav
|
||||||
|
|
@ -344,7 +353,6 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||||
viewPanel={viewPanel}
|
viewPanel={viewPanel}
|
||||||
editPanel={editPanel}
|
editPanel={editPanel}
|
||||||
scrollTop={approximateScrollTop}
|
scrollTop={approximateScrollTop}
|
||||||
isPanelEditorOpen={isPanelEditorOpen}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
|
|
@ -363,7 +371,6 @@ export const mapStateToProps = (state: StoreState) => ({
|
||||||
isInitSlow: state.dashboard.isInitSlow,
|
isInitSlow: state.dashboard.isInitSlow,
|
||||||
initError: state.dashboard.initError,
|
initError: state.dashboard.initError,
|
||||||
dashboard: state.dashboard.getModel(),
|
dashboard: state.dashboard.getModel(),
|
||||||
isPanelEditorOpen: state.panelEditor.isOpen,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,7 @@ export class SoloPanelPage extends Component<Props, State> {
|
||||||
|
|
||||||
// we just got a new dashboard
|
// we just got a new dashboard
|
||||||
if (!prevProps.dashboard || prevProps.dashboard.uid !== dashboard.uid) {
|
if (!prevProps.dashboard || prevProps.dashboard.uid !== dashboard.uid) {
|
||||||
const panelId = this.getPanelId();
|
const panel = dashboard.getPanelByUrlId(this.props.queryParams.panelId);
|
||||||
|
|
||||||
// need to expand parent row if this panel is inside a row
|
|
||||||
dashboard.expandParentRowFor(panelId);
|
|
||||||
|
|
||||||
const panel = dashboard.getPanelById(panelId);
|
|
||||||
|
|
||||||
if (!panel) {
|
if (!panel) {
|
||||||
this.setState({ notFound: true });
|
this.setState({ notFound: true });
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -22,7 +22,6 @@ export interface Props {
|
||||||
editPanel: PanelModel | null;
|
editPanel: PanelModel | null;
|
||||||
viewPanel: PanelModel | null;
|
viewPanel: PanelModel | null;
|
||||||
scrollTop: number;
|
scrollTop: number;
|
||||||
isPanelEditorOpen?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,17 @@ export class TimeSrv {
|
||||||
|
|
||||||
this.stopAutoRefresh();
|
this.stopAutoRefresh();
|
||||||
|
|
||||||
if (interval) {
|
const currentUrlState = locationService.getSearchObject();
|
||||||
|
|
||||||
|
if (!interval) {
|
||||||
|
// Clear URL state
|
||||||
|
if (currentUrlState.refresh) {
|
||||||
|
locationService.partial({ refresh: null }, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const validInterval = this.contextSrv.getValidInterval(interval);
|
const validInterval = this.contextSrv.getValidInterval(interval);
|
||||||
const intervalMs = rangeUtil.intervalToMs(validInterval);
|
const intervalMs = rangeUtil.intervalToMs(validInterval);
|
||||||
|
|
||||||
|
|
@ -219,13 +229,11 @@ export class TimeSrv {
|
||||||
this.startNextRefreshTimer(intervalMs);
|
this.startNextRefreshTimer(intervalMs);
|
||||||
this.refreshDashboard();
|
this.refreshDashboard();
|
||||||
}, intervalMs);
|
}, intervalMs);
|
||||||
}
|
|
||||||
|
|
||||||
if (interval) {
|
|
||||||
const refresh = this.contextSrv.getValidInterval(interval);
|
const refresh = this.contextSrv.getValidInterval(interval);
|
||||||
|
|
||||||
|
if (currentUrlState.refresh !== refresh) {
|
||||||
locationService.partial({ refresh }, true);
|
locationService.partial({ refresh }, true);
|
||||||
} else {
|
|
||||||
locationService.partial({ refresh: null }, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1051,17 +1051,22 @@ export class DashboardModel {
|
||||||
this.events.emit(CoreEvents.templateVariableValueUpdated);
|
this.events.emit(CoreEvents.templateVariableValueUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
expandParentRowFor(panelId: number) {
|
getPanelByUrlId(panelUrlId: string) {
|
||||||
|
const panelId = parseInt(panelUrlId ?? '0', 10);
|
||||||
|
|
||||||
|
// First try to find it in a collapsed row and exand it
|
||||||
for (const panel of this.panels) {
|
for (const panel of this.panels) {
|
||||||
if (panel.collapsed) {
|
if (panel.collapsed) {
|
||||||
for (const rowPanel of panel.panels) {
|
for (const rowPanel of panel.panels) {
|
||||||
if (rowPanel.id === panelId) {
|
if (rowPanel.id === panelId) {
|
||||||
this.toggleRow(panel);
|
this.toggleRow(panel);
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.getPanelById(panelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleLegendsForAll() {
|
toggleLegendsForAll() {
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,6 @@
|
||||||
@import 'components/dashboard_list';
|
@import 'components/dashboard_list';
|
||||||
@import 'components/page_header';
|
@import 'components/page_header';
|
||||||
@import 'components/dashboard_settings';
|
@import 'components/dashboard_settings';
|
||||||
@import 'components/panel_editor';
|
|
||||||
@import 'components/toolbar';
|
@import 'components/toolbar';
|
||||||
@import 'components/add_data_source.scss';
|
@import 'components/add_data_source.scss';
|
||||||
@import 'components/page_loader';
|
@import 'components/page_loader';
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
.panel-editor__scroll {
|
|
||||||
flex-grow: 1;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
min-height: 0;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-editor__content {
|
|
||||||
padding: 0 16px 16px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-in-fullscreen {
|
|
||||||
.search-container {
|
|
||||||
left: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue