couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From amaran...@apache.org
Subject [couchdb-fauxton] branch master updated: Update fauxton/notifications to use redux (#1139)
Date Mon, 15 Oct 2018 20:53:04 GMT
This is an automated email from the ASF dual-hosted git repository.

amaranhao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git


The following commit(s) were added to refs/heads/master by this push:
     new 2ee7cf7  Update fauxton/notifications to use redux (#1139)
2ee7cf7 is described below

commit 2ee7cf7d9d9a36addde90e48a84a3e349ad5e615
Author: Antonio Maranhao <30349380+Antonio-Maranhao@users.noreply.github.com>
AuthorDate: Mon Oct 15 16:52:59 2018 -0400

    Update fauxton/notifications to use redux (#1139)
    
    * Use redux
    
    * Update tests
---
 app/addons/components/layouts.js                   |   2 +-
 app/addons/config/layout.js                        |   2 +-
 app/addons/documents/layouts.js                    |   2 +-
 app/addons/documents/mangolayout.js                |   2 +-
 app/addons/fauxton/appwrapper.js                   |   9 +-
 app/addons/fauxton/base.js                         |  16 +-
 .../notifications/__tests__/actions.test.js        |  45 --
 .../notifications/__tests__/components.test.js     | 190 +++----
 .../__tests__/permanentNotification.test.js        |  45 +-
 .../notifications/__tests__/reducers.test.js       | 134 +++++
 .../fauxton/notifications/__tests__/stores.test.js |  95 ----
 app/addons/fauxton/notifications/actions.js        |  67 +--
 .../components/GlobalNotifications.js              | 135 +++++
 .../components/GlobalNotificationsContainer.js     |  30 ++
 .../notifications/components/Notification.js       |  91 ++++
 .../components/NotificationCenterButton.js         |  55 +++
 .../components/NotificationCenterPanel.js          | 145 ++++++
 .../components/NotificationPanelContainer.js       |  36 ++
 .../components/NotificationPanelRow.js             |  66 +++
 .../components/NotificationPanelWithTransition.js  |  64 +++
 .../components/PermanentNotification.js            |  52 ++
 .../components/PermanentNotificationContainer.js   |  15 +
 app/addons/fauxton/notifications/notifications.js  | 543 ---------------------
 app/addons/fauxton/notifications/reducers.js       | 188 +++++++
 app/addons/fauxton/notifications/stores.js         |  38 --
 app/core/base.js                                   |  41 +-
 26 files changed, 1187 insertions(+), 921 deletions(-)

diff --git a/app/addons/components/layouts.js b/app/addons/components/layouts.js
index cc1c30a..81389e5 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 87471b4..7774e06 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 3b921aa..3efae93 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 a873f8e..fd7958d 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 8510433..fca8a81 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 17812d8..8a3f417 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 0f91e8b..0000000
--- 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 9c3bb31..a12b15a 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 2354d0e..0ce231d 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 0000000..8b50dfe
--- /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 3c1e3ad..0000000
--- 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 32f40ab..0e57faf 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 0000000..5d63dd3
--- /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 0000000..8eb19b2
--- /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 0000000..ed7c6ca
--- /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 0000000..cf458a4
--- /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 0000000..a89e7c2
--- /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 0000000..ec03cdc
--- /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 0000000..f267c20
--- /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 0000000..e97f159
--- /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 0000000..ff5c396
--- /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 0000000..123d6db
--- /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 008d14a..0000000
--- 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 0000000..0f755d1
--- /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 dc8d054..a1304e1 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 2aa5f8e..0762f7a 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 }
     });
   },
 


Mime
View raw message