couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From benk...@apache.org
Subject fauxton commit: updated refs/heads/master to b3e5b44
Date Wed, 11 Mar 2015 23:16:13 GMT
Repository: couchdb-fauxton
Updated Branches:
  refs/heads/master b51fec6d3 -> b3e5b44e2


Porting over Changes page content to React

This also fixes COUCHDB-2216 by limiting the max number of
changes that can be output at one time to 1000.


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

Branch: refs/heads/master
Commit: b3e5b44e278c6aa750eb21c1b2f1025ede83c9a8
Parents: b51fec6
Author: Ben Keen <ben.keen@gmail.com>
Authored: Fri Feb 27 11:25:10 2015 -0800
Committer: Ben Keen <ben.keen@gmail.com>
Committed: Wed Mar 11 16:17:19 2015 -0700

----------------------------------------------------------------------
 app/addons/databases/resources.js               |   4 +
 app/addons/documents/assets/less/changes.less   |  17 +-
 app/addons/documents/changes/actions.js         |  11 +-
 app/addons/documents/changes/actiontypes.js     |   6 +-
 .../documents/changes/components.react.jsx      | 204 ++++++++++++++++---
 app/addons/documents/changes/stores.js          |  97 ++++++---
 app/addons/documents/routes-documents.js        |  14 +-
 app/addons/documents/templates/changes.html     |  69 -------
 .../tests/changes.componentsSpec.react.jsx      | 195 +++++++++++++++++-
 .../documents/tests/changes.storesSpec.js       |  74 ++++---
 .../documents/tests/nightwatch/changes.js       |   2 +-
 .../documents/tests/nightwatch/changesFilter.js |  48 +++++
 .../tests/nightwatch/previousFromView.js        |   2 +-
 app/addons/documents/tests/views-changesSpec.js |  94 ---------
 app/addons/documents/views-changes.js           |  89 ++------
 app/addons/fauxton/components.react.jsx         |  76 ++++++-
 assets/less/animations.less                     |  19 ++
 17 files changed, 673 insertions(+), 348 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/databases/resources.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/resources.js b/app/addons/databases/resources.js
index ba883b6..17113d4 100644
--- a/app/addons/databases/resources.js
+++ b/app/addons/databases/resources.js
@@ -77,6 +77,10 @@ function(app, FauxtonAPI, Documents) {
       return app.utils.safeURLName(this.id);
     },
     buildChanges: function (params) {
+      if (!params.limit) {
+        params.limit = 100;
+      }
+
       this.changes = new Databases.Changes({
         database: this,
         params: params

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/assets/less/changes.less
----------------------------------------------------------------------
diff --git a/app/addons/documents/assets/less/changes.less b/app/addons/documents/assets/less/changes.less
index b6cde72..0e98ecd 100644
--- a/app/addons/documents/assets/less/changes.less
+++ b/app/addons/documents/assets/less/changes.less
@@ -86,12 +86,23 @@
   height: 0px;
 });
 
-
-.toggleChangesFilter-enter {
+.toggle-changes-filter-enter {
   .animation(slideDownChangesFilter 1s both);
 }
 
-.toggleChangesFilter-leave {
+.toggle-changes-filter-leave {
   .animation(slideUpChangesFilter 1s both);
 }
 
+.toggle-changes-code-enter {
+  .animation(slideDown .6s both);
+}
+
+.toggle-changes-code-leave {
+  .animation(slideUp .6s both);
+}
+
+.changes-result-limit {
+  margin-left: 20px;
+}
+

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/changes/actions.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/changes/actions.js b/app/addons/documents/changes/actions.js
index bcc6002..a62a6fe 100644
--- a/app/addons/documents/changes/actions.js
+++ b/app/addons/documents/changes/actions.js
@@ -29,9 +29,6 @@ function (app, FauxtonAPI, ActionTypes) {
         type: ActionTypes.ADD_CHANGES_FILTER_ITEM,
         filter: filter
       });
-
-      // TODO for backward compatibility. Remove later.
-      FauxtonAPI.triggerRouteEvent('changesFilterAdd', filter);
     },
 
     removeFilter: function (filter) {
@@ -39,9 +36,13 @@ function (app, FauxtonAPI, ActionTypes) {
         type: ActionTypes.REMOVE_CHANGES_FILTER_ITEM,
         filter: filter
       });
+    },
 
-      // TODO for backward compatibility. Remove later.
-      FauxtonAPI.triggerRouteEvent('changesFilterRemove', filter);
+    setChanges: function (options) {
+      FauxtonAPI.dispatch({
+        type: ActionTypes.SET_CHANGES,
+        options: options
+      });
     }
   };
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/changes/actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/changes/actiontypes.js b/app/addons/documents/changes/actiontypes.js
index c76b844..7f91ae6 100644
--- a/app/addons/documents/changes/actiontypes.js
+++ b/app/addons/documents/changes/actiontypes.js
@@ -10,11 +10,13 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-define([], function() {
+define([], function () {
   return {
+    SET_CHANGES: 'SET_CHANGES',
     TOGGLE_CHANGES_TAB_VISIBILITY: 'TOGGLE_CHANGES_TAB_VISIBILITY',
     ADD_CHANGES_FILTER_ITEM: 'ADD_CHANGES_FILTER_ITEM',
     REMOVE_CHANGES_FILTER_ITEM: 'REMOVE_CHANGES_FILTER_ITEM',
-    UPDATE_CHANGES_FILTER: 'UPDATE_CHANGES_FILTER'
+    UPDATE_CHANGES_FILTER: 'UPDATE_CHANGES_FILTER',
+    TOGGLE_CHANGES_CODE_VISIBILITY: 'TOGGLE_CHANGES_CODE_VISIBILITY'
   };
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/changes/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/changes/components.react.jsx b/app/addons/documents/changes/components.react.jsx
index 977319e..019f3bf 100644
--- a/app/addons/documents/changes/components.react.jsx
+++ b/app/addons/documents/changes/components.react.jsx
@@ -11,34 +11,40 @@
 // the License.
 
 define([
+  'app',
+  'api',
   'react',
   'addons/documents/changes/actions',
-  'addons/documents/changes/stores'
-], function (React, Actions, Stores) {
+  'addons/documents/changes/stores',
+  'addons/fauxton/components.react',
+  'plugins/prettify'
+], function (app, FauxtonAPI, React, Actions, Stores, Components) {
 
+  var changesStore = Stores.changesStore;
   var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
 
-
-  // the top-level component for the Changes Filter section. Handles hiding/showing
-  var ChangesHeader = React.createClass({
+  // the top-level component for the Changes Filter section. Handles hiding/showing of the filters form
+  var ChangesHeaderController = React.createClass({
     getInitialState: function () {
+      return this.getStoreState();
+    },
+
+    getStoreState: function () {
       return {
-        showTabContent: Stores.changesHeaderStore.isTabVisible()
+        showTabContent: changesStore.isTabVisible()
       };
     },
 
     onChange: function () {
-      this.setState({
-        showTabContent: Stores.changesHeaderStore.isTabVisible()
-      });
+      this.setState(this.getStoreState());
     },
 
     componentDidMount: function () {
-      Stores.changesHeaderStore.on('change', this.onChange, this);
+      changesStore.on('change', this.onChange, this);
     },
 
     componentWillUnmount: function () {
-      Stores.changesHeaderStore.off('change', this.onChange);
+      changesStore.off('change', this.onChange);
     },
 
     toggleFilterSection: function () {
@@ -54,7 +60,7 @@ define([
       return (
         <div className="changes-header-section">
           <ChangesHeaderTab onToggle={this.toggleFilterSection} />
-          <ReactCSSTransitionGroup transitionName="toggleChangesFilter" component="div" className="changes-tab-content">
+          <ReactCSSTransitionGroup transitionName="toggle-changes-filter" component="div" className="changes-tab-content">
             {tabContent}
           </ReactCSSTransitionGroup>
         </div>
@@ -87,7 +93,7 @@ define([
   var ChangesFilter = React.createClass({
     getStoreState: function () {
       return {
-        filters: Stores.changesFilterStore.getFilters()
+        filters: changesStore.getFilters()
       };
     },
 
@@ -96,11 +102,11 @@ define([
     },
 
     componentDidMount: function () {
-      Stores.changesFilterStore.on('change', this.onChange, this);
+      changesStore.on('change', this.onChange, this);
     },
 
     componentWillUnmount: function () {
-      Stores.changesFilterStore.off('change', this.onChange);
+      changesStore.off('change', this.onChange);
     },
 
     getInitialState: function () {
@@ -125,19 +131,17 @@ define([
     },
 
     hasFilter: function (filter) {
-      return Stores.changesFilterStore.hasFilter(filter);
+      return changesStore.hasFilter(filter);
     },
 
     render: function () {
-      var filters = this.getFilters();
-
       return (
         <div className="tab-content">
           <div className="tab-pane active" ref="filterTab">
             <div className="changes-header js-filter">
               <AddFilterForm tooltip={this.props.tooltip} filter={this.state.filter} addFilter={this.addFilter}
                 hasFilter={this.hasFilter} />
-              <ul className="filter-list">{filters}</ul>
+              <ul className="filter-list">{this.getFilters()}</ul>
             </div>
           </div>
         </div>
@@ -268,18 +272,172 @@ define([
   });
 
 
+  var ChangesController = React.createClass({
+    getInitialState: function () {
+      return this.getStoreState();
+    },
+
+    getStoreState: function () {
+      return {
+        changes: changesStore.getChanges(),
+        databaseName: changesStore.getDatabaseName(),
+        isShowingSubset: changesStore.isShowingSubset()
+      };
+    },
+
+    onChange: function () {
+      this.setState(this.getStoreState());
+    },
+
+    componentDidMount: function () {
+      changesStore.on('change', this.onChange, this);
+    },
+
+    componentWillUnmount: function () {
+      changesStore.off('change', this.onChange);
+    },
+
+    showingSubsetMsg: function () {
+      var msg = '';
+      if (this.state.isShowingSubset) {
+        var numChanges = this.state.changes.length;
+        msg = <p className="changes-result-limit">Limiting results to latest <b>{numChanges}</b> changes.</p>;
+      }
+      return msg;
+    },
+
+    getRows: function () {
+      return _.map(this.state.changes, function (change) {
+        return <ChangeRow change={change} key={change.id} databaseName={this.state.databaseName} />;
+      }, this);
+    },
+
+    render: function () {
+      return (
+        <div className="js-changes-view">
+          {this.showingSubsetMsg()}
+          {this.getRows()}
+        </div>
+      );
+    }
+  });
+
+
+  var ChangeRow = React.createClass({
+    propTypes: function () {
+      return {
+        change: React.PropTypes.object,
+        databaseName: React.PropTypes.string.isRequired
+      };
+    },
+
+    getInitialState: function () {
+      return {
+        codeVisible: false
+      };
+    },
+
+    toggleJSON: function (e) {
+      e.preventDefault();
+      this.setState({ codeVisible: !this.state.codeVisible });
+    },
+
+    getChangesCode: function () {
+      var json = '';
+      if (this.state.codeVisible) {
+        json = <Components.CodeFormat key="changesCodeSection" code={this.getChangeCode()} />;
+      }
+      return json;
+    },
+
+    getChangeCode: function () {
+      return {
+        changes: this.props.change.changes,
+        doc: this.props.change.doc
+      };
+    },
+
+    render: function () {
+      var jsonBtnClasses = "btn btn-small " + (this.state.codeVisible ? 'btn-secondary' : 'btn-primary');
+
+      return (
+        <div className="change-wrapper">
+          <div className="change-box" data-id={this.props.change.id}>
+            <div className="row-fluid">
+              <div className="span2">seq</div>
+              <div className="span8">{this.props.change.seq}</div>
+              <div className="span2 text-right">
+                <Components.Clipboard text={this.props.change.seq} />
+              </div>
+            </div>
+
+            <div className="row-fluid">
+              <div className="span2">id</div>
+              <div className="span8">
+                <ChangeID id={this.props.change.id} deleted={this.props.change.deleted} databaseName={this.props.databaseName} />
+              </div>
+              <div className="span2 text-right">
+                <Components.Clipboard text={this.props.change.id} />
+              </div>
+            </div>
+
+            <div className="row-fluid">
+              <div className="span2">changes</div>
+              <div className="span10">
+                <button type="button" className={jsonBtnClasses} onClick={this.toggleJSON}>
+                  {this.state.codeVisible ? 'Close JSON' : 'View JSON'}
+                </button>
+              </div>
+            </div>
+
+            <ReactCSSTransitionGroup transitionName="toggle-changes-code" component="div" className="changesCodeSectionWrapper">
+              {this.getChangesCode()}
+            </ReactCSSTransitionGroup>
+
+            <div className="row-fluid">
+              <div className="span2">deleted</div>
+              <div className="span10">{this.props.change.deleted ? 'True' : 'False'}</div>
+            </div>
+          </div>
+        </div>
+      );
+    }
+  });
+
+
+  var ChangeID = React.createClass({
+    render: function () {
+      if (this.props.deleted) {
+        return (
+          <span className="js-doc-id">{this.props.id}</span>
+        );
+      } else {
+        var link = FauxtonAPI.urls('document', 'app', this.props.databaseName, this.props.id);
+        return (
+          <a href={link} className="js-doc-link">{this.props.id}</a>
+        );
+      }
+    }
+  });
+
+
   return {
     renderHeader: function (el) {
-      React.render(<ChangesHeader />, el);
+      React.render(<ChangesHeaderController />, el);
+    },
+    renderChanges: function (el) {
+      React.render(<ChangesController />, el);
     },
-    removeHeader: function (el) {
+    remove: function (el) {
       React.unmountComponentAtNode(el);
     },
 
     // exposed for testing purposes only
-    ChangesHeader: ChangesHeader,
+    ChangesHeaderController: ChangesHeaderController,
     ChangesHeaderTab: ChangesHeaderTab,
-    ChangesFilter: ChangesFilter
+    ChangesFilter: ChangesFilter,
+    ChangesController: ChangesController,
+    ChangeRow: ChangeRow
   };
 
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/changes/stores.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/changes/stores.js b/app/addons/documents/changes/stores.js
index e36b392..2e8f279 100644
--- a/app/addons/documents/changes/stores.js
+++ b/app/addons/documents/changes/stores.js
@@ -15,51 +15,62 @@ define([
   'addons/documents/changes/actiontypes'
 ], function (FauxtonAPI, ActionTypes) {
 
-  var Stores = {};
-
 
-  // tracks the state of the header (open/closed)
-  var ChangesHeaderStore = FauxtonAPI.Store.extend({
+  var ChangesStore = FauxtonAPI.Store.extend({
     initialize: function () {
       this.reset();
     },
 
     reset: function () {
       this._tabVisible = false;
+      this._filters = [];
+      this._changes = [];
+      this._databaseName = '';
+      this._maxChangesListed = 100;
+      this._showingSubset = false;
     },
 
-    toggleTabVisibility: function () {
-      this._tabVisible = !this._tabVisible;
+    setChanges: function (options) {
+      this._filters = options.filters;
+      this._databaseName = options.databaseName;
+      this._changes = _.map(options.changes.models, function (change) {
+        return {
+          id: change.get('id'),
+          seq: change.get('seq'),
+          deleted: change.get('deleted') ? change.get('deleted') : false,
+          changes: change.get('changes'),
+          doc: change.get('doc') // only populated with ?include_docs=true
+        };
+      });
     },
 
-    isTabVisible: function () {
-      return this._tabVisible;
+    getChanges: function () {
+      this._showingSubset = false;
+      var numMatches = 0;
+
+      return _.filter(this._changes, function (change) {
+        if (numMatches >= this._maxChangesListed) {
+          this._showingSubset = true;
+          return false;
+        }
+        var changeStr = JSON.stringify(change);
+        var match = _.every(this._filters, function (filter) {
+          return new RegExp(filter, 'i').test(changeStr);
+        });
+
+        if (match) {
+          numMatches++;
+        }
+        return match;
+      }, this);
     },
 
-    dispatch: function (action) {
-
-      // can I use an if-statement for a single item?
-      switch (action.type) {
-        case ActionTypes.TOGGLE_CHANGES_TAB_VISIBILITY:
-          this.toggleTabVisibility();
-          this.triggerChange();
-          break;
-      }
-    }
-  });
-
-  Stores.changesHeaderStore = new ChangesHeaderStore();
-  Stores.changesHeaderStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.changesHeaderStore.dispatch);
-
-
-  // tracks the list of filters
-  var ChangesFilterStore = FauxtonAPI.Store.extend({
-    initialize: function () {
-      this.reset();
+    toggleTabVisibility: function () {
+      this._tabVisible = !this._tabVisible;
     },
 
-    reset: function () {
-      this._filters = [];
+    isTabVisible: function () {
+      return this._tabVisible;
     },
 
     addFilter: function (filter) {
@@ -78,8 +89,29 @@ define([
       return _.contains(this._filters, filter);
     },
 
+    getDatabaseName: function () {
+      return this._databaseName;
+    },
+
+    isShowingSubset: function () {
+      return this._showingSubset;
+    },
+
+    // added to speed up the tests
+    setMaxChanges: function (num) {
+      this._maxChangesListed = num;
+    },
+
     dispatch: function (action) {
       switch (action.type) {
+        case ActionTypes.SET_CHANGES:
+          this.setChanges(action.options);
+          this.triggerChange();
+          break;
+        case ActionTypes.TOGGLE_CHANGES_TAB_VISIBILITY:
+          this.toggleTabVisibility();
+          this.triggerChange();
+          break;
         case ActionTypes.ADD_CHANGES_FILTER_ITEM:
           this.addFilter(action.filter);
           this.triggerChange();
@@ -92,9 +124,10 @@ define([
     }
   });
 
-  Stores.changesFilterStore = new ChangesFilterStore();
-  Stores.changesFilterStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.changesFilterStore.dispatch);
 
+  var Stores = {};
+  Stores.changesStore = new ChangesStore();
+  Stores.changesStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.changesStore.dispatch);
 
   return Stores;
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/routes-documents.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/routes-documents.js b/app/addons/documents/routes-documents.js
index c8b35b0..0131adc 100644
--- a/app/addons/documents/routes-documents.js
+++ b/app/addons/documents/routes-documents.js
@@ -48,8 +48,6 @@ function(app, FauxtonAPI, BaseRoute, Documents, Changes, Index, DocEditor,
 
     events: {
       "route:reloadDesignDocs": "reloadDesignDocs",
-      "route:changesFilterAdd": "addFilter",
-      "route:changesFilterRemove": "removeFilter",
       'route:updateAllDocs': 'updateAllDocsFromView',
       'route:paginate': 'paginate',
       'route:perPageChange': 'perPageChange',
@@ -194,7 +192,7 @@ function(app, FauxtonAPI, BaseRoute, Documents, Changes, Index, DocEditor,
       var docParams = app.getParams();
       this.database.buildChanges(docParams);
 
-      this.changesView = this.setView("#dashboard-lower-content", new Changes.Changes({
+      this.changesView = this.setView("#dashboard-lower-content", new Changes.ChangesReactWrapper({
         model: this.database
       }));
 
@@ -214,16 +212,6 @@ function(app, FauxtonAPI, BaseRoute, Documents, Changes, Index, DocEditor,
       };
     },
 
-    addFilter: function (filter) {
-      this.changesView.filters.push(filter);
-      this.changesView.render();
-    },
-
-    removeFilter: function (filter) {
-      this.changesView.filters.splice(this.changesView.filters.indexOf(filter), 1);
-      this.changesView.render();
-    },
-
     cleanup: function () {
       if (this.reactHeader) {
         this.removeView('#react-headerbar');

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/templates/changes.html
----------------------------------------------------------------------
diff --git a/app/addons/documents/templates/changes.html b/app/addons/documents/templates/changes.html
deleted file mode 100644
index e06e34b..0000000
--- a/app/addons/documents/templates/changes.html
+++ /dev/null
@@ -1,69 +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="changes-view">
-  <% _.each(changes, function (change) { %>
-    <div class="change-wrapper">
-      <div class="change-box">
-        <div class="row-fluid">
-          <div class="span2">
-            seq
-          </div>
-          <div class="span8 change-sequence">
-            <%- change.seq %>
-          </div>
-          <div class="span2 text-right">
-            <a class="js-copy" data-clipboard-text="<%- change.seq %>" data-bypass="true" href="#">
-              <i class="fonticon-clipboard"></i>
-            </a>
-          </div>
-        </div>
-        <div class="row-fluid">
-          <div class="span2">
-            id
-          </div>
-          <div class="span8">
-            <% if (change.deleted) { %>
-              <%- change.id %>
-            <% } else { %>
-              <a href="#<%- href(database.id, change.id) %>"><%- change.id %></a>
-            <% } %>    </div>
-          <div class="span2 text-right">
-            <a class="js-copy" data-clipboard-text="<%- change.id %>" data-bypass="true" href="#">
-              <i class="fonticon-clipboard"></i>
-            </a>
-          </div>
-        </div>
-        <div class="row-fluid">
-          <div class="span2">
-            changes
-          </div>
-          <div class="span10">
-            <button class="js-toggle-json btn btn-small btn-primary" type="button">View JSON</button>
-          </div>
-        </div>
-        <div class="js-json-container">
-          <pre class="prettyprint"><%- JSON.stringify({changes: change.changes, doc: change.doc}, null, " ") %></pre>
-        </div>
-        <div class="row-fluid">
-          <div class="span2">
-            deleted
-          </div>
-          <div class="span10">
-            <%- change.deleted ? "True" : "False" %>
-          </div>
-        </div>
-      </div>
-    </div>
-  <% }); %>
-</div>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/tests/changes.componentsSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/changes.componentsSpec.react.jsx b/app/addons/documents/tests/changes.componentsSpec.react.jsx
index 4f3dce8..ecfb8bb 100644
--- a/app/addons/documents/tests/changes.componentsSpec.react.jsx
+++ b/app/addons/documents/tests/changes.componentsSpec.react.jsx
@@ -25,6 +25,15 @@ define([
   var TestUtils = React.addons.TestUtils;
 
 
+  // suppresses unwanted console.log()'s on missing URLs
+  FauxtonAPI.registerUrls('document', {
+    server: function (database, doc) { return app.host + '/' + database + '/' + doc; },
+    app: function (database, doc) { return '/database/' + database + '/' + doc; },
+    apiurl: function (database, doc) { return window.location.origin + '/' + database + '/' + doc; },
+    'web-index': function (database, doc) { return '/database/' + database + '/' + doc; }
+  });
+
+
   describe('ChangesHeader', function () {
     var container, tab, spy;
 
@@ -32,11 +41,11 @@ define([
       beforeEach(function () {
         spy = sinon.spy(Actions, 'toggleTabVisibility');
         container = document.createElement('div');
-        tab = TestUtils.renderIntoDocument(<Changes.ChangesHeader />, container);
+        tab = TestUtils.renderIntoDocument(<Changes.ChangesHeaderController />, container);
       });
 
       afterEach(function () {
-        Stores.changesFilterStore.reset();
+        Stores.changesStore.reset();
         React.unmountComponentAtNode(container);
       });
 
@@ -58,7 +67,7 @@ define([
     });
 
     afterEach(function () {
-      Stores.changesFilterStore.reset();
+      Stores.changesStore.reset();
       React.unmountComponentAtNode(container);
     });
 
@@ -78,7 +87,7 @@ define([
     });
 
     afterEach(function () {
-      Stores.changesFilterStore.reset();
+      Stores.changesStore.reset();
       React.unmountComponentAtNode(container);
     });
 
@@ -179,7 +188,185 @@ define([
 
       assert.equal(1, $el.find('.js-remove-filter').length);
     });
+  });
+
+
+  // tests Changes Controller; includes tests in conjunction with ChangesHeaderController
+  describe('ChangesController', function () {
+    var containerEl, headerEl, $headerEl, changesEl, $changesEl;
+
+    var changesCollection = new Backbone.Collection([
+      { id: 'doc_1', seq: 4, deleted: false, changes: { code: 'here' } },
+      { id: 'doc_2', seq: 1, deleted: false, changes: { code: 'here' } },
+      { id: 'doc_3', seq: 6, deleted: true, changes: { code: 'here' } },
+      { id: 'doc_4', seq: 7, deleted: false, changes: { code: 'here' } },
+      { id: 'doc_5', seq: 1, deleted: true, changes: { code: 'here' } }
+    ]);
+
+    beforeEach(function () {
+      Actions.setChanges({
+        changes: changesCollection,
+        filters: [],
+        databaseName: 'testDatabase'
+      });
+      headerEl  = TestUtils.renderIntoDocument(<Changes.ChangesHeaderController />, containerEl);
+      $headerEl = $(headerEl.getDOMNode());
+      changesEl = TestUtils.renderIntoDocument(<Changes.ChangesController />, containerEl);
+      $changesEl = $(changesEl.getDOMNode());
+    });
+
+    afterEach(function () {
+      Stores.changesStore.reset();
+      React.unmountComponentAtNode(containerEl);
+    });
+
+
+    it('should list the right number of changes', function () {
+      assert.equal(changesCollection.length, $changesEl.find('.change-box').length);
+    });
+
+
+    it('"false"/"true" filter strings should apply to change deleted status', function () {
+      // expand the header
+      TestUtils.Simulate.click($headerEl.find('a')[0]);
+
+      // add a filter
+      var addItemField = $headerEl.find('.js-changes-filter-field')[0];
+      var submitBtn = $headerEl.find('[type="submit"]')[0];
+      addItemField.value = 'true';
+      TestUtils.Simulate.change(addItemField);
+      TestUtils.Simulate.submit(submitBtn);
+
+      // confirm only the two deleted items shows up and the IDs maps to the deleted rows
+      assert.equal(2, $changesEl.find('.change-box').length);
+      assert.equal('doc_3', $($changesEl.find('.js-doc-id').get(0)).html());
+      assert.equal('doc_5', $($changesEl.find('.js-doc-id').get(1)).html());
+    });
+
+
+    it('confirms that a filter affects the actual search results', function () {
+      // expand the header
+      TestUtils.Simulate.click($headerEl.find('a')[0]);
+
+      // add a filter
+      var addItemField = $headerEl.find('.js-changes-filter-field')[0];
+      var submitBtn = $headerEl.find('[type="submit"]')[0];
+      addItemField.value = '6'; // should match doc_3's sequence ID
+      TestUtils.Simulate.change(addItemField);
+      TestUtils.Simulate.submit(submitBtn);
+
+      // confirm only one item shows up and the ID maps to what we'd expect
+      assert.equal(1, $changesEl.find('.change-box').length);
+      assert.equal('doc_3', $($changesEl.find('.js-doc-id').get(0)).html());
+    });
+
+
+    // confirms that if there are multiple filters, ALL are applied to return the subset of results that match
+    // all filters
+    it('multiple filters should all be applied to results', function () {
+      TestUtils.Simulate.click($headerEl.find('a')[0]);
+
+      // add the filters
+      var addItemField = $headerEl.find('.js-changes-filter-field')[0];
+      var submitBtn = $headerEl.find('[type="submit"]')[0];
+
+      // *** should match doc_1, doc_2 and doc_5
+      addItemField.value = '1';
+      TestUtils.Simulate.change(addItemField);
+      TestUtils.Simulate.submit(submitBtn);
+
+      // *** should match doc_3 and doc_5
+      addItemField.value = 'true';
+      TestUtils.Simulate.change(addItemField);
+      TestUtils.Simulate.submit(submitBtn);
+
+      // confirm only one item shows up and that it's doc_5
+      assert.equal(1, $changesEl.find('.change-box').length);
+      assert.equal('doc_5', $($changesEl.find('.js-doc-id').get(0)).html());
+    });
+  });
+
+
+  describe('ChangesController max results', function () {
+    var containerEl, changesEl;
+    var maxChanges = 10;
+
+    beforeEach(function () {
+
+      // to keep the test speedy, override the default value (1000)
+      Stores.changesStore.setMaxChanges(maxChanges);
+
+      var changes = [];
+      _.times(maxChanges + 10, function (i) {
+        changes.push(new Backbone.Model({ id: 'doc_' + i, seq: 1, changes: { code: 'here' } }));
+      });
+      var changesCollection = new Backbone.Collection(changes);
+
+      Actions.setChanges({
+        changes: changesCollection,
+        filters: [],
+        databaseName: 'test'
+      });
+      changesEl = TestUtils.renderIntoDocument(<Changes.ChangesController />, containerEl);
+    });
+
+    afterEach(function () {
+      Stores.changesStore.reset();
+      React.unmountComponentAtNode(containerEl);
+    });
+
+    it('should truncate the number of results with very large # of changes', function () {
+      // check there's no more than maxChanges results
+      assert.equal(maxChanges, $(changesEl.getDOMNode()).find('.change-box').length);
+    });
+
+    it('should show a message if the results are truncated', function () {
+      assert.equal(1, $(changesEl.getDOMNode()).find('.changes-result-limit').length);
+    });
 
   });
 
+
+  describe('ChangeRow', function () {
+    var container;
+    var change = {
+      id: '123',
+      seq: 5,
+      deleted: false,
+      changes: { code: 'here' }
+    };
+
+    beforeEach(function () {
+      container = document.createElement('div');
+    });
+
+    afterEach(function () {
+      React.unmountComponentAtNode(container);
+    });
+
+
+    it('clicking the toggle-json button shows the code section', function () {
+      var changeRow = TestUtils.renderIntoDocument(<Changes.ChangeRow change={change} databaseName="testDatabase" />, container);
+
+      // confirm it's hidden by default
+      assert.equal(0, $(changeRow.getDOMNode()).find('.prettyprint').length);
+
+      // confirm clicking it shows the element
+      TestUtils.Simulate.click($(changeRow.getDOMNode()).find('button.btn')[0]);
+      assert.equal(1, $(changeRow.getDOMNode()).find('.prettyprint').length);
+    });
+
+    it('deleted docs should not be clickable', function () {
+      change.deleted = true;
+      var changeRow = TestUtils.renderIntoDocument(<Changes.ChangeRow change={change} databaseName="testDatabase" />, container);
+      assert.equal(0, $(changeRow.getDOMNode()).find('a.js-doc-link').length);
+    });
+
+    it('non-deleted docs should be clickable', function () {
+      change.deleted = false;
+      var changeRow = TestUtils.renderIntoDocument(<Changes.ChangeRow change={change} databaseName="testDatabase" />, container);
+      assert.equal(1, $(changeRow.getDOMNode()).find('a.js-doc-link').length);
+    });
+  });
+
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/tests/changes.storesSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/changes.storesSpec.js b/app/addons/documents/tests/changes.storesSpec.js
index f140c7c..9bcc023 100644
--- a/app/addons/documents/tests/changes.storesSpec.js
+++ b/app/addons/documents/tests/changes.storesSpec.js
@@ -21,31 +21,30 @@ define([
   var assert = utils.assert;
 
 
-  describe('ChangesHeaderStore', function () {
-    it('toggleTabVisibility() changes state in store', function() {
-      assert.ok(Stores.changesHeaderStore.isTabVisible() === false);
-      Stores.changesHeaderStore.toggleTabVisibility();
-      assert.ok(Stores.changesHeaderStore.isTabVisible() === true);
-    });
+  describe('ChangesStore', function () {
 
-    it('reset() changes tab visibility to hidden', function() {
-      Stores.changesHeaderStore.toggleTabVisibility();
-      Stores.changesHeaderStore.reset();
-      assert.ok(Stores.changesHeaderStore.isTabVisible() === false);
-    });
-  });
+    var collection = new Backbone.Collection();
 
+    afterEach(function () {
+      Stores.changesStore.reset();
+    });
 
-  describe('ChangesFilterStore', function () {
+    it('toggleTabVisibility() changes state in store', function() {
+      assert.ok(Stores.changesStore.isTabVisible() === false);
+      Stores.changesStore.toggleTabVisibility();
+      assert.ok(Stores.changesStore.isTabVisible() === true);
+    });
 
-    afterEach(function () {
-      Stores.changesFilterStore.reset();
+    it('reset() changes tab visibility to hidden', function() {
+      Stores.changesStore.toggleTabVisibility();
+      Stores.changesStore.reset();
+      assert.ok(Stores.changesStore.isTabVisible() === false);
     });
 
     it('addFilter() adds item in store', function () {
       var filter = 'My filter';
-      Stores.changesFilterStore.addFilter(filter);
-      var filters = Stores.changesFilterStore.getFilters();
+      Stores.changesStore.addFilter(filter);
+      var filters = Stores.changesStore.getFilters();
       assert.ok(filters.length === 1);
       assert.ok(filters[0] === filter);
     });
@@ -53,21 +52,50 @@ define([
     it('removeFilter() removes item from store', function () {
       var filter1 = 'My filter 1';
       var filter2 = 'My filter 2';
-      Stores.changesFilterStore.addFilter(filter1);
-      Stores.changesFilterStore.addFilter(filter2);
-      Stores.changesFilterStore.removeFilter(filter1);
+      Stores.changesStore.addFilter(filter1);
+      Stores.changesStore.addFilter(filter2);
+      Stores.changesStore.removeFilter(filter1);
 
-      var filters = Stores.changesFilterStore.getFilters();
+      var filters = Stores.changesStore.getFilters();
       assert.ok(filters.length === 1);
       assert.ok(filters[0] === filter2);
     });
 
     it('hasFilter() finds item in store', function () {
       var filter = 'My filter';
-      Stores.changesFilterStore.addFilter(filter);
-      assert.ok(Stores.changesFilterStore.hasFilter(filter) === true);
+      Stores.changesStore.addFilter(filter);
+      assert.ok(Stores.changesStore.hasFilter(filter) === true);
+    });
+
+
+    it('getDatabaseName() returns database name', function () {
+      var dbName = 'hoopoes';
+      Stores.changesStore.setChanges({ databaseName: dbName, changes: collection });
+      assert.equal(Stores.changesStore.getDatabaseName(), dbName);
+
+      Stores.changesStore.reset();
+      assert.equal(Stores.changesStore.getDatabaseName(), '');
     });
 
+    it("getChanges() should return a subset if there are a lot of changes", function () {
+
+      // to keep the test speedy, override the default max value
+      var maxChanges = 10;
+      Stores.changesStore.setMaxChanges(maxChanges);
+
+      var changes = [];
+      _.times(maxChanges + 10, function (i) {
+        changes.push(new Backbone.Model({ id: 'doc_' + i, seq: 1, changes: { } }));
+      });
+      var changesCollection = new Backbone.Collection(changes);
+      Stores.changesStore.setChanges({
+        changes: changesCollection,
+        databaseName: "test"
+      });
+
+      var results = Stores.changesStore.getChanges();
+      assert.equal(maxChanges, results.length);
+    });
   });
 
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/tests/nightwatch/changes.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/changes.js b/app/addons/documents/tests/nightwatch/changes.js
index 90eec6e..9f55fe0 100644
--- a/app/addons/documents/tests/nightwatch/changes.js
+++ b/app/addons/documents/tests/nightwatch/changes.js
@@ -21,7 +21,7 @@ module.exports = {
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
       .waitForElementPresent('.control-toggle-alternative-header', waitTime, false)
       .click('#changes')
-      .waitForElementVisible('.changes-view', waitTime, false)
+      .waitForElementPresent('.js-changes-view', waitTime, false)
       .assert.elementNotPresent('.control-toggle-alternative-header')
     .end();
   }

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/tests/nightwatch/changesFilter.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/changesFilter.js b/app/addons/documents/tests/nightwatch/changesFilter.js
new file mode 100644
index 0000000..8663fd1
--- /dev/null
+++ b/app/addons/documents/tests/nightwatch/changesFilter.js
@@ -0,0 +1,48 @@
+// 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.
+
+module.exports = {
+
+  // some basic test for the changes page. All of this and more is covered in the
+  // changes.componentsSpec.react.jsx mocha tests; this is more of a sanity end-to-end test
+  'Check changes filter results' : function (client) {
+
+    var waitTime = 10000,
+        newDatabaseName = client.globals.testDatabaseName,
+        baseUrl = client.globals.test_settings.launch_url;
+    
+    client
+      .loginToGUI()
+      .createDocument('doc_1', newDatabaseName)
+      .createDocument('doc_2', newDatabaseName)
+      .createDocument('doc_3', newDatabaseName)
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_changes')
+
+      // confirm all 3 changes are there
+      .waitForElementPresent('.change-box[data-id="doc_1"]', waitTime, false)
+      .waitForElementPresent('.change-box[data-id="doc_2"]', waitTime, false)
+      .waitForElementPresent('.change-box[data-id="doc_3"]', waitTime, false)
+
+      // add a filter
+      .click("#db-views-tabs-nav a")
+      .waitForElementVisible('.js-changes-filter-field', waitTime, false)
+      .setValue('.js-changes-filter-field', "doc_1")
+      .click('.js-filter-form button[type="submit"]')
+
+      // confirm only the single result is now listed in the page
+      .waitForElementVisible('span.label-info', waitTime, false)
+      .waitForElementPresent('.change-box[data-id="doc_1"]', waitTime, false)
+      .waitForElementNotPresent('.change-box[data-id="doc_2"]', waitTime, false)
+      .waitForElementNotPresent('.change-box[data-id="doc_3"]', waitTime, false)
+      .end();
+  }
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/tests/nightwatch/previousFromView.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/previousFromView.js b/app/addons/documents/tests/nightwatch/previousFromView.js
index 062427b..2abc379 100644
--- a/app/addons/documents/tests/nightwatch/previousFromView.js
+++ b/app/addons/documents/tests/nightwatch/previousFromView.js
@@ -24,7 +24,7 @@ module.exports = {
       .clickWhenVisible('#nav-design-function-keyviewviews')
       .clickWhenVisible('#keyview_keyview')
       .clickWhenVisible('.breadcrumb-back-link .fonticon-left-open')
-      .waitForElementPresent('.changes-view', waitTime)
+      .waitForElementPresent('.js-changes-view', waitTime)
     .end();
   }
 };

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/tests/views-changesSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/views-changesSpec.js b/app/addons/documents/tests/views-changesSpec.js
deleted file mode 100644
index 362a0e9..0000000
--- a/app/addons/documents/tests/views-changesSpec.js
+++ /dev/null
@@ -1,94 +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.
-define([
-        'addons/documents/views-changes',
-        'addons/databases/base',
-        'testUtils'
-], function (Views, Databases, testUtils) {
-  var assert = testUtils.assert,
-      ViewSandbox = testUtils.ViewSandbox,
-      viewSandbox;
-
-  describe('Documents Changes', function () {
-    var model,
-        filteredView,
-        handlerSpy,
-        view;
-
-
-    beforeEach(function (done) {
-      var database = new Databases.Model({id: 'bla'});
-
-      model = new Databases.Model({id: 'foo'});
-      model.buildChanges();
-
-      database.buildChanges({descending: 'true', limit: '100', include_docs: 'true'} );
-      filteredView = new Views.Changes({
-        model: database
-      });
-
-      handlerSpy = sinon.spy(Views.Changes.prototype, 'toggleJson');
-
-      view = new Views.Changes({
-        model: model
-      });
-      viewSandbox = new ViewSandbox();
-      viewSandbox.renderView(view, done);
-    });
-
-    afterEach(function () {
-      handlerSpy.restore();
-      view.afterRender.restore && view.afterRender.restore();
-      viewSandbox.remove();
-    });
-
-    it('does not keep filters in memory', function () {
-      view.filters.push('cat');
-      view = new Views.Changes({
-        model: model
-      });
-
-      view.filters.push('mat');
-
-      assert.deepEqual(view.filters, ['mat']);
-    });
-
-    it('filter false in case of deleted documents in the changes feed', function () {
-      filteredView.filters = [false];
-      var res = filteredView.createFilteredData([
-        {id: 'LALA', bar: 'ENTE'},
-        {id: '1', bar: '1', deleted: true},
-        {id: '2', bar: '2'}
-      ]);
-
-      assert.equal(res.length, 2);
-    });
-
-    it('the toggle-json button calls a handler', function () {
-      view.$('.js-toggle-json').trigger('click');
-      assert.ok(handlerSpy.calledOnce);
-    });
-
-    it('rerenders on the sync event', function () {
-      var spy = sinon.spy(view, 'afterRender');
-      model.changes.trigger('sync');
-
-      assert.ok(spy.calledOnce);
-    });
-
-    it('rerenders on the cachesync event', function () {
-      var spy = sinon.spy(view, 'afterRender');
-      model.changes.trigger('cachesync');
-      assert.ok(spy.calledOnce);
-    });
-  });
-});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/views-changes.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/views-changes.js b/app/addons/documents/views-changes.js
index 11add17..fd56412 100644
--- a/app/addons/documents/views-changes.js
+++ b/app/addons/documents/views-changes.js
@@ -18,108 +18,49 @@ define([
        // Libs
        "addons/fauxton/components",
   'addons/documents/changes/components.react',
-
-       // Plugins
-       "plugins/prettify"
+  'addons/documents/changes/actions'
 ],
 
-function(app, FauxtonAPI, Components, Changes, prettify, ZeroClipboard) {
+function(app, FauxtonAPI, Components, Changes, ChangesActions) {
 
   var Views = {};
 
 
-  // wrapper for React component. The wrapper allows us to tie the React component into the Fauxton
+  // wrappers for React components. The wrapper allows us to tie the React component into the Fauxton
   // page load lifecycle
   Views.ChangesHeaderReactWrapper = FauxtonAPI.View.extend({
     afterRender: function () {
       Changes.renderHeader(this.el);
     },
     cleanup: function () {
-      Changes.removeHeader(this.el);
+      Changes.remove(this.el);
     }
   });
 
 
-  Views.Changes = Components.FilteredView.extend({
-    template: "addons/documents/templates/changes",
-
+  Views.ChangesReactWrapper = FauxtonAPI.View.extend({
     initialize: function () {
-      this.listenTo(this.model.changes, 'sync', this.render);
-      this.listenTo(this.model.changes, 'cachesync', this.render);
       this.filters = [];
     },
 
-    events: {
-      "click button.js-toggle-json": "toggleJson"
-    },
-
-    toggleJson: function(event) {
-      event.preventDefault();
-
-      var $button = this.$(event.target),
-          $container = $button.closest('.change-box').find(".js-json-container");
-
-      if (!$container.is(":visible")) {
-        $button
-          .text("Close JSON")
-          .addClass("btn-secondary")
-          .removeClass("btn-primary");
-      } else {
-        $button.text("View JSON")
-          .addClass("btn-primary")
-          .removeClass("btn-secondary");
-      }
-
-      $container.slideToggle();
+    afterRender: function () {
+      ChangesActions.setChanges({
+        changes: this.model.changes,
+        filters: this.filters,
+        databaseName: this.model.id
+      });
+      Changes.renderChanges(this.el);
     },
 
     establish: function() {
-      return [ this.model.changes.fetchOnce({prefill: true})];
-    },
-
-    serialize: function () {
-      var json = this.model.changes.toJSON(),
-          filteredData = this.createFilteredData(json);
-
-      return {
-        changes: filteredData,
-        database: this.model,
-        href: function (db, id) {
-          return FauxtonAPI.urls('document', 'app', db, id);
-        }
-      };
-    },
-
-    createFilteredData: function (json) {
-      return _.reduce(this.filters, function (elements, filter) {
-        return _.filter(elements, function (element) {
-          var match = false;
-
-          // make deleted searchable
-          if (!element.deleted) {
-            element.deleted = false;
-          }
-          _.each(element, function (value) {
-            if (new RegExp(filter, 'i').test(value.toString())) {
-              match = true;
-            }
-          });
-          return match;
-        });
-
-
-      }, json, this);
+      return [this.model.changes.fetchOnce({ prefill: true })];
     },
 
-    afterRender: function(){
-      prettyPrint();
-      var client = new Components.Clipboard({
-        $el: this.$('.js-copy')
-      });
+    cleanup: function () {
+      Changes.remove(this.el);
     }
   });
 
 
-
   return Views;
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/fauxton/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/components.react.jsx b/app/addons/fauxton/components.react.jsx
index eb54a1c..32a07c7 100644
--- a/app/addons/fauxton/components.react.jsx
+++ b/app/addons/fauxton/components.react.jsx
@@ -11,13 +11,15 @@
 // the License.
 
 define([
+  'app',
   'api',
   'react',
   'addons/fauxton/stores',
-  'addons/fauxton/actions'
+  'addons/fauxton/actions',
+  'plugins/zeroclipboard/ZeroClipboard'
 ],
 
-function(FauxtonAPI, React, Stores, Actions) {
+function(app, FauxtonAPI, React, Stores, Actions, ZeroClipboard) {
   var navBarStore = Stores.navBarStore;
 
   var Footer = React.createClass({
@@ -150,12 +152,78 @@ function(FauxtonAPI, React, Stores, Actions) {
   });
 
 
+  // super basic right now, but can be expanded later to handle all the varieties of copy-to-clipboards
+  // (target content element, custom label, classes, notifications, etc.)
+  var Clipboard = React.createClass({
+    propTypes: function () {
+      return {
+        text: React.PropTypes.string.isRequired
+      };
+    },
+
+    componentWillMount: function () {
+      ZeroClipboard.config({ moviePath: app.zeroClipboardPath });
+    },
+
+    componentDidMount: function () {
+      var el = this.getDOMNode();
+      this.clipboard = new ZeroClipboard(el);
+    },
+
+    render: function () {
+      return (
+        <a href="#" ref="copy" data-clipboard-text={this.props.text} data-bypass="true" title="Copy to clipboard">
+          <i className="fonticon-clipboard"></i>
+        </a>
+      );
+    }
+  });
+
+  // formats a block of code and pretty-prints it in the page. Currently uses the prettyPrint plugin
+  var CodeFormat = React.createClass({
+    getDefaultProps: function () {
+      return {
+        lang: "js"
+      };
+    },
+
+    getClasses: function () {
+      // added for forward compatibility. This component defines an api via it's props so you can pass lang="N" and
+      // not the class that prettyprint requires for that lang. If (when, hopefully!) we drop prettyprint we won't
+      // have any change this component's props API and break things
+      var classMap = {
+        js: 'lang-js'
+      };
+
+      var classNames = 'prettyprint';
+      if (_.has(classMap, this.props.lang)) {
+        classNames += ' ' + classMap[this.props.lang];
+      }
+      return classNames;
+    },
+
+    componentDidMount: function () {
+      // this one function is all the lib offers. It parses the entire page and pretty-prints anything with
+      // a .prettyprint class; only executes on an element once
+      prettyPrint();
+    },
+
+    render: function () {
+      var code = JSON.stringify(this.props.code, null, " ");
+      return (
+        <div><pre className={this.getClasses()}>{code}</pre></div>
+      );
+    }
+  });
+
+
   return {
     renderNavBar: function (el) {
       React.render(<NavBar/>, el);
     },
-
-    Burger: Burger
+    Burger: Burger,
+    Clipboard: Clipboard,
+    CodeFormat: CodeFormat
   };
 
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/assets/less/animations.less
----------------------------------------------------------------------
diff --git a/assets/less/animations.less b/assets/less/animations.less
index 3f80f85..b7269d2 100644
--- a/assets/less/animations.less
+++ b/assets/less/animations.less
@@ -46,3 +46,22 @@
   from { background: @red; }
   to { background: white; }
 }
+
+/* a generic slide-up/down effect that looks smooth for items with unknown heights */
+.keyframes(slideDown, {
+  opacity: 0;
+  max-height: 0px;
+},
+{
+  opacity: 1;
+  max-height: 1000px;
+});
+
+.keyframes(slideUp, {
+  max-height: 1000px;
+  opacity: 1;
+},
+{
+  max-height: 0px;
+  opacity: 0;
+});


Mime
View raw message