mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Big refactoring for dashboard init redux actions
This commit is contained in:
41
public/app/core/components/AlertBox/AlertBox.tsx
Normal file
41
public/app/core/components/AlertBox/AlertBox.tsx
Normal file
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
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> {
|
|||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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
|
|||||||
});
|
});
|
||||||
|
|
||||||
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(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
15
public/app/core/utils/errors.ts
Normal file
15
public/app/core/utils/errors.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -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: ' + moment(meta.created).calendar();
|
|
||||||
if (meta.expires) {
|
|
||||||
this.titleTooltip += '<br>Expires: ' + 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);
|
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
export { DashNavCtrl } from './DashNavCtrl';
|
|
||||||
import DashNav from './DashNav';
|
import DashNav from './DashNav';
|
||||||
export { DashNav };
|
export { DashNav };
|
||||||
|
|||||||
@@ -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
|
|||||||
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', () => {
|
|||||||
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';
|
|||||||
|
|
||||||
// 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';
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
<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) => ({
|
|||||||
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) => ({
|
|||||||
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';
|
|||||||
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 => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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 => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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';
|
|||||||
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(
|
|||||||
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(
|
|||||||
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> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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> {
|
|||||||
|
|
||||||
// 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));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
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,26 +28,59 @@ export const dashboardReducer = reducerFactory(initialState)
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.addMapper({
|
.addMapper({
|
||||||
filter: setDashboardLoadingState,
|
filter: dashboardInitFetching,
|
||||||
mapper: (state, action) => ({
|
mapper: state => ({
|
||||||
...state,
|
...state,
|
||||||
loadingState: action.payload
|
initPhase: DashboardInitPhase.Fetching,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.addMapper({
|
.addMapper({
|
||||||
filter: setDashboardModel,
|
filter: dashboardInitServices,
|
||||||
|
mapper: state => ({
|
||||||
|
...state,
|
||||||
|
initPhase: DashboardInitPhase.Services,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.addMapper({
|
||||||
|
filter: dashboardInitSlow,
|
||||||
|
mapper: state => ({
|
||||||
|
...state,
|
||||||
|
isInitSlow: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.addMapper({
|
||||||
|
filter: dashboardInitFailed,
|
||||||
mapper: (state, action) => ({
|
mapper: (state, action) => ({
|
||||||
...state,
|
...state,
|
||||||
|
initPhase: DashboardInitPhase.Failed,
|
||||||
|
isInitSlow: false,
|
||||||
|
initError: action.payload,
|
||||||
|
model: new DashboardModel({ title: 'Dashboard init failed' }, { canSave: false, canEdit: false }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.addMapper({
|
||||||
|
filter: dashboardInitCompleted,
|
||||||
|
mapper: (state, action) => ({
|
||||||
|
...state,
|
||||||
|
initPhase: DashboardInitPhase.Completed,
|
||||||
model: action.payload,
|
model: action.payload,
|
||||||
isLoadingSlow: false,
|
isInitSlow: false,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.addMapper({
|
.addMapper({
|
||||||
filter: setDashboardLoadingSlow,
|
filter: cleanUpDashboard,
|
||||||
mapper: (state, action) => ({
|
mapper: (state, action) => {
|
||||||
...state,
|
// tear down current dashboard
|
||||||
isLoadingSlow: true,
|
state.model.destroy();
|
||||||
}),
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
initPhase: DashboardInitPhase.NotStarted,
|
||||||
|
model: null,
|
||||||
|
isInitSlow: false,
|
||||||
|
initError: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
|||||||
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;
|
|||||||
|
|
||||||
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 {
|
|||||||
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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user