You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@couchdb.apache.org by GitBox <gi...@apache.org> on 2018/10/15 20:53:01 UTC

[GitHub] Antonio-Maranhao closed pull request #1139: Update fauxton/notifications to use redux

Antonio-Maranhao closed pull request #1139: Update fauxton/notifications to use redux
URL: https://github.com/apache/couchdb-fauxton/pull/1139
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/app/addons/components/layouts.js b/app/addons/components/layouts.js
index cc1c30a5f..81389e59d 100644
--- a/app/addons/components/layouts.js
+++ b/app/addons/components/layouts.js
@@ -14,7 +14,7 @@ import PropTypes from 'prop-types';
 
 import React from 'react';
 import ReactDOM from 'react-dom';
-import {NotificationCenterButton} from '../fauxton/notifications/notifications';
+import NotificationCenterButton from '../fauxton/notifications/components/NotificationCenterButton';
 import {JSONLink, DocLink} from './components/apibar';
 import {Breadcrumbs} from './header-breadcrumbs';
 import Helpers from '../../helpers';
diff --git a/app/addons/config/layout.js b/app/addons/config/layout.js
index 87471b45e..7774e06c1 100644
--- a/app/addons/config/layout.js
+++ b/app/addons/config/layout.js
@@ -16,7 +16,7 @@ import ConfigTableContainer from './components/ConfigTableContainer';
 import ConfigTabs from './components/ConfigTabs';
 import CORSComponents from '../cors/components';
 import { Breadcrumbs } from '../components/header-breadcrumbs';
-import { NotificationCenterButton } from '../fauxton/notifications/notifications';
+import NotificationCenterButton from '../fauxton/notifications/components/NotificationCenterButton';
 import { ApiBarWrapper } from '../components/layouts';
 
 export const ConfigHeader = ({ node, crumbs, docURL, endpoint }) => {
diff --git a/app/addons/documents/layouts.js b/app/addons/documents/layouts.js
index 3b921aa98..3efae93c4 100644
--- a/app/addons/documents/layouts.js
+++ b/app/addons/documents/layouts.js
@@ -13,7 +13,7 @@
 import PropTypes from 'prop-types';
 
 import React from 'react';
-import { NotificationCenterButton } from '../fauxton/notifications/notifications';
+import NotificationCenterButton from '../fauxton/notifications/components/NotificationCenterButton';
 import SidebarControllerContainer from "./sidebar/SidebarControllerContainer";
 import HeaderDocsLeft from './components/header-docs-left';
 import ChangesContainer from './changes/components/ChangesContainer';
diff --git a/app/addons/documents/mangolayout.js b/app/addons/documents/mangolayout.js
index a873f8e5f..fd7958dee 100644
--- a/app/addons/documents/mangolayout.js
+++ b/app/addons/documents/mangolayout.js
@@ -14,7 +14,7 @@ import React, { Component } from 'react';
 import { connect } from 'react-redux';
 import app from "../../app";
 import { Breadcrumbs } from '../components/header-breadcrumbs';
-import { NotificationCenterButton } from '../fauxton/notifications/notifications';
+import NotificationCenterButton from '../fauxton/notifications/components/NotificationCenterButton';
 import MangoComponents from "./mango/mango.components";
 import * as MangoAPI from "./mango/mango.api";
 import IndexResultsContainer from './index-results/containers/IndexResultsContainer';
diff --git a/app/addons/fauxton/appwrapper.js b/app/addons/fauxton/appwrapper.js
index 8510433e9..fca8a81a1 100644
--- a/app/addons/fauxton/appwrapper.js
+++ b/app/addons/fauxton/appwrapper.js
@@ -11,7 +11,9 @@
 // the License.
 
 import React from 'react';
-import {NotificationController, PermanentNotification} from "./notifications/notifications";
+import GlobalNotificationsContainer from './notifications/components/GlobalNotificationsContainer';
+import NotificationPanelContainer from './notifications/components/NotificationPanelContainer';
+import PermanentNotificationContainer from './notifications/components/PermanentNotificationContainer';
 import NavBar from './navigation/container/NavBar';
 import NavbarActions from './navigation/actions';
 import Stores from './navigation/stores';
@@ -80,9 +82,10 @@ export default class App extends React.Component {
 
     return (
       <div>
-        <PermanentNotification />
+        <PermanentNotificationContainer />
         <div id="notifications">
-          <NotificationController />
+          <GlobalNotificationsContainer />
+          <NotificationPanelContainer />
         </div>
         <div role="main" id="main"  className={mainClass}>
           <div id="app-container">
diff --git a/app/addons/fauxton/base.js b/app/addons/fauxton/base.js
index 17812d886..8a3f41784 100644
--- a/app/addons/fauxton/base.js
+++ b/app/addons/fauxton/base.js
@@ -10,18 +10,18 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import app from "../../app";
-import FauxtonAPI from "../../core/api";
-import NavigationActions from "./navigation/actions";
-
-import "./assets/less/fauxton.less";
+import app from '../../app';
+import FauxtonAPI from '../../core/api';
+import NavigationActions from './navigation/actions';
+import notificationsReducer from './notifications/reducers';
+import './assets/less/fauxton.less';
 
 const Fauxton = FauxtonAPI.addon();
 
 Fauxton.initialize = () => {
   const versionInfo = new Fauxton.VersionInfo();
   versionInfo.fetch().then(function () {
-    NavigationActions.setNavbarVersionInfo(versionInfo.get("version"));
+    NavigationActions.setNavbarVersionInfo(versionInfo.get('version'));
   });
 };
 
@@ -31,4 +31,8 @@ Fauxton.VersionInfo = Backbone.Model.extend({
   }
 });
 
+FauxtonAPI.addReducers({
+  notifications: notificationsReducer
+});
+
 export default Fauxton;
diff --git a/app/addons/fauxton/notifications/__tests__/actions.test.js b/app/addons/fauxton/notifications/__tests__/actions.test.js
deleted file mode 100644
index 0f91e8bc7..000000000
--- a/app/addons/fauxton/notifications/__tests__/actions.test.js
+++ /dev/null
@@ -1,45 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-import Views from "../notifications";
-import Stores from "../stores";
-import Actions from "../actions";
-import utils from "../../../../../test/mocha/testUtils";
-import React from "react";
-import ReactDOM from "react-dom";
-import {mount} from 'enzyme';
-import sinon from "sinon";
-
-const store = Stores.notificationStore;
-const {restore, assert} = utils;
-
-describe('NotificationPanel', () => {
-  beforeEach(() => {
-    store.reset();
-  });
-
-  afterEach(() => {
-    restore(Actions.clearAllNotifications);
-  });
-
-  it('clear all action fires', () => {
-    var stub = sinon.stub(Actions, 'clearAllNotifications');
-
-    var panelEl = mount(<Views.NotificationCenterPanel
-      notifications={[]}
-      style={{x: 1}}
-      filter={'all'}
-      visible={true} />);
-
-    panelEl.find('footer input').simulate('click');
-    assert.ok(stub.calledOnce);
-  });
-});
diff --git a/app/addons/fauxton/notifications/__tests__/components.test.js b/app/addons/fauxton/notifications/__tests__/components.test.js
index 9c3bb3168..a12b15a6b 100644
--- a/app/addons/fauxton/notifications/__tests__/components.test.js
+++ b/app/addons/fauxton/notifications/__tests__/components.test.js
@@ -9,52 +9,22 @@
 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 // License for the specific language governing permissions and limitations under
 // the License.
-import FauxtonAPI from "../../../../core/api";
-import Views from "../notifications";
-import Stores from "../stores";
-import utils from "../../../../../test/mocha/testUtils";
+
 import React from "react";
-import ReactDOM from "react-dom";
 import moment from "moment";
 import { mount } from 'enzyme';
 import sinon from 'sinon';
-const assert = utils.assert;
-var store = Stores.notificationStore;
-
-
-describe('NotificationController', () => {
-
-  beforeEach(() => {
-    store.reset();
-  });
+import Notification from '../components/Notification';
+import NotificationCenterPanel from '../components/NotificationCenterPanel';
+import NotificationPanelRow from '../components/NotificationPanelRow';
+import utils from '../../../../../test/mocha/testUtils';
 
-  it('notifications should be escaped by default', (done) => {
-    store._notificationCenterVisible = true;
-    const component = mount(<Views.NotificationController />);
-    FauxtonAPI.addNotification({ msg: '<script>window.whatever=1;</script>' });
-    //use timer so that controller is displayed first
-    setTimeout(() => {
-      done();
-      assert.ok(/&lt;script&gt;window.whatever=1;&lt;\/script&gt;/.test(component.html()));
-    });
-  });
-
-  it('notifications should be able to render unescaped', (done) => {
-    store._notificationCenterVisible = true;
-    const component = mount(<Views.NotificationController />);
-    FauxtonAPI.addNotification({ msg: '<script>window.whatever=1;</script>', escape: false });
-    setTimeout(() => {
-      done();
-      assert.ok(/<script>window.whatever=1;<\/script>/.test(component.html()));
-    });
-  });
-});
+const assert = utils.assert;
 
 describe('Notification', () => {
   it('startHide is only called after visible time is out', (done) => {
-    store._notificationCenterVisible = true;
     const spy = sinon.spy();
-    const component = mount(<Views.Notification
+    const component = mount(<Notification
       notificationId={'some id'}
       isHiding={false}
       key={11}
@@ -75,6 +45,33 @@ describe('Notification', () => {
       done();
     }, 3000);
   });
+
+  it('notification text should be escaped by default', () => {
+    const wrapper = mount(<Notification
+      notificationId={123}
+      isHiding={false}
+      msg={'<script>window.whatever=1;</script>'}
+      type={'error'}
+      style={{opacity:1}}
+      onStartHide={() => {}}
+      onHideComplete={() => {}}
+    />);
+    assert.ok(/&lt;script&gt;window.whatever=1;&lt;\/script&gt;/.test(wrapper.html()));
+  });
+
+  it('notification text can be rendered unescaped', () => {
+    const wrapper = mount(<Notification
+      notificationId={123}
+      isHiding={false}
+      msg={'<script>window.whatever=1;</script>'}
+      type={'error'}
+      escape={false}
+      style={{opacity:1}}
+      onStartHide={() => {}}
+      onHideComplete={() => {}}
+    />);
+    assert.ok(/<script>window.whatever=1;<\/script>/.test(wrapper.html()));
+  });
 });
 
 describe('NotificationPanelRow', () => {
@@ -107,29 +104,30 @@ describe('NotificationPanelRow', () => {
     height: 64
   };
 
+  const defaultProps = {
+    style,
+    isVisible: true,
+    filter: 'all',
+    clearSingleNotification: () => {}
+  };
+
   it('shows all notification types when "all" filter applied', () => {
-    const row1 = mount(<Views.NotificationPanelRow
-      style={style}
-      isVisible={true}
-      filter="all"
+    const row1 = mount(<NotificationPanelRow
+      {...defaultProps}
       item={notifications.success}
     />);
 
     assert.notOk(row1.find('li').prop('aria-hidden'));
 
-    const row2 = mount(<Views.NotificationPanelRow
-      style={style}
-      isVisible={true}
-      filter="all"
+    const row2 = mount(<NotificationPanelRow
+      {...defaultProps}
       item={notifications.error}
     />
     );
     assert.notOk(row2.find('li').prop('aria-hidden'));
 
-    const row3 = mount(<Views.NotificationPanelRow
-      style={style}
-      isVisible={true}
-      filter="all"
+    const row3 = mount(<NotificationPanelRow
+      {...defaultProps}
       item={notifications.info} />
     );
     assert.notOk(row3.find('li').prop('aria-hidden'));
@@ -137,9 +135,8 @@ describe('NotificationPanelRow', () => {
 
   it('hides notification when filter doesn\'t match', () => {
     var rowEl = mount(
-      <Views.NotificationPanelRow
-        style={style}
-        isVisible={true}
+      <NotificationPanelRow
+        {...defaultProps}
         filter="success"
         item={notifications.info}
       />);
@@ -148,9 +145,8 @@ describe('NotificationPanelRow', () => {
 
   it('shows notification when filter exact match', () => {
     const rowEl = mount(
-      <Views.NotificationPanelRow
-        style={style}
-        isVisible={true}
+      <NotificationPanelRow
+        {...defaultProps}
         filter="info"
         item={notifications.info}
       />);
@@ -160,75 +156,25 @@ describe('NotificationPanelRow', () => {
 
 
 describe('NotificationCenterPanel', () => {
-  beforeEach(() => {
-    store.reset();
-  });
-
-  it('shows all notifications by default', (done) => {
-    store.addNotification({ type: 'success', msg: 'Success are okay' });
-    store.addNotification({ type: 'success', msg: 'another success.' });
-    store.addNotification({ type: 'info', msg: 'A single info message' });
-    store.addNotification({ type: 'error', msg: 'Error #1' });
-    store.addNotification({ type: 'error', msg: 'Error #2' });
-    store.addNotification({ type: 'error', msg: 'Error #3' });
-
-    var panelEl = mount(
-      <Views.NotificationCenterPanel
-        style={{ x: 1 }}
-        visible={true}
-        filter="all"
-        notifications={store.getNotifications()}
-      />);
-
-    setTimeout(() => {
-      done();
-      assert.equal(panelEl.find('.notification-list li[aria-hidden=false]').length, 6);
-    });
-  });
-
-  it('appropriate filters are applied - 1', (done) => {
-    store.addNotification({ type: 'success', msg: 'Success are okay' });
-    store.addNotification({ type: 'success', msg: 'another success.' });
-    store.addNotification({ type: 'info', msg: 'A single info message' });
-    store.addNotification({ type: 'error', msg: 'Error #1' });
-    store.addNotification({ type: 'error', msg: 'Error #2' });
-    store.addNotification({ type: 'error', msg: 'Error #3' });
-
-    var panelEl = mount(
-      <Views.NotificationCenterPanel
-        style={{ x: 1 }}
-        visible={true}
-        filter="success"
-        notifications={store.getNotifications()}
-      />);
 
-    // there are 2 success messages
-    setTimeout(() => {
-      done();
-      assert.equal(panelEl.find('.notification-list li[aria-hidden=false]').length, 2);
-    });
-  });
+  const defaultProps = {
+    hideNotificationCenter: () => {},
+    selectNotificationFilter: () => {},
+    clearAllNotifications: () => {},
+    clearSingleNotification: () => {},
+    isVisible: true,
+    style:{ opacity: 1 }
+  };
 
-  it('appropriate filters are applied - 2', (done) => {
-    store.addNotification({ type: 'success', msg: 'Success are okay' });
-    store.addNotification({ type: 'success', msg: 'another success.' });
-    store.addNotification({ type: 'info', msg: 'A single info message' });
-    store.addNotification({ type: 'error', msg: 'Error #1' });
-    store.addNotification({ type: 'error', msg: 'Error #2' });
-    store.addNotification({ type: 'error', msg: 'Error #3' });
-
-    var panelEl = mount(
-      <Views.NotificationCenterPanel
-        style={{ x: 1 }}
-        visible={true}
-        filter="error"
-        notifications={store.getNotifications()}
-      />);
+  it('clear all notifications', () => {
+    const stub = sinon.stub();
+    const panelEl = mount(<NotificationCenterPanel
+      {...defaultProps}
+      notifications={[]}
+      clearAllNotifications={stub}
+      filter={'all'} />);
 
-    // 3 errors
-    setTimeout(() => {
-      done();
-      assert.equal(panelEl.find('.notification-list li[aria-hidden=false]').length, 3);
-    });
+    panelEl.find('footer input').simulate('click');
+    sinon.assert.calledOnce(stub);
   });
 });
diff --git a/app/addons/fauxton/notifications/__tests__/permanentNotification.test.js b/app/addons/fauxton/notifications/__tests__/permanentNotification.test.js
index 2354d0ef1..0ce231daa 100644
--- a/app/addons/fauxton/notifications/__tests__/permanentNotification.test.js
+++ b/app/addons/fauxton/notifications/__tests__/permanentNotification.test.js
@@ -9,40 +9,48 @@
 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 // License for the specific language governing permissions and limitations under
 // the License.
-import { PermanentNotification } from "../notifications";
-import Stores from "../stores";
-import FauxtonAPI from "../../../../core/api";
-import ActionTypes from "../actiontypes";
-import { mount } from "enzyme";
-import React from "react";
-import ReactDOM from "react-dom";
-
-const store = Stores.notificationStore;
+import { mount } from 'enzyme';
+import React from 'react';
+import thunk from 'redux-thunk';
+import { Provider } from 'react-redux';
+import PermanentNotification from '../components/PermanentNotification';
+import PermanentNotificationContainer from '../components/PermanentNotificationContainer';
+import ActionTypes from '../actiontypes';
+import { createStore, applyMiddleware, combineReducers } from 'redux';
+import notificationsReducer from '../reducers';
 
 describe('PermanentNotification', () => {
-  beforeEach(() => {
-    store.reset();
-  });
 
   it('doesn\'t render content by default', () => {
-    const wrapper = mount(<PermanentNotification />);
+    const wrapper = mount(<PermanentNotification visible={false}/>);
     expect(wrapper.find('.perma-warning__content').length).toBe(0);
   });
+});
 
-  it('shows/hides content when the display flag is switched', () => {
-    const wrapper = mount(<PermanentNotification />);
+describe('PermanentNotificationContainer', () => {
+  const middlewares = [thunk];
+  const store = createStore(
+    combineReducers({ notifications: notificationsReducer }),
+    applyMiddleware(...middlewares)
+  );
 
-    FauxtonAPI.dispatch({
+  it('shows/hides content when the display flag is switched', () => {
+    const wrapper = mount(
+      <Provider store={store}>
+        <PermanentNotificationContainer />
+      </Provider>
+    );
+    store.dispatch({
       type: ActionTypes.SHOW_PERMANENT_NOTIFICATION,
       options: {
-        msg: "Hello World!"
+        msg: 'Hello World!'
       }
     });
 
     wrapper.update();
     expect(wrapper.find('.perma-warning__content').html()).toMatch(/Hello World!/);
 
-    FauxtonAPI.dispatch({
+    store.dispatch({
       type: ActionTypes.HIDE_PERMANENT_NOTIFICATION
     });
 
@@ -50,3 +58,4 @@ describe('PermanentNotification', () => {
     expect(wrapper.find('.perma-warning__content').length).toBe(0);
   });
 });
+
diff --git a/app/addons/fauxton/notifications/__tests__/reducers.test.js b/app/addons/fauxton/notifications/__tests__/reducers.test.js
new file mode 100644
index 000000000..8b50dfe01
--- /dev/null
+++ b/app/addons/fauxton/notifications/__tests__/reducers.test.js
@@ -0,0 +1,134 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import reducer from '../reducers';
+import ActionTypes from '../actiontypes';
+import testUtils from '../../../../../test/mocha/testUtils';
+
+const assert = testUtils.assert;
+
+describe('Notifications Reducer', () => {
+
+  it('sets reasonable defaults', () => {
+    const newState = reducer(undefined, { type: 'DO_NOTHING'});
+    assert.equal(newState.notifications.length, 0);
+    assert.equal(newState.notificationCenterVisible, false);
+    assert.equal(newState.selectedNotificationFilter, 'all');
+  });
+
+  it('confirm only known notification types get added', () => {
+    const action = {
+      type: ActionTypes.ADD_NOTIFICATION,
+      options: {
+        info: { msg: 'Hey there!', type: 'info' }
+      }
+    };
+
+    let newState = reducer(undefined, action);
+    assert.equal(newState.notifications.length, 1);
+
+    action.options.info.type = 'success';
+    newState = reducer(newState, action);
+    assert.equal(newState.notifications.length, 2);
+
+    action.options.info.type = 'error';
+    newState = reducer(newState, action);
+    assert.equal(newState.notifications.length, 3);
+
+    action.options.info.type = 'rhubarb';
+    newState = reducer(newState, action);
+    assert.equal(newState.notifications.length, 3);
+  });
+
+  it('notifications should be escaped by default', () => {
+    const action = {
+      type: ActionTypes.ADD_NOTIFICATION,
+      options: {
+        info: { msg: '<script>window.whatever=1;</script>', type: 'info' }
+      }
+    };
+    let newState = reducer(undefined, action);
+    assert.equal(newState.notifications[0].escape, true);
+  });
+
+  it('clears a specific notification', () => {
+    const action = {
+      type: ActionTypes.ADD_NOTIFICATION,
+      options: {
+        info: { msg: 'one', type: 'success' }
+      }
+    };
+
+    let newState = reducer(undefined, action);
+    action.options.info.msg = 'two';
+    newState = reducer(newState, action);
+    action.options.info.msg = 'three';
+    newState = reducer(newState, action);
+    action.options.info.msg = 'four';
+    newState = reducer(newState, action);
+    assert.equal(newState.notifications.length, 4);
+
+    const idToRemove = newState.notifications[1].notificationId;
+    const msgToRemove = newState.notifications[1].msg;
+    newState = reducer(newState, {
+      type: ActionTypes.CLEAR_SINGLE_NOTIFICATION,
+      options: { notificationId: idToRemove }
+    });
+    assert.equal(newState.notifications.length, 3);
+    const item = newState.notifications.find(el => {
+      return el.msg === msgToRemove;
+    });
+    assert.isUndefined(item);
+  });
+
+  it('setNotificationFilter only sets for known notification types', () => {
+    const action = {
+      type: ActionTypes.SELECT_NOTIFICATION_FILTER,
+      options: { filter: 'all' }
+    };
+    let newState = reducer(undefined, { type: 'DO_NOTHING' });
+    const validFilters = ['all', 'success', 'error', 'info'];
+    validFilters.forEach(filter => {
+      action.options.filter = filter;
+      newState = reducer(newState, action);
+      assert.equal(newState.selectedNotificationFilter, filter);
+    });
+
+    action.options.filter = 'invalid_filter';
+    newState = reducer(newState, action);
+    assert.equal(newState.selectedNotificationFilter, validFilters[validFilters.length - 1]);
+  });
+
+  it('clear all notifications', () => {
+    const action = {
+      type: ActionTypes.ADD_NOTIFICATION,
+      options: {
+        info: { msg: 'one', type: 'success' }
+      }
+    };
+
+    let newState = reducer(undefined, action);
+    action.options.info.msg = 'two';
+    newState = reducer(newState, action);
+    action.options.info.msg = 'three';
+    newState = reducer(newState, action);
+    action.options.info.msg = 'four';
+    newState = reducer(newState, action);
+    assert.equal(newState.notifications.length, 4);
+
+    newState = reducer(newState, {
+      type: ActionTypes.CLEAR_ALL_NOTIFICATIONS
+    });
+    assert.equal(newState.notifications.length, 0);
+  });
+
+});
diff --git a/app/addons/fauxton/notifications/__tests__/stores.test.js b/app/addons/fauxton/notifications/__tests__/stores.test.js
deleted file mode 100644
index 3c1e3ad25..000000000
--- a/app/addons/fauxton/notifications/__tests__/stores.test.js
+++ /dev/null
@@ -1,95 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-import utils from "../../../../../test/mocha/testUtils";
-import Stores from "../stores";
-
-const assert = utils.assert;
-const store = Stores.notificationStore;
-
-describe('Notification Store', () => {
-
-  beforeEach(() => {
-    store.reset();
-  });
-
-  it("sets reasonable defaults", () => {
-    assert.equal(store.getNotifications().length, 0);
-    assert.equal(store.isNotificationCenterVisible(), false);
-    assert.equal(store.getNotificationFilter(), 'all');
-  });
-
-  it("confirm only known notification types get added", () => {
-    assert.equal(store.getNotifications().length, 0);
-    store.addNotification({ type: 'success', msg: 'Success are okay' });
-
-    assert.equal(store.getNotifications().length, 1);
-    store.addNotification({ type: 'info', msg: 'Infos are also okay' });
-
-    assert.equal(store.getNotifications().length, 2);
-    store.addNotification({ type: 'error', msg: 'Errors? Bring em on' });
-
-    assert.equal(store.getNotifications().length, 3);
-    store.addNotification({ type: 'rhubarb', msg: 'But rhubarb is NOT a valid notification type' });
-
-    // confirm it wasn't added
-    assert.equal(store.getNotifications().length, 3);
-  });
-
-  it("clearNotification clears a specific notification", () => {
-    store.addNotification({ type: 'success', msg: 'one' });
-    store.addNotification({ type: 'success', msg: 'two' });
-    store.addNotification({ type: 'success', msg: 'three' });
-    store.addNotification({ type: 'success', msg: 'four' });
-
-    const notifications = store.getNotifications();
-    assert.equal(notifications.length, 4);
-
-    // find the notification ID of the "three" message
-    const notification = _.find(notifications, { msg: 'three' });
-    store.clearNotification(notification.notificationId);
-
-    // confirm it was removed
-    const updatedNotifications = store.getNotifications();
-    assert.equal(updatedNotifications.length, 3);
-    assert.equal(_.find(updatedNotifications, { msg: 'three' }), undefined);
-  });
-
-  it("setNotificationFilter only sets for known notification types", () => {
-    store.setNotificationFilter('all');
-    assert.equal(store.getNotificationFilter(), 'all');
-
-    store.setNotificationFilter('success');
-    assert.equal(store.getNotificationFilter(), 'success');
-
-    store.setNotificationFilter('error');
-    assert.equal(store.getNotificationFilter(), 'error');
-
-    store.setNotificationFilter('info');
-    assert.equal(store.getNotificationFilter(), 'info');
-
-    store.setNotificationFilter('broccoli');
-    assert.equal(store.getNotificationFilter(), 'info'); // this check it's still set to the previously set value
-  });
-
-  it("clear all notifications", () => {
-    store.addNotification({ type: 'success', msg: 'one' });
-    store.addNotification({ type: 'success', msg: 'two' });
-    store.addNotification({ type: 'success', msg: 'three' });
-    store.addNotification({ type: 'success', msg: 'four' });
-    assert.equal(store.getNotifications().length, 4);
-
-    store.clearNotifications();
-    assert.equal(store.getNotifications().length, 0);
-  });
-
-});
diff --git a/app/addons/fauxton/notifications/actions.js b/app/addons/fauxton/notifications/actions.js
index 32f40ab60..0e57faf2b 100644
--- a/app/addons/fauxton/notifications/actions.js
+++ b/app/addons/fauxton/notifications/actions.js
@@ -10,78 +10,65 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import FauxtonAPI from "../../../core/api";
-import ActionTypes from "./actiontypes";
+import ActionTypes from './actiontypes';
 
-function addNotification (notificationInfo) {
-  FauxtonAPI.dispatch({
+export const addNotification = (notificationInfo) => (dispatch) => {
+  dispatch({
     type: ActionTypes.ADD_NOTIFICATION,
     options: {
       info: notificationInfo
     }
   });
-}
+};
 
-function showNotificationCenter () {
-  FauxtonAPI.dispatch({ type: ActionTypes.SHOW_NOTIFICATION_CENTER });
-}
+export const showNotificationCenter = () => (dispatch) => {
+  dispatch({ type: ActionTypes.SHOW_NOTIFICATION_CENTER });
+};
 
-function hideNotificationCenter () {
-  FauxtonAPI.dispatch({ type: ActionTypes.HIDE_NOTIFICATION_CENTER });
-}
+export const hideNotificationCenter = () => (dispatch) => {
+  dispatch({ type: ActionTypes.HIDE_NOTIFICATION_CENTER });
+};
 
-function clearAllNotifications () {
-  FauxtonAPI.dispatch({ type: ActionTypes.CLEAR_ALL_NOTIFICATIONS });
-}
+export const clearAllNotifications = () => (dispatch) => {
+  dispatch({ type: ActionTypes.CLEAR_ALL_NOTIFICATIONS });
+};
 
-function clearSingleNotification (notificationId) {
-  FauxtonAPI.dispatch({
+export const clearSingleNotification = (notificationId) => (dispatch) => {
+  dispatch({
     type: ActionTypes.CLEAR_SINGLE_NOTIFICATION,
     options: {
       notificationId: notificationId
     }
   });
-}
+};
 
-function selectNotificationFilter (filter) {
-  FauxtonAPI.dispatch({
+export const selectNotificationFilter = (filter) => (dispatch) => {
+  dispatch({
     type: ActionTypes.SELECT_NOTIFICATION_FILTER,
     options: {
       filter: filter
     }
   });
-}
+};
 
-function startHidingNotification (notificationId) {
-  FauxtonAPI.dispatch({
+export const startHidingNotification = (notificationId) => (dispatch) => {
+  dispatch({
     type: ActionTypes.START_HIDING_NOTIFICATION,
     options: {
       notificationId: notificationId
     }
   });
-}
+};
 
-function hideNotification (notificationId) {
-  FauxtonAPI.dispatch({
+export const hideNotification = (notificationId) => (dispatch) => {
+  dispatch({
     type: ActionTypes.HIDE_NOTIFICATION,
     options: {
       notificationId: notificationId
     }
   });
-}
-
-function hideAllVisibleNotifications () {
-  FauxtonAPI.dispatch({ type: ActionTypes.HIDE_ALL_NOTIFICATIONS });
-}
+};
 
-export default {
-  addNotification: addNotification,
-  showNotificationCenter: showNotificationCenter,
-  hideNotificationCenter: hideNotificationCenter,
-  clearAllNotifications: clearAllNotifications,
-  clearSingleNotification: clearSingleNotification,
-  selectNotificationFilter: selectNotificationFilter,
-  startHidingNotification: startHidingNotification,
-  hideNotification: hideNotification,
-  hideAllVisibleNotifications: hideAllVisibleNotifications
+export const hideAllVisibleNotifications = () => (dispatch) => {
+  dispatch({ type: ActionTypes.HIDE_ALL_NOTIFICATIONS });
 };
diff --git a/app/addons/fauxton/notifications/components/GlobalNotifications.js b/app/addons/fauxton/notifications/components/GlobalNotifications.js
new file mode 100644
index 000000000..5d63dd37e
--- /dev/null
+++ b/app/addons/fauxton/notifications/components/GlobalNotifications.js
@@ -0,0 +1,135 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import {TransitionMotion, spring, presets} from 'react-motion';
+import Notification from './Notification';
+
+export default class GlobalNotifications extends React.Component {
+  static propTypes = {
+    notifications: PropTypes.array.isRequired,
+    hideAllVisibleNotifications: PropTypes.func.isRequired,
+    startHidingNotification: PropTypes.func.isRequired,
+    hideNotification: PropTypes.func.isRequired
+  };
+
+  componentDidMount() {
+    document.addEventListener('keydown', this.onKeyDown);
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener('keydown', this.onKeyDown);
+  }
+
+  onKeyDown = (e) => {
+    const code = e.keyCode || e.which;
+    if (code === 27) {
+      this.props.hideAllVisibleNotifications();
+    }
+  };
+
+  getNotifications = () => {
+    if (!this.props.notifications.length) {
+      return null;
+    }
+
+    return this.props.notifications.map((notification, index) => {
+
+      // notifications are completely removed from the DOM once they're
+      if (!notification.visible) {
+        return;
+      }
+
+      return (
+        <Notification
+          notificationId={notification.notificationId}
+          isHiding={notification.isHiding}
+          key={index}
+          msg={notification.msg}
+          type={notification.type}
+          escape={notification.escape}
+          visibleTime={notification.visibleTime}
+          onStartHide={this.props.startHidingNotification}
+          onHideComplete={this.props.hideNotification} />
+      );
+    });
+  };
+
+  getchildren = (items) => {
+    const notifications = items.map(({key, data, style}) => {
+      const notification = data;
+      return (
+        <Notification
+          key={key}
+          style={style}
+          notificationId={notification.notificationId}
+          isHiding={notification.isHiding}
+          msg={notification.msg}
+          type={notification.type}
+          escape={notification.escape}
+          visibleTime={notification.visibleTime}
+          onStartHide={this.props.startHidingNotification}
+          onHideComplete={this.props.hideNotification} />
+      );
+    });
+
+    return (
+      <div>
+        {notifications}
+      </div>
+    );
+  };
+
+  getStyles = (prevItems) => {
+    if (!prevItems) {
+      prevItems = [];
+    }
+
+    return this.props.notifications
+      .filter(notification => notification.visible)
+      .map(notification => {
+        let item = prevItems.find(style => style.key === (notification.notificationId.toString()));
+        let style = !item ? {opacity: 0.5, minHeight: 50} : false;
+
+        if (!style && !notification.isHiding) {
+          style = {
+            opacity: spring(1, presets.stiff),
+            minHeight: spring(64)
+          };
+        } else if (!style && notification.isHiding) {
+          style = {
+            opacity: spring(0, presets.stiff),
+            minHeight: spring(0, presets.stiff)
+          };
+        }
+
+        return {
+          key: notification.notificationId.toString(),
+          style,
+          data: notification
+        };
+      });
+  };
+
+  render() {
+    return (
+      <div id="global-notifications">
+        <TransitionMotion
+          styles={this.getStyles}
+        >
+          {this.getchildren}
+        </TransitionMotion>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/fauxton/notifications/components/GlobalNotificationsContainer.js b/app/addons/fauxton/notifications/components/GlobalNotificationsContainer.js
new file mode 100644
index 000000000..8eb19b267
--- /dev/null
+++ b/app/addons/fauxton/notifications/components/GlobalNotificationsContainer.js
@@ -0,0 +1,30 @@
+import { connect } from 'react-redux';
+import * as Actions from '../actions';
+import GlobalNotifications from './GlobalNotifications';
+
+const mapStateToProps = ({ notifications }) => {
+  return {
+    notifications: notifications.notifications
+  };
+};
+
+const mapDispatchToProps = (dispatch) => {
+  return {
+    startHidingNotification: (notificationId) => {
+      dispatch(Actions.startHidingNotification(notificationId));
+    },
+    hideNotification: (notificationId) => {
+      dispatch(Actions.hideNotification(notificationId));
+    },
+    hideAllVisibleNotifications: () => {
+      dispatch(Actions.hideAllVisibleNotifications());
+    }
+  };
+};
+
+const GlobalNotificationsContainer = connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(GlobalNotifications);
+
+export default GlobalNotificationsContainer;
diff --git a/app/addons/fauxton/notifications/components/Notification.js b/app/addons/fauxton/notifications/components/Notification.js
new file mode 100644
index 000000000..ed7c6ca66
--- /dev/null
+++ b/app/addons/fauxton/notifications/components/Notification.js
@@ -0,0 +1,91 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+
+export default class Notification extends React.Component {
+  static propTypes = {
+    notificationId: PropTypes.number.isRequired,
+    msg: PropTypes.string.isRequired,
+    onStartHide: PropTypes.func.isRequired,
+    onHideComplete: PropTypes.func.isRequired,
+    type: PropTypes.oneOf(['error', 'info', 'success']),
+    escape: PropTypes.bool,
+    isHiding: PropTypes.bool.isRequired,
+    visibleTime: PropTypes.number
+  };
+
+  static defaultProps = {
+    type: 'info',
+    visibleTime: 8000,
+    escape: true
+  };
+
+  componentWillUnmount() {
+    if (this.timeout) {
+      window.clearTimeout(this.timeout);
+    }
+  }
+
+  componentDidMount() {
+    this.timeout = setTimeout(this.hide, this.props.visibleTime);
+  }
+
+  hide = (e) => {
+    if (e) {
+      e.preventDefault();
+    }
+    this.props.onStartHide(this.props.notificationId);
+  };
+
+  // many messages contain HTML, hence the need for dangerouslySetInnerHTML
+  getMsg = () => {
+    var msg = (this.props.escape) ? _.escape(this.props.msg) : this.props.msg;
+    return {
+      __html: msg
+    };
+  };
+
+  onAnimationComplete = () => {
+    if (this.props.isHiding) {
+      window.setTimeout(() => this.props.onHideComplete(this.props.notificationId));
+    }
+  };
+
+  render() {
+    const {style, notificationId} = this.props;
+    const iconMap = {
+      error: 'fonticon-attention-circled',
+      info: 'fonticon-info-circled',
+      success: 'fonticon-ok-circled'
+    };
+
+    if (style.opacity === 0 && this.props.isHiding) {
+      this.onAnimationComplete();
+    }
+
+    return (
+      <div
+        key={notificationId.toString()} className="notification-wrapper" style={{opacity: style.opacity, minHeight: style.minHeight + 'px'}}>
+        <div
+          style={{opacity: style.opacity, minHeight: style.minHeight + 'px'}}
+          className={'global-notification alert alert-' + this.props.type}
+          ref={node => this.notification = node}>
+          <a data-bypass href="#" onClick={this.hide}><i className="pull-right fonticon-cancel" /></a>
+          <i className={'notification-icon ' + iconMap[this.props.type]} />
+          <span dangerouslySetInnerHTML={this.getMsg()}></span>
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/fauxton/notifications/components/NotificationCenterButton.js b/app/addons/fauxton/notifications/components/NotificationCenterButton.js
new file mode 100644
index 000000000..cf458a44b
--- /dev/null
+++ b/app/addons/fauxton/notifications/components/NotificationCenterButton.js
@@ -0,0 +1,55 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import { connect } from 'react-redux';
+import { showNotificationCenter } from '../actions';
+
+class ShowPanelButton extends React.Component {
+
+  static propTypes = {
+    onClick: PropTypes.func.isRequired,
+    visible: PropTypes.bool.isRequired
+  };
+
+  render() {
+    const classes = 'fonticon fonticon-bell' + ((!this.props.visible) ? ' hide' : '');
+    return (
+      <div className={classes} onClick={this.props.onClick}></div>
+    );
+  }
+}
+
+const mapStateToProps = ({ notifications }) => {
+  return {
+    visible: !notifications.notificationCenterVisible
+  };
+};
+
+const mapDispatchToProps = (dispatch) => {
+  return {
+    onClick: () => {
+      dispatch(showNotificationCenter());
+    }
+  };
+};
+
+const NotificationCenterButton = connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(ShowPanelButton);
+
+export default NotificationCenterButton;
+
+
+
diff --git a/app/addons/fauxton/notifications/components/NotificationCenterPanel.js b/app/addons/fauxton/notifications/components/NotificationCenterPanel.js
new file mode 100644
index 000000000..a89e7c293
--- /dev/null
+++ b/app/addons/fauxton/notifications/components/NotificationCenterPanel.js
@@ -0,0 +1,145 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import {TransitionMotion, spring, presets} from 'react-motion';
+import NotificationPanelRow from './NotificationPanelRow';
+
+export default class NotificationCenterPanel extends React.Component {
+  static propTypes = {
+    isVisible: PropTypes.bool.isRequired,
+    filter: PropTypes.string.isRequired,
+    notifications: PropTypes.array.isRequired,
+    hideNotificationCenter: PropTypes.func.isRequired,
+    selectNotificationFilter: PropTypes.func.isRequired,
+    clearAllNotifications: PropTypes.func.isRequired,
+    clearSingleNotification: PropTypes.func.isRequired
+  };
+
+  getNotifications = (items) => {
+    let notifications;
+    if (!items.length && !this.props.notifications.length) {
+      notifications = <li className="no-notifications">
+          No notifications.
+      </li>;
+    } else {
+      notifications = items
+        .map(({key, data: notification, style}) => {
+          return (
+            <NotificationPanelRow
+              item={notification}
+              filter={this.props.filter}
+              clearSingleNotification={this.props.clearSingleNotification}
+              key={key}
+              style={style}
+            />
+          );
+        });
+    }
+    return (
+      <ul className="notification-list">
+        {notifications}
+      </ul>
+    );
+  };
+
+  getStyles = (prevItems = []) => {
+    const styles = this.props.notifications
+      .map(notification => {
+        let item = prevItems.find(style => style.key === (notification.notificationId.toString()));
+        let style = !item ? {opacity: 0, height: 0} : false;
+
+        if (!style && (notification.type === this.props.filter || this.props.filter === 'all')) {
+          style = {
+            opacity: spring(1, presets.stiff),
+            height: spring(61, presets.stiff)
+          };
+        } else if (notification.type !== this.props.filter) {
+          style = {
+            opacity: spring(0, presets.stiff),
+            height: spring(0, presets.stiff)
+          };
+        }
+
+        return {
+          key: notification.notificationId.toString(),
+          style,
+          data: notification
+        };
+      });
+    return styles;
+  };
+
+  render() {
+    if (!this.props.isVisible && this.props.style.x === 0) {
+      // panelClasses += ' visible';
+      return null;
+    }
+
+    const filterClasses = {
+      all: 'flex-body',
+      success: 'flex-body',
+      error: 'flex-body',
+      info: 'flex-body'
+    };
+    filterClasses[this.props.filter] += ' selected';
+
+    const maskClasses = `notification-page-mask ${((this.props.isVisible) ? ' visible' : '')}`;
+    const panelClasses = 'notification-center-panel flex-layout flex-col visible';
+    return (
+      <div id="notification-center">
+        <div className={panelClasses} style={{transform: `translate(${this.props.style.x}px)`}}>
+
+          <header className="flex-layout flex-row">
+            <span className="fonticon fonticon-bell" />
+            <h1 className="flex-body">Notifications</h1>
+            <button type="button" onClick={this.props.hideNotificationCenter}>×</button>
+          </header>
+
+          <ul className="notification-filter flex-layout flex-row">
+            <li className={filterClasses.all} title="All notifications" data-filter="all"
+              onClick={() => this.props.selectNotificationFilter('all')}>All</li>
+            <li className={filterClasses.success} title="Success notifications" data-filter="success"
+              onClick={() => this.props.selectNotificationFilter('success')}>
+              <span className="fonticon fonticon-ok-circled" />
+            </li>
+            <li className={filterClasses.error} title="Error notifications" data-filter="error"
+              onClick={() => this.props.selectNotificationFilter('error')}>
+              <span className="fonticon fonticon-attention-circled" />
+            </li>
+            <li className={filterClasses.info} title="Info notifications" data-filter="info"
+              onClick={() => this.props.selectNotificationFilter('info')}>
+              <span className="fonticon fonticon-info-circled" />
+            </li>
+          </ul>
+
+          <div className="flex-body">
+            <TransitionMotion styles={this.getStyles}>
+              {this.getNotifications}
+            </TransitionMotion>
+          </div>
+
+          <footer>
+            <input
+              type="button"
+              value="Clear All"
+              className="btn btn-small btn-secondary"
+              onClick={this.props.clearAllNotifications} />
+          </footer>
+        </div>
+
+        <div className={maskClasses} onClick={this.props.hideNotificationCenter}></div>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/fauxton/notifications/components/NotificationPanelContainer.js b/app/addons/fauxton/notifications/components/NotificationPanelContainer.js
new file mode 100644
index 000000000..ec03cdc17
--- /dev/null
+++ b/app/addons/fauxton/notifications/components/NotificationPanelContainer.js
@@ -0,0 +1,36 @@
+import { connect } from 'react-redux';
+import * as Actions from '../actions';
+import NotificationPanelWithTransition from './NotificationPanelWithTransition';
+
+const mapStateToProps = ({ notifications }) => {
+  return {
+    isVisible: notifications.notificationCenterVisible,
+    filter: notifications.selectedNotificationFilter,
+    notifications: notifications.notifications
+  };
+};
+
+const mapDispatchToProps = (dispatch) => {
+  return {
+    hideNotificationCenter: () => {
+      dispatch(Actions.hideNotificationCenter());
+    },
+    selectNotificationFilter: (filter) => {
+      dispatch(Actions.selectNotificationFilter(filter));
+    },
+    clearAllNotifications: () => {
+      dispatch(Actions.clearAllNotifications());
+    },
+    clearSingleNotification: (notificationId) => {
+      dispatch(Actions.clearSingleNotification(notificationId));
+    }
+  };
+};
+
+
+const NotificationPanelContainer = connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(NotificationPanelWithTransition);
+
+export default NotificationPanelContainer;
diff --git a/app/addons/fauxton/notifications/components/NotificationPanelRow.js b/app/addons/fauxton/notifications/components/NotificationPanelRow.js
new file mode 100644
index 000000000..f267c202b
--- /dev/null
+++ b/app/addons/fauxton/notifications/components/NotificationPanelRow.js
@@ -0,0 +1,66 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import uuid from 'uuid';
+import Components from '../../../components/react-components';
+const {Copy} = Components;
+
+export default class NotificationPanelRow extends React.Component {
+  static propTypes = {
+    item: PropTypes.object.isRequired,
+    filter: PropTypes.string.isRequired,
+    clearSingleNotification: PropTypes.func.isRequired
+  };
+
+  clearNotification = () => {
+    const {notificationId} = this.props.item;
+    this.props.clearSingleNotification(notificationId);
+  };
+
+  render() {
+    const iconMap = {
+      success: 'fonticon-ok-circled',
+      error: 'fonticon-attention-circled',
+      info: 'fonticon-info-circled'
+    };
+
+    const timeElapsed = this.props.item.time.fromNow();
+
+    // we can safely do this because the store ensures all notifications are of known types
+    const rowIconClasses = 'fonticon ' + iconMap[this.props.item.type];
+    const hidden = (this.props.filter === 'all' || this.props.filter === this.props.item.type) ? false : true;
+    const {style} = this.props;
+    const {opacity, height} = style;
+    if (opacity === 0 && height === 0) {
+      return null;
+    }
+    // N.B. wrapper <div> needed to ensure smooth hide/show transitions
+    return (
+      <li style={{opactiy: opacity, height: height + 'px', borderBottomColor: `rgba(34, 34, 34, ${opacity})`}} aria-hidden={hidden}>
+        <div className="flex-layout flex-row">
+          <span className={rowIconClasses}></span>
+          <div className="flex-body">
+            <p>{this.props.item.cleanMsg}</p>
+            <div className="notification-actions">
+              <span className="time-elapsed">{timeElapsed}</span>
+              <span className="divider">|</span>
+              <Copy uniqueKey={uuid.v4()} text={this.props.item.cleanMsg} displayType="text" />
+            </div>
+          </div>
+          <button type="button" onClick={this.clearNotification}>×</button>
+        </div>
+      </li>
+    );
+  }
+}
diff --git a/app/addons/fauxton/notifications/components/NotificationPanelWithTransition.js b/app/addons/fauxton/notifications/components/NotificationPanelWithTransition.js
new file mode 100644
index 000000000..e97f159a3
--- /dev/null
+++ b/app/addons/fauxton/notifications/components/NotificationPanelWithTransition.js
@@ -0,0 +1,64 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from 'react';
+import {TransitionMotion, spring} from 'react-motion';
+// import GlobalNotifications from './GlobalNotifications';
+import NotificationCenterPanel from './NotificationCenterPanel';
+
+// The one-stop-shop for Fauxton notifications. This controller handler the header notifications and the rightmost
+// notification center panel
+export default class NotificationPanelWithTransition extends React.Component {
+
+  getStyles = () => {
+    let item = {
+      key: '1',
+      style: {
+        x: 320
+      }
+    };
+
+    if (this.props.isVisible) {
+      item.style = {
+        x: spring(0)
+      };
+    } else {
+      item.style = {
+        x: spring(320)
+      };
+    }
+    return [item];
+  };
+
+  getNotificationCenterPanel = (items) => {
+    const panel = items.map(({style}) => {
+      return <NotificationCenterPanel
+        key={'1'}
+        style={style}
+        {...this.props} />;
+    });
+    return (
+      <span>
+        {panel}
+      </span>
+    );
+  };
+
+  render() {
+    return (
+      <TransitionMotion
+        styles={this.getStyles}>
+        {this.getNotificationCenterPanel}
+      </TransitionMotion>
+    );
+  }
+}
diff --git a/app/addons/fauxton/notifications/components/PermanentNotification.js b/app/addons/fauxton/notifications/components/PermanentNotification.js
new file mode 100644
index 000000000..ff5c3969e
--- /dev/null
+++ b/app/addons/fauxton/notifications/components/PermanentNotification.js
@@ -0,0 +1,52 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+
+export default class PermanentNotification extends React.Component {
+
+  static defaultProps = {
+    visible: false
+  };
+
+  static propTypes = {
+    visible: PropTypes.bool.isRequired,
+    message: PropTypes.string
+  };
+
+  constructor (props) {
+    super(props);
+  }
+
+  // many messages contain HTML, hence the need for dangerouslySetInnerHTML
+  getMsg () {
+    return {__html: this.props.message};
+  }
+
+  getContent () {
+    if (!this.props.visible || !this.props.message) {
+      return null;
+    }
+    return (
+      <p className="perma-warning__content" dangerouslySetInnerHTML={this.getMsg()}></p>
+    );
+  }
+
+  render () {
+    return (
+      <div id="perma-warning">
+        {this.getContent()}
+      </div>
+    );
+  }
+}
diff --git a/app/addons/fauxton/notifications/components/PermanentNotificationContainer.js b/app/addons/fauxton/notifications/components/PermanentNotificationContainer.js
new file mode 100644
index 000000000..123d6dbad
--- /dev/null
+++ b/app/addons/fauxton/notifications/components/PermanentNotificationContainer.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux';
+import PermanentNotification from './PermanentNotification';
+
+const mapStateToProps = ({ notifications }) => {
+  return {
+    visible: notifications.permanentNotificationVisible,
+    message: notifications.permanentNotificationMessage
+  };
+};
+
+const PermanentNotificationContainer = connect(
+  mapStateToProps
+)(PermanentNotification);
+
+export default PermanentNotificationContainer;
diff --git a/app/addons/fauxton/notifications/notifications.js b/app/addons/fauxton/notifications/notifications.js
deleted file mode 100644
index 008d14a09..000000000
--- a/app/addons/fauxton/notifications/notifications.js
+++ /dev/null
@@ -1,543 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-import PropTypes from 'prop-types';
-
-import React from "react";
-import ReactDOM from "react-dom";
-import Actions from "./actions";
-import Stores from "./stores";
-import Components from "../../components/react-components";
-import {TransitionMotion, spring, presets} from 'react-motion';
-import uuid from 'uuid';
-
-var store = Stores.notificationStore;
-const {Copy} = Components;
-
-// The one-stop-shop for Fauxton notifications. This controller handler the header notifications and the rightmost
-// notification center panel
-export class NotificationController extends React.Component {
-  getStoreState = () => {
-    return {
-      notificationCenterVisible: store.isNotificationCenterVisible(),
-      notificationCenterFilter: store.getNotificationFilter(),
-      notifications: store.getNotifications()
-    };
-  };
-
-  componentDidMount() {
-    store.on('change', this.onChange, this);
-  }
-
-  componentWillUnmount() {
-    store.off('change', this.onChange);
-  }
-
-  onChange = () => {
-    this.setState(this.getStoreState());
-  };
-
-  getStyles = () => {
-    const isVisible = this.state.notificationCenterVisible;
-    let item = {
-      key: '1',
-      style: {
-        x: 320
-      }
-    };
-
-    if (isVisible) {
-      item.style = {
-        x: spring(0)
-      };
-    }
-
-    if (!isVisible) {
-      item.style = {
-        x: spring(320)
-      };
-    }
-    return [item];
-  };
-
-  getNotificationCenterPanel = (items) => {
-    const panel = items.map(({style}) => {
-      return <NotificationCenterPanel
-        key={'1'}
-        style={style}
-        visible={this.state.notificationCenterVisible}
-        filter={this.state.notificationCenterFilter}
-        notifications={this.state.notifications} />;
-    });
-    return (
-      <span>
-        {panel}
-      </span>
-    );
-  };
-
-  state = this.getStoreState();
-
-  render() {
-    return (
-      <div>
-        <GlobalNotifications
-          notifications={this.state.notifications} />
-        <TransitionMotion
-          styles={this.getStyles}>
-          {this.getNotificationCenterPanel}
-        </TransitionMotion>
-      </div>
-    );
-  }
-}
-
-
-class GlobalNotifications extends React.Component {
-  static propTypes = {
-    notifications: PropTypes.array.isRequired
-  };
-
-  componentDidMount() {
-    document.addEventListener('keydown', this.onKeyDown);
-  }
-
-  componentWillUnmount() {
-    document.removeEventListener('keydown', this.onKeyDown);
-  }
-
-  onKeyDown = (e) => {
-    const code = e.keyCode || e.which;
-    if (code === 27) {
-      Actions.hideAllVisibleNotifications();
-    }
-  };
-
-  getNotifications = () => {
-    if (!this.props.notifications.length) {
-      return null;
-    }
-
-    return _.map(this.props.notifications, (notification, index) => {
-
-      // notifications are completely removed from the DOM once they're
-      if (!notification.visible) {
-        return;
-      }
-
-      return (
-        <Notification
-          notificationId={notification.notificationId}
-          isHiding={notification.isHiding}
-          key={index}
-          msg={notification.msg}
-          type={notification.type}
-          escape={notification.escape}
-          visibleTime={notification.visibleTime}
-          onStartHide={Actions.startHidingNotification}
-          onHideComplete={Actions.hideNotification} />
-      );
-    });
-  };
-
-  getchildren = (items) => {
-    const notifications = items.map(({key, data, style}) => {
-      const notification = data;
-      return (
-        <Notification
-          key={key}
-          style={style}
-          notificationId={notification.notificationId}
-          isHiding={notification.isHiding}
-          msg={notification.msg}
-          type={notification.type}
-          escape={notification.escape}
-          visibleTime={notification.visibleTime}
-          onStartHide={Actions.startHidingNotification}
-          onHideComplete={Actions.hideNotification} />
-      );
-    });
-
-    return (
-      <div>
-        {notifications}
-      </div>
-    );
-  };
-
-  getStyles = (prevItems) => {
-    if (!prevItems) {
-      prevItems = [];
-    }
-
-    return this.props.notifications
-      .filter(notification => notification.visible)
-      .map(notification => {
-        let item = prevItems.find(style => style.key === (notification.notificationId.toString()));
-        let style = !item ? {opacity: 0.5, minHeight: 50} : false;
-
-        if (!style && !notification.isHiding) {
-          style = {
-            opacity: spring(1, presets.stiff),
-            minHeight: spring(64)
-          };
-        } else if (!style && notification.isHiding) {
-          style = {
-            opacity: spring(0, presets.stiff),
-            minHeight: spring(0, presets.stiff)
-          };
-        }
-
-        return {
-          key: notification.notificationId.toString(),
-          style,
-          data: notification
-        };
-      });
-  };
-
-  render() {
-    return (
-      <div id="global-notifications">
-        <TransitionMotion
-          styles={this.getStyles}
-        >
-          {this.getchildren}
-        </TransitionMotion>
-      </div>
-    );
-  }
-}
-
-class Notification extends React.Component {
-  static propTypes = {
-    msg: PropTypes.string.isRequired,
-    onStartHide: PropTypes.func.isRequired,
-    onHideComplete: PropTypes.func.isRequired,
-    type: PropTypes.oneOf(['error', 'info', 'success']),
-    escape: PropTypes.bool,
-    isHiding: PropTypes.bool.isRequired,
-    visibleTime: PropTypes.number
-  };
-
-  static defaultProps = {
-    type: 'info',
-    visibleTime: 8000,
-    escape: true
-  };
-
-  componentWillUnmount() {
-    if (this.timeout) {
-      window.clearTimeout(this.timeout);
-    }
-  }
-
-  componentDidMount() {
-    this.timeout = setTimeout(this.hide, this.props.visibleTime);
-  }
-
-  hide = (e) => {
-    if (e) {
-      e.preventDefault();
-    }
-    this.props.onStartHide(this.props.notificationId);
-  };
-
-  // many messages contain HTML, hence the need for dangerouslySetInnerHTML
-  getMsg = () => {
-    var msg = (this.props.escape) ? _.escape(this.props.msg) : this.props.msg;
-    return {
-      __html: msg
-    };
-  };
-
-  onAnimationComplete = () => {
-    if (this.props.isHiding) {
-      window.setTimeout(() => this.props.onHideComplete(this.props.notificationId));
-    }
-  };
-
-  render() {
-    const {style, notificationId} = this.props;
-    const iconMap = {
-      error: 'fonticon-attention-circled',
-      info: 'fonticon-info-circled',
-      success: 'fonticon-ok-circled'
-    };
-
-    if (style.opacity === 0 && this.props.isHiding) {
-      this.onAnimationComplete();
-    }
-
-    return (
-      <div
-        key={notificationId.toString()} className="notification-wrapper" style={{opacity: style.opacity, minHeight: style.minHeight + 'px'}}>
-        <div
-          style={{opacity: style.opacity, minHeight: style.minHeight + 'px'}}
-          className={'global-notification alert alert-' + this.props.type}
-          ref={node => this.notification = node}>
-          <a data-bypass href="#" onClick={this.hide}><i className="pull-right fonticon-cancel" /></a>
-          <i className={'notification-icon ' + iconMap[this.props.type]} />
-          <span dangerouslySetInnerHTML={this.getMsg()}></span>
-        </div>
-      </div>
-    );
-  }
-}
-
-
-export class NotificationCenterButton extends React.Component {
-  state = {
-    visible: true
-  };
-
-  hide = () => {
-    this.setState({ visible: false });
-  };
-
-  show = () => {
-    this.setState({ visible: true });
-  };
-
-  render() {
-    var classes = 'fonticon fonticon-bell' + ((!this.state.visible) ? ' hide' : '');
-    return (
-      <div className={classes} onClick={Actions.showNotificationCenter}></div>
-    );
-  }
-}
-
-
-class NotificationCenterPanel extends React.Component {
-  static propTypes = {
-    visible: PropTypes.bool.isRequired,
-    filter: PropTypes.string.isRequired,
-    notifications: PropTypes.array.isRequired
-  };
-
-  getNotifications = (items) => {
-    let notifications;
-    if (!items.length && !this.props.notifications.length) {
-      notifications = <li className="no-notifications">
-          No notifications.
-      </li>;
-    } else {
-      notifications = items
-        .map(({key, data: notification, style}) => {
-          return (
-            <NotificationPanelRow
-              isVisible={this.props.visible}
-              item={notification}
-              filter={this.props.filter}
-              key={key}
-              style={style}
-            />
-          );
-        });
-    }
-
-    return (
-      <ul className="notification-list">
-        {notifications}
-      </ul>
-    );
-  };
-
-  getStyles = (prevItems = []) => {
-    return this.props.notifications
-      .map(notification => {
-        let item = prevItems.find(style => style.key === (notification.notificationId.toString()));
-        let style = !item ? {opacity: 0, height: 0} : false;
-
-        if (!style && (notification.type === this.props.filter || this.props.filter === 'all')) {
-          style = {
-            opacity: spring(1, presets.stiff),
-            height: spring(61, presets.stiff)
-          };
-        } else if (notification.type !== this.props.filter) {
-          style = {
-            opacity: spring(0, presets.stiff),
-            height: spring(0, presets.stiff)
-          };
-        }
-
-        return {
-          key: notification.notificationId.toString(),
-          style,
-          data: notification
-        };
-      });
-  };
-
-  render() {
-    if (!this.props.visible && this.props.style.x === 0) {
-      // panelClasses += ' visible';
-      return null;
-    }
-
-    const filterClasses = {
-      all: 'flex-body',
-      success: 'flex-body',
-      error: 'flex-body',
-      info: 'flex-body'
-    };
-    filterClasses[this.props.filter] += ' selected';
-
-    const maskClasses = `notification-page-mask ${((this.props.visible) ? ' visible' : '')}`;
-    const panelClasses = 'notification-center-panel flex-layout flex-col visible';
-    return (
-      <div id="notification-center">
-        <div className={panelClasses} style={{transform: `translate(${this.props.style.x}px)`}}>
-
-          <header className="flex-layout flex-row">
-            <span className="fonticon fonticon-bell" />
-            <h1 className="flex-body">Notifications</h1>
-            <button type="button" onClick={Actions.hideNotificationCenter}>×</button>
-          </header>
-
-          <ul className="notification-filter flex-layout flex-row">
-            <li className={filterClasses.all} title="All notifications" data-filter="all"
-              onClick={Actions.selectNotificationFilter.bind(this, 'all')}>All</li>
-            <li className={filterClasses.success} title="Success notifications" data-filter="success"
-              onClick={Actions.selectNotificationFilter.bind(this, 'success')}>
-              <span className="fonticon fonticon-ok-circled" />
-            </li>
-            <li className={filterClasses.error} title="Error notifications" data-filter="error"
-              onClick={Actions.selectNotificationFilter.bind(this, 'error')}>
-              <span className="fonticon fonticon-attention-circled" />
-            </li>
-            <li className={filterClasses.info} title="Info notifications" data-filter="info"
-              onClick={Actions.selectNotificationFilter.bind(this, 'info')}>
-              <span className="fonticon fonticon-info-circled" />
-            </li>
-          </ul>
-
-          <div className="flex-body">
-            <TransitionMotion styles={this.getStyles}>
-              {this.getNotifications}
-            </TransitionMotion>
-          </div>
-
-          <footer>
-            <input
-              type="button"
-              value="Clear All"
-              className="btn btn-small btn-secondary"
-              onClick={Actions.clearAllNotifications} />
-          </footer>
-        </div>
-
-        <div className={maskClasses} onClick={Actions.hideNotificationCenter}></div>
-      </div>
-    );
-  }
-}
-
-class NotificationPanelRow extends React.Component {
-  static propTypes = {
-    item: PropTypes.object.isRequired
-  };
-
-  clearNotification = () => {
-    const {notificationId} = this.props.item;
-    Actions.clearSingleNotification(notificationId);
-  };
-
-  render() {
-    const iconMap = {
-      success: 'fonticon-ok-circled',
-      error: 'fonticon-attention-circled',
-      info: 'fonticon-info-circled'
-    };
-
-    const timeElapsed = this.props.item.time.fromNow();
-
-    // we can safely do this because the store ensures all notifications are of known types
-    const rowIconClasses = 'fonticon ' + iconMap[this.props.item.type];
-    const hidden = (this.props.filter === 'all' || this.props.filter === this.props.item.type) ? false : true;
-    const {style} = this.props;
-    const {opacity, height} = style;
-    if (opacity === 0 && height === 0) {
-      return null;
-    }
-
-    // N.B. wrapper <div> needed to ensure smooth hide/show transitions
-    return (
-      <li style={{opactiy: opacity, height: height + 'px', borderBottomColor: `rgba(34, 34, 34, ${opacity})`}} aria-hidden={hidden}>
-        <div className="flex-layout flex-row">
-          <span className={rowIconClasses}></span>
-          <div className="flex-body">
-            <p>{this.props.item.cleanMsg}</p>
-            <div className="notification-actions">
-              <span className="time-elapsed">{timeElapsed}</span>
-              <span className="divider">|</span>
-              <Copy uniqueKey={uuid.v4()} text={this.props.item.cleanMsg} displayType="text" />
-            </div>
-          </div>
-          <button type="button" onClick={this.clearNotification}>×</button>
-        </div>
-      </li>
-    );
-  }
-}
-
-export class PermanentNotification extends React.Component {
-  constructor (props) {
-    super(props);
-    this.state = this.getStoreState();
-  }
-
-  getStoreState () {
-    return {
-      display: store.isPermanentNotificationVisible(),
-      msg: store.getPermanentNotificationMessage()
-    };
-  }
-
-  onChange () {
-    this.setState(this.getStoreState);
-  }
-
-  componentDidMount () {
-    store.on('change', this.onChange, this);
-  }
-
-  // many messages contain HTML, hence the need for dangerouslySetInnerHTML
-  getMsg () {
-    return {__html: this.state.msg};
-  }
-
-  getContent () {
-    if (!this.state.display || !this.state.msg) {
-      return;
-    }
-    return (
-      <p className="perma-warning__content" dangerouslySetInnerHTML={this.getMsg()}></p>
-    );
-  }
-
-  render () {
-    return (
-      <div id="perma-warning">
-        {this.getContent()}
-      </div>
-    );
-  }
-}
-
-export default {
-  NotificationController,
-  NotificationCenterButton,
-  NotificationCenterPanel,
-  NotificationPanelRow,
-  Notification
-};
diff --git a/app/addons/fauxton/notifications/reducers.js b/app/addons/fauxton/notifications/reducers.js
new file mode 100644
index 000000000..0f755d11a
--- /dev/null
+++ b/app/addons/fauxton/notifications/reducers.js
@@ -0,0 +1,188 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import moment from 'moment';
+import app from '../../../app';
+import ActionTypes from './actiontypes';
+
+const initialState = {
+  notifications: [],
+  notificationCenterVisible: false,
+  selectedNotificationFilter: 'all',
+  permanentNotificationVisible: false,
+  permanentNotificationMessage: ''
+};
+
+let counter = 0;
+const validNotificationTypes = ['success', 'error', 'info'];
+const validFilters = ['all', 'success', 'error', 'info'];
+
+function addNotification ({ notifications }, info) {
+  const newNotifications = notifications.slice();
+  info = { ...info };
+  info.notificationId = ++counter;
+  info.cleanMsg = app.utils.stripHTML(info.msg);
+  info.time = moment();
+  if (info.escape !== true && info.escape !== false) {
+    info.escape = true;
+  }
+
+  // all new notifications are visible by default. They get hidden after their time expires, by the component
+  info.visible = true;
+  info.isHiding = false;
+
+  // clear: true causes all visible messages to be hidden
+  if (info.clear) {
+    newNotifications.forEach((notification) => {
+      if (notification.visible) {
+        notification.isHiding = true;
+      }
+    });
+  }
+  newNotifications.unshift(info);
+  return newNotifications;
+}
+
+function clearNotification({ notifications }, notificationId) {
+  const idx = notifications.findIndex(el => {
+    return el.notificationId === notificationId;
+  });
+  if (idx === -1) {
+    // no changes
+    return notifications;
+  }
+
+  const newNotifications = [].concat(notifications);
+  newNotifications.splice(idx, 1);
+  return newNotifications;
+}
+
+function startHidingNotification({ notifications }, notificationId) {
+  const idx = notifications.findIndex(el => {
+    return el.notificationId === notificationId;
+  });
+  if (idx === -1) {
+    // no changes
+    return notifications;
+  }
+
+  const newNotifications = [].concat(notifications);
+  newNotifications[idx].isHiding = true;
+  return newNotifications;
+}
+
+function hideNotification({ notifications }, notificationId) {
+  const idx = notifications.findIndex(el => {
+    return el.notificationId === notificationId;
+  });
+  if (idx === -1) {
+    // no changes
+    return notifications;
+  }
+
+  const newNotifications = [].concat(notifications);
+  newNotifications[idx].visible = false;
+  newNotifications[idx].isHiding = false;
+  return newNotifications;
+}
+
+function hideAllNotifications({ notifications }) {
+  const newNotifications = [].concat(notifications);
+  newNotifications.forEach((notification) => {
+    if (notification.visible) {
+      notification.isHiding = true;
+    }
+  });
+  return newNotifications;
+}
+
+export default function notifications(state = initialState, action) {
+  const { options, type } = action;
+  switch (type) {
+
+    case ActionTypes.ADD_NOTIFICATION:
+      if (!validNotificationTypes.includes(options.info.type)) {
+        return state;
+      }
+      return {
+        ...state,
+        notifications: addNotification(state, options.info)
+      };
+
+    case ActionTypes.CLEAR_ALL_NOTIFICATIONS:
+      return {
+        ...state,
+        notifications: []
+      };
+
+    case ActionTypes.CLEAR_SINGLE_NOTIFICATION:
+      return {
+        ...state,
+        notifications: clearNotification(state, options.notificationId)
+      };
+
+    case ActionTypes.START_HIDING_NOTIFICATION:
+      return {
+        ...state,
+        notifications: startHidingNotification(state, options.notificationId)
+      };
+
+    case ActionTypes.HIDE_NOTIFICATION:
+      return {
+        ...state,
+        notifications: hideNotification(state, options.notificationId)
+      };
+
+    case ActionTypes.HIDE_ALL_NOTIFICATIONS:
+      return {
+        ...state,
+        notifications: hideAllNotifications(state)
+      };
+
+    case ActionTypes.SHOW_NOTIFICATION_CENTER:
+      return {
+        ...state,
+        notificationCenterVisible: true
+      };
+
+    case ActionTypes.HIDE_NOTIFICATION_CENTER:
+      return {
+        ...state,
+        notificationCenterVisible: false
+      };
+
+    case ActionTypes.SELECT_NOTIFICATION_FILTER:
+      if (!validFilters.includes(options.filter)) {
+        return state;
+      }
+      return {
+        ...state,
+        selectedNotificationFilter: options.filter
+      };
+
+    case ActionTypes.SHOW_PERMANENT_NOTIFICATION:
+      return {
+        ...state,
+        permanentNotificationVisible: true,
+        permanentNotificationMessage: options.msg
+      };
+
+    case ActionTypes.HIDE_PERMANENT_NOTIFICATION:
+      return {
+        ...state,
+        permanentNotificationVisible: false
+      };
+
+    default:
+      return state;
+  }
+}
diff --git a/app/addons/fauxton/notifications/stores.js b/app/addons/fauxton/notifications/stores.js
index dc8d0541d..a1304e186 100644
--- a/app/addons/fauxton/notifications/stores.js
+++ b/app/addons/fauxton/notifications/stores.js
@@ -33,25 +33,7 @@ var validNotificationTypes = ['success', 'error', 'info'];
  */
 
 Stores.NotificationStore = FauxtonAPI.Store.extend({
-  initialize () {
-    this.reset();
-  },
-
-  reset () {
-    this._notifications = [];
-    this._notificationCenterVisible = false;
-    this._selectedNotificationFilter = 'all';
-    this._permanentNotificationVisible = false;
-    this._permanentNotificationMessage = '';
-  },
-
-  isNotificationCenterVisible () {
-    return this._notificationCenterVisible;
-  },
 
-  isPermanentNotificationVisible () {
-    return this._permanentNotificationVisible;
-  },
 
   addNotification (info) {
     if (_.isEmpty(info.type) || !_.includes(validNotificationTypes, info.type)) {
@@ -78,26 +60,10 @@ Stores.NotificationStore = FauxtonAPI.Store.extend({
     this._notifications.unshift(info);
   },
 
-  getNotifications () {
-    return this._notifications;
-  },
-
-  getPermanentNotificationMessage () {
-    return this._permanentNotificationMessage;
-  },
-
-  setPermanentNotificationMessage (content) {
-    this._permanentNotificationMessage = content;
-  },
-
   clearNotification (notificationId) {
     this._notifications = _.without(this._notifications, _.find(this._notifications, { notificationId: notificationId }));
   },
 
-  clearNotifications () {
-    this._notifications = [];
-  },
-
   hideNotification (notificationId) {
     var notification = _.find(this._notifications, { notificationId: notificationId });
     notification.visible = false;
@@ -117,10 +83,6 @@ Stores.NotificationStore = FauxtonAPI.Store.extend({
     notification.isHiding = true;
   },
 
-  getNotificationFilter () {
-    return this._selectedNotificationFilter;
-  },
-
   setNotificationFilter (filter) {
     if ((_.isEmpty(filter) || !_.includes(validNotificationTypes, filter)) && filter !== 'all') {
       console.warn('Invalid notification filter: ', filter);
diff --git a/app/core/base.js b/app/core/base.js
index 2aa5f8eda..0762f7acc 100644
--- a/app/core/base.js
+++ b/app/core/base.js
@@ -41,20 +41,47 @@ var FauxtonAPI = {
       link: link
     });
   },
-  addNotification: function (options) {
 
-    options = _.extend({
+  /**
+   * Displays a notification message. The message is only displayed for a few seconds.
+   * The option visibleTime can be provided to set for how long the message should be displayed.
+   *
+   * @param {object} options Options are of the form
+   * {
+   *  message: "string",
+   *  type: "success"|"error"|"info",
+   *  clear: true|false,
+   *  escape: true|false,
+   *  visibleTime: number
+   * }
+   */
+  addNotification: function (options) {
+    options = Object.assign({
       msg: 'Notification Event Triggered!',
       type: 'info',
       escape: true,
       clear: false
     }, options);
 
-    FauxtonAPI.dispatch({
-      type: 'ADD_NOTIFICATION',
-      options: {
-        info: options
-      }
+    if (FauxtonAPI.reduxDispatch) {
+      FauxtonAPI.reduxDispatch({
+        type: 'ADD_NOTIFICATION',
+        options: {
+          info: options
+        }
+      });
+    }
+  },
+
+  /**
+   * Shows a permanent notification message
+   *
+   * @param {object} message
+   */
+  showPermanentNotification: function (message) {
+    FauxtonAPI.reduxDispatch({
+      type: 'SHOW_PERMANENT_NOTIFICATION',
+      options: { msg: message }
     });
   },
 


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services