diff --git a/public/app/core/actions/appNotification.ts b/public/app/core/actions/appNotification.ts new file mode 100644 index 00000000000..b79b642eef1 --- /dev/null +++ b/public/app/core/actions/appNotification.ts @@ -0,0 +1,28 @@ +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, +}); diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index 451a13dae99..f7ce2dda945 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,4 +1,5 @@ import { updateLocation } from './location'; import { updateNavIndex, UpdateNavIndexAction } from './navModel'; +import { notifyApp, clearAppNotification } from './appNotification'; -export { updateLocation, updateNavIndex, UpdateNavIndexAction }; +export { updateLocation, updateNavIndex, UpdateNavIndexAction, notifyApp, clearAppNotification }; diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 6974d40aac8..7be28272f11 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -5,10 +5,12 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; import { SideMenu } from './components/sidemenu/SideMenu'; +import AppNotificationList from './components/AppNotifications/AppNotificationList'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); react2AngularDirective('sidemenu', SideMenu, []); + react2AngularDirective('appNotificationsList', AppNotificationList, []); react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']); react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); react2AngularDirective('searchResult', SearchResult, []); diff --git a/public/app/core/components/AppNotifications/AppNotificationItem.tsx b/public/app/core/components/AppNotifications/AppNotificationItem.tsx new file mode 100644 index 00000000000..5169c39e7a0 --- /dev/null +++ b/public/app/core/components/AppNotifications/AppNotificationItem.tsx @@ -0,0 +1,38 @@ +import React, { Component } from 'react'; +import { AppNotification } from 'app/types'; + +interface Props { + appNotification: AppNotification; + onClearNotification: (id) => void; +} + +export default class AppNotificationItem extends Component { + 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 ( +
+
+ +
+
+
{appNotification.title}
+
{appNotification.text}
+
+ +
+ ); + } +} diff --git a/public/app/core/components/AppNotifications/AppNotificationList.tsx b/public/app/core/components/AppNotifications/AppNotificationList.tsx new file mode 100644 index 00000000000..c91f8372384 --- /dev/null +++ b/public/app/core/components/AppNotifications/AppNotificationList.tsx @@ -0,0 +1,60 @@ +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 { + 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 ( +
+ {appNotifications.map((appNotification, index) => { + return ( + this.onClearAppNotification(id)} + /> + ); + })} +
+ ); + } +} + +const mapStateToProps = (state: StoreState) => ({ + appNotifications: state.appNotifications.appNotifications, +}); + +const mapDispatchToProps = { + notifyApp, + clearAppNotification, +}; + +export default connectWithStore(AppNotificationList, mapStateToProps, mapDispatchToProps); diff --git a/public/app/core/copy/appNotification.ts b/public/app/core/copy/appNotification.ts new file mode 100644 index 00000000000..c34480d7aad --- /dev/null +++ b/public/app/core/copy/appNotification.ts @@ -0,0 +1,46 @@ +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(), +}); diff --git a/public/app/core/reducers/appNotification.test.ts b/public/app/core/reducers/appNotification.test.ts new file mode 100644 index 00000000000..183b699f5fc --- /dev/null +++ b/public/app/core/reducers/appNotification.test.ts @@ -0,0 +1,51 @@ +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); + }); +}); diff --git a/public/app/core/reducers/appNotification.ts b/public/app/core/reducers/appNotification.ts new file mode 100644 index 00000000000..2c8bbbbd84d --- /dev/null +++ b/public/app/core/reducers/appNotification.ts @@ -0,0 +1,19 @@ +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; +}; diff --git a/public/app/core/reducers/index.ts b/public/app/core/reducers/index.ts index be13528c91c..1c8670ed0d6 100644 --- a/public/app/core/reducers/index.ts +++ b/public/app/core/reducers/index.ts @@ -1,7 +1,9 @@ import { navIndexReducer as navIndex } from './navModel'; import { locationReducer as location } from './location'; +import { appNotificationsReducer as appNotifications } from './appNotification'; export default { navIndex, location, + appNotifications, }; diff --git a/public/app/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts index 2d447651b75..4995b148abd 100644 --- a/public/app/core/services/alert_srv.ts +++ b/public/app/core/services/alert_srv.ts @@ -1,100 +1,12 @@ -import angular from 'angular'; -import _ from 'lodash'; import coreModule from 'app/core/core_module'; -import appEvents from 'app/core/app_events'; export class AlertSrv { - list: any[]; + constructor() {} - /** @ngInject */ - constructor(private $timeout, private $rootScope) { - 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 = []; + set() { + console.log('old depricated alert srv being used'); } } +// this is just added to not break old plugins that might be using it coreModule.service('alertSrv', AlertSrv); diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 3e8132a695b..144567efeb9 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -9,7 +9,7 @@ export class BackendSrv { private noBackendCache: boolean; /** @ngInject */ - constructor(private $http, private alertSrv, private $q, private $timeout, private contextSrv) {} + constructor(private $http, private $q, private $timeout, private contextSrv) {} get(url, params?) { return this.request({ method: 'GET', url: url, params: params }); @@ -49,14 +49,14 @@ export class BackendSrv { } if (err.status === 422) { - this.alertSrv.set('Validation failed', data.message, 'warning', 4000); + appEvents.emit('alert-warning', ['Validation failed', data.message]); throw data; } - data.severity = 'error'; + let severity = 'error'; if (err.status < 500) { - data.severity = 'warning'; + severity = 'warning'; } if (data.message) { @@ -66,7 +66,8 @@ export class BackendSrv { description = message; message = 'Error'; } - this.alertSrv.set(message, description, data.severity, 10000); + + appEvents.emit('alert-' + severity, [message, description]); } throw data; @@ -93,7 +94,7 @@ export class BackendSrv { if (options.method !== 'GET') { if (results && results.data.message) { if (options.showSuccessAlert !== false) { - this.alertSrv.set(results.data.message, '', 'success', 3000); + appEvents.emit('alert-success', [results.data.message]); } } } diff --git a/public/app/core/specs/backend_srv.test.ts b/public/app/core/specs/backend_srv.test.ts index 2e35b87deb4..a6cb5a7d331 100644 --- a/public/app/core/specs/backend_srv.test.ts +++ b/public/app/core/specs/backend_srv.test.ts @@ -9,7 +9,7 @@ describe('backend_srv', () => { return Promise.resolve({}); }; - const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {}); + const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}); describe('when handling errors', () => { it('should return the http status code', async () => { diff --git a/public/app/core/utils/connectWithReduxStore.tsx b/public/app/core/utils/connectWithReduxStore.tsx new file mode 100644 index 00000000000..92c61db4e77 --- /dev/null +++ b/public/app/core/utils/connectWithReduxStore.tsx @@ -0,0 +1,11 @@ +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 ; + }; +} diff --git a/public/app/features/dashboard/permissions/DashboardPermissions.tsx b/public/app/features/dashboard/permissions/DashboardPermissions.tsx index 5651242a485..c07bef42930 100644 --- a/public/app/features/dashboard/permissions/DashboardPermissions.tsx +++ b/public/app/features/dashboard/permissions/DashboardPermissions.tsx @@ -1,5 +1,4 @@ import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { StoreState, FolderInfo } from 'app/types'; @@ -13,7 +12,7 @@ import { import PermissionList from 'app/core/components/PermissionList/PermissionList'; import AddPermission from 'app/core/components/PermissionList/AddPermission'; import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo'; -import { store } from 'app/store/configureStore'; +import { connectWithStore } from '../../../core/utils/connectWithReduxStore'; export interface Props { dashboardId: number; @@ -95,13 +94,6 @@ export class DashboardPermissions extends PureComponent { } } -function connectWithStore(WrappedComponent, ...args) { - const ConnectedWrappedComponent = connect(...args)(WrappedComponent); - return props => { - return ; - }; -} - const mapStateToProps = (state: StoreState) => ({ permissions: state.dashboard.permissions, }); diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index 42871327eb6..ec4ad9a03cb 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -11,7 +11,7 @@ const template = ` `; /** @ngInject */ -function uploadDashboardDirective(timer, alertSrv, $location) { +function uploadDashboardDirective(timer, $location) { return { restrict: 'E', template: template, @@ -59,7 +59,7 @@ function uploadDashboardDirective(timer, alertSrv, $location) { // Something elem[0].addEventListener('change', file_selected, false); } 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']); } }, }; diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index 5dfa8622614..75a34ac01c0 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -17,7 +17,6 @@ export class GrafanaCtrl { /** @ngInject */ constructor( $scope, - alertSrv, utilSrv, $rootScope, $controller, @@ -41,11 +40,8 @@ export class GrafanaCtrl { $scope._ = _; profiler.init(config, $rootScope); - alertSrv.init(); utilSrv.init(); bridgeSrv.init(); - - $scope.dashAlerts = alertSrv; }; $rootScope.colors = colors; diff --git a/public/app/types/appNotifications.ts b/public/app/types/appNotifications.ts new file mode 100644 index 00000000000..81e6cfd55e1 --- /dev/null +++ b/public/app/types/appNotifications.ts @@ -0,0 +1,25 @@ +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[]; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 27c1644e6ab..4b03ef70714 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -22,6 +22,12 @@ import { } from './series'; import { PanelProps } from './panel'; import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins'; +import { + AppNotification, + AppNotificationSeverity, + AppNotificationsState, + AppNotificationTimeout, +} from './appNotifications'; export { Team, @@ -70,6 +76,10 @@ export { DataQueryResponse, DataQueryOptions, PluginDashboard, + AppNotification, + AppNotificationsState, + AppNotificationSeverity, + AppNotificationTimeout, }; export interface StoreState { @@ -82,4 +92,5 @@ export interface StoreState { dashboard: DashboardState; dataSources: DataSourcesState; users: UsersState; + appNotifications: AppNotificationsState; } diff --git a/public/sass/components/_alerts.scss b/public/sass/components/_alerts.scss index 3420dcfdfaf..710c4d1ec0f 100644 --- a/public/sass/components/_alerts.scss +++ b/public/sass/components/_alerts.scss @@ -7,13 +7,13 @@ .alert { 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); background: $alert-error-bg; position: relative; color: $white; text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); - border-radius: 2px; + border-radius: $border-radius; display: flex; flex-direction: row; } diff --git a/public/views/index.template.html b/public/views/index.template.html index c39d5e08321..ced39d9af28 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -200,21 +200,8 @@ + -
-
-
- -
-
-
{{alert.title}}
-
-
- -
-