Commit e7f6cdc6 by Torkel Ödegaard

Merge branch '13739/alert-to-react'

parents 41125910 f1660aa2
import { AppNotification } from 'app/types/';
export enum ActionTypes {
AddAppNotification = 'ADD_APP_NOTIFICATION',
ClearAppNotification = 'CLEAR_APP_NOTIFICATION',
}
interface AddAppNotificationAction {
type: ActionTypes.AddAppNotification;
payload: AppNotification;
}
interface ClearAppNotificationAction {
type: ActionTypes.ClearAppNotification;
payload: number;
}
export type Action = AddAppNotificationAction | ClearAppNotificationAction;
export const clearAppNotification = (appNotificationId: number) => ({
type: ActionTypes.ClearAppNotification,
payload: appNotificationId,
});
export const notifyApp = (appNotification: AppNotification) => ({
type: ActionTypes.AddAppNotification,
payload: appNotification,
});
import { updateLocation } from './location'; import { updateLocation } from './location';
import { updateNavIndex, UpdateNavIndexAction } from './navModel'; import { updateNavIndex, UpdateNavIndexAction } from './navModel';
import { notifyApp, clearAppNotification } from './appNotification';
export { updateLocation, updateNavIndex, UpdateNavIndexAction }; export { updateLocation, updateNavIndex, UpdateNavIndexAction, notifyApp, clearAppNotification };
...@@ -5,10 +5,12 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; ...@@ -5,10 +5,12 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
import { SearchResult } from './components/search/SearchResult'; import { SearchResult } from './components/search/SearchResult';
import { TagFilter } from './components/TagFilter/TagFilter'; import { TagFilter } from './components/TagFilter/TagFilter';
import { SideMenu } from './components/sidemenu/SideMenu'; import { SideMenu } from './components/sidemenu/SideMenu';
import AppNotificationList from './components/AppNotifications/AppNotificationList';
export function registerAngularDirectives() { export function registerAngularDirectives() {
react2AngularDirective('passwordStrength', PasswordStrength, ['password']); react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
react2AngularDirective('sidemenu', SideMenu, []); react2AngularDirective('sidemenu', SideMenu, []);
react2AngularDirective('appNotificationsList', AppNotificationList, []);
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']); react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
react2AngularDirective('searchResult', SearchResult, []); react2AngularDirective('searchResult', SearchResult, []);
......
import React, { Component } from 'react';
import { AppNotification } from 'app/types';
interface Props {
appNotification: AppNotification;
onClearNotification: (id) => void;
}
export default class AppNotificationItem extends Component<Props> {
shouldComponentUpdate(nextProps) {
return this.props.appNotification.id !== nextProps.appNotification.id;
}
componentDidMount() {
const { appNotification, onClearNotification } = this.props;
setTimeout(() => {
onClearNotification(appNotification.id);
}, appNotification.timeout);
}
render() {
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>
);
}
}
import React, { PureComponent } from 'react';
import appEvents from 'app/core/app_events';
import AppNotificationItem from './AppNotificationItem';
import { notifyApp, clearAppNotification } from 'app/core/actions';
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
import { AppNotification, StoreState } from 'app/types';
import {
createErrorNotification,
createSuccessNotification,
createWarningNotification,
} from '../../copy/appNotification';
export interface Props {
appNotifications: AppNotification[];
notifyApp: typeof notifyApp;
clearAppNotification: typeof clearAppNotification;
}
export class AppNotificationList extends PureComponent<Props> {
componentDidMount() {
const { notifyApp } = this.props;
appEvents.on('alert-warning', options => notifyApp(createWarningNotification(options[0], options[1])));
appEvents.on('alert-success', options => notifyApp(createSuccessNotification(options[0], options[1])));
appEvents.on('alert-error', options => notifyApp(createErrorNotification(options[0], options[1])));
}
onClearAppNotification = id => {
this.props.clearAppNotification(id);
};
render() {
const { appNotifications } = this.props;
return (
<div>
{appNotifications.map((appNotification, index) => {
return (
<AppNotificationItem
key={`${appNotification.id}-${index}`}
appNotification={appNotification}
onClearNotification={id => this.onClearAppNotification(id)}
/>
);
})}
</div>
);
}
}
const mapStateToProps = (state: StoreState) => ({
appNotifications: state.appNotifications.appNotifications,
});
const mapDispatchToProps = {
notifyApp,
clearAppNotification,
};
export default connectWithStore(AppNotificationList, mapStateToProps, mapDispatchToProps);
import { AppNotification, AppNotificationSeverity, AppNotificationTimeout } from 'app/types';
const defaultSuccessNotification: AppNotification = {
title: '',
text: '',
severity: AppNotificationSeverity.Success,
icon: 'fa fa-check',
timeout: AppNotificationTimeout.Success,
};
const defaultWarningNotification: AppNotification = {
title: '',
text: '',
severity: AppNotificationSeverity.Warning,
icon: 'fa fa-exclamation',
timeout: AppNotificationTimeout.Warning,
};
const defaultErrorNotification: AppNotification = {
title: '',
text: '',
severity: AppNotificationSeverity.Error,
icon: 'fa fa-exclamation-triangle',
timeout: AppNotificationTimeout.Error,
};
export const createSuccessNotification = (title: string, text?: string): AppNotification => ({
...defaultSuccessNotification,
title: title,
text: text,
id: Date.now(),
});
export const createErrorNotification = (title: string, text?: string): AppNotification => ({
...defaultErrorNotification,
title: title,
text: text,
id: Date.now(),
});
export const createWarningNotification = (title: string, text?: string): AppNotification => ({
...defaultWarningNotification,
title: title,
text: text,
id: Date.now(),
});
import { appNotificationsReducer } from './appNotification';
import { ActionTypes } from '../actions/appNotification';
import { AppNotificationSeverity, AppNotificationTimeout } from 'app/types/';
describe('clear alert', () => {
it('should filter alert', () => {
const id1 = 1540301236048;
const id2 = 1540301248293;
const initialState = {
appNotifications: [
{
id: id1,
severity: AppNotificationSeverity.Success,
icon: 'success',
title: 'test',
text: 'test alert',
timeout: AppNotificationTimeout.Success,
},
{
id: id2,
severity: AppNotificationSeverity.Warning,
icon: 'warning',
title: 'test2',
text: 'test alert fail 2',
timeout: AppNotificationTimeout.Warning,
},
],
};
const result = appNotificationsReducer(initialState, {
type: ActionTypes.ClearAppNotification,
payload: id2,
});
const expectedResult = {
appNotifications: [
{
id: id1,
severity: AppNotificationSeverity.Success,
icon: 'success',
title: 'test',
text: 'test alert',
timeout: AppNotificationTimeout.Success,
},
],
};
expect(result).toEqual(expectedResult);
});
});
import { AppNotification, AppNotificationsState } from 'app/types/';
import { Action, ActionTypes } from '../actions/appNotification';
export const initialState: AppNotificationsState = {
appNotifications: [] as AppNotification[],
};
export const appNotificationsReducer = (state = initialState, action: Action): AppNotificationsState => {
switch (action.type) {
case ActionTypes.AddAppNotification:
return { ...state, appNotifications: state.appNotifications.concat([action.payload]) };
case ActionTypes.ClearAppNotification:
return {
...state,
appNotifications: state.appNotifications.filter(appNotification => appNotification.id !== action.payload),
};
}
return state;
};
import { navIndexReducer as navIndex } from './navModel'; import { navIndexReducer as navIndex } from './navModel';
import { locationReducer as location } from './location'; import { locationReducer as location } from './location';
import { appNotificationsReducer as appNotifications } from './appNotification';
export default { export default {
navIndex, navIndex,
location, location,
appNotifications,
}; };
import angular from 'angular';
import _ from 'lodash';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
export class AlertSrv { export class AlertSrv {
list: any[]; constructor() {}
/** @ngInject */ set() {
constructor(private $timeout, private $rootScope) { console.log('old depricated alert srv being used');
this.list = [];
}
init() {
this.$rootScope.onAppEvent(
'alert-error',
(e, alert) => {
this.set(alert[0], alert[1], 'error', 12000);
},
this.$rootScope
);
this.$rootScope.onAppEvent(
'alert-warning',
(e, alert) => {
this.set(alert[0], alert[1], 'warning', 5000);
},
this.$rootScope
);
this.$rootScope.onAppEvent(
'alert-success',
(e, alert) => {
this.set(alert[0], alert[1], 'success', 3000);
},
this.$rootScope
);
appEvents.on('alert-warning', options => this.set(options[0], options[1], 'warning', 5000));
appEvents.on('alert-success', options => this.set(options[0], options[1], 'success', 3000));
appEvents.on('alert-error', options => this.set(options[0], options[1], 'error', 7000));
}
getIconForSeverity(severity) {
switch (severity) {
case 'success':
return 'fa fa-check';
case 'error':
return 'fa fa-exclamation-triangle';
default:
return 'fa fa-exclamation';
}
}
set(title, text, severity, timeout) {
if (_.isObject(text)) {
console.log('alert error', text);
if (text.statusText) {
text = `HTTP Error (${text.status}) ${text.statusText}`;
}
}
const newAlert = {
title: title || '',
text: text || '',
severity: severity || 'info',
icon: this.getIconForSeverity(severity),
};
const newAlertJson = angular.toJson(newAlert);
// remove same alert if it already exists
_.remove(this.list, value => {
return angular.toJson(value) === newAlertJson;
});
this.list.push(newAlert);
if (timeout > 0) {
this.$timeout(() => {
this.list = _.without(this.list, newAlert);
}, timeout);
}
if (!this.$rootScope.$$phase) {
this.$rootScope.$digest();
}
return newAlert;
}
clear(alert) {
this.list = _.without(this.list, alert);
}
clearAll() {
this.list = [];
} }
} }
// this is just added to not break old plugins that might be using it
coreModule.service('alertSrv', AlertSrv); coreModule.service('alertSrv', AlertSrv);
...@@ -9,7 +9,7 @@ export class BackendSrv { ...@@ -9,7 +9,7 @@ export class BackendSrv {
private noBackendCache: boolean; private noBackendCache: boolean;
/** @ngInject */ /** @ngInject */
constructor(private $http, private alertSrv, private $q, private $timeout, private contextSrv) {} constructor(private $http, private $q, private $timeout, private contextSrv) {}
get(url, params?) { get(url, params?) {
return this.request({ method: 'GET', url: url, params: params }); return this.request({ method: 'GET', url: url, params: params });
...@@ -49,14 +49,14 @@ export class BackendSrv { ...@@ -49,14 +49,14 @@ export class BackendSrv {
} }
if (err.status === 422) { if (err.status === 422) {
this.alertSrv.set('Validation failed', data.message, 'warning', 4000); appEvents.emit('alert-warning', ['Validation failed', data.message]);
throw data; throw data;
} }
data.severity = 'error'; let severity = 'error';
if (err.status < 500) { if (err.status < 500) {
data.severity = 'warning'; severity = 'warning';
} }
if (data.message) { if (data.message) {
...@@ -66,7 +66,8 @@ export class BackendSrv { ...@@ -66,7 +66,8 @@ export class BackendSrv {
description = message; description = message;
message = 'Error'; message = 'Error';
} }
this.alertSrv.set(message, description, data.severity, 10000);
appEvents.emit('alert-' + severity, [message, description]);
} }
throw data; throw data;
...@@ -93,7 +94,7 @@ export class BackendSrv { ...@@ -93,7 +94,7 @@ export class BackendSrv {
if (options.method !== 'GET') { if (options.method !== 'GET') {
if (results && results.data.message) { if (results && results.data.message) {
if (options.showSuccessAlert !== false) { if (options.showSuccessAlert !== false) {
this.alertSrv.set(results.data.message, '', 'success', 3000); appEvents.emit('alert-success', [results.data.message]);
} }
} }
} }
......
...@@ -9,7 +9,7 @@ describe('backend_srv', () => { ...@@ -9,7 +9,7 @@ describe('backend_srv', () => {
return Promise.resolve({}); return Promise.resolve({});
}; };
const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {}); const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {});
describe('when handling errors', () => { describe('when handling errors', () => {
it('should return the http status code', async () => { it('should return the http status code', async () => {
......
import React from 'react';
import { connect } from 'react-redux';
import { store } from '../../store/configureStore';
export function connectWithStore(WrappedComponent, ...args) {
const ConnectedWrappedComponent = connect(...args)(WrappedComponent);
return props => {
return <ConnectedWrappedComponent {...props} store={store} />;
};
}
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import Tooltip from 'app/core/components/Tooltip/Tooltip'; import Tooltip from 'app/core/components/Tooltip/Tooltip';
import SlideDown from 'app/core/components/Animations/SlideDown'; import SlideDown from 'app/core/components/Animations/SlideDown';
import { StoreState, FolderInfo } from 'app/types'; import { StoreState, FolderInfo } from 'app/types';
...@@ -13,7 +12,7 @@ import { ...@@ -13,7 +12,7 @@ import {
import PermissionList from 'app/core/components/PermissionList/PermissionList'; import PermissionList from 'app/core/components/PermissionList/PermissionList';
import AddPermission from 'app/core/components/PermissionList/AddPermission'; import AddPermission from 'app/core/components/PermissionList/AddPermission';
import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo'; import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
import { store } from 'app/store/configureStore'; import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
export interface Props { export interface Props {
dashboardId: number; dashboardId: number;
...@@ -95,13 +94,6 @@ export class DashboardPermissions extends PureComponent<Props, State> { ...@@ -95,13 +94,6 @@ export class DashboardPermissions extends PureComponent<Props, State> {
} }
} }
function connectWithStore(WrappedComponent, ...args) {
const ConnectedWrappedComponent = connect(...args)(WrappedComponent);
return props => {
return <ConnectedWrappedComponent {...props} store={store} />;
};
}
const mapStateToProps = (state: StoreState) => ({ const mapStateToProps = (state: StoreState) => ({
permissions: state.dashboard.permissions, permissions: state.dashboard.permissions,
}); });
......
...@@ -11,7 +11,7 @@ const template = ` ...@@ -11,7 +11,7 @@ const template = `
`; `;
/** @ngInject */ /** @ngInject */
function uploadDashboardDirective(timer, alertSrv, $location) { function uploadDashboardDirective(timer, $location) {
return { return {
restrict: 'E', restrict: 'E',
template: template, template: template,
...@@ -59,7 +59,7 @@ function uploadDashboardDirective(timer, alertSrv, $location) { ...@@ -59,7 +59,7 @@ function uploadDashboardDirective(timer, alertSrv, $location) {
// Something // Something
elem[0].addEventListener('change', file_selected, false); elem[0].addEventListener('change', file_selected, false);
} else { } else {
alertSrv.set('Oops', 'Sorry, the HTML5 File APIs are not fully supported in this browser.', 'error'); appEvents.emit('alert-error', ['Oops', 'The HTML5 File APIs are not fully supported in this browser']);
} }
}, },
}; };
......
...@@ -17,7 +17,6 @@ export class GrafanaCtrl { ...@@ -17,7 +17,6 @@ export class GrafanaCtrl {
/** @ngInject */ /** @ngInject */
constructor( constructor(
$scope, $scope,
alertSrv,
utilSrv, utilSrv,
$rootScope, $rootScope,
$controller, $controller,
...@@ -41,11 +40,8 @@ export class GrafanaCtrl { ...@@ -41,11 +40,8 @@ export class GrafanaCtrl {
$scope._ = _; $scope._ = _;
profiler.init(config, $rootScope); profiler.init(config, $rootScope);
alertSrv.init();
utilSrv.init(); utilSrv.init();
bridgeSrv.init(); bridgeSrv.init();
$scope.dashAlerts = alertSrv;
}; };
$rootScope.colors = colors; $rootScope.colors = colors;
......
export interface AppNotification {
id?: number;
severity: AppNotificationSeverity;
icon: string;
title: string;
text: string;
timeout: AppNotificationTimeout;
}
export enum AppNotificationSeverity {
Success = 'success',
Warning = 'warning',
Error = 'error',
Info = 'info',
}
export enum AppNotificationTimeout {
Warning = 5000,
Success = 3000,
Error = 7000,
}
export interface AppNotificationsState {
appNotifications: AppNotification[];
}
...@@ -22,6 +22,12 @@ import { ...@@ -22,6 +22,12 @@ import {
} from './series'; } from './series';
import { PanelProps } from './panel'; import { PanelProps } from './panel';
import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins'; import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
import {
AppNotification,
AppNotificationSeverity,
AppNotificationsState,
AppNotificationTimeout,
} from './appNotifications';
export { export {
Team, Team,
...@@ -70,6 +76,10 @@ export { ...@@ -70,6 +76,10 @@ export {
DataQueryResponse, DataQueryResponse,
DataQueryOptions, DataQueryOptions,
PluginDashboard, PluginDashboard,
AppNotification,
AppNotificationsState,
AppNotificationSeverity,
AppNotificationTimeout,
}; };
export interface StoreState { export interface StoreState {
...@@ -82,4 +92,5 @@ export interface StoreState { ...@@ -82,4 +92,5 @@ export interface StoreState {
dashboard: DashboardState; dashboard: DashboardState;
dataSources: DataSourcesState; dataSources: DataSourcesState;
users: UsersState; users: UsersState;
appNotifications: AppNotificationsState;
} }
...@@ -7,13 +7,13 @@ ...@@ -7,13 +7,13 @@
.alert { .alert {
padding: 1.25rem 2rem 1.25rem 1.5rem; padding: 1.25rem 2rem 1.25rem 1.5rem;
margin-bottom: $line-height-base; margin-bottom: $panel-margin / 2;
text-shadow: 0 2px 0 rgba(255, 255, 255, 0.5); text-shadow: 0 2px 0 rgba(255, 255, 255, 0.5);
background: $alert-error-bg; background: $alert-error-bg;
position: relative; position: relative;
color: $white; color: $white;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
border-radius: 2px; border-radius: $border-radius;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
......
...@@ -200,21 +200,8 @@ ...@@ -200,21 +200,8 @@
<grafana-app class="grafana-app" ng-cloak> <grafana-app class="grafana-app" ng-cloak>
<sidemenu class="sidemenu"></sidemenu> <sidemenu class="sidemenu"></sidemenu>
<app-notifications-list class="page-alert-list"></app-notifications-list>
<div class="page-alert-list">
<div ng-repeat='alert in dashAlerts.list' class="alert-{{alert.severity}} alert">
<div class="alert-icon">
<i class="{{alert.icon}}"></i>
</div>
<div class="alert-body">
<div class="alert-title">{{alert.title}}</div>
<div class="alert-text" ng-bind='alert.text'></div>
</div>
<button type="button" class="alert-close" ng-click="dashAlerts.clear(alert)">
<i class="fa fa fa-remove"></i>
</button>
</div>
</div>
<div class="main-view"> <div class="main-view">
<div class="scroll-canvas" page-scrollbar> <div class="scroll-canvas" page-scrollbar>
......
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