Commit dd0afd0a by Torkel Ödegaard

Big refactoring for dashboard init redux actions

parent 8574dca0
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>
);
};
import React, { Component } from 'react'; import React, { Component } from 'react';
import { AppNotification } from 'app/types'; import { AppNotification } from 'app/types';
import { AlertBox } from '../AlertBox/AlertBox';
interface Props { interface Props {
appNotification: AppNotification; appNotification: AppNotification;
...@@ -22,18 +23,13 @@ export default class AppNotificationItem extends Component<Props> { ...@@ -22,18 +23,13 @@ export default class AppNotificationItem extends Component<Props> {
const { appNotification, onClearNotification } = this.props; const { appNotification, onClearNotification } = this.props;
return ( return (
<div className={`alert-${appNotification.severity} alert`}> <AlertBox
<div className="alert-icon"> severity={appNotification.severity}
<i className={appNotification.icon} /> title={appNotification.title}
</div> text={appNotification.text}
<div className="alert-body"> icon={appNotification.icon}
<div className="alert-title">{appNotification.title}</div> onClose={() => onClearNotification(appNotification.id)}
<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>
); );
} }
} }
import _ from 'lodash';
import { AppNotification, AppNotificationSeverity, AppNotificationTimeout } from 'app/types'; import { AppNotification, AppNotificationSeverity, AppNotificationTimeout } from 'app/types';
import { getMessageFromError } from 'app/core/utils/errors';
const defaultSuccessNotification: AppNotification = { const defaultSuccessNotification: AppNotification = {
title: '', title: '',
...@@ -33,21 +33,10 @@ export const createSuccessNotification = (title: string, text?: string): AppNoti ...@@ -33,21 +33,10 @@ export const createSuccessNotification = (title: string, text?: string): AppNoti
}); });
export const createErrorNotification = (title: string, text?: any): AppNotification => { 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 { return {
...defaultErrorNotification, ...defaultErrorNotification,
title: title, title: title,
text: text, text: getMessageFromError(text),
id: Date.now(), id: Date.now(),
}; };
}; };
......
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;
}
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);
export { DashNavCtrl } from './DashNavCtrl';
import DashNav from './DashNav'; import DashNav from './DashNav';
export { DashNav }; export { DashNav };
...@@ -2,8 +2,8 @@ import React from 'react'; ...@@ -2,8 +2,8 @@ import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { DashboardPage, Props, State } from './DashboardPage'; import { DashboardPage, Props, State } from './DashboardPage';
import { DashboardModel } from '../state'; import { DashboardModel } from '../state';
import { setDashboardModel } from '../state/actions'; import { cleanUpDashboard } from '../state/actions';
import { DashboardRouteInfo, DashboardLoadingState } from 'app/types'; import { DashboardRouteInfo, DashboardInitPhase } from 'app/types';
jest.mock('sass/_variables.scss', () => ({ jest.mock('sass/_variables.scss', () => ({
panelhorizontalpadding: 10, panelhorizontalpadding: 10,
...@@ -22,13 +22,13 @@ function setup(propOverrides?: Partial<Props>): ShallowWrapper<Props, State, Das ...@@ -22,13 +22,13 @@ function setup(propOverrides?: Partial<Props>): ShallowWrapper<Props, State, Das
routeInfo: DashboardRouteInfo.Normal, routeInfo: DashboardRouteInfo.Normal,
urlEdit: false, urlEdit: false,
urlFullscreen: false, urlFullscreen: false,
loadingState: DashboardLoadingState.Done, initPhase: DashboardInitPhase.Completed,
isLoadingSlow: false, isInitSlow: false,
initDashboard: jest.fn(), initDashboard: jest.fn(),
updateLocation: jest.fn(), updateLocation: jest.fn(),
notifyApp: jest.fn(), notifyApp: jest.fn(),
dashboard: null, dashboard: null,
setDashboardModel: setDashboardModel, cleanUpDashboard: cleanUpDashboard,
}; };
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
...@@ -66,7 +66,7 @@ describe('DashboardPage', () => { ...@@ -66,7 +66,7 @@ describe('DashboardPage', () => {
canEdit: true, canEdit: true,
canSave: true, canSave: true,
}); });
wrapper.setProps({ dashboard, loadingState: DashboardLoadingState.Done }); wrapper.setProps({ dashboard, initPhase: DashboardInitPhase.Completed });
}); });
it('Should update title', () => { it('Should update title', () => {
......
...@@ -7,6 +7,7 @@ import classNames from 'classnames'; ...@@ -7,6 +7,7 @@ import classNames from 'classnames';
// Services & Utils // Services & Utils
import { createErrorNotification } from 'app/core/copy/appNotification'; import { createErrorNotification } from 'app/core/copy/appNotification';
import { getMessageFromError } from 'app/core/utils/errors';
// Components // Components
import { DashboardGrid } from '../dashgrid/DashboardGrid'; import { DashboardGrid } from '../dashgrid/DashboardGrid';
...@@ -14,15 +15,22 @@ import { DashNav } from '../components/DashNav'; ...@@ -14,15 +15,22 @@ import { DashNav } from '../components/DashNav';
import { SubMenu } from '../components/SubMenu'; import { SubMenu } from '../components/SubMenu';
import { DashboardSettings } from '../components/DashboardSettings'; import { DashboardSettings } from '../components/DashboardSettings';
import { CustomScrollbar } from '@grafana/ui'; import { CustomScrollbar } from '@grafana/ui';
import { AlertBox } from 'app/core/components/AlertBox/AlertBox';
// Redux // Redux
import { initDashboard } from '../state/initDashboard'; import { initDashboard } from '../state/initDashboard';
import { setDashboardModel } from '../state/actions'; import { cleanUpDashboard } from '../state/actions';
import { updateLocation } from 'app/core/actions'; import { updateLocation } from 'app/core/actions';
import { notifyApp } from 'app/core/actions'; import { notifyApp } from 'app/core/actions';
// Types // 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'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
export interface Props { export interface Props {
...@@ -37,11 +45,12 @@ export interface Props { ...@@ -37,11 +45,12 @@ export interface Props {
routeInfo: DashboardRouteInfo; routeInfo: DashboardRouteInfo;
urlEdit: boolean; urlEdit: boolean;
urlFullscreen: boolean; urlFullscreen: boolean;
loadingState: DashboardLoadingState; initPhase: DashboardInitPhase;
isLoadingSlow: boolean; isInitSlow: boolean;
dashboard: DashboardModel | null; dashboard: DashboardModel | null;
initError?: DashboardInitError;
initDashboard: typeof initDashboard; initDashboard: typeof initDashboard;
setDashboardModel: typeof setDashboardModel; cleanUpDashboard: typeof cleanUpDashboard;
notifyApp: typeof notifyApp; notifyApp: typeof notifyApp;
updateLocation: typeof updateLocation; updateLocation: typeof updateLocation;
} }
...@@ -83,7 +92,7 @@ export class DashboardPage extends PureComponent<Props, State> { ...@@ -83,7 +92,7 @@ export class DashboardPage extends PureComponent<Props, State> {
componentWillUnmount() { componentWillUnmount() {
if (this.props.dashboard) { if (this.props.dashboard) {
this.props.dashboard.destroy(); this.props.dashboard.destroy();
this.props.setDashboardModel(null); this.props.cleanUpDashboard();
} }
} }
...@@ -204,23 +213,37 @@ export class DashboardPage extends PureComponent<Props, State> { ...@@ -204,23 +213,37 @@ export class DashboardPage extends PureComponent<Props, State> {
this.setState({ scrollTop: 0 }); this.setState({ scrollTop: 0 });
}; };
renderLoadingState() { renderSlowInitState() {
return ( return (
<div className="dashboard-loading"> <div className="dashboard-loading">
<div className="dashboard-loading__text"> <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>
</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() { render() {
const { dashboard, editview, $injector, isLoadingSlow } = this.props; const { dashboard, editview, $injector, isInitSlow, initError } = this.props;
const { isSettingsOpening, isEditing, isFullscreen, scrollTop } = this.state; const { isSettingsOpening, isEditing, isFullscreen, scrollTop } = this.state;
if (!dashboard) { if (!dashboard) {
if (isLoadingSlow) { if (isInitSlow) {
return this.renderLoadingState(); return this.renderSlowInitState();
} }
return null; return null;
} }
...@@ -249,6 +272,8 @@ export class DashboardPage extends PureComponent<Props, State> { ...@@ -249,6 +272,8 @@ export class DashboardPage extends PureComponent<Props, State> {
<CustomScrollbar autoHeightMin={'100%'} setScrollTop={this.setScrollTop} scrollTop={scrollTop}> <CustomScrollbar autoHeightMin={'100%'} setScrollTop={this.setScrollTop} scrollTop={scrollTop}>
{editview && <DashboardSettings dashboard={dashboard} />} {editview && <DashboardSettings dashboard={dashboard} />}
{initError && this.renderInitFailedState()}
<div className={gridWrapperClasses}> <div className={gridWrapperClasses}>
{dashboard.meta.submenuEnabled && <SubMenu dashboard={dashboard} />} {dashboard.meta.submenuEnabled && <SubMenu dashboard={dashboard} />}
<DashboardGrid dashboard={dashboard} isEditing={isEditing} isFullscreen={isFullscreen} /> <DashboardGrid dashboard={dashboard} isEditing={isEditing} isFullscreen={isFullscreen} />
...@@ -269,14 +294,15 @@ const mapStateToProps = (state: StoreState) => ({ ...@@ -269,14 +294,15 @@ const mapStateToProps = (state: StoreState) => ({
urlFolderId: state.location.query.folderId, urlFolderId: state.location.query.folderId,
urlFullscreen: state.location.query.fullscreen === true, urlFullscreen: state.location.query.fullscreen === true,
urlEdit: state.location.query.edit === true, urlEdit: state.location.query.edit === true,
loadingState: state.dashboard.loadingState, initPhase: state.dashboard.initPhase,
isLoadingSlow: state.dashboard.isLoadingSlow, isInitSlow: state.dashboard.isInitSlow,
initError: state.dashboard.initError,
dashboard: state.dashboard.model as DashboardModel, dashboard: state.dashboard.model as DashboardModel,
}); });
const mapDispatchToProps = { const mapDispatchToProps = {
initDashboard, initDashboard,
setDashboardModel, cleanUpDashboard,
notifyApp, notifyApp,
updateLocation, updateLocation,
}; };
......
...@@ -100,7 +100,6 @@ const mapStateToProps = (state: StoreState) => ({ ...@@ -100,7 +100,6 @@ const mapStateToProps = (state: StoreState) => ({
urlSlug: state.location.routeParams.slug, urlSlug: state.location.routeParams.slug,
urlType: state.location.routeParams.type, urlType: state.location.routeParams.type,
urlPanelId: state.location.query.panelId, urlPanelId: state.location.query.panelId,
loadingState: state.dashboard.loadingState,
dashboard: state.dashboard.model as DashboardModel, dashboard: state.dashboard.model as DashboardModel,
}); });
......
...@@ -8,20 +8,36 @@ import { loadPluginDashboards } from '../../plugins/state/actions'; ...@@ -8,20 +8,36 @@ import { loadPluginDashboards } from '../../plugins/state/actions';
import { notifyApp } from 'app/core/actions'; import { notifyApp } from 'app/core/actions';
// Types // Types
import { ThunkResult } from 'app/types';
import { import {
ThunkResult,
DashboardAcl, DashboardAcl,
DashboardAclDTO, DashboardAclDTO,
PermissionLevel, PermissionLevel,
DashboardAclUpdateDTO, DashboardAclUpdateDTO,
NewDashboardAclItem, NewDashboardAclItem,
} from 'app/types/acl'; MutableDashboard,
import { DashboardLoadingState, MutableDashboard } from 'app/types/dashboard'; DashboardInitError,
} from 'app/types';
export const loadDashboardPermissions = actionCreatorFactory<DashboardAclDTO[]>('LOAD_DASHBOARD_PERMISSIONS').create(); 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 dashboardInitFetching = noPayloadActionCreatorFactory('DASHBOARD_INIT_FETCHING').create();
export const setDashboardLoadingSlow = noPayloadActionCreatorFactory('SET_DASHBOARD_LOADING_SLOW').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> { export function getDashboardPermissions(id: number): ThunkResult<void> {
return async dispatch => { return async dispatch => {
......
import configureMockStore from 'redux-mock-store'; import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { initDashboard, InitDashboardArgs } from './initDashboard'; import { initDashboard, InitDashboardArgs } from './initDashboard';
import { DashboardRouteInfo, DashboardLoadingState } from 'app/types'; import { DashboardRouteInfo } from 'app/types';
const mockStore = configureMockStore([thunk]); const mockStore = configureMockStore([thunk]);
...@@ -98,13 +98,11 @@ describeInitScenario('Initializing new dashboard', ctx => { ...@@ -98,13 +98,11 @@ describeInitScenario('Initializing new dashboard', ctx => {
}); });
it('Should send action to set loading state to fetching', () => { it('Should send action to set loading state to fetching', () => {
expect(ctx.actions[0].type).toBe('SET_DASHBOARD_LOADING_STATE'); expect(ctx.actions[0].type).toBe('DASHBOARD_INIT_FETCHING');
expect(ctx.actions[0].payload).toBe(DashboardLoadingState.Fetching);
}); });
it('Should send action to set loading state to Initializing', () => { it('Should send action to set loading state to Initializing', () => {
expect(ctx.actions[1].type).toBe('SET_DASHBOARD_LOADING_STATE'); expect(ctx.actions[1].type).toBe('DASHBOARD_INIT_SERVICES');
expect(ctx.actions[1].payload).toBe(DashboardLoadingState.Initializing);
}); });
it('Should update location with orgId query param', () => { it('Should update location with orgId query param', () => {
...@@ -113,7 +111,7 @@ describeInitScenario('Initializing new dashboard', ctx => { ...@@ -113,7 +111,7 @@ describeInitScenario('Initializing new dashboard', ctx => {
}); });
it('Should send action to set dashboard model', () => { 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'); expect(ctx.actions[3].payload.title).toBe('New dashboard');
}); });
......
...@@ -12,17 +12,16 @@ import { KeybindingSrv } from 'app/core/services/keybindingSrv'; ...@@ -12,17 +12,16 @@ import { KeybindingSrv } from 'app/core/services/keybindingSrv';
import { updateLocation } from 'app/core/actions'; import { updateLocation } from 'app/core/actions';
import { notifyApp } from 'app/core/actions'; import { notifyApp } from 'app/core/actions';
import locationUtil from 'app/core/utils/location_util'; import locationUtil from 'app/core/utils/location_util';
import { setDashboardLoadingState, setDashboardModel, setDashboardLoadingSlow } from './actions'; import {
dashboardInitFetching,
dashboardInitCompleted,
dashboardInitFailed,
dashboardInitSlow,
dashboardInitServices,
} from './actions';
// Types // Types
import { import { DashboardRouteInfo, StoreState, ThunkDispatch, ThunkResult, DashboardDTO } from 'app/types';
DashboardLoadingState,
DashboardRouteInfo,
StoreState,
ThunkDispatch,
ThunkResult,
DashboardDTO,
} from 'app/types';
import { DashboardModel } from './DashboardModel'; import { DashboardModel } from './DashboardModel';
export interface InitDashboardArgs { export interface InitDashboardArgs {
...@@ -106,8 +105,7 @@ async function fetchDashboard( ...@@ -106,8 +105,7 @@ async function fetchDashboard(
throw { message: 'Unknown route ' + args.routeInfo }; throw { message: 'Unknown route ' + args.routeInfo };
} }
} catch (err) { } catch (err) {
dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); dispatch(dashboardInitFailed({ message: 'Failed to fetch dashboard', error: err }));
dispatch(notifyApp(createErrorNotification('Dashboard fetch failed', err)));
console.log(err); console.log(err);
return null; return null;
} }
...@@ -125,13 +123,13 @@ async function fetchDashboard( ...@@ -125,13 +123,13 @@ async function fetchDashboard(
export function initDashboard(args: InitDashboardArgs): ThunkResult<void> { export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
// set fetching state // set fetching state
dispatch(setDashboardLoadingState(DashboardLoadingState.Fetching)); dispatch(dashboardInitFetching());
// Detect slow loading / initializing and set state flag // 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 // This is in order to not show loading indication for fast loading dashboards as it creates blinking/flashing
setTimeout(() => { setTimeout(() => {
if (getState().dashboard.model === null) { if (getState().dashboard.model === null) {
dispatch(setDashboardLoadingSlow()); dispatch(dashboardInitSlow());
} }
}, 500); }, 500);
...@@ -144,15 +142,14 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> { ...@@ -144,15 +142,14 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
} }
// set initializing state // set initializing state
dispatch(setDashboardLoadingState(DashboardLoadingState.Initializing)); dispatch(dashboardInitServices());
// create model // create model
let dashboard: DashboardModel; let dashboard: DashboardModel;
try { try {
dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta);
} catch (err) { } catch (err) {
dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); dispatch(dashboardInitFailed({ message: 'Failed create dashboard model', error: err }));
dispatch(notifyApp(createErrorNotification('Dashboard model initializing failure', err)));
console.log(err); console.log(err);
return; return;
} }
...@@ -203,8 +200,8 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> { ...@@ -203,8 +200,8 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
// legacy srv state // legacy srv state
dashboardSrv.setCurrent(dashboard); dashboardSrv.setCurrent(dashboard);
// set model in redux (even though it's mutable) // yay we are done
dispatch(setDashboardModel(dashboard)); dispatch(dashboardInitCompleted(dashboard));
}; };
} }
......
import { DashboardState, DashboardLoadingState } from 'app/types/dashboard'; import { DashboardState, DashboardInitPhase } from 'app/types';
import { loadDashboardPermissions, setDashboardLoadingState, setDashboardModel, setDashboardLoadingSlow } from './actions'; import {
loadDashboardPermissions,
dashboardInitFetching,
dashboardInitSlow,
dashboardInitServices,
dashboardInitFailed,
dashboardInitCompleted,
cleanUpDashboard,
} from './actions';
import { reducerFactory } from 'app/core/redux'; import { reducerFactory } from 'app/core/redux';
import { processAclItems } from 'app/core/utils/acl'; import { processAclItems } from 'app/core/utils/acl';
import { DashboardModel } from './DashboardModel';
export const initialState: DashboardState = { export const initialState: DashboardState = {
loadingState: DashboardLoadingState.NotStarted, initPhase: DashboardInitPhase.NotStarted,
isLoadingSlow: false, isInitSlow: false,
model: null, model: null,
permissions: [], permissions: [],
}; };
...@@ -19,27 +28,60 @@ export const dashboardReducer = reducerFactory(initialState) ...@@ -19,27 +28,60 @@ export const dashboardReducer = reducerFactory(initialState)
}), }),
}) })
.addMapper({ .addMapper({
filter: setDashboardLoadingState, filter: dashboardInitFetching,
mapper: (state, action) => ({ mapper: state => ({
...state,
initPhase: DashboardInitPhase.Fetching,
}),
})
.addMapper({
filter: dashboardInitServices,
mapper: state => ({
...state,
initPhase: DashboardInitPhase.Services,
}),
})
.addMapper({
filter: dashboardInitSlow,
mapper: state => ({
...state, ...state,
loadingState: action.payload isInitSlow: true,
}), }),
}) })
.addMapper({ .addMapper({
filter: setDashboardModel, filter: dashboardInitFailed,
mapper: (state, action) => ({ mapper: (state, action) => ({
...state, ...state,
model: action.payload, initPhase: DashboardInitPhase.Failed,
isLoadingSlow: false, isInitSlow: false,
initError: action.payload,
model: new DashboardModel({ title: 'Dashboard init failed' }, { canSave: false, canEdit: false }),
}), }),
}) })
.addMapper({ .addMapper({
filter: setDashboardLoadingSlow, filter: dashboardInitCompleted,
mapper: (state, action) => ({ mapper: (state, action) => ({
...state, ...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(); .create();
export default { export default {
......
...@@ -2,6 +2,7 @@ import { DashboardAcl } from './acl'; ...@@ -2,6 +2,7 @@ import { DashboardAcl } from './acl';
export interface MutableDashboard { export interface MutableDashboard {
meta: DashboardMeta; meta: DashboardMeta;
destroy: () => void;
} }
export interface DashboardDTO { export interface DashboardDTO {
...@@ -44,12 +45,17 @@ export enum DashboardRouteInfo { ...@@ -44,12 +45,17 @@ export enum DashboardRouteInfo {
Scripted = 'scripted-dashboard', Scripted = 'scripted-dashboard',
} }
export enum DashboardLoadingState { export enum DashboardInitPhase {
NotStarted = 'Not started', NotStarted = 'Not started',
Fetching = 'Fetching', Fetching = 'Fetching',
Initializing = 'Initializing', Services = 'Services',
Error = 'Error', Failed = 'Failed',
Done = 'Done', Completed = 'Completed',
}
export interface DashboardInitError {
message: string;
error: any;
} }
export const KIOSK_MODE_TV = 'tv'; export const KIOSK_MODE_TV = 'tv';
...@@ -57,7 +63,8 @@ export type KioskUrlValue = 'tv' | '1' | true; ...@@ -57,7 +63,8 @@ export type KioskUrlValue = 'tv' | '1' | true;
export interface DashboardState { export interface DashboardState {
model: MutableDashboard | null; model: MutableDashboard | null;
loadingState: DashboardLoadingState; initPhase: DashboardInitPhase;
isLoadingSlow: boolean; isInitSlow: boolean;
initError?: DashboardInitError;
permissions: DashboardAcl[] | null; permissions: DashboardAcl[] | null;
} }
...@@ -282,6 +282,11 @@ div.flot-text { ...@@ -282,6 +282,11 @@ div.flot-text {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.alert {
max-width: 600px;
min-width: 600px;
}
} }
.dashboard-loading__text { .dashboard-loading__text {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment