123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 |
- import { css } from '@emotion/css';
- import classnames from 'classnames';
- import React, { PureComponent } from 'react';
- import { connect, ConnectedProps } from 'react-redux';
- import { GrafanaTheme2, TimeRange } from '@grafana/data';
- import { selectors } from '@grafana/e2e-selectors';
- import { locationService } from '@grafana/runtime';
- import { CustomScrollbar, stylesFactory, Themeable2, withTheme2 } from '@grafana/ui';
- import { notifyApp } from 'app/core/actions';
- import { Branding } from 'app/core/components/Branding/Branding';
- import { createErrorNotification } from 'app/core/copy/appNotification';
- import { getKioskMode } from 'app/core/navigation/kiosk';
- import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
- import { PanelModel } from 'app/features/dashboard/state';
- import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
- import { KioskMode, StoreState } from 'app/types';
- import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events';
- import { cancelVariables, templateVarsChangedInUrl } from '../../variables/state/actions';
- import { findTemplateVarChanges } from '../../variables/utils';
- import { DashNav } from '../components/DashNav';
- import { DashboardFailed } from '../components/DashboardLoading/DashboardFailed';
- import { DashboardLoading } from '../components/DashboardLoading/DashboardLoading';
- import { DashboardPrompt } from '../components/DashboardPrompt/DashboardPrompt';
- import { DashboardSettings } from '../components/DashboardSettings';
- import { PanelInspector } from '../components/Inspector/PanelInspector';
- import { PanelEditor } from '../components/PanelEditor/PanelEditor';
- import { SubMenu } from '../components/SubMenu/SubMenu';
- import { DashboardGrid } from '../dashgrid/DashboardGrid';
- import { liveTimer } from '../dashgrid/liveTimer';
- import { getTimeSrv } from '../services/TimeSrv';
- import { cleanUpDashboardAndVariables } from '../state/actions';
- import { initDashboard } from '../state/initDashboard';
- export interface DashboardPageRouteParams {
- uid?: string;
- type?: string;
- slug?: string;
- }
- type DashboardPageRouteSearchParams = {
- tab?: string;
- folderId?: string;
- editPanel?: string;
- viewPanel?: string;
- editview?: string;
- inspect?: string;
- from?: string;
- to?: string;
- refresh?: string;
- };
- export const mapStateToProps = (state: StoreState) => ({
- initPhase: state.dashboard.initPhase,
- initError: state.dashboard.initError,
- dashboard: state.dashboard.getModel(),
- });
- const mapDispatchToProps = {
- initDashboard,
- cleanUpDashboardAndVariables,
- notifyApp,
- cancelVariables,
- templateVarsChangedInUrl,
- };
- const connector = connect(mapStateToProps, mapDispatchToProps);
- export type Props = Themeable2 &
- GrafanaRouteComponentProps<DashboardPageRouteParams, DashboardPageRouteSearchParams> &
- ConnectedProps<typeof connector>;
- export interface State {
- editPanel: PanelModel | null;
- viewPanel: PanelModel | null;
- updateScrollTop?: number;
- rememberScrollTop: number;
- showLoadingState: boolean;
- panelNotFound: boolean;
- editPanelAccessDenied: boolean;
- scrollElement?: HTMLDivElement;
- }
- export class UnthemedDashboardPage extends PureComponent<Props, State> {
- private forceRouteReloadCounter = 0;
- state: State = this.getCleanState();
- getCleanState(): State {
- return {
- editPanel: null,
- viewPanel: null,
- showLoadingState: false,
- rememberScrollTop: 0,
- panelNotFound: false,
- editPanelAccessDenied: false,
- };
- }
- componentDidMount() {
- this.initDashboard();
- this.forceRouteReloadCounter = (this.props.history.location.state as any)?.routeReloadCounter || 0;
- }
- componentWillUnmount() {
- this.closeDashboard();
- }
- closeDashboard() {
- this.props.cleanUpDashboardAndVariables();
- this.setState(this.getCleanState());
- }
- initDashboard() {
- const { dashboard, match, queryParams } = this.props;
- if (dashboard) {
- this.closeDashboard();
- }
- this.props.initDashboard({
- urlSlug: match.params.slug,
- urlUid: match.params.uid,
- urlType: match.params.type,
- urlFolderId: queryParams.folderId,
- routeName: this.props.route.routeName,
- fixUrl: true,
- });
- // small delay to start live updates
- setTimeout(this.updateLiveTimer, 250);
- }
- componentDidUpdate(prevProps: Props, prevState: State) {
- const { dashboard, match, templateVarsChangedInUrl } = this.props;
- const routeReloadCounter = (this.props.history.location.state as any)?.routeReloadCounter;
- if (!dashboard) {
- return;
- }
- // if we just got dashboard update title
- if (prevProps.dashboard !== dashboard) {
- document.title = dashboard.title + ' - ' + Branding.AppTitle;
- }
- if (
- prevProps.match.params.uid !== match.params.uid ||
- (routeReloadCounter !== undefined && this.forceRouteReloadCounter !== routeReloadCounter)
- ) {
- this.initDashboard();
- this.forceRouteReloadCounter = routeReloadCounter;
- return;
- }
- if (prevProps.location.search !== this.props.location.search) {
- const prevUrlParams = prevProps.queryParams;
- const urlParams = this.props.queryParams;
- if (urlParams?.from !== prevUrlParams?.from || urlParams?.to !== prevUrlParams?.to) {
- getTimeSrv().updateTimeRangeFromUrl();
- this.updateLiveTimer();
- }
- if (!prevUrlParams?.refresh && urlParams?.refresh) {
- getTimeSrv().setAutoRefresh(urlParams.refresh);
- }
- const templateVarChanges = findTemplateVarChanges(this.props.queryParams, prevProps.queryParams);
- if (templateVarChanges) {
- templateVarsChangedInUrl(dashboard.uid, templateVarChanges);
- }
- }
- // entering edit mode
- if (this.state.editPanel && !prevState.editPanel) {
- dashboardWatcher.setEditingState(true);
- // Some panels need to be notified when entering edit mode
- this.props.dashboard?.events.publish(new PanelEditEnteredEvent(this.state.editPanel.id));
- }
- // leaving edit mode
- if (!this.state.editPanel && prevState.editPanel) {
- dashboardWatcher.setEditingState(false);
- // Some panels need kicked when leaving edit mode
- this.props.dashboard?.events.publish(new PanelEditExitedEvent(prevState.editPanel.id));
- }
- if (this.state.editPanelAccessDenied) {
- this.props.notifyApp(createErrorNotification('Permission to edit panel denied'));
- locationService.partial({ editPanel: null });
- }
- if (this.state.panelNotFound) {
- this.props.notifyApp(createErrorNotification(`Panel not found`));
- locationService.partial({ editPanel: null, viewPanel: null });
- }
- }
- updateLiveTimer = () => {
- let tr: TimeRange | undefined = undefined;
- if (this.props.dashboard?.liveNow) {
- tr = getTimeSrv().timeRange();
- }
- liveTimer.setLiveTimeRange(tr);
- };
- static getDerivedStateFromProps(props: Props, state: State) {
- 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, rememberScrollTop: state.scrollElement?.scrollTop };
- } else {
- return { ...state, editPanelAccessDenied: true };
- }
- }
- // Leaving edit mode
- else if (state.editPanel && !urlEditPanelId) {
- return { ...state, editPanel: null, updateScrollTop: state.rememberScrollTop };
- }
- // 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.scrollElement?.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;
- }
- onAddPanel = () => {
- const { dashboard } = this.props;
- if (!dashboard) {
- return;
- }
- // Return if the "Add panel" exists already
- if (dashboard.panels.length > 0 && dashboard.panels[0].type === 'add-panel') {
- return;
- }
- dashboard.addPanel({
- type: 'add-panel',
- gridPos: { x: 0, y: 0, w: 12, h: 8 },
- title: 'Panel Title',
- });
- // scroll to top after adding panel
- this.setState({ updateScrollTop: 0 });
- };
- setScrollRef = (scrollElement: HTMLDivElement): void => {
- this.setState({ scrollElement });
- };
- getInspectPanel() {
- const { dashboard, queryParams } = this.props;
- const inspectPanelId = queryParams.inspect;
- if (!dashboard || !inspectPanelId) {
- return null;
- }
- const inspectPanel = dashboard.getPanelById(parseInt(inspectPanelId, 10));
- // cannot inspect panels plugin is not already loaded
- if (!inspectPanel) {
- return null;
- }
- return inspectPanel;
- }
- render() {
- const { dashboard, initError, queryParams, theme } = this.props;
- const { editPanel, viewPanel, updateScrollTop } = this.state;
- const kioskMode = getKioskMode();
- const styles = getStyles(theme, kioskMode);
- if (!dashboard) {
- return <DashboardLoading initPhase={this.props.initPhase} />;
- }
- const inspectPanel = this.getInspectPanel();
- const containerClassNames = classnames(styles.dashboardContainer, {
- 'panel-in-fullscreen': viewPanel,
- });
- const showSubMenu = !editPanel && kioskMode === KioskMode.Off && !this.props.queryParams.editview;
- return (
- <div className={containerClassNames}>
- {kioskMode !== KioskMode.Full && (
- <header data-testid={selectors.pages.Dashboard.DashNav.navV2}>
- <DashNav
- dashboard={dashboard}
- title={dashboard.title}
- folderTitle={dashboard.meta.folderTitle}
- isFullscreen={!!viewPanel}
- onAddPanel={this.onAddPanel}
- kioskMode={kioskMode}
- hideTimePicker={dashboard.timepicker.hidden}
- />
- </header>
- )}
- <DashboardPrompt dashboard={dashboard} />
- <div className={styles.dashboardScroll}>
- <CustomScrollbar
- autoHeightMin="100%"
- scrollRefCallback={this.setScrollRef}
- scrollTop={updateScrollTop}
- hideHorizontalTrack={true}
- updateAfterMountMs={500}
- >
- <div className={styles.dashboardContent}>
- {initError && <DashboardFailed />}
- {showSubMenu && (
- <section aria-label={selectors.pages.Dashboard.SubMenu.submenu}>
- <SubMenu dashboard={dashboard} annotations={dashboard.annotations.list} links={dashboard.links} />
- </section>
- )}
- <DashboardGrid dashboard={dashboard} viewPanel={viewPanel} editPanel={editPanel} />
- </div>
- </CustomScrollbar>
- </div>
- {inspectPanel && <PanelInspector dashboard={dashboard} panel={inspectPanel} />}
- {editPanel && <PanelEditor dashboard={dashboard} sourcePanel={editPanel} tab={this.props.queryParams.tab} />}
- {queryParams.editview && <DashboardSettings dashboard={dashboard} editview={queryParams.editview} />}
- </div>
- );
- }
- }
- /*
- * Styles
- */
- export const getStyles = stylesFactory((theme: GrafanaTheme2, kioskMode: KioskMode) => {
- const contentPadding = kioskMode !== KioskMode.Full ? theme.spacing(0, 2, 2) : theme.spacing(2);
- return {
- dashboardContainer: css`
- width: 100%;
- height: 100%;
- display: flex;
- flex: 1 1 0;
- flex-direction: column;
- min-height: 0;
- `,
- dashboardScroll: css`
- width: 100%;
- flex-grow: 1;
- min-height: 0;
- display: flex;
- `,
- dashboardContent: css`
- padding: ${contentPadding};
- flex-basis: 100%;
- flex-grow: 1;
- `,
- };
- });
- export const DashboardPage = withTheme2(UnthemedDashboardPage);
- DashboardPage.displayName = 'DashboardPage';
- export default connector(DashboardPage);
|