You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by be...@apache.org on 2016/05/18 21:58:34 UTC

fauxton commit: updated refs/heads/master to 0518374

Repository: couchdb-fauxton
Updated Branches:
  refs/heads/master 5b718dd71 -> 05183749e


Notifications Update

This updates the global notifications to use the same store
as the notification panel and drop Backbone.


Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/05183749
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/05183749
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/05183749

Branch: refs/heads/master
Commit: 05183749e53f173b8f2fff52c9c2d93a053c3bca
Parents: 5b718dd
Author: Ben Keen <be...@gmail.com>
Authored: Wed Mar 30 14:35:50 2016 -0700
Committer: Ben Keen <be...@gmail.com>
Committed: Wed May 18 08:50:55 2016 -0700

----------------------------------------------------------------------
 .../databases/tests/nightwatch/zeroclipboard.js |   3 +-
 .../tests/nightwatch/uploadAttachment.js        |   4 +-
 app/addons/fauxton/base.js                      | 111 +-------
 app/addons/fauxton/notifications/actions.js     |  27 +-
 app/addons/fauxton/notifications/actiontypes.js |   5 +-
 .../notifications/notifications.react.jsx       | 282 ++++++++++++++++---
 app/addons/fauxton/notifications/stores.js      |  46 +++
 .../fauxton/notifications/tests/actionsSpec.jsx |  58 ++++
 .../tests/componentsSpec.react.jsx              | 115 +++++---
 app/addons/fauxton/templates/notification.html  |  19 --
 app/addons/fauxton/tests/baseSpec.js            |  62 ----
 assets/index.underscore                         |   3 +-
 assets/less/fauxton.less                        |   3 +-
 assets/less/templates.less                      |   7 +-
 package.json                                    |   1 +
 .../custom-commands/closeNotification.js        |   2 +-
 16 files changed, 464 insertions(+), 284 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/databases/tests/nightwatch/zeroclipboard.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/tests/nightwatch/zeroclipboard.js b/app/addons/databases/tests/nightwatch/zeroclipboard.js
index 708bb64..cd83582 100644
--- a/app/addons/databases/tests/nightwatch/zeroclipboard.js
+++ b/app/addons/databases/tests/nightwatch/zeroclipboard.js
@@ -24,10 +24,9 @@ module.exports = {
     }
 
     client
+      .deleteDatabase(newDatabaseName)
       .loginToGUI()
-      .deleteDatabase(newDatabaseName) //need to delete the automatic database 'fauxton-selenium-tests' that has been set up before each test
       .url(baseUrl)
-
       .clickWhenVisible('.control-toggle-api-url')
       .pause(1000) // needed for reliability. The tray slides in from the top so the pos of the copy button changes
       .waitForElementVisible('.copy-button', waitTime, false)

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/documents/tests/nightwatch/uploadAttachment.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/uploadAttachment.js b/app/addons/documents/tests/nightwatch/uploadAttachment.js
index 4005984..b534823 100644
--- a/app/addons/documents/tests/nightwatch/uploadAttachment.js
+++ b/app/addons/documents/tests/nightwatch/uploadAttachment.js
@@ -27,8 +27,8 @@ module.exports = {
       .waitForElementVisible('input[name="_attachments"]', waitTime)
       .setValue('input[name="_attachments"]', require('path').resolve(__dirname + '/uploadAttachment.js'))
       .clickWhenVisible('#upload-btn')
-      .waitForElementVisible('#global-notification-id', waitTime, false)
-      .getText('#global-notification-id', function (result) {
+      .waitForElementVisible('.global-notification', waitTime, false)
+      .getText('.global-notification', function (result) {
         var data = result.value;
         this.verify.ok(data, 'Document saved successfully.');
       })

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/base.js
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/base.js b/app/addons/fauxton/base.js
index 83fb727..7d22bb1 100644
--- a/app/addons/fauxton/base.js
+++ b/app/addons/fauxton/base.js
@@ -29,17 +29,14 @@ function (app, FauxtonAPI, Components, NotificationComponents, Actions, NavbarRe
   var Fauxton = FauxtonAPI.addon();
   FauxtonAPI.addNotification = function (options) {
     options = _.extend({
-      msg: "Notification Event Triggered!",
-      type: "info",
-      selector: "#global-notifications",
-      escape: true
+      msg: 'Notification Event Triggered!',
+      type: 'info',
+      escape: true,
+      clear: false
     }, options);
 
     // log all notifications in a store
     Actions.addNotification(options);
-
-    var view = new Fauxton.Notification(options);
-    return view.renderNotification();
   };
 
   FauxtonAPI.UUID = FauxtonAPI.Model.extend({
@@ -103,11 +100,10 @@ function (app, FauxtonAPI, Components, NotificationComponents, Actions, NavbarRe
       NavbarReactComponents.renderNavBar(primaryNavBarEl);
     }
 
-    var notificationCenterEl = $('#notification-center')[0];
-    if (notificationCenterEl) {
-      NotificationComponents.renderNotificationCenter(notificationCenterEl);
+    var notificationEl = $('#notifications')[0];
+    if (notificationEl) {
+      NotificationComponents.renderNotificationController(notificationEl);
     }
-
     var versionInfo = new Fauxton.VersionInfo();
 
     versionInfo.fetch().then(function () {
@@ -121,98 +117,5 @@ function (app, FauxtonAPI, Components, NotificationComponents, Actions, NavbarRe
     }
   });
 
-  Fauxton.Notification = FauxtonAPI.View.extend({
-    animationTimer: 8000,
-    id: 'global-notification-id',
-    events: {
-      'click .js-dismiss': 'onClickRemoveWithAnimation'
-    },
-
-    initialize: function (options) {
-      this.htmlToRender = options.msg;
-      // escape always, except the value is false
-      if (options.escape !== false) {
-        this.htmlToRender = _.escape(this.htmlToRender);
-      }
-      this.type = options.type || "info";
-      this.selector = options.selector;
-      this.fade = options.fade === undefined ? true : options.fade;
-      this.data = options.data || "";
-      this.template = options.template || "addons/fauxton/templates/notification";
-    },
-
-    serialize: function () {
-      var icon;
-
-      switch (this.type) {
-        case 'error':
-          icon = 'fonticon-attention-circled';
-        break;
-        case 'info':
-          icon = 'fonticon-info-circled';
-        break;
-        case 'success':
-          icon = 'fonticon-ok-circled';
-        break;
-        default:
-          icon = 'fonticon-info-circled';
-        break;
-      }
-
-      return {
-        icon: icon,
-        data: this.data,
-        htmlToRender: this.htmlToRender,
-        type: this.type
-      };
-    },
-
-    onClickRemoveWithAnimation: function (event) {
-      event.preventDefault();
-      window.clearTimeout(this.timeout);
-      this.removeWithAnimation();
-    },
-
-    removeWithAnimation: function () {
-      this.$el.velocity('reverse', FauxtonAPI.constants.MISC.TRAY_TOGGLE_SPEED, function () {
-        this.$el.remove();
-        this.removeCloseListener();
-      }.bind(this));
-    },
-
-    addCloseListener: function () {
-      $(document).on('keydown.notificationClose', this.onKeyDown.bind(this));
-    },
-
-    onKeyDown: function (e) {
-      var code = e.keyCode || e.which;
-      if (code === 27) { // ESC key
-        this.removeWithAnimation();
-      }
-    },
-
-    removeCloseListener: function () {
-      $(document).off('keydown.notificationClose', this.removeWithAnimation);
-    },
-
-    delayedRemoval: function () {
-      this.timeout = setTimeout(function () {
-        this.removeWithAnimation();
-      }.bind(this), this.animationTimer);
-    },
-
-    renderNotification: function (selector) {
-      selector = selector || this.selector;
-      if (this.clear) {
-        $(selector).html('');
-      }
-      this.render().$el.appendTo(selector);
-      this.$el.velocity('transition.slideDownIn', FauxtonAPI.constants.MISC.TRAY_TOGGLE_SPEED);
-      this.delayedRemoval();
-      this.addCloseListener();
-      return this;
-    }
-  });
-
   return Fauxton;
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/notifications/actions.js
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/notifications/actions.js b/app/addons/fauxton/notifications/actions.js
index 38b9255..91fa822 100644
--- a/app/addons/fauxton/notifications/actions.js
+++ b/app/addons/fauxton/notifications/actions.js
@@ -55,13 +55,38 @@ function (FauxtonAPI, ActionTypes) {
     });
   }
 
+  function startHidingNotification (notificationId) {
+    FauxtonAPI.dispatch({
+      type: ActionTypes.START_HIDING_NOTIFICATION,
+      options: {
+        notificationId: notificationId
+      }
+    });
+  }
+
+  function hideNotification (notificationId) {
+    FauxtonAPI.dispatch({
+      type: ActionTypes.HIDE_NOTIFICATION,
+      options: {
+        notificationId: notificationId
+      }
+    });
+  }
+
+  function hideAllVisibleNotifications () {
+    FauxtonAPI.dispatch({ type: ActionTypes.HIDE_ALL_NOTIFICATIONS });
+  }
+
   return {
     addNotification: addNotification,
     showNotificationCenter: showNotificationCenter,
     hideNotificationCenter: hideNotificationCenter,
     clearAllNotifications: clearAllNotifications,
     clearSingleNotification: clearSingleNotification,
-    selectNotificationFilter: selectNotificationFilter
+    selectNotificationFilter: selectNotificationFilter,
+    startHidingNotification: startHidingNotification,
+    hideNotification: hideNotification,
+    hideAllVisibleNotifications: hideAllVisibleNotifications
   };
 
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/notifications/actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/notifications/actiontypes.js b/app/addons/fauxton/notifications/actiontypes.js
index 103ec58..e346c24 100644
--- a/app/addons/fauxton/notifications/actiontypes.js
+++ b/app/addons/fauxton/notifications/actiontypes.js
@@ -17,6 +17,9 @@ define([],  function () {
     HIDE_NOTIFICATION_CENTER: 'HIDE_NOTIFICATION_CENTER',
     CLEAR_SINGLE_NOTIFICATION: 'CLEAR_SINGLE_NOTIFICATION',
     CLEAR_ALL_NOTIFICATIONS: 'CLEAR_ALL_NOTIFICATIONS',
-    SELECT_NOTIFICATION_FILTER: 'SELECT_NOTIFICATION_FILTER'
+    SELECT_NOTIFICATION_FILTER: 'SELECT_NOTIFICATION_FILTER',
+    START_HIDING_NOTIFICATION: 'START_HIDING_NOTIFICATION',
+    HIDE_NOTIFICATION: 'HIDE_NOTIFICATION',
+    HIDE_ALL_NOTIFICATIONS: 'HIDE_ALL_NOTIFICATIONS'
   };
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/notifications/notifications.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/notifications/notifications.react.jsx b/app/addons/fauxton/notifications/notifications.react.jsx
index 2e53ebb..cd88fe5 100644
--- a/app/addons/fauxton/notifications/notifications.react.jsx
+++ b/app/addons/fauxton/notifications/notifications.react.jsx
@@ -19,81 +19,268 @@ define([
   './stores',
   '../components.react',
 
-  // needed to run the test individually. Don't remove
+  'velocity-react',
   "velocity-animate/velocity",
   "velocity-animate/velocity.ui"
 ],
 
-function (app, FauxtonAPI, React, ReactDOM, Actions, Stores, Components) {
+function (app, FauxtonAPI, React, ReactDOM, Actions, Stores, Components, VelocityReact) {
 
-  var notificationStore = Stores.notificationStore;
+  var store = Stores.notificationStore;
   var Clipboard = Components.Clipboard;
+  var VelocityComponent = VelocityReact.VelocityComponent;
 
 
-  var NotificationCenterButton = React.createClass({
+  // The one-stop-shop for Fauxton notifications. This controller handler the header notifications and the rightmost
+  // notification center panel
+  var NotificationController = React.createClass({
+
     getInitialState: function () {
+      return this.getStoreState();
+    },
+
+    getStoreState: function () {
       return {
-        visible: true
+        notificationCenterVisible: store.isNotificationCenterVisible(),
+        notificationCenterFilter: store.getNotificationFilter(),
+        notifications: store.getNotifications()
       };
     },
 
-    hide: function () {
-      this.setState({ visible: false });
+    componentDidMount: function () {
+      store.on('change', this.onChange, this);
     },
 
-    show: function () {
-      this.setState({ visible: true });
+    componentWillUnmount: function () {
+      store.off('change', this.onChange);
+    },
+
+    onChange: function () {
+      if (this.isMounted()) {
+        this.setState(this.getStoreState());
+      }
     },
 
     render: function () {
-      var classes = 'fonticon fonticon-bell' + ((!this.state.visible) ? ' hide' : '');
       return (
-        <div className={classes} onClick={Actions.showNotificationCenter}></div>
+        <div>
+          <GlobalNotifications
+            notifications={this.state.notifications} />
+          <NotificationCenterPanel
+            visible={this.state.notificationCenterVisible}
+            filter={this.state.notificationCenterFilter}
+            notifications={this.state.notifications} />
+        </div>
       );
     }
   });
 
-  var NotificationCenterPanel = React.createClass({
 
-    getInitialState: function () {
-      return this.getStoreState();
+  var GlobalNotifications = React.createClass({
+    propTypes: {
+      notifications: React.PropTypes.array.isRequired
     },
 
-    getStoreState: function () {
+    componentDidMount: function () {
+      $(document).on('keydown.notificationClose', this.onKeyDown);
+    },
+
+    componentWillUnmount: function () {
+      $(document).off('keydown.notificationClose', this.onKeyDown);
+    },
+
+    onKeyDown: function (e) {
+      var code = e.keyCode || e.which;
+      if (code === 27) {
+        Actions.hideAllVisibleNotifications();
+      }
+    },
+
+    getNotifications: function () {
+      if (!this.props.notifications.length) {
+        return null;
+      }
+
+      return _.map(this.props.notifications, function (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}
+            onStartHide={Actions.startHidingNotification}
+            onHideComplete={Actions.hideNotification} />
+        );
+      }, this);
+    },
+
+    render: function () {
+      return (
+        <div id="global-notifications">
+          {this.getNotifications()}
+        </div>
+      );
+    }
+  });
+
+
+  var Notification = React.createClass({
+    propTypes: {
+      msg: React.PropTypes.string.isRequired,
+      onStartHide: React.PropTypes.func.isRequired,
+      onHideComplete: React.PropTypes.func.isRequired,
+      type: React.PropTypes.oneOf(['error', 'info', 'success']),
+      escape: React.PropTypes.bool,
+      isHiding: React.PropTypes.bool.isRequired,
+      visibleTime: React.PropTypes.number
+    },
+
+    getDefaultProps: function () {
+      return {
+        type: 'info',
+        visibleTime: 8000,
+        escape: true,
+        slideInTime: 200,
+        slideOutTime: 200
+      };
+    },
+
+    componentWillUnmount: function () {
+      if (this.timeout) {
+        window.clearTimeout(this.timeout);
+      }
+    },
+
+    getInitialState: function () {
       return {
-        isVisible: notificationStore.isNotificationCenterVisible(),
-        filter: notificationStore.getNotificationFilter(),
-        notifications: notificationStore.getNotifications()
+        animation: { opacity: 0, minHeight: 0 }
       };
     },
 
     componentDidMount: function () {
-      notificationStore.on('change', this.onChange, this);
+      this.setState({
+        animation: {
+          opacity: (this.props.isHiding) ? 0 : 1,
+          minHeight: (this.props.isHiding) ? 0 : ReactDOM.findDOMNode(this.refs.notification).offsetHeight
+        }
+      });
+
+      this.timeout = setTimeout(function () {
+        this.hide();
+      }.bind(this), this.props.visibleTime);
     },
 
-    componentWillUnmount: function () {
-      notificationStore.off('change', this.onChange);
+    componentDidUpdate: function (prevProps) {
+      if (!prevProps.isHiding && this.props.isHiding) {
+        this.setState({
+          animation: {
+            opacity: 0,
+            minHeight: 0
+          }
+        });
+      }
     },
 
-    onChange: function () {
-      if (this.isMounted()) {
-        this.setState(this.getStoreState());
+    getHeight: function () {
+      return $(ReactDOM.findDOMNode(this)).outerHeight(true);
+    },
+
+    hide: function (e) {
+      if (e) {
+        e.preventDefault();
       }
+      this.props.onStartHide(this.props.notificationId);
+    },
+
+    // many messages contain HTML, hence the need for dangerouslySetInnerHTML
+    getMsg: function () {
+      var msg = (this.props.escape) ? _.escape(this.props.msg) : this.props.msg;
+      return {
+        __html: msg
+      };
+    },
+
+    onAnimationComplete: function () {
+      if (this.props.isHiding) {
+        this.props.onHideComplete(this.props.notificationId);
+      }
+    },
+
+    render: function () {
+      var iconMap = {
+        error: 'fonticon-attention-circled',
+        info: 'fonticon-info-circled',
+        success: 'fonticon-ok-circled'
+      };
+
+      return (
+        <VelocityComponent animation={this.state.animation}
+          runOnMount={true} duration={this.props.slideInTime} complete={this.onAnimationComplete}>
+            <div className="notification-wrapper">
+              <div className={'global-notification alert alert-' + this.props.type} ref="notification">
+                <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>
+        </VelocityComponent>
+      );
+    }
+  });
+
+
+  var NotificationCenterButton = React.createClass({
+    getInitialState: function () {
+      return {
+        visible: true
+      };
+    },
+
+    hide: function () {
+      this.setState({ visible: false });
+    },
+
+    show: function () {
+      this.setState({ visible: true });
+    },
+
+    render: function () {
+      var classes = 'fonticon fonticon-bell' + ((!this.state.visible) ? ' hide' : '');
+      return (
+        <div className={classes} onClick={Actions.showNotificationCenter}></div>
+      );
+    }
+  });
+
+
+  var NotificationCenterPanel = React.createClass({
+    propTypes: {
+      visible: React.PropTypes.bool.isRequired,
+      filter: React.PropTypes.string.isRequired,
+      notifications: React.PropTypes.array.isRequired
     },
 
     getNotifications: function () {
-      if (!this.state.notifications.length) {
+      if (!this.props.notifications.length) {
         return (
           <li className="no-notifications">No notifications.</li>
         );
       }
 
-      return _.map(this.state.notifications, function (notification, i) {
+      return _.map(this.props.notifications, function (notification) {
         return (
-          <NotificationRow
-            isVisible={this.state.isVisible}
+          <NotificationPanelRow
+            isVisible={this.props.visible}
             item={notification}
-            filter={this.state.filter}
+            filter={this.props.filter}
             key={notification.notificationId}
           />
         );
@@ -102,7 +289,7 @@ function (app, FauxtonAPI, React, ReactDOM, Actions, Stores, Components) {
 
     render: function () {
       var panelClasses = 'notification-center-panel flex-layout flex-col';
-      if (this.state.isVisible) {
+      if (this.props.visible) {
         panelClasses += ' visible';
       }
 
@@ -112,39 +299,39 @@ function (app, FauxtonAPI, React, ReactDOM, Actions, Stores, Components) {
         error: 'flex-body',
         info: 'flex-body'
       };
-      filterClasses[this.state.filter] += ' selected';
+      filterClasses[this.props.filter] += ' selected';
 
-      var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
+      var maskClasses = 'notification-page-mask' + ((this.props.visible) ? ' visible' : '');
       return (
-        <div>
+        <div id="notification-center">
           <div className={panelClasses}>
 
             <header className="flex-layout flex-row">
-              <span className="fonticon fonticon-bell"></span>
+              <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>
+                  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"></span>
+                  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"></span>
+                  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"></span>
+                  onClick={Actions.selectNotificationFilter.bind(this, 'info')}>
+                <span className="fonticon fonticon-info-circled" />
               </li>
             </ul>
 
             <div className="flex-body">
               <ul className="notification-list">
-              {this.getNotifications()}
+                {this.getNotifications()}
               </ul>
             </div>
 
@@ -159,7 +346,8 @@ function (app, FauxtonAPI, React, ReactDOM, Actions, Stores, Components) {
     }
   });
 
-  var NotificationRow = React.createClass({
+
+  var NotificationPanelRow = React.createClass({
     propTypes: {
       item: React.PropTypes.object.isRequired,
       filter: React.PropTypes.string.isRequired,
@@ -252,12 +440,14 @@ function (app, FauxtonAPI, React, ReactDOM, Actions, Stores, Components) {
 
 
   return {
+    NotificationController: NotificationController,
     NotificationCenterButton: NotificationCenterButton,
     NotificationCenterPanel: NotificationCenterPanel,
-    NotificationRow: NotificationRow,
+    NotificationPanelRow: NotificationPanelRow,
+    Notification: Notification,
 
-    renderNotificationCenter: function (el) {
-      return ReactDOM.render(<NotificationCenterPanel />, el);
+    renderNotificationController: function (el) {
+      return ReactDOM.render(<NotificationController />, el);
     }
   };
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/notifications/stores.js
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/notifications/stores.js b/app/addons/fauxton/notifications/stores.js
index 028bb2f..08becfb 100644
--- a/app/addons/fauxton/notifications/stores.js
+++ b/app/addons/fauxton/notifications/stores.js
@@ -61,6 +61,18 @@ function (FauxtonAPI, app, ActionTypes, moment) {
       info.cleanMsg = app.utils.stripHTML(info.msg);
       info.time = moment();
 
+      // 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) {
+        this._notifications.forEach(function (notification) {
+          if (notification.visible) {
+            notification.isHiding = true;
+          }
+        });
+      }
       this._notifications.unshift(info);
     },
 
@@ -76,6 +88,25 @@ function (FauxtonAPI, app, ActionTypes, moment) {
       this._notifications = [];
     },
 
+    hideNotification: function (notificationId) {
+      var notification = _.findWhere(this._notifications, { notificationId: notificationId });
+      notification.visible = false;
+      notification.isHiding = false;
+    },
+
+    hideAllNotifications: function () {
+      this._notifications.forEach(function (notification) {
+        if (notification.visible) {
+          notification.isHiding = true;
+        }
+      });
+    },
+
+      startHidingNotification: function (notificationId) {
+      var notification = _.findWhere(this._notifications, { notificationId: notificationId });
+      notification.isHiding = true;
+    },
+
     getNotificationFilter: function () {
       return this._selectedNotificationFilter;
     },
@@ -104,6 +135,21 @@ function (FauxtonAPI, app, ActionTypes, moment) {
           this.clearNotification(action.options.notificationId);
         break;
 
+        case ActionTypes.START_HIDING_NOTIFICATION:
+          this.startHidingNotification(action.options.notificationId);
+          this.triggerChange();
+        break;
+
+        case ActionTypes.HIDE_NOTIFICATION:
+          this.hideNotification(action.options.notificationId);
+          this.triggerChange();
+        break;
+
+        case ActionTypes.HIDE_ALL_NOTIFICATIONS:
+          this.hideAllNotifications();
+          this.triggerChange();
+        break;
+
         case ActionTypes.SHOW_NOTIFICATION_CENTER:
           this._notificationCenterVisible = true;
           this.triggerChange();

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/notifications/tests/actionsSpec.jsx
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/notifications/tests/actionsSpec.jsx b/app/addons/fauxton/notifications/tests/actionsSpec.jsx
new file mode 100644
index 0000000..7121143
--- /dev/null
+++ b/app/addons/fauxton/notifications/tests/actionsSpec.jsx
@@ -0,0 +1,58 @@
+// 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.
+define([
+  '../../../../core/api',
+  '../notifications.react',
+  '../stores',
+  '../../../../../test/mocha/testUtils',
+  'react',
+  'react-dom',
+  'moment',
+  'react-addons-test-utils',
+  'sinon'
+], function (FauxtonAPI, Views, Stores, utils, React, ReactDOM, moment, TestUtils) {
+  var assert = utils.assert;
+  var store = Stores.notificationStore;
+
+
+  describe('NotificationPanel', function () {
+    var container;
+
+    beforeEach(function () {
+      container = document.createElement('div');
+      store.reset();
+    });
+
+    afterEach(function () {
+      ReactDOM.unmountComponentAtNode(container);
+    });
+
+    it('clear all action fires', function () {
+      var panelEl = TestUtils.renderIntoDocument(<Views.NotificationPanel />, container);
+
+      var stub = sinon.stub(Actions, 'clearAllNotifications');
+      TestUtils.Simulate.click($(ReactDOM.findDOMNode(panelEl)).find('footer input')[0]);
+      assert.ok(stub.calledOnce);
+      Actions.clearAllNotifications.restore();
+    });
+
+    it('switch filter action fires', function () {
+      var panelEl = TestUtils.renderIntoDocument(<Views.NotificationPanel />, container);
+
+      var stub = sinon.stub(Actions, 'clearAllNotifications');
+      TestUtils.Simulate.click($(ReactDOM.findDOMNode(panelEl)).find('footer input')[0]);
+      assert.ok(stub.calledOnce);
+      Actions.clearAllNotifications.restore();
+    });
+
+  });
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/notifications/tests/componentsSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/notifications/tests/componentsSpec.react.jsx b/app/addons/fauxton/notifications/tests/componentsSpec.react.jsx
index ab9f9a7..0f4d72a 100644
--- a/app/addons/fauxton/notifications/tests/componentsSpec.react.jsx
+++ b/app/addons/fauxton/notifications/tests/componentsSpec.react.jsx
@@ -13,17 +13,44 @@ define([
   '../../../../core/api',
   '../notifications.react',
   '../stores',
+  '../actions',
   '../../../../../test/mocha/testUtils',
   'react',
   'react-dom',
   'moment',
   'react-addons-test-utils',
   'sinon'
-], function (FauxtonAPI, Views, Stores, utils, React, ReactDOM, moment, TestUtils) {
+], function (FauxtonAPI, Views, Stores, Actions, utils, React, ReactDOM, moment, TestUtils) {
   var assert = utils.assert;
   var store = Stores.notificationStore;
 
-  describe('NotificationRow', function () {
+
+  describe('NotificationController', function () {
+    var container;
+
+    beforeEach(function () {
+      container = document.createElement('div');
+      store.reset();
+    });
+
+    afterEach(function () {
+      ReactDOM.unmountComponentAtNode(container);
+    });
+
+    it('notifications should be escaped by default', function () {
+      var component = TestUtils.renderIntoDocument(<Views.NotificationController />, container);
+      FauxtonAPI.addNotification({ msg: '<script>window.whatever=1;</script>' });
+      assert.ok(/&lt;script&gt;window.whatever=1;&lt;\/script&gt;/.test(ReactDOM.findDOMNode(component).innerHTML));
+    });
+
+    it('notifications should be able to render unescaped', function () {
+      var component = TestUtils.renderIntoDocument(<Views.NotificationController />, container);
+      FauxtonAPI.addNotification({ msg: '<script>window.whatever=1;</script>', escape: false });
+      assert.ok(/<script>window.whatever=1;<\/script>/.test(ReactDOM.findDOMNode(component).innerHTML));
+    });
+  });
+
+  describe('NotificationPanelRow', function () {
     var container;
 
     var notifications = {
@@ -57,21 +84,21 @@ define([
 
     it('shows all notification types when "all" filter applied', function () {
       var row1 = TestUtils.renderIntoDocument(
-        <Views.NotificationRow filter="all" item={notifications.success} />,
+        <Views.NotificationPanelRow filter="all" item={notifications.success}/>,
         container
       );
       assert.equal($(ReactDOM.findDOMNode(row1)).attr('aria-hidden'), 'false');
       ReactDOM.unmountComponentAtNode(container);
 
       var row2 = TestUtils.renderIntoDocument(
-        <Views.NotificationRow filter="all" item={notifications.error} />,
+        <Views.NotificationPanelRow filter="all" item={notifications.error}/>,
         container
       );
       assert.equal($(ReactDOM.findDOMNode(row2)).attr('aria-hidden'), 'false');
       ReactDOM.unmountComponentAtNode(container);
 
       var row3 = TestUtils.renderIntoDocument(
-        <Views.NotificationRow filter="all" item={notifications.info} />,
+        <Views.NotificationPanelRow filter="all" item={notifications.info}/>,
         container
       );
       assert.equal($(ReactDOM.findDOMNode(row3)).attr('aria-hidden'), 'false');
@@ -80,7 +107,7 @@ define([
 
     it('hides notification when filter doesn\'t match', function () {
       var rowEl = TestUtils.renderIntoDocument(
-        <Views.NotificationRow filter="success" item={notifications.info} />,
+        <Views.NotificationPanelRow filter="success" item={notifications.info}/>,
         container
       );
       assert.equal($(ReactDOM.findDOMNode(rowEl)).attr('aria-hidden'), 'true');
@@ -88,12 +115,11 @@ define([
 
     it('shows notification when filter exact match', function () {
       var rowEl = TestUtils.renderIntoDocument(
-        <Views.NotificationRow filter="info" item={notifications.info} />,
+        <Views.NotificationPanelRow filter="info" item={notifications.info}/>,
         container
       );
       assert.equal($(ReactDOM.findDOMNode(rowEl)).attr('aria-hidden'), 'false');
     });
-
   });
 
 
@@ -110,51 +136,56 @@ define([
     });
 
     it('shows all notifications by default', function () {
-      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 = TestUtils.renderIntoDocument(<Views.NotificationCenterPanel />, container);
+      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 = TestUtils.renderIntoDocument(
+        <Views.NotificationCenterPanel
+          filter="all"
+          notifications={store.getNotifications()}
+        />, container);
+
       assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 6);
     });
 
-    it('clicking on a filter icon filters applies appropriate filter', function () {
-      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' });
+    it('appropriate filters are applied - 1', function () {
+      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 = TestUtils.renderIntoDocument(<Views.NotificationCenterPanel />, container);
+      var panelEl = TestUtils.renderIntoDocument(
+        <Views.NotificationCenterPanel
+          filter="success"
+          notifications={store.getNotifications()}
+        />, container);
 
       // there are 2 success messages
-      TestUtils.Simulate.click($(ReactDOM.findDOMNode(panelEl)).find('.notification-filter li[data-filter="success"]')[0]);
       assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 2);
-
-      // 3 errors
-      TestUtils.Simulate.click($(ReactDOM.findDOMNode(panelEl)).find('.notification-filter li[data-filter="error"]')[0]);
-      assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 3);
-
-      // 1 info
-      TestUtils.Simulate.click($(ReactDOM.findDOMNode(panelEl)).find('.notification-filter li[data-filter="info"]')[0]);
-      assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 1);
     });
 
-    it('clear all clears all notifications', function () {
-      store.addNotification({ type: 'success', msg: 'Success are okay' });
-      store.addNotification({ type: 'info', msg: 'A single info message' });
-      store.addNotification({ type: 'error', msg: 'Error #2' });
-      store.addNotification({ type: 'error', msg: 'Error #3' });
+    it('appropriate filters are applied - 2', function () {
+      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 = TestUtils.renderIntoDocument(<Views.NotificationCenterPanel />, container);
-      assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 4);
-      TestUtils.Simulate.click($(ReactDOM.findDOMNode(panelEl)).find('footer input')[0]);
+      var panelEl = TestUtils.renderIntoDocument(
+        <Views.NotificationCenterPanel
+          filter="error"
+          notifications={store.getNotifications()}
+        />, container);
 
-      assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 0);
+      // 3 errors
+      assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 3);
     });
 
   });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/templates/notification.html
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/templates/notification.html b/app/addons/fauxton/templates/notification.html
deleted file mode 100644
index 84ac7ac..0000000
--- a/app/addons/fauxton/templates/notification.html
+++ /dev/null
@@ -1,19 +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.
-*/%>
-
-<div class="global-notification alert alert-<%- type %>">
-  <a data-bypass href="#" class="js-dismiss"><i class="pull-right fonticon-cancel"></i></a>
-  <i class="notification-icon <%- icon %>"></i>
-  <%= htmlToRender %><!-- every caller has to escape on it's own -->
-</div>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/tests/baseSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/tests/baseSpec.js b/app/addons/fauxton/tests/baseSpec.js
index 55d0dca..3e4638f 100644
--- a/app/addons/fauxton/tests/baseSpec.js
+++ b/app/addons/fauxton/tests/baseSpec.js
@@ -66,8 +66,6 @@ define([
           setView: function () {}
         }
       };
-
-
     });
 
     after(function () {
@@ -85,66 +83,6 @@ define([
       testRouteObject.renderWith('the-route', mockLayout, 'args');
       assert.ok(setViewCalled, 'Set Breadcrumbs was called');
     });
-
   });
 
-  describe('Fauxton Notifications', function () {
-
-    it('should escape by default', function () {
-      window.fauxton_xss_test_escaped = true;
-      var view = FauxtonAPI.addNotification({
-        msg: '<script>window.fauxton_xss_test_escaped = false;</script>',
-        selector: 'body'
-      });
-      view.$el.remove();
-      assert.ok(window.fauxton_xss_test_escaped);
-      delete window.fauxton_xss_test_escaped;
-    });
-
-    it('should be able to render unescaped', function (done) {
-      var view = FauxtonAPI.addNotification({
-        msg: '<script>window.fauxton_xss_test_unescaped = true;</script>',
-        selector: 'body',
-        escape: false
-      });
-
-      view.promise().then(function () {
-        view.$el.remove();
-        assert.ok(window.fauxton_xss_test_unescaped);
-        delete window.fauxton_xss_test_unescaped;
-        done();
-      });
-    });
-
-    it('should render escaped if the escape value is not explicitly false, ' +
-    'e.g. was forgotten in a direct call', function () {
-
-      window.fauxton_xss_test2_escaped = true;
-      var view = new Base.Notification({
-        msg: '<script>window.fauxton_xss_test2_escaped = false;</script>',
-        selector: 'body'
-      }).render();
-
-      view.$el.remove();
-      assert.ok(window.fauxton_xss_test2_escaped);
-      delete window.fauxton_xss_test2_escaped;
-    });
-
-    it('should close notification when ESCAPE key used', function () {
-      var notification = FauxtonAPI.addNotification({
-        msg: 'Close me!',
-        selector: 'body'
-      });
-      var removeWithAnimationSpy = sinon.spy(notification, 'removeWithAnimation');
-
-      notification.render();
-
-      // manually trigger an ESCAPE key click
-      $(document).trigger($.Event("keydown", { keyCode: 27 }));
-
-      // confirm the remove method has now been called
-      assert.ok(removeWithAnimationSpy.calledOnce);
-    });
-
-  });
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/assets/index.underscore
----------------------------------------------------------------------
diff --git a/assets/index.underscore b/assets/index.underscore
index c179ec4..782ca96 100644
--- a/assets/index.underscore
+++ b/assets/index.underscore
@@ -29,8 +29,7 @@
 
 <body id="home">
 
-  <div id="global-notifications" class="container errors-container"></div>
-  <div id="notification-center"></div>
+  <div id="notifications"></div>
 
   <!-- Main container. -->
   <div role="main" id="main">

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/assets/less/fauxton.less
----------------------------------------------------------------------
diff --git a/assets/less/fauxton.less b/assets/less/fauxton.less
index f6dae31..5ed50d9 100644
--- a/assets/less/fauxton.less
+++ b/assets/less/fauxton.less
@@ -184,7 +184,8 @@ table.databases {
     text-shadow: none;
     .box-shadow(0px 4px 0px rgba(0,0,0,0.45));
     .border-radius(0);
-    border-bottom: 1px solid #3a2c2b;�
+    border-bottom: 1px solid #3a2c2b;
+    box-shadow: 0 4px 0 0 rgba(0, 0, 0, 0.4);
     a, a:hover {
       color: #cecece;
       text-decoration: underline;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/assets/less/templates.less
----------------------------------------------------------------------
diff --git a/assets/less/templates.less b/assets/less/templates.less
index 172de0f..ef1aca5 100644
--- a/assets/less/templates.less
+++ b/assets/less/templates.less
@@ -26,7 +26,7 @@
 
 #global-notifications {
   position: fixed;
-  top: 0px;
+  top: 0;
   display: block;
   z-index: 100000;
   width: 100%;
@@ -513,3 +513,8 @@ with_tabs_sidebar.html
     }
   }
 }
+
+.notification-wrapper {
+  opacity: 0;
+  height: 0;
+}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/package.json
----------------------------------------------------------------------
diff --git a/package.json b/package.json
index 05a8310..9265a0d 100644
--- a/package.json
+++ b/package.json
@@ -89,6 +89,7 @@
     "urls": "~0.0.3",
     "velocity-animate": "^1.2.3",
     "visualizeRevTree": "git+https://github.com/neojski/visualizeRevTree.git#gh-pages",
+    "velocity-react": "^1.1.4",
     "webpack": "^1.12.12",
     "webpack-dev-server": "^1.14.1",
     "zeroclipboard": "^2.2.0"

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/test/nightwatch_tests/custom-commands/closeNotification.js
----------------------------------------------------------------------
diff --git a/test/nightwatch_tests/custom-commands/closeNotification.js b/test/nightwatch_tests/custom-commands/closeNotification.js
index 04120bc..5c29d56 100644
--- a/test/nightwatch_tests/custom-commands/closeNotification.js
+++ b/test/nightwatch_tests/custom-commands/closeNotification.js
@@ -14,7 +14,7 @@ var helpers = require('../helpers/helpers.js');
 
 exports.command = function () {
   var client = this,
-      dismissSelector = '#global-notifications .js-dismiss';
+      dismissSelector = '#global-notifications .fonticon-cancel';
 
   client
     .waitForElementPresent(dismissSelector, helpers.maxWaitTime, false)