couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From sebastia...@apache.org
Subject fauxton commit: updated refs/heads/master to 187745c
Date Thu, 07 May 2015 19:23:35 GMT
Repository: couchdb-fauxton
Updated Branches:
  refs/heads/master 4730b1144 -> 187745ca9


databases in react


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

Branch: refs/heads/master
Commit: 187745ca91910ba41c12d7d8846af6f09a8cfa00
Parents: 4730b11
Author: sebastianrothbucher <sebastianrothbucher@googlemail.com>
Authored: Thu Apr 9 16:35:39 2015 +0200
Committer: sebastianrothbucher <sebastianrothbucher@googlemail.com>
Committed: Thu May 7 20:46:22 2015 +0200

----------------------------------------------------------------------
 app/addons/databases/actions.js                 | 123 +++++++
 app/addons/databases/actiontypes.js             |  20 ++
 app/addons/databases/assets/less/databases.less |   2 +-
 app/addons/databases/base.js                    |  11 +-
 app/addons/databases/components.react.jsx       | 319 +++++++++++++++++++
 app/addons/databases/resources.js               |   6 +
 app/addons/databases/routes.js                  |  47 +--
 app/addons/databases/stores.js                  | 128 ++++++++
 .../databases/templates/footer_alldbs.html      |  17 -
 .../databases/templates/header_alldbs.html      |  21 --
 app/addons/databases/templates/item.html        |  30 --
 app/addons/databases/templates/jump_to_db.html  |  19 --
 app/addons/databases/templates/list.html        |  26 --
 app/addons/databases/templates/newdatabase.html |  20 --
 app/addons/databases/tests/actionsSpec.js       | 251 +++++++++++++++
 .../databases/tests/componentsSpec.react.jsx    | 190 +++++++++++
 app/addons/databases/tests/resourcesSpec.js     |   3 +-
 app/addons/databases/tests/storesSpec.js        |  74 +++++
 app/addons/databases/views.js                   | 242 --------------
 app/addons/fauxton/components.react.jsx         | 141 +++++++-
 .../fauxton/tests/componentsSpec.react.jsx      | 197 ++++++++++++
 21 files changed, 1459 insertions(+), 428 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/actions.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/actions.js b/app/addons/databases/actions.js
new file mode 100644
index 0000000..af67d25
--- /dev/null
+++ b/app/addons/databases/actions.js
@@ -0,0 +1,123 @@
+// 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([
+  'app',
+  'api',
+  'addons/databases/stores',
+  'addons/databases/actiontypes',
+  'addons/databases/resources'
+],
+function (app, FauxtonAPI, Stores, ActionTypes, Resources) {
+  return {
+
+    init: function (databases) {
+      var params = app.getParams();
+      var page = params.page ? parseInt(params.page, 10) : 1;
+      var perPage = FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE;
+
+      this.setStartLoading();
+      FauxtonAPI.when(databases.fetch({ cache: false })).then(function () {
+        FauxtonAPI.when(databases.paginated(page, perPage).map(function (database) {
+          return database.status.fetchOnce();
+        })).always(function () {
+          //make this always so that even if a user is not allowed access to a database
+          //they will still see a list of all databases
+          FauxtonAPI.dispatch({
+            type: ActionTypes.DATABASES_INIT,
+            options: {
+              collection: databases.paginated(page, perPage),
+              backboneCollection: databases,
+              page: page
+            }
+          });
+        }.bind(this));
+      }.bind(this));
+    },
+
+    setPage: function (page) {
+      FauxtonAPI.dispatch({
+        type: ActionTypes.DATABASES_SETPAGE,
+        options: {
+          page: page
+        }
+      });
+    },
+
+    setStartLoading: function () {
+      FauxtonAPI.dispatch({
+        type: ActionTypes.DATABASES_STARTLOADING
+      });
+    },
+
+    setLoadComplete: function () {
+      FauxtonAPI.dispatch({
+        type: ActionTypes.DATABASES_LOADCOMPLETE
+      });
+    },
+
+    createNewDatabase: function (databaseName) {
+      if (_.isNull(databaseName) || databaseName.trim().length === 0) {
+        FauxtonAPI.addNotification({
+          msg: 'Please enter a valid database name',
+          type: 'error',
+          clear: true
+        });
+        return;
+      }
+      databaseName = databaseName.trim();
+      // name accepted, make sure prompt can be removed
+      FauxtonAPI.dispatch({
+        type: ActionTypes.DATABASES_SET_PROMPT_VISIBLE,
+        options: {
+          visible: false
+        }
+      });
+
+      var db = Stores.databasesStore.obtainNewDatabaseModel(databaseName);
+      FauxtonAPI.addNotification({ msg: 'Creating database.' });
+      db.save().done(function () {
+          FauxtonAPI.addNotification({
+            msg: 'Database created successfully',
+            type: 'success',
+            clear: true
+          });
+          var route = FauxtonAPI.urls('allDocs', 'app', app.utils.safeURLName(databaseName), '?limit=' + Resources.DocLimit);
+          app.router.navigate(route, { trigger: true });
+        }
+      ).error(function (xhr) {
+          var responseText = JSON.parse(xhr.responseText).reason;
+          FauxtonAPI.addNotification({
+            msg: 'Create database failed: ' + responseText,
+            type: 'error',
+            clear: true
+          });
+        }
+      );
+    },
+
+    jumpToDatabase: function (databaseName) {
+      if (_.isNull(databaseName) || databaseName.trim().length === 0) {
+        return;
+      }
+      databaseName = databaseName.trim();
+      if (Stores.databasesStore.doesDatabaseExist(databaseName)) {
+        var url = FauxtonAPI.urls('allDocs', 'app', app.utils.safeURLName(databaseName), "");
+        FauxtonAPI.navigate(url);
+      } else {
+        FauxtonAPI.addNotification({
+          msg: 'Database does not exist.',
+          type: 'error'
+        });
+      }
+    }
+  };
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/actiontypes.js b/app/addons/databases/actiontypes.js
new file mode 100644
index 0000000..7be561e
--- /dev/null
+++ b/app/addons/databases/actiontypes.js
@@ -0,0 +1,20 @@
+// 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([], function () {
+  return {
+    DATABASES_INIT: 'DATABASES_INIT',
+    DATABASES_SETPAGE: 'DATABASES_SETPAGE',
+    DATABASES_SET_PROMPT_VISIBLE: 'DATABASES_SET_PROMPT_VISIBLE',
+    DATABASES_STARTLOADING: 'DATABASES_STARTLOADING',
+    DATABASES_LOADCOMPLETE: 'DATABASES_LOADCOMPLETE'
+  };
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/assets/less/databases.less
----------------------------------------------------------------------
diff --git a/app/addons/databases/assets/less/databases.less b/app/addons/databases/assets/less/databases.less
index ca3a666..b6986ed 100644
--- a/app/addons/databases/assets/less/databases.less
+++ b/app/addons/databases/assets/less/databases.less
@@ -55,7 +55,7 @@
   a.btn {
     color: white;
     background-color: #af2d24;
-    margin-left: -4px;
+    margin-left: 0;
     line-height: 1.5em;
     border: 0px;
     padding: 10px 10px 9px;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/base.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/base.js b/app/addons/databases/base.js
index ad07c42..56a76b0 100644
--- a/app/addons/databases/base.js
+++ b/app/addons/databases/base.js
@@ -12,18 +12,11 @@
 
 define([
   "app",
-
   "api",
-
-  // Modules
-  "addons/databases/routes",
-  // Views
-  "addons/databases/views"
-
+  "addons/databases/routes"
 ],
 
-function (app, FauxtonAPI, Databases, Views) {
-  Databases.Views = Views;
+function (app, FauxtonAPI, Databases) {
 
   Databases.initialize = function () {
     FauxtonAPI.addHeaderLink({

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/databases/components.react.jsx b/app/addons/databases/components.react.jsx
new file mode 100644
index 0000000..0bf290c
--- /dev/null
+++ b/app/addons/databases/components.react.jsx
@@ -0,0 +1,319 @@
+// 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([
+  'app',
+  'api',
+  'react',
+  'addons/components/react-components.react',
+  'addons/fauxton/components.react',
+  'addons/databases/stores',
+  'addons/databases/resources',
+  'addons/databases/actions',
+  'helpers'
+], function (app, FauxtonAPI, React, Components, ComponentsReact, Stores, Resources, Actions, Helpers) {
+
+  var databasesStore = Stores.databasesStore;
+
+  var DatabasesController = React.createClass({
+
+    getStoreState: function () {
+      return {
+        collection: databasesStore.getCollection(),
+        loading: databasesStore.isLoading()
+      };
+    },
+
+    getInitialState: function () {
+      return this.getStoreState();
+    },
+
+    componentDidMount: function () {
+      databasesStore.on('change', this.onChange, this);
+    },
+
+    componentWillUnmount: function () {
+      databasesStore.off('change', this.onChange, this);
+    },
+
+    onChange: function () {
+      this.setState(this.getStoreState());
+    },
+
+    render: function () {
+      var collection = this.state.collection;
+      var loading = this.state.loading;
+      return (
+        <DatabaseTable body={collection} loading={loading} />
+      );
+    }
+  });
+
+  var DatabaseTable = React.createClass({
+
+    createRows: function () {
+      return _.map(this.props.body, function (item, iteration) {
+        return (
+          <DatabaseRow row={item} key={iteration} />
+        );
+      });
+    },
+
+    render: function () {
+      if (this.props.loading) {
+        return (
+          <div className="view">
+            <Components.LoadLines />
+          </div>
+        );
+      }
+      var rows = this.createRows();
+      return (
+        <div className="view">
+          <table className="databases table table-striped">
+            <thead>
+              <th>Name</th>
+              <th>Size</th>
+              <th># of Docs</th>
+              <th>Update Seq</th>
+              <th>Actions</th>
+            </thead>
+            <tbody>
+            {rows}
+            </tbody>
+          </table>
+        </div>
+      );
+    }
+  });
+
+  var DatabaseRow = React.createClass({
+
+    renderGraveyard: function (row) {
+      if (row.status.isGraveYard()) {
+        return (
+          <GraveyardInfo row={row} />
+        );
+      } else {
+        return null;
+      }
+    },
+
+    render: function () {
+      var row = this.props.row;
+      var name = row.get("name");
+      var encoded = app.utils.safeURLName(name);
+      var size = Helpers.formatSize(row.status.dataSize());
+      return (
+        <tr>
+          <td>
+            <a href={"#/database/"+encoded+"/_all_docs"}>{name}</a>
+          </td>
+          <td>{size}</td>
+          <td>{row.status.numDocs()} {this.renderGraveyard(row)}</td>
+          <td>{row.status.updateSeq()}</td>
+          <td>
+            <a className="db-actions btn fonticon-replicate set-replication-start" title={"Replicate "+name} href={"#/replication/"+encoded}></a>&#160;
+            <a className="db-actions btn icon-lock set-permissions" title={"Set permissions for "+name} href={"#/database/"+encoded+"/permissions"}></a>
+          </td>
+        </tr>
+      );
+    }
+  });
+
+  var GraveyardInfo = React.createClass({
+
+    componentDidMount: function () {
+      $(this.refs.myself.getDOMNode()).tooltip();
+    },
+
+    render: function () {
+      var row = this.props.row;
+      var graveyardTitle = "This database has just " + row.status.numDocs() +
+        " docs and " + row.status.numDeletedDocs() + " deleted docs";
+      return (
+        <i className="js-db-graveyard icon icon-exclamation-sign" ref="myself" title={graveyardTitle}></i>
+      );
+    }
+  });
+
+  var RightDatabasesHeader = React.createClass({
+
+    render: function () {
+      return (
+        <div className="header-right">
+          <AddDatabaseWidget />
+          <JumpToDatabaseWidget />
+        </div>
+      );
+    }
+  });
+
+  var AddDatabaseWidget = React.createClass({
+
+    onTrayToggle: function (e) {
+      e.preventDefault();
+      this.refs.newDbTray.toggle(function (shown) {
+        if (shown) {
+          this.refs.newDbName.getDOMNode().focus();
+        }
+      }.bind(this));
+    },
+
+    onKeyUpInInput: function (e) {
+      if (e.which === 13) {
+        this.onAddDatabase();
+      }
+    },
+
+    componentDidMount: function () {
+      databasesStore.on('change', this.onChange, this);
+    },
+
+    componentWillUnmount: function () {
+      databasesStore.off('change', this.onChange, this);
+    },
+
+    onChange: function () {
+      if (this.isMounted()) {
+        this.refs.newDbTray.setVisible(databasesStore.isPromptVisible());
+      }
+    },
+
+    onAddDatabase: function () {
+      var databaseName = this.refs.newDbName.getDOMNode().value;
+      Actions.createNewDatabase(databaseName);
+    },
+
+    render: function () {
+
+      return (
+        <div className="button" id="add-db-button">
+          <a id="add-new-database" href="#" className="add-new-database-btn" onClick={this.onTrayToggle} data-bypass="true">
+                <i className="header-icon fonticon-new-database"></i>
+                Add New Database
+          </a>
+          <ComponentsReact.Tray ref="newDbTray" className="new-database-tray">
+            <span className="add-on">Add New Database</span>
+            <input id="js-new-database-name" type="text" onKeyUp={this.onKeyUpInInput} ref="newDbName" className="input-xxlarge" placeholder="Name of database" />
+            <a className="btn" id="js-create-database" onClick={this.onAddDatabase}>Create</a>
+          </ComponentsReact.Tray>
+        </div>
+      );
+    }
+  });
+
+  var JumpToDatabaseWidget = React.createClass({
+
+    getStoreState: function () {
+      return {
+        databaseNames: databasesStore.getDatabaseNames()
+      };
+    },
+
+    getInitialState: function () {
+      return this.getStoreState();
+    },
+
+    componentDidMount: function () {
+      databasesStore.on('change', this.onChange, this);
+      $(this.refs.searchDbName.getDOMNode()).typeahead({
+        source: this.state.databaseNames,
+        updater: function (item) {
+          this.jumpToDb(item);
+        }.bind(this)
+      });
+    },
+
+    componentWillUnmount: function () {
+      databasesStore.off('change', this.onChange, this);
+    },
+
+    onChange: function () {
+      this.setState(this.getStoreState());
+    },
+
+    jumpToDb: function (databaseName) {
+      databaseName = databaseName || this.refs.searchDbName.getDOMNode().value;
+      Actions.jumpToDatabase(databaseName);
+    },
+
+    jumpToDbHandler: function (e) {
+      e.preventDefault();
+      this.jumpToDb();
+    },
+
+    render: function () {
+      return (
+        <div className="searchbox-wrapper">
+          <div id="header-search" className="js-search searchbox-container">
+            <form onSubmit={this.jumpToDbHandler} id="jump-to-db" className="navbar-form pull-right database-search">
+              <div className="input-append">
+                <input type="text" className="search-autocomplete" ref="searchDbName" name="search-query" placeholder="Database name" autoComplete="off" />
+                <button className="btn btn-primary" type="submit"><i className="icon icon-search"></i></button>
+              </div>
+            </form>
+          </div>
+        </div>
+      );
+    }
+  });
+
+  var DatabasePagination = React.createClass({
+
+    getStoreState: function () {
+      return {
+        databaseNames: databasesStore.getDatabaseNames(),
+        page: databasesStore.getPage()
+      };
+    },
+
+    getInitialState: function () {
+      return this.getStoreState();
+    },
+
+    componentDidMount: function () {
+      databasesStore.on('change', this.onChange, this);
+    },
+
+    componentWillUnmount: function () {
+      databasesStore.off('change', this.onChange, this);
+    },
+
+    onChange: function () {
+      this.setState(this.getStoreState());
+    },
+
+    render: function () {
+      var page = this.state.page;
+      var total = this.props.total || this.state.databaseNames.length;
+      return (
+        <footer className="all-db-footer pagination-footer">
+          <div id="database-pagination">
+            <ComponentsReact.Pagination page={page} total={total} urlPrefix="#/_all_dbs?page=" />
+          </div>
+        </footer>
+      );
+    }
+  });
+
+  return {
+    DatabasesController: DatabasesController,
+    DatabaseTable: DatabaseTable,
+    DatabaseRow: DatabaseRow,
+    RightDatabasesHeader: RightDatabasesHeader,
+    GraveyardInfo: GraveyardInfo,
+    AddDatabaseWidget: AddDatabaseWidget,
+    JumpToDatabaseWidget: JumpToDatabaseWidget,
+    DatabasePagination: DatabasePagination
+  };
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/resources.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/resources.js b/app/addons/databases/resources.js
index 882f89d..2d277de 100644
--- a/app/addons/databases/resources.js
+++ b/app/addons/databases/resources.js
@@ -200,6 +200,12 @@ function (app, FauxtonAPI, Documents) {
           name: database
         };
       });
+    },
+
+    paginated: function (page, perPage) {
+      var start = (page - 1) * perPage;
+      var end = page * perPage;
+      return this.slice(start, end);
     }
   });
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/routes.js b/app/addons/databases/routes.js
index 53cf52b..b65c49d 100644
--- a/app/addons/databases/routes.js
+++ b/app/addons/databases/routes.js
@@ -12,18 +12,13 @@
 
 define([
   "app",
-
   "api",
-
-  // Modules
   "addons/databases/resources",
-  // TODO:: fix the include flow modules so we don't have to require views here
-  'addons/databases/views',
-  'addons/fauxton/components'
-
+  "addons/databases/actions",
+  'addons/databases/components.react'
 ],
 
-function (app, FauxtonAPI, Databases, Views, Components) {
+function (app, FauxtonAPI, Databases, Actions, Components) {
 
   var AllDbsRouteObject = FauxtonAPI.RouteObject.extend({
     layout: 'one_pane',
@@ -47,42 +42,14 @@ function (app, FauxtonAPI, Databases, Views, Components) {
     },
 
     allDatabases: function () {
-      var params = app.getParams(),
-          dbPage = params.page ? parseInt(params.page, 10) : 1,
-          perPage = FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE,
-          pagination;
-
-      pagination = new Components.Pagination({
-        page: dbPage,
-        perPage: perPage,
-        collection: this.databases,
-        urlFun: function (page) {
-          return '#/_all_dbs?page=' + page;
-        }
-      });
-
-      this.footer = this.setView('#footer', new Views.Footer());
-      this.setView('#database-pagination', pagination);
-
-      this.databasesView = this.setView("#dashboard-content", new Views.List({
-        collection: this.databases,
-        perPage: perPage,
-        page: dbPage
-      }));
-
-      this.rightHeader = this.setView("#right-header", new Views.RightAllDBsHeader({
-        collection: this.databases,
-      }));
-
-      this.databasesView.setPage(dbPage);
+      Actions.init(this.databases);
+      this.setComponent("#right-header", Components.RightDatabasesHeader);
+      this.setComponent("#dashboard-content", Components.DatabasesController);
+      this.setComponent("#footer", Components.DatabasePagination);
     },
 
     apiUrl: function () {
       return [this.databases.url("apiurl"), this.databases.documentation()];
-    },
-
-    establish: function () {
-      return [this.databases.fetch({ cache: false })];
     }
   });
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/stores.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/stores.js b/app/addons/databases/stores.js
new file mode 100644
index 0000000..4da6593
--- /dev/null
+++ b/app/addons/databases/stores.js
@@ -0,0 +1,128 @@
+// 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([
+  'app',
+  'api',
+  'addons/databases/actiontypes',
+  'addons/databases/resources'
+], function (app, FauxtonAPI, ActionTypes, Resources) {
+
+  var DatabasesStore = FauxtonAPI.Store.extend({
+
+    initialize: function () {
+      this._collection = {};
+      this._loading = false;
+      this._promptVisible = false;
+    },
+
+    init: function (collection, backboneCollection) {
+      this._collection = collection;
+      this._backboneCollection = backboneCollection;
+    },
+
+    setPage: function (page) {
+      this._page = page;
+    },
+
+    getPage: function () {
+      if (this._page) {
+        return this._page;
+      } else {
+        return 1;
+      }
+    },
+
+    isLoading: function () {
+      return this._loading;
+    },
+
+    setLoading: function (loading) {
+      this._loading = loading;
+    },
+
+    isPromptVisible: function () {
+      return this._promptVisible;
+    },
+
+    setPromptVisible: function (promptVisible) {
+      this._promptVisible = promptVisible;
+    },
+
+    obtainNewDatabaseModel: function (databaseName, nameAccCallback) {
+      return new this._backboneCollection.model({
+        id: databaseName,
+        name: databaseName
+      });
+    },
+
+    getCollection: function () {
+      return this._collection;
+    },
+
+    getDatabaseNames: function () {
+      if (this._backboneCollection) {
+        return _.map(this._backboneCollection.toJSON(), function (item, key) {
+          return item.name;
+        });
+      } else {
+        return [];
+      }
+    },
+
+    doesDatabaseExist: function (databaseName) {
+      return this.getDatabaseNames().indexOf(databaseName) >= 0;
+    },
+
+    dispatch: function (action) {
+      switch (action.type) {
+
+        case ActionTypes.DATABASES_INIT:
+          this.init(action.options.collection, action.options.backboneCollection);
+          this.setPage(action.options.page);
+          this.setLoading(false);
+          this.triggerChange();
+        break;
+
+        case ActionTypes.DATABASES_SETPAGE:
+          this.setPage(action.options.page);
+          this.triggerChange();
+        break;
+
+        case ActionTypes.DATABASES_SET_PROMPT_VISIBLE:
+          this.setPromptVisible(action.options.visible);
+          this.triggerChange();
+        break;
+
+        case ActionTypes.DATABASES_STARTLOADING:
+          this.setLoading(true);
+          this.triggerChange();
+        break;
+
+        case ActionTypes.DATABASES_LOADCOMPLETE:
+          this.setLoading(false);
+          this.triggerChange();
+        break;
+
+        default:
+        return;
+      }
+    }
+  });
+
+  var databasesStore = new DatabasesStore();
+  databasesStore.dispatchToken = FauxtonAPI.dispatcher.register(databasesStore.dispatch.bind(databasesStore));
+  return {
+    databasesStore: databasesStore
+  };
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/templates/footer_alldbs.html
----------------------------------------------------------------------
diff --git a/app/addons/databases/templates/footer_alldbs.html b/app/addons/databases/templates/footer_alldbs.html
deleted file mode 100644
index 9afcaad..0000000
--- a/app/addons/databases/templates/footer_alldbs.html
+++ /dev/null
@@ -1,17 +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.
-*/ %>
-
-<footer class="all-db-footer pagination-footer">
-  <div id="database-pagination"></div>
-</footer>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/templates/header_alldbs.html
----------------------------------------------------------------------
diff --git a/app/addons/databases/templates/header_alldbs.html b/app/addons/databases/templates/header_alldbs.html
deleted file mode 100644
index 375745e..0000000
--- a/app/addons/databases/templates/header_alldbs.html
+++ /dev/null
@@ -1,21 +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.
-*/ %>
-
-<!-- add database-->
-<div class="button" id="add-db-button"></div>
-
-<!-- search (jump to doc)-->
-<div class="searchbox-wrapper">
-  <div id="header-search" class="js-search searchbox-container"></div>
-</div>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/templates/item.html
----------------------------------------------------------------------
diff --git a/app/addons/databases/templates/item.html b/app/addons/databases/templates/item.html
deleted file mode 100644
index 610c8c6..0000000
--- a/app/addons/databases/templates/item.html
+++ /dev/null
@@ -1,30 +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.
-*/%>
-
-<td>
-  <a href="#/database/<%=encoded%>/_all_docs"><%= database.get("name") %></a>
-</td>
-<td><%= formatSize(database.status.dataSize()) %></td>
-<td>
-  <%= database.status.numDocs() %>
-  <% if (database.status.isGraveYard()) { %>
-    <i class="js-db-graveyard icon icon-exclamation-sign" data-toggle="tooltip"
-      title="This database has just <%= database.status.numDocs() %> docs and <%= database.status.numDeletedDocs() %> deleted docs"></i>
-  <% } %>
-</td>
-<td><%= database.status.updateSeq() %></td>
-<td>
-  <a class="db-actions btn fonticon-replicate set-replication-start" title="Replicate <%-database.get("name")%>" href="#/replication/<%-encoded%>"></a>
-  <a class="db-actions btn icon-lock set-permissions" title="Set permissions for <%-database.get("name")%>" href="#/database/<%-encoded%>/permissions"></a>
-</td>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/templates/jump_to_db.html
----------------------------------------------------------------------
diff --git a/app/addons/databases/templates/jump_to_db.html b/app/addons/databases/templates/jump_to_db.html
deleted file mode 100644
index e3e7912..0000000
--- a/app/addons/databases/templates/jump_to_db.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.
--->
-<form id="jump-to-db" class="navbar-form pull-right database-search">
-  <div class="input-append">
-    <input type="text" class="search-autocomplete" autocomplete="off" name="search-query" placeholder="Database name" />
-    <button class="btn btn-primary" type="submit"><i class="icon icon-search"></i></button>
-  </div>
-</form>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/templates/list.html
----------------------------------------------------------------------
diff --git a/app/addons/databases/templates/list.html b/app/addons/databases/templates/list.html
deleted file mode 100644
index c1b625e..0000000
--- a/app/addons/databases/templates/list.html
+++ /dev/null
@@ -1,26 +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="view">
-  <table class="databases table table-striped">
-    <thead>
-      <th>Name</th>
-      <th>Size</th>
-      <th># of Docs</th>
-      <th>Update Seq</th>
-      <th>Actions</th>
-    </thead>
-    <tbody>
-    </tbody>
-  </table>
-</div>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/templates/newdatabase.html
----------------------------------------------------------------------
diff --git a/app/addons/databases/templates/newdatabase.html b/app/addons/databases/templates/newdatabase.html
deleted file mode 100644
index bda32b8..0000000
--- a/app/addons/databases/templates/newdatabase.html
+++ /dev/null
@@ -1,20 +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.
-*/%>
-<a id="add-new-database" class="add-new-database-btn" href="#"><i class="header-icon fonticon-new-database"></i> Add New Database</a>
-
-<div class="new-database-tray tray">
-  <span class="add-on">Add New Database</span>
-  <input id="js-new-database-name" type="text" class="input-xxlarge" placeholder="Name of database">
-  <a class="btn" id="js-create-database">Create</a>
-</div>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/tests/actionsSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/tests/actionsSpec.js b/app/addons/databases/tests/actionsSpec.js
new file mode 100644
index 0000000..cd69116
--- /dev/null
+++ b/app/addons/databases/tests/actionsSpec.js
@@ -0,0 +1,251 @@
+// 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([
+  'app',
+  'api',
+  'testUtils',
+  'addons/databases/stores',
+  'addons/databases/actions',
+  'addons/databases/actiontypes',
+  'addons/databases/resources'
+], function (app, FauxtonAPI, utils, Stores, Actions, ActionTypes, Resources) {
+
+  var assert = utils.assert;
+
+  describe('Databases Actions', function () {
+
+    describe('Initialization', function () {
+
+      var oldDispatch, oldWhen, oldGetParams;
+      var dispatchEvents, thenCallback, alwaysCallback;
+      var databasesMock;
+
+      beforeEach(function () {
+        oldDispatch = FauxtonAPI.dispatch;
+        dispatchEvents = [];
+        FauxtonAPI.dispatch = function (what) {
+          dispatchEvents.push(what);
+        };
+        oldWhen = FauxtonAPI.when;
+        FauxtonAPI.when = function () {
+          return {
+            then: function (callback) {
+              thenCallback = callback;
+              callback();
+            },
+            always: function (callback) {
+              alwaysCallback = callback;
+              callback();
+            }
+          };
+        };
+        // (replace on demand)
+        oldGetParams = app.getParams;
+        databasesMock = {
+          fetch: function () {
+          },
+          paginated: function () {
+            return [];
+          }
+        };
+      });
+
+      afterEach(function () {
+        FauxtonAPI.dispatch = oldDispatch;
+        FauxtonAPI.when = oldWhen;
+        app.getParams = oldGetParams;
+      });
+
+      it('Starts loading first', function () {
+        app.getParams = function () {
+          return {};
+        };
+        Actions.init(databasesMock);
+        assert(!!thenCallback || !!alwaysCallback);
+        // now we should have resolved it all
+        assert.equal(2, dispatchEvents.length);
+        assert.equal(ActionTypes.DATABASES_STARTLOADING, dispatchEvents[0].type);
+        assert.equal(ActionTypes.DATABASES_INIT, dispatchEvents[1].type);
+        assert.equal(1, dispatchEvents[1].options.page);
+      });
+
+      it('Accepts page params', function () {
+        app.getParams = function () {
+          return {
+            page: 33
+          };
+        };
+        Actions.init(databasesMock);
+        // now we should have resolved it all
+        assert.equal(2, dispatchEvents.length);
+        assert.equal(ActionTypes.DATABASES_INIT, dispatchEvents[1].type);
+        assert.equal(33, dispatchEvents[1].options.page);
+      });
+
+    });
+
+    describe('Add database', function () {
+
+      var oldColl, oldBackbone, oldRouter, oldNotification, oldDispatch;
+      var passedId, doneCallback, errorCallback, navigationTarget, notificationText, dispatchEvents;
+
+      beforeEach(function () {
+        oldColl = Stores.databasesStore._collection;
+        oldBackbone = Stores.databasesStore._backboneCollection;
+        passedId = null;
+        Stores.databasesStore._backboneCollection = {};
+        Stores.databasesStore._backboneCollection.model = function (options) {
+          passedId = options.id;
+          return {
+            "save": function () {
+              var res = {
+                "done": function (callback) {
+                  doneCallback = callback;
+                  return res;
+                },
+                "error": function (callback) {
+                  errorCallback = callback;
+                  return res;
+                }
+              };
+              return res;
+            }
+          };
+        };
+        oldRouter = app.router;
+        navigationTarget = null;
+        app.router = {
+          "navigate": function (target) {
+            navigationTarget = target;
+          }
+        };
+        oldNotification = FauxtonAPI.addNotification;
+        notificationText = [];
+        FauxtonAPI.addNotification = function (options) {
+          notificationText.push(options.msg);
+        };
+        oldDispatch = FauxtonAPI.dispatch;
+        dispatchEvents = [];
+        FauxtonAPI.dispatch = function (what) {
+          dispatchEvents.push(what);
+        };
+      });
+
+      afterEach(function () {
+        Stores.databasesStore._collection = oldColl;
+        Stores.databasesStore._backboneCollection = oldBackbone;
+        app.router = oldRouter;
+        FauxtonAPI.addNotification = oldNotification;
+        FauxtonAPI.dispatch = oldDispatch;
+      });
+
+      it("Creates database in backend", function () {
+        Actions.createNewDatabase("testdb");
+        doneCallback();
+        assert.equal("testdb", passedId);
+        assert.equal(1, _.map(dispatchEvents, function (item) {
+          if (item.type === ActionTypes.DATABASES_SET_PROMPT_VISIBLE) {
+            return item;
+          }
+        }).length);
+        assert.equal(2, notificationText.length);
+        assert(notificationText[0].indexOf("Creating") >= 0);
+        assert(notificationText[1].indexOf("success") >= 0);
+        assert(navigationTarget.indexOf("testdb") >= 0);
+      });
+
+      it("Creates no database without name", function () {
+        Actions.createNewDatabase("   ");
+        assert(passedId === null);
+        assert.equal(0, _.map(dispatchEvents, function (item) {
+          if (item.type === ActionTypes.DATABASES_SET_PROMPT_VISIBLE) {
+            return item;
+          }
+        }).length);
+        assert.equal(1, notificationText.length);
+        assert(notificationText[0].indexOf("valid database name") >= 0);
+      });
+
+      it("Shows error message on create fail", function () {
+        Actions.createNewDatabase("testdb");
+        errorCallback({"responseText": JSON.stringify({"reason": "testerror"})});
+        assert.equal("testdb", passedId);
+        assert.equal(2, notificationText.length);
+        assert(notificationText[0].indexOf("Creating") >= 0);
+        assert(notificationText[1].indexOf("failed") >= 0);
+        assert(notificationText[1].indexOf("testerror") >= 0);
+        assert(navigationTarget === null);
+      });
+
+    });
+
+    describe('Jump to database', function () {
+
+      var container, jumpEl, oldNavigate, oldAddNotification, oldGetDatabaseNames, old$;
+      var navigationTarget, notificationText;
+
+      beforeEach(function () {
+        old$ = $;
+        // simulate typeahead
+        $ = function (selector) {
+          var res = old$(selector);
+          res.typeahead = function () {};
+          return res;
+        };
+        oldNavigate = FauxtonAPI.navigate;
+        navigationTarget = null;
+        FauxtonAPI.navigate = function (url) {
+          navigationTarget = url;
+        };
+        oldAddNotification = FauxtonAPI.addNotification;
+        notificationText = [];
+        FauxtonAPI.addNotification = function (options) {
+          notificationText.push(options.msg);
+        };
+        oldGetDatabaseNames = Stores.databasesStore.getDatabaseNames;
+        Stores.databasesStore.getDatabaseNames = function () {
+          return ["db1", "db2"];
+        };
+      });
+
+      afterEach(function () {
+        $ = old$;
+        FauxtonAPI.navigate = oldNavigate;
+        FauxtonAPI.addNotification = oldAddNotification;
+        Stores.databasesStore.getDatabaseNames = oldGetDatabaseNames;
+      });
+
+      it("jumps to an existing DB", function () {
+        Actions.jumpToDatabase("db1");
+        assert(navigationTarget.indexOf("db1") >= 0);
+        assert.equal(0, notificationText.length);
+      });
+
+      it("does nothing on empty name", function () {
+        Actions.jumpToDatabase("  ");
+        assert(navigationTarget === null);
+        assert.equal(0, notificationText.length);
+      });
+
+      it("shows a message on non-existent DB", function () {
+        Actions.jumpToDatabase("db3");
+        assert(navigationTarget === null);
+        assert.equal(1, notificationText.length);
+        assert(notificationText[0].indexOf("not exist") >= 0);
+      });
+
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/tests/componentsSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/databases/tests/componentsSpec.react.jsx b/app/addons/databases/tests/componentsSpec.react.jsx
new file mode 100644
index 0000000..5a790a4
--- /dev/null
+++ b/app/addons/databases/tests/componentsSpec.react.jsx
@@ -0,0 +1,190 @@
+// 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([
+  'api',
+  'addons/databases/components.react',
+  'addons/databases/actions',
+  'addons/databases/stores',
+  'testUtils',
+  "react"
+], function (FauxtonAPI, Views, Actions, Stores, utils, React) {
+
+  var assert = utils.assert;
+  var TestUtils = React.addons.TestUtils;
+
+  describe('DatabasesController', function () {
+
+    var container, dbEl, oldGetCollection;
+
+    beforeEach(function () {
+      // define our own collection
+      oldGetCollection = Stores.databasesStore.getCollection;
+      Stores.databasesStore.getCollection = function () {
+        return [
+          {
+            "get": function (what) {
+              if ("name" === what) {
+                return "db1";
+              } else {
+                throw "Unknown get('" + what + "')";
+              }
+            },
+            "status": {
+              "dataSize": function () {
+                return 2 * 1024 * 1024;
+              },
+              "numDocs": function () {
+                return 88;
+              },
+              "isGraveYard": function () {
+                return false;
+              },
+              "updateSeq": function () {
+                return 99;
+              }
+            }
+          },
+          {
+            "get": function (what) {
+              if ("name" === what) {
+                return "db2";
+              } else {
+                throw "Unknown get('" + what + "')";
+              }
+            },
+            "status": {
+              "dataSize": function () {
+                return 1024;
+              },
+              "numDocs": function () {
+                return 188;
+              },
+              "numDeletedDocs": function () {
+                return 222;
+              },
+              "isGraveYard": function () {
+                return true;
+              },
+              "updateSeq": function () {
+                return 399;
+              }
+            }
+          }
+        ];
+      };
+      container = document.createElement('div');
+      dbEl = React.renderComponent(React.createElement(Views.DatabasesController, {}), container);
+    });
+
+    afterEach(function () {
+      Stores.databasesStore.getCollection = oldGetCollection;
+      React.unmountComponentAtNode(container);
+    });
+
+    it('renders base data of DBs', function () {
+      assert.equal(1 + 2, dbEl.getDOMNode().getElementsByTagName('tr').length);
+      assert.equal("db1", dbEl.getDOMNode().getElementsByTagName('tr')[1].getElementsByTagName('td')[0].innerText.trim());
+      assert.equal("2.0 MB", dbEl.getDOMNode().getElementsByTagName('tr')[1].getElementsByTagName('td')[1].innerText.trim());
+      assert.equal("88", dbEl.getDOMNode().getElementsByTagName('tr')[1].getElementsByTagName('td')[2].innerText.trim());
+      assert.equal(0, dbEl.getDOMNode().getElementsByTagName('tr')[1].getElementsByTagName('td')[2].getElementsByTagName("i").length);
+      assert.equal(2, dbEl.getDOMNode().getElementsByTagName('tr')[1].getElementsByTagName('td')[4].getElementsByTagName("a").length);
+      assert.equal("db2", dbEl.getDOMNode().getElementsByTagName('tr')[2].getElementsByTagName('td')[0].innerText.trim());
+      assert.equal(1, dbEl.getDOMNode().getElementsByTagName('tr')[2].getElementsByTagName('td')[2].getElementsByTagName("i").length);
+    });
+
+  });
+
+  describe('AddDatabaseWidget', function () {
+
+    var container, addEl, oldCreateNewDatabase;
+    var createCalled, passedDbName;
+
+    beforeEach(function () {
+      oldCreateNewDatabase = Actions.createNewDatabase;
+      Actions.createNewDatabase = function (dbName) {
+        createCalled = true;
+        passedDbName = dbName;
+      };
+      container = document.createElement('div');
+      addEl = React.renderComponent(React.createElement(Views.AddDatabaseWidget, {}), container);
+    });
+
+    afterEach(function () {
+      Actions.createNewDatabase = oldCreateNewDatabase;
+      React.unmountComponentAtNode(container);
+    });
+
+    it("Creates a database with given name", function () {
+      createCalled = false;
+      passedDbName = null;
+      TestUtils.findRenderedDOMComponentWithTag(addEl, 'input').getDOMNode().value = "testdb";
+      addEl.onAddDatabase();
+      assert.equal(true, createCalled);
+      assert.equal("testdb", passedDbName);
+    });
+
+  });
+
+  describe('JumpToDatabaseWidget', function () {
+
+    var container, jumpEl, oldJumpToDatabase, oldGetDatabaseNames, old$;
+    var jumpCalled, passedDbName;
+
+    beforeEach(function () {
+      old$ = $;
+      // simulate typeahead
+      $ = function (selector) {
+        var res = old$(selector);
+        res.typeahead = function () {};
+        return res;
+      };
+      oldJumpToDatabase = Actions.jumpToDatabase;
+      Actions.jumpToDatabase = function (dbName) {
+        jumpCalled = true;
+        passedDbName = dbName;
+      };
+      oldGetDatabaseNames = Stores.databasesStore.getDatabaseNames;
+      Stores.databasesStore.getDatabaseNames = function () {
+        return ["db1", "db2"];
+      };
+      container = document.createElement('div');
+      jumpEl = React.renderComponent(React.createElement(Views.JumpToDatabaseWidget, {}), container);
+    });
+
+    afterEach(function () {
+      $ = old$;
+      Actions.jumpToDatabase = oldJumpToDatabase;
+      Stores.databasesStore.getDatabaseNames = oldGetDatabaseNames;
+      React.unmountComponentAtNode(container);
+    });
+
+    it("Jumps to a database with given name", function () {
+      jumpCalled = false;
+      passedDbName = null;
+      jumpEl.jumpToDb("db1");
+      assert.equal(true, jumpCalled);
+      assert.equal("db1", passedDbName);
+    });
+
+    it("jumps to an existing DB from input", function () {
+      jumpCalled = false;
+      passedDbName = null;
+      TestUtils.findRenderedDOMComponentWithTag(jumpEl, 'input').getDOMNode().value = "db2";
+      jumpEl.jumpToDb();
+      assert.equal(true, jumpCalled);
+      assert.equal("db2", passedDbName);
+    });
+
+  });
+
+});
+

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/tests/resourcesSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/tests/resourcesSpec.js b/app/addons/databases/tests/resourcesSpec.js
index cf3289d..d3c0bab 100644
--- a/app/addons/databases/tests/resourcesSpec.js
+++ b/app/addons/databases/tests/resourcesSpec.js
@@ -12,9 +12,8 @@
 define([
       'api',
       'addons/databases/resources',
-      'addons/databases/views',
       'testUtils'
-], function (FauxtonAPI, Resources, Views, testUtils) {
+], function (FauxtonAPI, Resources, testUtils) {
   var assert = testUtils.assert,
       ViewSandbox = testUtils.ViewSandbox;
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/tests/storesSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/tests/storesSpec.js b/app/addons/databases/tests/storesSpec.js
new file mode 100644
index 0000000..2eb9fbc
--- /dev/null
+++ b/app/addons/databases/tests/storesSpec.js
@@ -0,0 +1,74 @@
+// 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([
+  'app',
+  'api',
+  'testUtils',
+  'addons/databases/stores',
+  'addons/databases/actiontypes',
+  'addons/databases/resources'
+], function (app, FauxtonAPI, utils, Stores, ActionTypes, Resources) {
+
+  var assert = utils.assert;
+
+  describe('Databases Store', function () {
+
+    var oldColl, oldBackbone;
+    var passedId, doneCallback, errorCallback, navigationTarget;
+
+    beforeEach(function () {
+      oldColl = Stores.databasesStore._collection;
+      oldBackbone = Stores.databasesStore._backboneCollection;
+      Stores.databasesStore._backboneCollection = {};
+    });
+
+    afterEach(function () {
+      Stores.databasesStore._collection = oldColl;
+      Stores.databasesStore._backboneCollection = oldBackbone;
+    });
+
+    it("inits based on what we pass", function () {
+      Stores.databasesStore.init({"name": "col1"}, {"name": "col2"});
+      assert.equal("col1", Stores.databasesStore.getCollection().name);
+      assert.equal("col2", Stores.databasesStore._backboneCollection.name);
+    });
+
+    describe("database collection info", function () {
+
+      beforeEach(function () {
+        Stores.databasesStore._backboneCollection.toJSON = function () {
+          return {
+            "db1": {
+              "name": "db1"
+            },
+            "db2": {
+              "name": "db2"
+            }
+          };
+        };
+      });
+
+      it("determines database names", function () {
+        assert.ok(JSON.stringify(["db1", "db2"]) == JSON.stringify(Stores.databasesStore.getDatabaseNames().sort()));
+      });
+
+      it("determines database availability", function () {
+        assert(Stores.databasesStore.doesDatabaseExist("db1"));
+        assert(!Stores.databasesStore.doesDatabaseExist("db3"));
+      });
+
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/databases/views.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/views.js b/app/addons/databases/views.js
deleted file mode 100644
index cbbebb1..0000000
--- a/app/addons/databases/views.js
+++ /dev/null
@@ -1,242 +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([
-  'app',
-  'addons/fauxton/components',
-  'api',
-  'addons/databases/resources'
-],
-
-function (app, Components, FauxtonAPI, Databases) {
-
-  var Views = {};
-
-  Views.Footer = FauxtonAPI.View.extend({
-    template: 'addons/databases/templates/footer_alldbs',
-  });
-
-  Views.RightAllDBsHeader = FauxtonAPI.View.extend({
-    className: 'header-right',
-    template: 'addons/databases/templates/header_alldbs',
-
-    beforeRender: function () {
-      this.headerSearch = this.insertView('#header-search', new JumpToDBView({
-        collection: this.collection
-      }));
-
-      this.newbutton = this.insertView('#add-db-button', new NewDatabaseView({
-        collection: this.collection
-      }));
-    }
-  });
-
-  Views.Item = FauxtonAPI.View.extend({
-    template: 'addons/databases/templates/item',
-    tagName: 'tr',
-
-    establish: function () {
-      return [this.model.fetch()];
-    },
-
-    serialize: function () {
-      return {
-        encoded: app.utils.safeURLName(this.model.get('name')),
-        database: this.model
-      };
-    },
-
-    afterRender: function () {
-      this.$el.find('.js-db-graveyard').tooltip();
-    }
-  });
-
-  Views.List = FauxtonAPI.View.extend({
-    template: 'addons/databases/templates/list',
-    events: {
-      'click button.all': 'selectAll'
-    },
-
-    initialize: function (options) {
-      var params = app.getParams();
-    },
-
-    serialize: function () {
-      return {
-        databases: this.collection
-      };
-    },
-    establish: function () {
-      var currentDBs = this.paginated();
-      var deferred = FauxtonAPI.Deferred();
-
-      FauxtonAPI.when(currentDBs.map(function (database) {
-        return database.status.fetchOnce();
-      })).always(function (resp) {
-        //make this always so that even if a user is not allowed access to a database
-        //they will still see a list of all databases
-        deferred.resolve();
-      });
-      return [deferred];
-    },
-
-    paginated: function () {
-      var start = (this.page - 1) * this.perPage;
-      var end = this.page * this.perPage;
-      return this.collection.slice(start, end);
-    },
-
-    beforeRender: function () {
-      _.each(this.paginated(), function (database) {
-        this.insertView('table.databases tbody', new Views.Item({
-          model: database
-        }));
-      }, this);
-    },
-
-    setPage: function (page) {
-      this.page = page || 1;
-    },
-
-    selectAll: function (event) {
-      $('input:checkbox').attr('checked', !$(event.target).hasClass('active'));
-    }
-  });
-
-
-  // private Views
-
-  var JumpToDBView = FauxtonAPI.View.extend({
-    template: 'addons/databases/templates/jump_to_db',
-    events: {
-      'submit form#jump-to-db': 'switchDatabaseHandler'
-    },
-
-    initialize: function () {
-      var params = app.getParams();
-      this.page = params.page ? parseInt(params.page, 10) : 1;
-      this.listenTo(FauxtonAPI.Events, 'jumptodb:update', this.switchDatabase);
-    },
-
-    establish: function () {
-      var currentDBs = this.paginated();
-      var deferred = FauxtonAPI.Deferred();
-
-      FauxtonAPI.when(currentDBs.map(function (database) {
-        return database.status.fetchOnce();
-      })).always(function (resp) {
-        // make this always so that even if a user is not allowed access to a database
-        // they will still see a list of all databases
-        deferred.resolve();
-      });
-      return [deferred];
-    },
-
-    switchDatabase: function (selectedName) {
-      var dbname = this.$el.find('[name="search-query"]').val().trim();
-
-      if (selectedName) {
-        dbname = selectedName;
-      }
-      if (dbname && this.collection.where({ id: app.utils.safeURLName(dbname) }).length > 0) {
-        // TODO: switch to using a model, or Databases.databaseUrl()
-        // Neither of which are in scope right now
-        // var db = new Database.Model({id: dbname});
-        var url = FauxtonAPI.urls('allDocs', 'app', app.utils.safeURLName(dbname), '');
-        FauxtonAPI.navigate(url);
-      } else {
-        FauxtonAPI.addNotification({
-          msg: 'Database does not exist.',
-          type: 'error'
-        });
-      }
-    },
-
-    switchDatabaseHandler: function (event) {
-      event.preventDefault();
-      this.switchDatabase();
-    },
-
-    afterRender: function () {
-      var AllDBsArray = _.map(this.collection.toJSON(), function (item, key) {
-            return item.name;
-          });
-
-      this.dbSearchTypeahead = new Components.Typeahead({
-        el: 'input.search-autocomplete',
-        source: AllDBsArray,
-        onUpdateEventName: 'jumptodb:update'
-      });
-      this.dbSearchTypeahead.render();
-    }
-  });
-
-  var NewDatabaseView = Components.Tray.extend({
-    template: 'addons/databases/templates/newdatabase',
-    events: {
-      'click #js-create-database': 'createDatabase',
-      'keyup #js-new-database-name': 'processKey'
-    },
-
-    initialize: function () {
-      this.initTray({ toggleTrayBtnSelector: '#add-new-database' });
-    },
-
-    processKey: function (e) {
-      if (e.which === 13) {
-        this.createDatabase(e);
-      }
-    },
-
-    createDatabase: function (e) {
-      e.preventDefault();
-
-      var databaseName = $.trim(this.$('#js-new-database-name').val());
-      if (databaseName.length === 0) {
-        FauxtonAPI.addNotification({
-          msg: 'Please enter a valid database name',
-          type: 'error',
-          clear: true
-        });
-        return;
-      }
-      this.hideTray();
-
-      var db = new this.collection.model({
-        id: databaseName,
-        name: databaseName
-      });
-      FauxtonAPI.addNotification({ msg: 'Creating database.' });
-
-      db.save().done(function () {
-          FauxtonAPI.addNotification({
-            msg: 'Database created successfully',
-            type: 'success',
-            clear: true
-          });
-          var route = '#/database/' + app.utils.safeURLName(databaseName) + '/_all_docs?limit=' + Databases.DocLimit;
-          app.router.navigate(route, { trigger: true });
-        }
-      ).error(function (xhr) {
-          var responseText = JSON.parse(xhr.responseText).reason;
-          FauxtonAPI.addNotification({
-            msg: 'Create database failed: ' + responseText,
-            type: 'error',
-            clear: true
-          });
-        }
-      );
-    }
-  });
-
-  return Views;
-});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/fauxton/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/components.react.jsx b/app/addons/fauxton/components.react.jsx
index 57ced03..5e7e891 100644
--- a/app/addons/fauxton/components.react.jsx
+++ b/app/addons/fauxton/components.react.jsx
@@ -83,10 +83,149 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
     }
   });
 
+  var _NextTrayInternalId = 0;
+  var Tray = React.createClass({
+
+    getInitialState: function () {
+      return {
+        show: false,
+        internalid: (_NextTrayInternalId++)
+      };
+    },
+
+    toggle: function (done) {
+      if (this.state.show) {
+        this.hide(done);
+      } else {
+        this.show(done);
+      }
+    },
+
+    setVisible: function (visible, done) {
+      if (this.state.show && !visible) {
+        this.hide(done);
+      } else if (!this.state.show && visible) {
+        this.show(done);
+      }
+    },
+
+    componentDidMount: function () {
+      $('body').on('click.Tray-' + this.state.internalid, function (e) {
+        var tgt = $(e.target);
+        if (this.state.show && tgt.closest('.tray').length === 0) {
+          this.hide();
+        }
+      }.bind(this));
+    },
+
+    componentWillUnmount: function () {
+      $('body').off('click.Tray-' + this.state.internalid);
+    },
+
+    show: function (done) {
+      this.setState({show: true});
+      $(this.refs.myself.getDOMNode()).velocity('transition.slideDownIn', FauxtonAPI.constants.MISC.TRAY_TOGGLE_SPEED, function () {
+        if (done) {
+          done(true);
+        }
+      });
+    },
+
+    hide: function (done) {
+      $(this.refs.myself.getDOMNode()).velocity('reverse', FauxtonAPI.constants.MISC.TRAY_TOGGLE_SPEED, function () {
+        this.setState({show: false});
+        if (done) {
+          done(false);
+        }
+      }.bind(this));
+    },
+
+    render: function () {
+      var styleSpec = this.state.show ? {"display": "block", "opacity": 1} :  {"display": "none", "opacity": 0};
+      var classSpec = this.props.className || "";
+      classSpec += " tray";
+      return (
+        <div ref="myself" style={styleSpec} className={classSpec}>{this.props.children}</div>
+      );
+    }
+  });
+
+  var Pagination = React.createClass({
+
+    getInitialState: function () {
+      return {};
+    },
+
+    getDefaultProps: function () {
+      return {
+        perPage: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE,
+        page: 1,
+        total: 0
+      };
+    },
+
+    getVisiblePages: function (page, totalPages) {
+      var from, to;
+      if (totalPages < 10) {
+        from = 1;
+        to = totalPages + 1;
+      } else {
+        from = page - 5;
+        to = page + 5;
+        if (from <= 1) {
+          from = 1;
+          to = 11;
+        }
+        if (to > totalPages + 1) {
+          from =  totalPages - 9;
+          to = totalPages + 1;
+        }
+      }
+      return {
+        from: from,
+        to: to
+      };
+    },
+
+    createItemsForPage: function (visiblePages, page, prefix, suffix) {
+      return _.range(visiblePages.from, visiblePages.to).map(function (i) {
+        return (
+          <li key={i} className={(page === i ? "active" : null)}>
+            <a href={prefix + i + suffix}>{i}</a>
+          </li>
+        );
+      });
+    },
+
+    render: function () {
+      var page = this.state.page || this.props.page;
+      var total = this.state.total || this.props.total;
+      var perPage = this.props.perPage;
+      var prefix = this.props.urlPrefix || "";
+      var suffix = this.props.urlSuffix || "";
+      var totalPages = total === 0 ? 1 : Math.ceil(total / perPage);
+      var visiblePages = this.getVisiblePages(page, totalPages);
+      var rangeItems = this.createItemsForPage(visiblePages, page, prefix, suffix);
+      return (
+        <ul className="pagination">
+          <li className={(page === 1 ? "disabled" : null)}>
+            <a href={prefix + Math.max(page - 1, 1) + suffix}>&laquo;</a>
+          </li>
+          {rangeItems}
+          <li className={(page < totalPages ? null : "disabled")}>
+            <a href={prefix + Math.min(page + 1, totalPages) + suffix}>&raquo;</a>
+          </li>
+        </ul>
+      );
+    }
+  });
+
 
   return {
     Clipboard: Clipboard,
-    CodeFormat: CodeFormat
+    CodeFormat: CodeFormat,
+    Tray: Tray,
+    Pagination: Pagination
   };
 
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/187745ca/app/addons/fauxton/tests/componentsSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/tests/componentsSpec.react.jsx b/app/addons/fauxton/tests/componentsSpec.react.jsx
new file mode 100644
index 0000000..52a00d0
--- /dev/null
+++ b/app/addons/fauxton/tests/componentsSpec.react.jsx
@@ -0,0 +1,197 @@
+// 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([
+  'api',
+  'addons/fauxton/components.react',
+  'testUtils',
+  "react"
+], function (FauxtonAPI, Views, utils, React) {
+
+  var assert = utils.assert;
+  var TestUtils = React.addons.TestUtils;
+
+  describe('Tray', function () {
+
+    var container, anotherContainer, trayEl, done, old$;
+    // trace registrations
+    var handlersOn, handlersOff, velocities;
+
+    beforeEach(function () {
+      handlersOn = [];
+      handlersOff = [];
+      velocities = [];
+      done = sinon.spy();
+      // simulate $ to test registration
+      old$ = $;
+      $ = function (what) {
+        this.on = function (id, handler) {
+          handlersOn.push([what, id]);
+        };
+        this.off = function (id) {
+          handlersOff.push([what, id]);
+        };
+        this.velocity = function (trans, speed, callback) {
+          velocities.push([what, trans]);
+          callback();
+        };
+        return this;
+      };
+      container = document.createElement('div');
+      // when we want to control the diff, we have to render directly
+      trayEl = React.renderComponent(React.createElement(Views.Tray, {className: "traytest"}), container);
+    });
+
+    afterEach(function () {
+      $ = old$;
+      React.unmountComponentAtNode(container);
+      if (anotherContainer) {
+        React.unmountComponentAtNode(anotherContainer);
+      }
+    });
+
+    it('renders trayid and custom classes', function () {
+      assert(trayEl.getDOMNode().getAttribute("class").indexOf("traytest") >= 0);
+    });
+
+    it('registers handler with body', function () {
+      assert.equal(1, handlersOn.length);
+      assert.equal("body", handlersOn[0][0]);
+      assert.equal("click.Tray-", handlersOn[0][1].substring(0, "click.Tray-".length));
+      assert.equal(0, handlersOff.length);
+      // also a 2nd time
+      anotherContainer = document.createElement('div');
+      React.renderComponent(React.createElement(Views.Tray, {className: "traytest"}), anotherContainer);
+      assert.equal(2, handlersOn.length);
+      assert.equal("body", handlersOn[1][0]);
+      assert.equal("click.Tray-", handlersOn[1][1].substring(0, "click.Tray-".length));
+      assert.equal(0, handlersOff.length);
+      // (we have different IDs)
+      assert(handlersOn[0][1] != handlersOn[1][1]);
+      // and we also unregister properly
+      var unmountSuccess = React.unmountComponentAtNode(anotherContainer);
+      assert(unmountSuccess);
+      assert.equal(1, handlersOff.length);
+      assert.equal("body", handlersOff[0][0]);
+      assert.equal("click.Tray-", handlersOff[0][1].substring(0, "click.Tray-".length));
+      // (the ID of the 2nd element)
+      assert(handlersOn[1][1] == handlersOff[0][1]);
+    });
+
+    it('is initially closed', function () {
+      assert.equal("none", trayEl.getDOMNode().style.display);
+    });
+
+    it('shows when requested', function () {
+      trayEl.setVisible(true);
+      assert.equal(1, velocities.length);
+      assert.equal("block", trayEl.getDOMNode().style.display);
+    });
+
+    it('hides when requested', function () {
+      trayEl.show();
+      trayEl.setVisible(false);
+      assert.equal(2, velocities.length);
+      assert.equal("none", trayEl.getDOMNode().style.display);
+    });
+
+    it('does nothing when already hidden', function () {
+      trayEl.setVisible(false);
+      assert.equal(0, velocities.length);
+    });
+
+    it('toggles open with callback', function () {
+      trayEl.toggle(done);
+      assert.ok(done.calledOnce);
+      assert.equal(1, velocities.length);
+      assert.equal("block", trayEl.getDOMNode().style.display);
+    });
+
+    it('toggles close again with callback', function () {
+      trayEl.show();
+      trayEl.toggle(done);
+      assert.ok(done.calledOnce);
+      assert.equal(2, velocities.length);
+      assert.equal("none", trayEl.getDOMNode().style.display);
+    });
+
+  });
+
+  describe('Pagination', function () {
+
+    var nvl, container;
+
+    beforeEach(function () {
+      // helper for empty strings
+      nvl = function (str) {
+        return str === null ? "" : str;
+      };
+      container = document.createElement('div');
+      // create element individually to parameterize
+    });
+
+    afterEach(function () {
+      React.unmountComponentAtNode(container);
+    });
+
+    it("renders 20-wise pages per default", function () {
+      var pageEl = React.renderComponent(React.createElement(Views.Pagination, {page: 3, total: 55, urlPrefix: "?prefix=", urlSuffix: "&suffix=88"}), container);
+      var lis = pageEl.getDOMNode().getElementsByTagName("li");
+      assert.equal(1 + 3 + 1, lis.length);
+      assert(nvl(lis[0].getAttribute("class")).indexOf("disabled") < 0);
+      assert(nvl(lis[1].getAttribute("class")).indexOf("active") < 0);
+      assert(nvl(lis[2].getAttribute("class")).indexOf("active") < 0);
+      assert(nvl(lis[3].getAttribute("class")).indexOf("active") >= 0);
+      assert(nvl(lis[4].getAttribute("class")).indexOf("disabled") >= 0);
+      assert.equal("2", lis[2].innerText);
+      assert.equal("?prefix=2&suffix=88", lis[2].getElementsByTagName("a")[0].getAttribute("href"));
+    });
+
+    it("can overwrite collection size", function () {
+      var pageEl = React.renderComponent(React.createElement(Views.Pagination, {perPage: 10, page: 3, total: 55}), container);
+      var lis = pageEl.getDOMNode().getElementsByTagName("li");
+      assert.equal(1 + 6 + 1, lis.length);
+    });
+
+    it("handles large collections properly - beginning", function () {
+      var pageEl = React.renderComponent(React.createElement(Views.Pagination, {page: 3, total: 600}), container);
+      var lis = pageEl.getDOMNode().getElementsByTagName("li");
+      assert.equal(1 + 10 + 1, lis.length);
+      assert(nvl(lis[3].getAttribute("class")).indexOf("active") >= 0);
+      assert.equal("3", lis[3].innerText);
+      assert.equal("7", lis[7].innerText);
+      assert.equal("10", lis[10].innerText);
+    });
+
+    it("handles large collections properly - middle", function () {
+      var pageEl = React.renderComponent(React.createElement(Views.Pagination, {page: 10, total: 600}), container);
+      var lis = pageEl.getDOMNode().getElementsByTagName("li");
+      assert.equal(1 + 10 + 1, lis.length);
+      assert(nvl(lis[6].getAttribute("class")).indexOf("active") >= 0);
+      assert.equal("7", lis[3].innerText);
+      assert.equal("11", lis[7].innerText);
+      assert.equal("14", lis[10].innerText);
+    });
+
+    it("handles large collections properly - end", function () {
+      var pageEl = React.renderComponent(React.createElement(Views.Pagination, {page: 29, total: 600}), container);
+      var lis = pageEl.getDOMNode().getElementsByTagName("li");
+      assert.equal(1 + 10 + 1, lis.length);
+      assert(nvl(lis[9].getAttribute("class")).indexOf("active") >= 0);
+      assert.equal("23", lis[3].innerText);
+      assert.equal("27", lis[7].innerText);
+      assert.equal("30", lis[10].innerText);
+    });
+
+  });
+
+});
+


Mime
View raw message