Browse Source

Big refactoring for dashboard init redux actions

Torkel Ödegaard 6 years ago
parent
commit
dd0afd0a0b

+ 41 - 0
public/app/core/components/AlertBox/AlertBox.tsx

@@ -0,0 +1,41 @@
+import React, { FunctionComponent } from 'react';
+import { AppNotificationSeverity } from 'app/types';
+
+interface Props {
+  title: string;
+  icon?: string;
+  text?: string;
+  severity: AppNotificationSeverity;
+  onClose?: () => void;
+}
+
+function getIconFromSeverity(severity: AppNotificationSeverity): string {
+  switch (severity) {
+    case AppNotificationSeverity.Error: {
+      return 'fa fa-exclamation-triangle';
+    }
+    case AppNotificationSeverity.Success: {
+      return 'fa fa-check';
+    }
+    default: return null;
+  }
+}
+
+export const AlertBox: FunctionComponent<Props> = ({ title, icon, text, severity, onClose }) => {
+  return (
+    <div className={`alert alert-${severity}`}>
+      <div className="alert-icon">
+        <i className={icon || getIconFromSeverity(severity)} />
+      </div>
+      <div className="alert-body">
+        <div className="alert-title">{title}</div>
+        {text && <div className="alert-text">{text}</div>}
+      </div>
+      {onClose && (
+        <button type="button" className="alert-close" onClick={onClose}>
+          <i className="fa fa fa-remove" />
+        </button>
+      )}
+    </div>
+  );
+};

+ 8 - 12
public/app/core/components/AppNotifications/AppNotificationItem.tsx

@@ -1,5 +1,6 @@
 import React, { Component } from 'react';
 import { AppNotification } from 'app/types';
+import { AlertBox } from '../AlertBox/AlertBox';
 
 interface Props {
   appNotification: AppNotification;
@@ -22,18 +23,13 @@ export default class AppNotificationItem extends Component<Props> {
     const { appNotification, onClearNotification } = this.props;
 
     return (
-      <div className={`alert-${appNotification.severity} alert`}>
-        <div className="alert-icon">
-          <i className={appNotification.icon} />
-        </div>
-        <div className="alert-body">
-          <div className="alert-title">{appNotification.title}</div>
-          <div className="alert-text">{appNotification.text}</div>
-        </div>
-        <button type="button" className="alert-close" onClick={() => onClearNotification(appNotification.id)}>
-          <i className="fa fa fa-remove" />
-        </button>
-      </div>
+      <AlertBox
+        severity={appNotification.severity}
+        title={appNotification.title}
+        text={appNotification.text}
+        icon={appNotification.icon}
+        onClose={() => onClearNotification(appNotification.id)}
+      />
     );
   }
 }

+ 2 - 13
public/app/core/copy/appNotification.ts

@@ -1,5 +1,5 @@
-import _ from 'lodash';
 import { AppNotification, AppNotificationSeverity, AppNotificationTimeout } from 'app/types';
+import { getMessageFromError } from 'app/core/utils/errors';
 
 const defaultSuccessNotification: AppNotification = {
   title: '',
@@ -33,21 +33,10 @@ export const createSuccessNotification = (title: string, text?: string): AppNoti
 });
 
 export const createErrorNotification = (title: string, text?: any): AppNotification => {
-  // Handling if text is an error object
-  if (text && !_.isString(text)) {
-    if (text.message) {
-      text = text.message;
-    } else if (text.data && text.data.message) {
-      text = text.data.message;
-    } else {
-      text = text.toString();
-    }
-  }
-
   return {
     ...defaultErrorNotification,
     title: title,
-    text: text,
+    text: getMessageFromError(text),
     id: Date.now(),
   };
 };

+ 15 - 0
public/app/core/utils/errors.ts

@@ -0,0 +1,15 @@
+import _ from 'lodash';
+
+export function getMessageFromError(err: any): string | null {
+  if (err && !_.isString(err)) {
+    if (err.message) {
+      return err.message;
+    } else if (err.data && err.data.message) {
+      return err.data.message;
+    } else {
+      return JSON.stringify(err);
+    }
+  }
+
+  return null;
+}

+ 0 - 117
public/app/features/dashboard/components/DashNav/DashNavCtrl.ts

@@ -1,117 +0,0 @@
-import moment from 'moment';
-import angular from 'angular';
-import { appEvents, NavModel } from 'app/core/core';
-import { DashboardModel } from '../../state/DashboardModel';
-
-export class DashNavCtrl {
-  dashboard: DashboardModel;
-  navModel: NavModel;
-  titleTooltip: string;
-
-  /** @ngInject */
-  constructor(private $scope, private dashboardSrv, private $location, public playlistSrv) {
-    if (this.dashboard.meta.isSnapshot) {
-      const meta = this.dashboard.meta;
-      this.titleTooltip = 'Created: &nbsp;' + moment(meta.created).calendar();
-      if (meta.expires) {
-        this.titleTooltip += '<br>Expires: &nbsp;' + moment(meta.expires).fromNow() + '<br>';
-      }
-    }
-  }
-
-  toggleSettings() {
-    const search = this.$location.search();
-    if (search.editview) {
-      delete search.editview;
-    } else {
-      search.editview = 'settings';
-    }
-    this.$location.search(search);
-  }
-
-  toggleViewMode() {
-    appEvents.emit('toggle-kiosk-mode');
-  }
-
-  close() {
-    const search = this.$location.search();
-    if (search.editview) {
-      delete search.editview;
-    } else if (search.fullscreen) {
-      delete search.fullscreen;
-      delete search.edit;
-      delete search.tab;
-      delete search.panelId;
-    }
-    this.$location.search(search);
-  }
-
-  starDashboard() {
-    this.dashboardSrv.starDashboard(this.dashboard.id, this.dashboard.meta.isStarred).then(newState => {
-      this.dashboard.meta.isStarred = newState;
-    });
-  }
-
-  shareDashboard(tabIndex) {
-    const modalScope = this.$scope.$new();
-    modalScope.tabIndex = tabIndex;
-    modalScope.dashboard = this.dashboard;
-
-    appEvents.emit('show-modal', {
-      src: 'public/app/features/dashboard/components/ShareModal/template.html',
-      scope: modalScope,
-    });
-  }
-
-  hideTooltip(evt) {
-    angular.element(evt.currentTarget).tooltip('hide');
-  }
-
-  saveDashboard() {
-    return this.dashboardSrv.saveDashboard();
-  }
-
-  showSearch() {
-    if (this.dashboard.meta.fullscreen) {
-      this.close();
-      return;
-    }
-
-    appEvents.emit('show-dash-search');
-  }
-
-  addPanel() {
-    appEvents.emit('dash-scroll', { animate: true, evt: 0 });
-
-    if (this.dashboard.panels.length > 0 && this.dashboard.panels[0].type === 'add-panel') {
-      return; // Return if the "Add panel" exists already
-    }
-
-    this.dashboard.addPanel({
-      type: 'add-panel',
-      gridPos: { x: 0, y: 0, w: 12, h: 8 },
-      title: 'Panel Title',
-    });
-  }
-
-  navItemClicked(navItem, evt) {
-    if (navItem.clickHandler) {
-      navItem.clickHandler();
-      evt.preventDefault();
-    }
-  }
-}
-
-export function dashNavDirective() {
-  return {
-    restrict: 'E',
-    templateUrl: 'public/app/features/dashboard/components/DashNav/template.html',
-    controller: DashNavCtrl,
-    bindToController: true,
-    controllerAs: 'ctrl',
-    transclude: true,
-    scope: { dashboard: '=' },
-  };
-}
-
-angular.module('grafana.directives').directive('dashnav', dashNavDirective);

+ 0 - 1
public/app/features/dashboard/components/DashNav/index.ts

@@ -1,3 +1,2 @@
-export { DashNavCtrl } from './DashNavCtrl';
 import DashNav from './DashNav';
 export { DashNav };

+ 6 - 6
public/app/features/dashboard/containers/DashboardPage.test.tsx

@@ -2,8 +2,8 @@ import React from 'react';
 import { shallow, ShallowWrapper } from 'enzyme';
 import { DashboardPage, Props, State } from './DashboardPage';
 import { DashboardModel } from '../state';
-import { setDashboardModel } from '../state/actions';
-import { DashboardRouteInfo, DashboardLoadingState } from 'app/types';
+import { cleanUpDashboard } from '../state/actions';
+import { DashboardRouteInfo, DashboardInitPhase } from 'app/types';
 
 jest.mock('sass/_variables.scss', () => ({
   panelhorizontalpadding: 10,
@@ -22,13 +22,13 @@ function setup(propOverrides?: Partial<Props>): ShallowWrapper<Props, State, Das
     routeInfo: DashboardRouteInfo.Normal,
     urlEdit: false,
     urlFullscreen: false,
-    loadingState: DashboardLoadingState.Done,
-    isLoadingSlow: false,
+    initPhase: DashboardInitPhase.Completed,
+    isInitSlow: false,
     initDashboard: jest.fn(),
     updateLocation: jest.fn(),
     notifyApp: jest.fn(),
     dashboard: null,
-    setDashboardModel: setDashboardModel,
+    cleanUpDashboard: cleanUpDashboard,
   };
 
   Object.assign(props, propOverrides);
@@ -66,7 +66,7 @@ describe('DashboardPage', () => {
         canEdit: true,
         canSave: true,
       });
-      wrapper.setProps({ dashboard, loadingState: DashboardLoadingState.Done });
+      wrapper.setProps({ dashboard, initPhase: DashboardInitPhase.Completed });
     });
 
     it('Should update title', () => {

+ 40 - 14
public/app/features/dashboard/containers/DashboardPage.tsx

@@ -7,6 +7,7 @@ import classNames from 'classnames';
 
 // Services & Utils
 import { createErrorNotification } from 'app/core/copy/appNotification';
+import { getMessageFromError } from 'app/core/utils/errors';
 
 // Components
 import { DashboardGrid } from '../dashgrid/DashboardGrid';
@@ -14,15 +15,22 @@ import { DashNav } from '../components/DashNav';
 import { SubMenu } from '../components/SubMenu';
 import { DashboardSettings } from '../components/DashboardSettings';
 import { CustomScrollbar } from '@grafana/ui';
+import { AlertBox } from 'app/core/components/AlertBox/AlertBox';
 
 // Redux
 import { initDashboard } from '../state/initDashboard';
-import { setDashboardModel } from '../state/actions';
+import { cleanUpDashboard } from '../state/actions';
 import { updateLocation } from 'app/core/actions';
 import { notifyApp } from 'app/core/actions';
 
 // Types
-import { StoreState, DashboardLoadingState, DashboardRouteInfo } from 'app/types';
+import {
+  StoreState,
+  DashboardInitPhase,
+  DashboardRouteInfo,
+  DashboardInitError,
+  AppNotificationSeverity,
+} from 'app/types';
 import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
 
 export interface Props {
@@ -37,11 +45,12 @@ export interface Props {
   routeInfo: DashboardRouteInfo;
   urlEdit: boolean;
   urlFullscreen: boolean;
-  loadingState: DashboardLoadingState;
-  isLoadingSlow: boolean;
+  initPhase: DashboardInitPhase;
+  isInitSlow: boolean;
   dashboard: DashboardModel | null;
+  initError?: DashboardInitError;
   initDashboard: typeof initDashboard;
-  setDashboardModel: typeof setDashboardModel;
+  cleanUpDashboard: typeof cleanUpDashboard;
   notifyApp: typeof notifyApp;
   updateLocation: typeof updateLocation;
 }
@@ -83,7 +92,7 @@ export class DashboardPage extends PureComponent<Props, State> {
   componentWillUnmount() {
     if (this.props.dashboard) {
       this.props.dashboard.destroy();
-      this.props.setDashboardModel(null);
+      this.props.cleanUpDashboard();
     }
   }
 
@@ -204,23 +213,37 @@ export class DashboardPage extends PureComponent<Props, State> {
     this.setState({ scrollTop: 0 });
   };
 
-  renderLoadingState() {
+  renderSlowInitState() {
     return (
       <div className="dashboard-loading">
         <div className="dashboard-loading__text">
-          <i className="fa fa-spinner fa-spin" /> Dashboard {this.props.loadingState}
+          <i className="fa fa-spinner fa-spin" /> {this.props.initPhase}
         </div>
       </div>
     );
   }
 
+  renderInitFailedState() {
+    const { initError } = this.props;
+
+    return (
+      <div className="dashboard-loading">
+        <AlertBox
+          severity={AppNotificationSeverity.Error}
+          title={initError.message}
+          text={getMessageFromError(initError.error)}
+        />
+      </div>
+    );
+  }
+
   render() {
-    const { dashboard, editview, $injector, isLoadingSlow } = this.props;
+    const { dashboard, editview, $injector, isInitSlow, initError } = this.props;
     const { isSettingsOpening, isEditing, isFullscreen, scrollTop } = this.state;
 
     if (!dashboard) {
-      if (isLoadingSlow) {
-        return this.renderLoadingState();
+      if (isInitSlow) {
+        return this.renderSlowInitState();
       }
       return null;
     }
@@ -249,6 +272,8 @@ export class DashboardPage extends PureComponent<Props, State> {
           <CustomScrollbar autoHeightMin={'100%'} setScrollTop={this.setScrollTop} scrollTop={scrollTop}>
             {editview && <DashboardSettings dashboard={dashboard} />}
 
+            {initError && this.renderInitFailedState()}
+
             <div className={gridWrapperClasses}>
               {dashboard.meta.submenuEnabled && <SubMenu dashboard={dashboard} />}
               <DashboardGrid dashboard={dashboard} isEditing={isEditing} isFullscreen={isFullscreen} />
@@ -269,14 +294,15 @@ const mapStateToProps = (state: StoreState) => ({
   urlFolderId: state.location.query.folderId,
   urlFullscreen: state.location.query.fullscreen === true,
   urlEdit: state.location.query.edit === true,
-  loadingState: state.dashboard.loadingState,
-  isLoadingSlow: state.dashboard.isLoadingSlow,
+  initPhase: state.dashboard.initPhase,
+  isInitSlow: state.dashboard.isInitSlow,
+  initError: state.dashboard.initError,
   dashboard: state.dashboard.model as DashboardModel,
 });
 
 const mapDispatchToProps = {
   initDashboard,
-  setDashboardModel,
+  cleanUpDashboard,
   notifyApp,
   updateLocation,
 };

+ 0 - 1
public/app/features/dashboard/containers/SoloPanelPage.tsx

@@ -100,7 +100,6 @@ const mapStateToProps = (state: StoreState) => ({
   urlSlug: state.location.routeParams.slug,
   urlType: state.location.routeParams.type,
   urlPanelId: state.location.query.panelId,
-  loadingState: state.dashboard.loadingState,
   dashboard: state.dashboard.model as DashboardModel,
 });
 

+ 22 - 6
public/app/features/dashboard/state/actions.ts

@@ -8,20 +8,36 @@ import { loadPluginDashboards } from '../../plugins/state/actions';
 import { notifyApp } from 'app/core/actions';
 
 // Types
-import { ThunkResult } from 'app/types';
 import {
+  ThunkResult,
   DashboardAcl,
   DashboardAclDTO,
   PermissionLevel,
   DashboardAclUpdateDTO,
   NewDashboardAclItem,
-} from 'app/types/acl';
-import { DashboardLoadingState, MutableDashboard } from 'app/types/dashboard';
+  MutableDashboard,
+  DashboardInitError,
+} from 'app/types';
 
 export const loadDashboardPermissions = actionCreatorFactory<DashboardAclDTO[]>('LOAD_DASHBOARD_PERMISSIONS').create();
-export const setDashboardLoadingState = actionCreatorFactory<DashboardLoadingState>('SET_DASHBOARD_LOADING_STATE').create();
-export const setDashboardModel = actionCreatorFactory<MutableDashboard>('SET_DASHBOARD_MODEL').create();
-export const setDashboardLoadingSlow = noPayloadActionCreatorFactory('SET_DASHBOARD_LOADING_SLOW').create();
+
+export const dashboardInitFetching = noPayloadActionCreatorFactory('DASHBOARD_INIT_FETCHING').create();
+
+export const dashboardInitServices = noPayloadActionCreatorFactory('DASHBOARD_INIT_SERVICES').create();
+
+export const dashboardInitSlow = noPayloadActionCreatorFactory('SET_DASHBOARD_INIT_SLOW').create();
+
+export const dashboardInitCompleted = actionCreatorFactory<MutableDashboard>('DASHBOARD_INIT_COMLETED').create();
+
+/*
+ * Unrecoverable init failure (fetch or model creation failed)
+ */
+export const dashboardInitFailed = actionCreatorFactory<DashboardInitError>('DASHBOARD_INIT_FAILED').create();
+
+/*
+ * When leaving dashboard, resets state
+ * */
+export const cleanUpDashboard = noPayloadActionCreatorFactory('DASHBOARD_CLEAN_UP').create();
 
 export function getDashboardPermissions(id: number): ThunkResult<void> {
   return async dispatch => {

+ 4 - 6
public/app/features/dashboard/state/initDashboard.test.ts

@@ -1,7 +1,7 @@
 import configureMockStore from 'redux-mock-store';
 import thunk from 'redux-thunk';
 import { initDashboard, InitDashboardArgs } from './initDashboard';
-import { DashboardRouteInfo, DashboardLoadingState } from 'app/types';
+import { DashboardRouteInfo } from 'app/types';
 
 const mockStore = configureMockStore([thunk]);
 
@@ -98,13 +98,11 @@ describeInitScenario('Initializing new dashboard', ctx => {
   });
 
   it('Should send action to set loading state to fetching', () => {
-    expect(ctx.actions[0].type).toBe('SET_DASHBOARD_LOADING_STATE');
-    expect(ctx.actions[0].payload).toBe(DashboardLoadingState.Fetching);
+    expect(ctx.actions[0].type).toBe('DASHBOARD_INIT_FETCHING');
   });
 
   it('Should send action to set loading state to Initializing', () => {
-    expect(ctx.actions[1].type).toBe('SET_DASHBOARD_LOADING_STATE');
-    expect(ctx.actions[1].payload).toBe(DashboardLoadingState.Initializing);
+    expect(ctx.actions[1].type).toBe('DASHBOARD_INIT_SERVICES');
   });
 
   it('Should update location with orgId query param', () => {
@@ -113,7 +111,7 @@ describeInitScenario('Initializing new dashboard', ctx => {
   });
 
   it('Should send action to set dashboard model', () => {
-    expect(ctx.actions[3].type).toBe('SET_DASHBOARD_MODEL');
+    expect(ctx.actions[3].type).toBe('DASHBOARD_INIT_COMLETED');
     expect(ctx.actions[3].payload.title).toBe('New dashboard');
   });
 

+ 15 - 18
public/app/features/dashboard/state/initDashboard.ts

@@ -12,17 +12,16 @@ import { KeybindingSrv } from 'app/core/services/keybindingSrv';
 import { updateLocation } from 'app/core/actions';
 import { notifyApp } from 'app/core/actions';
 import locationUtil from 'app/core/utils/location_util';
-import { setDashboardLoadingState, setDashboardModel, setDashboardLoadingSlow } from './actions';
+import {
+  dashboardInitFetching,
+  dashboardInitCompleted,
+  dashboardInitFailed,
+  dashboardInitSlow,
+  dashboardInitServices,
+} from './actions';
 
 // Types
-import {
-  DashboardLoadingState,
-  DashboardRouteInfo,
-  StoreState,
-  ThunkDispatch,
-  ThunkResult,
-  DashboardDTO,
-} from 'app/types';
+import { DashboardRouteInfo, StoreState, ThunkDispatch, ThunkResult, DashboardDTO } from 'app/types';
 import { DashboardModel } from './DashboardModel';
 
 export interface InitDashboardArgs {
@@ -106,8 +105,7 @@ async function fetchDashboard(
         throw { message: 'Unknown route ' + args.routeInfo };
     }
   } catch (err) {
-    dispatch(setDashboardLoadingState(DashboardLoadingState.Error));
-    dispatch(notifyApp(createErrorNotification('Dashboard fetch failed', err)));
+    dispatch(dashboardInitFailed({ message: 'Failed to fetch dashboard', error: err }));
     console.log(err);
     return null;
   }
@@ -125,13 +123,13 @@ async function fetchDashboard(
 export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
   return async (dispatch, getState) => {
     // set fetching state
-    dispatch(setDashboardLoadingState(DashboardLoadingState.Fetching));
+    dispatch(dashboardInitFetching());
 
     // Detect slow loading / initializing and set state flag
     // This is in order to not show loading indication for fast loading dashboards as it creates blinking/flashing
     setTimeout(() => {
       if (getState().dashboard.model === null) {
-        dispatch(setDashboardLoadingSlow());
+        dispatch(dashboardInitSlow());
       }
     }, 500);
 
@@ -144,15 +142,14 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
     }
 
     // set initializing state
-    dispatch(setDashboardLoadingState(DashboardLoadingState.Initializing));
+    dispatch(dashboardInitServices());
 
     // create model
     let dashboard: DashboardModel;
     try {
       dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta);
     } catch (err) {
-      dispatch(setDashboardLoadingState(DashboardLoadingState.Error));
-      dispatch(notifyApp(createErrorNotification('Dashboard model initializing failure', err)));
+      dispatch(dashboardInitFailed({ message: 'Failed create dashboard model', error: err }));
       console.log(err);
       return;
     }
@@ -203,8 +200,8 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
 
     // legacy srv state
     dashboardSrv.setCurrent(dashboard);
-    // set model in redux (even though it's mutable)
-    dispatch(setDashboardModel(dashboard));
+    // yay we are done
+    dispatch(dashboardInitCompleted(dashboard));
   };
 }
 

+ 54 - 12
public/app/features/dashboard/state/reducers.ts

@@ -1,11 +1,20 @@
-import { DashboardState, DashboardLoadingState } from 'app/types/dashboard';
-import { loadDashboardPermissions, setDashboardLoadingState, setDashboardModel, setDashboardLoadingSlow } from './actions';
+import { DashboardState, DashboardInitPhase } from 'app/types';
+import {
+  loadDashboardPermissions,
+  dashboardInitFetching,
+  dashboardInitSlow,
+  dashboardInitServices,
+  dashboardInitFailed,
+  dashboardInitCompleted,
+  cleanUpDashboard,
+} from './actions';
 import { reducerFactory } from 'app/core/redux';
 import { processAclItems } from 'app/core/utils/acl';
+import { DashboardModel } from './DashboardModel';
 
 export const initialState: DashboardState = {
-  loadingState: DashboardLoadingState.NotStarted,
-  isLoadingSlow: false,
+  initPhase: DashboardInitPhase.NotStarted,
+  isInitSlow: false,
   model: null,
   permissions: [],
 };
@@ -19,27 +28,60 @@ export const dashboardReducer = reducerFactory(initialState)
     }),
   })
   .addMapper({
-    filter: setDashboardLoadingState,
-    mapper: (state, action) => ({
+    filter: dashboardInitFetching,
+    mapper: state => ({
+      ...state,
+      initPhase: DashboardInitPhase.Fetching,
+    }),
+  })
+  .addMapper({
+    filter: dashboardInitServices,
+    mapper: state => ({
+      ...state,
+      initPhase: DashboardInitPhase.Services,
+    }),
+  })
+  .addMapper({
+    filter: dashboardInitSlow,
+    mapper: state => ({
       ...state,
-      loadingState: action.payload
+      isInitSlow: true,
     }),
   })
   .addMapper({
-    filter: setDashboardModel,
+    filter: dashboardInitFailed,
     mapper: (state, action) => ({
       ...state,
-      model: action.payload,
-      isLoadingSlow: false,
+      initPhase: DashboardInitPhase.Failed,
+      isInitSlow: false,
+      initError: action.payload,
+      model: new DashboardModel({ title: 'Dashboard init failed' }, { canSave: false, canEdit: false }),
     }),
   })
   .addMapper({
-    filter: setDashboardLoadingSlow,
+    filter: dashboardInitCompleted,
     mapper: (state, action) => ({
       ...state,
-      isLoadingSlow: true,
+      initPhase: DashboardInitPhase.Completed,
+      model: action.payload,
+      isInitSlow: false,
     }),
   })
+  .addMapper({
+    filter: cleanUpDashboard,
+    mapper: (state, action) => {
+      // tear down current dashboard
+      state.model.destroy();
+
+      return {
+        ...state,
+        initPhase: DashboardInitPhase.NotStarted,
+        model: null,
+        isInitSlow: false,
+        initError: null,
+      };
+    },
+  })
   .create();
 
 export default {

+ 13 - 6
public/app/types/dashboard.ts

@@ -2,6 +2,7 @@ import { DashboardAcl } from './acl';
 
 export interface MutableDashboard {
   meta: DashboardMeta;
+  destroy: () => void;
 }
 
 export interface DashboardDTO {
@@ -44,12 +45,17 @@ export enum DashboardRouteInfo {
   Scripted = 'scripted-dashboard',
 }
 
-export enum DashboardLoadingState {
+export enum DashboardInitPhase {
   NotStarted = 'Not started',
   Fetching = 'Fetching',
-  Initializing = 'Initializing',
-  Error = 'Error',
-  Done = 'Done',
+  Services = 'Services',
+  Failed = 'Failed',
+  Completed = 'Completed',
+}
+
+export interface DashboardInitError {
+  message: string;
+  error: any;
 }
 
 export const KIOSK_MODE_TV = 'tv';
@@ -57,7 +63,8 @@ export type KioskUrlValue = 'tv' | '1' | true;
 
 export interface DashboardState {
   model: MutableDashboard | null;
-  loadingState: DashboardLoadingState;
-  isLoadingSlow: boolean;
+  initPhase: DashboardInitPhase;
+  isInitSlow: boolean;
+  initError?: DashboardInitError;
   permissions: DashboardAcl[] | null;
 }

+ 5 - 0
public/sass/pages/_dashboard.scss

@@ -282,6 +282,11 @@ div.flot-text {
   display: flex;
   align-items: center;
   justify-content: center;
+
+  .alert {
+    max-width: 600px;
+    min-width: 600px;
+  }
 }
 
 .dashboard-loading__text {