// Libraries import $ from 'jquery'; import React, { MouseEvent, PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; // Services & Utils import { createErrorNotification } from 'app/core/copy/appNotification'; import { getMessageFromError } from 'app/core/utils/errors'; import { Branding } from 'app/core/components/Branding/Branding'; // Components import { DashboardGrid } from '../dashgrid/DashboardGrid'; import { DashNav } from '../components/DashNav'; import { DashboardSettings } from '../components/DashboardSettings'; import { PanelEditor } from '../components/PanelEditor/PanelEditor'; import { Alert, Button, CustomScrollbar, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui'; // Redux import { initDashboard } from '../state/initDashboard'; import { notifyApp, updateLocation } from 'app/core/actions'; // Types import { AppNotificationSeverity, DashboardInitError, DashboardInitPhase, DashboardRouteInfo, StoreState, } from 'app/types'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { InspectTab } from '../components/Inspector/types'; import { PanelInspector } from '../components/Inspector/PanelInspector'; import { SubMenu } from '../components/SubMenu/SubMenu'; import { cleanUpDashboardAndVariables } from '../state/actions'; import { cancelVariables } from '../../variables/state/actions'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; export interface Props { urlUid?: string; urlSlug?: string; urlType?: string; editview?: string; urlPanelId?: string; urlFolderId?: string; inspectPanelId?: string; $scope: any; $injector: any; routeInfo: DashboardRouteInfo; urlEditPanelId?: string; urlViewPanelId?: string; initPhase: DashboardInitPhase; isInitSlow: boolean; dashboard: DashboardModel | null; initError?: DashboardInitError; initDashboard: typeof initDashboard; cleanUpDashboardAndVariables: typeof cleanUpDashboardAndVariables; notifyApp: typeof notifyApp; updateLocation: typeof updateLocation; inspectTab?: InspectTab; isPanelEditorOpen?: boolean; cancelVariables: typeof cancelVariables; } export interface State { editPanel: PanelModel | null; viewPanel: PanelModel | null; scrollTop: number; updateScrollTop?: number; rememberScrollTop: number; showLoadingState: boolean; } export class DashboardPage extends PureComponent { state: State = { editPanel: null, viewPanel: null, showLoadingState: false, scrollTop: 0, rememberScrollTop: 0, }; async componentDidMount() { this.props.initDashboard({ $injector: this.props.$injector, $scope: this.props.$scope, urlSlug: this.props.urlSlug, urlUid: this.props.urlUid, urlType: this.props.urlType, urlFolderId: this.props.urlFolderId, routeInfo: this.props.routeInfo, fixUrl: true, }); } componentWillUnmount() { this.props.cleanUpDashboardAndVariables(); this.setPanelFullscreenClass(false); } componentDidUpdate(prevProps: Props) { const { dashboard, urlEditPanelId, urlViewPanelId, urlUid } = this.props; const { editPanel, viewPanel } = this.state; if (!dashboard) { return; } // if we just got dashboard update title if (!prevProps.dashboard) { document.title = dashboard.title + ' - ' + Branding.AppTitle; } // Due to the angular -> react url bridge we can ge an update here with new uid before the container unmounts // Can remove this condition after we switch to react router if (prevProps.urlUid !== urlUid) { return; } // entering edit mode if (!editPanel && urlEditPanelId) { 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 if (editPanel && !urlEditPanelId) { dashboardWatcher.setEditingState(false); this.setState({ editPanel: null }); } // entering view mode if (!viewPanel && urlViewPanelId) { this.getPanelByIdFromUrlParam(urlViewPanelId, panel => { this.setPanelFullscreenClass(true); dashboard.initViewPanel(panel); this.setState({ viewPanel: panel, rememberScrollTop: this.state.scrollTop, updateScrollTop: 0, }); }); } // leaving view mode if (viewPanel && !urlViewPanelId) { 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 this.props.updateLocation({ query: { editPanel: null, viewPanel: null, }, partial: true, }); 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) { $('body').toggleClass('panel-in-fullscreen', isFullscreen); } setScrollTop = (e: MouseEvent): void => { const target = e.target as HTMLElement; this.setState({ scrollTop: target.scrollTop, updateScrollTop: undefined }); }; 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 }); }; cancelVariables = () => { this.props.updateLocation({ path: '/' }); }; renderSlowInitState() { return (
{this.props.initPhase} {' '}
); } renderInitFailedState() { const { initError } = this.props; if (!initError) { return null; } return (
); } getInspectPanel() { const { dashboard, inspectPanelId } = this.props; 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, editview, $injector, isInitSlow, initError, inspectTab, isPanelEditorOpen, updateLocation, } = this.props; const { editPanel, viewPanel, scrollTop, updateScrollTop } = this.state; if (!dashboard) { if (isInitSlow) { return this.renderSlowInitState(); } return null; } // Only trigger render when the scroll has moved by 25 const approximateScrollTop = Math.round(scrollTop / 25) * 25; const inspectPanel = this.getInspectPanel(); return (
{initError && this.renderInitFailedState()} {!editPanel && }
{inspectPanel && } {editPanel && } {editview && }
); } } export const mapStateToProps = (state: StoreState) => ({ urlUid: state.location.routeParams.uid, urlSlug: state.location.routeParams.slug, urlType: state.location.routeParams.type, editview: state.location.query.editview, urlPanelId: state.location.query.panelId, urlFolderId: state.location.query.folderId, urlEditPanelId: state.location.query.editPanel, urlViewPanelId: state.location.query.viewPanel, inspectPanelId: state.location.query.inspect, initPhase: state.dashboard.initPhase, isInitSlow: state.dashboard.isInitSlow, initError: state.dashboard.initError, dashboard: state.dashboard.getModel() as DashboardModel, inspectTab: state.location.query.inspectTab, isPanelEditorOpen: state.panelEditor.isOpen, }); const mapDispatchToProps = { initDashboard, cleanUpDashboardAndVariables, notifyApp, updateLocation, cancelVariables, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage));