Browse Source

Merge branch '13739/alert-to-react'

Torkel Ödegaard 7 years ago
parent
commit
e7f6cdc625

+ 28 - 0
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,
+});

+ 2 - 1
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 };

+ 2 - 0
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, []);

+ 38 - 0
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<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>
+    );
+  }
+}

+ 60 - 0
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<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);

+ 46 - 0
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(),
+});

+ 51 - 0
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);
+  });
+});

+ 19 - 0
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;
+};

+ 2 - 0
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,
 };

+ 4 - 92
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);

+ 7 - 6
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]);
             }
           }
         }

+ 1 - 1
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 () => {

+ 11 - 0
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 <ConnectedWrappedComponent {...props} store={store} />;
+  };
+}

+ 1 - 9
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<Props, State> {
   }
 }
 
-function connectWithStore(WrappedComponent, ...args) {
-  const ConnectedWrappedComponent = connect(...args)(WrappedComponent);
-  return props => {
-    return <ConnectedWrappedComponent {...props} store={store} />;
-  };
-}
-
 const mapStateToProps = (state: StoreState) => ({
   permissions: state.dashboard.permissions,
 });

+ 2 - 2
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']);
       }
     },
   };

+ 0 - 4
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;

+ 25 - 0
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[];
+}

+ 11 - 0
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;
 }

+ 2 - 2
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;
 }

+ 1 - 14
public/views/index.template.html

@@ -200,21 +200,8 @@
 
   <grafana-app class="grafana-app" ng-cloak>
     <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="scroll-canvas" page-scrollbar>