[partitioned-dbs] Support create and list databases (#1125)

* Support create and list partitioned databases
diff --git a/app/addons/databases/__tests__/components.test.js b/app/addons/databases/__tests__/components.test.js
index 1c0bc5b..e2e3a5e 100644
--- a/app/addons/databases/__tests__/components.test.js
+++ b/app/addons/databases/__tests__/components.test.js
@@ -10,7 +10,6 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 import FauxtonAPI from "../../../core/api";
-import Views from "../components";
 import Actions from "../actions";
 import Stores from "../stores";
 import utils from "../../../../test/mocha/testUtils";
@@ -18,37 +17,35 @@
 import ReactDOM from "react-dom";
 import { mount } from 'enzyme';
 import sinon from 'sinon';
+import Views from "../components";
 
 const assert = utils.assert;
 
 const store = Stores.databasesStore;
 
 describe('AddDatabaseWidget', () => {
-  let oldCreateNewDatabase;
-  let createCalled, passedDbName;
 
   beforeEach(() => {
-    oldCreateNewDatabase = Actions.createNewDatabase;
-    Actions.createNewDatabase = function (dbName) {
-      createCalled = true;
-      passedDbName = dbName;
-    };
+    sinon.stub(Actions, 'createNewDatabase');
   });
 
   afterEach(() => {
-    Actions.createNewDatabase = oldCreateNewDatabase;
+    Actions.createNewDatabase.restore();
   });
 
-  it("Creates a database with given name", () => {
-    createCalled = false;
-    passedDbName = null;
+  it("creates a database with given name", () => {
     const el = mount(<Views.AddDatabaseWidget />);
     el.setState({databaseName: 'my-db'});
     el.instance().onAddDatabase();
-    assert.equal(true, createCalled);
-    assert.equal("my-db", passedDbName);
+    sinon.assert.calledWith(Actions.createNewDatabase, 'my-db');
   });
 
+  it('creates a partitioned database', () => {
+    const el = mount(<Views.AddDatabaseWidget showPartitionedOption={true}/>);
+    el.setState({databaseName: 'my-db', partitionedSelected: true});
+    el.instance().onAddDatabase();
+    sinon.assert.calledWith(Actions.createNewDatabase, 'my-db', true);
+  });
 });
 
 
@@ -123,7 +120,7 @@
     FauxtonAPI.registerExtension('DatabaseTable:head', ColHeader3);
 
     var table = mount(
-      <Views.DatabaseTable showDeleteDatabaseModal={{showModal: false}} loading={false} dbList={[]} />
+      <Views.DatabaseTable showDeleteDatabaseModal={{showModal: false}} loading={false} dbList={[]} showPartitionedColumn={false}/>
     );
     var cols = table.find('th');
 
@@ -150,7 +147,7 @@
     const list = store.getDbList();
 
     var databaseRow = mount(
-      <Views.DatabaseTable showDeleteDatabaseModal={{showModal: false}} dbList={list} loading={false}/>
+      <Views.DatabaseTable showDeleteDatabaseModal={{showModal: false}} dbList={list} loading={false} showPartitionedColumn={false}/>
     );
     var links = databaseRow.find('td');
 
@@ -173,7 +170,7 @@
     const list = store.getDbList();
 
     var databaseRow = mount(
-      <Views.DatabaseTable showDeleteDatabaseModal={{showModal: false}} dbList={list} loading={false} />
+      <Views.DatabaseTable showDeleteDatabaseModal={{showModal: false}} dbList={list} loading={false} showPartitionedColumn={false}/>
     );
     assert.equal(databaseRow.find('.database-load-fail').length, 1);
   });
@@ -189,9 +186,55 @@
     const list = store.getDbList();
 
     var databaseRow = mount(
-      <Views.DatabaseTable showDeleteDatabaseModal={{showModal: false}} dbList={list} loading={false} />
+      <Views.DatabaseTable showDeleteDatabaseModal={{showModal: false}} dbList={list} loading={false} showPartitionedColumn={false}/>
     );
 
     assert.equal(databaseRow.find('.database-load-fail').length, 0);
   });
+
+  it('shows Partitioned column only when prop is set to true', () => {
+    Actions.updateDatabases({
+      dbList: ['db1'],
+      databaseDetails: [{db_name: 'db1', doc_count: 0, doc_del_count: 0, props: {partitioned: true}}],
+      failedDbs: [],
+      fullDbList: ['db1']
+    });
+
+    const list = store.getDbList();
+
+    const withPartColumn = mount(
+      <Views.DatabaseTable showDeleteDatabaseModal={{showModal: false}} dbList={list} loading={false} showPartitionedColumn={true}/>
+    );
+    const colHeaders = withPartColumn.find('th');
+    assert.equal(colHeaders.length, 5);
+    assert.equal(colHeaders.get(3).props.children, 'Partitioned');
+
+    const withoutPartColumn = mount(
+      <Views.DatabaseTable showDeleteDatabaseModal={{showModal: false}} dbList={list} loading={false} showPartitionedColumn={false}/>
+    );
+    assert.equal(withoutPartColumn.find('th').length, 4);
+  });
+
+  it('shows correct values in the Partitioned column', () => {
+    Actions.updateDatabases({
+      dbList: ['db1', 'db2'],
+      databaseDetails: [
+        {db_name: 'db1', doc_count: 1, doc_del_count: 0, props: {partitioned: true}},
+        {db_name: 'db2', doc_count: 2, doc_del_count: 0, props: {partitioned: false}}
+      ],
+      failedDbs: [],
+      fullDbList: ['db1', 'db2']
+    });
+
+    const list = store.getDbList();
+
+    const dbTable = mount(
+      <Views.DatabaseTable showDeleteDatabaseModal={{showModal: false}} dbList={list} loading={false} showPartitionedColumn={true}/>
+    );
+    const colCells = dbTable.find('td');
+    // 2 rows with 5 cells each
+    assert.equal(colCells.length, 10);
+    assert.equal(colCells.get(3).props.children, 'Yes');
+    assert.equal(colCells.get(8).props.children, 'No');
+  });
 });
diff --git a/app/addons/databases/__tests__/databasepagination.test.js b/app/addons/databases/__tests__/databasepagination.test.js
index 6ec9f59..2777680 100644
--- a/app/addons/databases/__tests__/databasepagination.test.js
+++ b/app/addons/databases/__tests__/databasepagination.test.js
@@ -13,10 +13,10 @@
 import Stores from "../stores";
 import React from 'react';
 import ReactDOM from 'react-dom';
-import DatabaseComponents from "../components";
 import "../../documents/base";
 import DatabaseActions from "../actions";
 import {mount} from 'enzyme';
+import DatabaseComponents from "../components";
 
 const store = Stores.databasesStore;
 
diff --git a/app/addons/databases/actions.js b/app/addons/databases/actions.js
index 5e21781..6b1c570 100644
--- a/app/addons/databases/actions.js
+++ b/app/addons/databases/actions.js
@@ -13,6 +13,7 @@
 import Helpers from "../../helpers";
 import FauxtonAPI from "../../core/api";
 import { get } from "../../core/ajax";
+import DatabasesBase from '../databases/base';
 import Stores from "./stores";
 import ActionTypes from "./actiontypes";
 import Resources from "./resources";
@@ -126,7 +127,7 @@
     });
   },
 
-  createNewDatabase: function (databaseName) {
+  createNewDatabase: function (databaseName, partitioned) {
     if (_.isNull(databaseName) || databaseName.trim().length === 0) {
       FauxtonAPI.addNotification({
         msg: 'Please enter a valid database name',
@@ -144,7 +145,7 @@
       }
     });
 
-    var db = Stores.databasesStore.obtainNewDatabaseModel(databaseName);
+    const db = Stores.databasesStore.obtainNewDatabaseModel(databaseName, partitioned);
     FauxtonAPI.addNotification({ msg: 'Creating database.' });
     db.save().done(function () {
       FauxtonAPI.addNotification({
@@ -152,11 +153,11 @@
         type: 'success',
         clear: true
       });
-      var route = FauxtonAPI.urls('allDocs', 'app', app.utils.safeURLName(databaseName), '?limit=' + Resources.DocLimit);
+      const route = FauxtonAPI.urls('allDocs', 'app', app.utils.safeURLName(databaseName), '?limit=' + Resources.DocLimit);
       app.router.navigate(route, { trigger: true });
     }
     ).fail(function (xhr) {
-      var responseText = JSON.parse(xhr.responseText).reason;
+      const responseText = JSON.parse(xhr.responseText).reason;
       FauxtonAPI.addNotification({
         msg: 'Create database failed: ' + responseText,
         type: 'error',
@@ -195,5 +196,27 @@
       });
       callback(null, { options: options });
     });
+  },
+
+  setPartitionedDatabasesAvailable(available) {
+    FauxtonAPI.dispatch({
+      type: ActionTypes.DATABASES_PARTITIONED_DB_AVAILABLE,
+      options: {
+        available
+      }
+    });
+  },
+
+  checkPartitionedQueriesIsAvailable() {
+    const exts = FauxtonAPI.getExtensions(DatabasesBase.PARTITONED_DB_CHECK_EXTENSION);
+    let promises = exts.map(checkFunction => {
+      return checkFunction();
+    });
+    FauxtonAPI.Promise.all(promises).then(results => {
+      const isAvailable = results.every(check => check === true);
+      this.setPartitionedDatabasesAvailable(isAvailable);
+    }).catch(() => {
+      // ignore as the default is false
+    });
   }
 };
diff --git a/app/addons/databases/actiontypes.js b/app/addons/databases/actiontypes.js
index e9665c0..e8d3a53 100644
--- a/app/addons/databases/actiontypes.js
+++ b/app/addons/databases/actiontypes.js
@@ -14,6 +14,6 @@
   DATABASES_SET_PROMPT_VISIBLE: 'DATABASES_SET_PROMPT_VISIBLE',
   DATABASES_STARTLOADING: 'DATABASES_STARTLOADING',
   DATABASES_LOADCOMPLETE: 'DATABASES_LOADCOMPLETE',
-
-  DATABASES_UPDATE: 'DATABASES_UPDATE'
+  DATABASES_UPDATE: 'DATABASES_UPDATE',
+  DATABASES_PARTITIONED_DB_AVAILABLE: 'DATABASES_PARTITIONED_DB_AVAILABLE'
 };
diff --git a/app/addons/databases/base.js b/app/addons/databases/base.js
index 72d50e3..92f2e75 100644
--- a/app/addons/databases/base.js
+++ b/app/addons/databases/base.js
@@ -12,8 +12,10 @@
 
 import app from "../../app";
 import Helpers from "../../helpers";
+import { get } from "../../core/ajax";
 import FauxtonAPI from "../../core/api";
 import Databases from "./routes";
+import Actions from "./actions";
 import "./assets/less/databases.less";
 
 Databases.initialize = function () {
@@ -23,8 +25,26 @@
     icon: "fonticon-database",
     className: 'databases'
   });
+  Actions.checkPartitionedQueriesIsAvailable();
 };
 
+function checkPartitionedDatabaseFeature () {
+  // Checks if the CouchDB server supports Partitioned Databases
+  return get(Helpers.getServerUrl("/")).then((couchdb) => {
+    //TODO: needs to be updated with the correct feature name
+    return couchdb.features && couchdb.features.includes('partitioned-dbs');
+  }).catch(() => {
+    return false;
+  });
+}
+
+// This extension can be used by addons to add extra checks when
+// deciding if the partitioned database feature should be enabled.
+// The registered element should be a function that returns a
+// Promise resolving to either true or false.
+Databases.PARTITONED_DB_CHECK_EXTENSION = 'Databases:PartitionedDbCheck';
+FauxtonAPI.registerExtension(Databases.PARTITONED_DB_CHECK_EXTENSION, checkPartitionedDatabaseFeature);
+
 // Utility functions
 Databases.databaseUrl = function (database) {
   var name = _.isObject(database) ? database.id : database,
diff --git a/app/addons/databases/components.js b/app/addons/databases/components.js
index b3a69ed..203ea49 100644
--- a/app/addons/databases/components.js
+++ b/app/addons/databases/components.js
@@ -14,7 +14,7 @@
 
 import PropTypes from 'prop-types';
 
-import React from "react";
+import React, { Fragment } from "react";
 import ReactDOM from "react-dom";
 import Components from "../components/react-components";
 import ComponentsStore from "../components/stores";
@@ -36,7 +36,8 @@
     return {
       dbList: databasesStore.getDbList(),
       loading: databasesStore.isLoading(),
-      showDeleteDatabaseModal: deleteDbModalStore.getShowDeleteDatabaseModal()
+      showDeleteDatabaseModal: deleteDbModalStore.getShowDeleteDatabaseModal(),
+      showPartitionedColumn: databasesStore.isPartitionedDatabasesAvailable()
     };
   };
 
@@ -63,7 +64,8 @@
       <DatabaseTable
         showDeleteDatabaseModal={this.state.showDeleteDatabaseModal}
         dbList={dbList}
-        loading={loading} />
+        loading={loading}
+        showPartitionedColumn={this.state.showPartitionedColumn} />
     );
   }
 }
@@ -73,12 +75,13 @@
     dbList: PropTypes.array.isRequired,
     showDeleteDatabaseModal: PropTypes.object.isRequired,
     loading: PropTypes.bool.isRequired,
+    showPartitionedColumn: PropTypes.bool.isRequired
   };
 
   createRows = (dbList) => {
     return dbList.map((item, k) => {
       return (
-        <DatabaseRow item={item} key={k} />
+        <DatabaseRow item={item} key={k} showPartitionedColumn={this.props.showPartitionedColumn}/>
       );
     });
   };
@@ -117,6 +120,7 @@
               <th>Name</th>
               <th>Size</th>
               <th># of Docs</th>
+              {this.props.showPartitionedColumn ? (<th>Partitioned</th>) : null}
               {this.getExtensionColumns()}
               <th>Actions</th>
             </tr>
@@ -132,7 +136,18 @@
 
 class DatabaseRow extends React.Component {
   static propTypes = {
-    row: PropTypes.object
+    item: PropTypes.shape({
+      id: PropTypes.string.isRequired,
+      encodedId: PropTypes.string.isRequired,
+      url: PropTypes.string.isRequired,
+      failed: PropTypes.bool.isRequired,
+      dataSize: PropTypes.string,
+      docCount: PropTypes.number,
+      docDelCount: PropTypes.number,
+      isPartitioned: PropTypes.bool,
+      showTombstoneWarning: PropTypes.bool
+    }).isRequired,
+    showPartitionedColumn: PropTypes.bool.isRequired
   };
 
   getExtensionColumns = (row) => {
@@ -151,7 +166,7 @@
       item
     } = this.props;
 
-    const {encodedId, id, url, dataSize, docCount, docDelCount, showTombstoneWarning, failed } = item;
+    const {encodedId, id, url, dataSize, docCount, docDelCount, showTombstoneWarning, failed, isPartitioned } = item;
     const tombStoneWarning = showTombstoneWarning ?
       (<GraveyardInfo docCount={docCount} docDelCount={docDelCount} />) : null;
 
@@ -164,7 +179,9 @@
         </tr>
       );
     }
-
+    const partitionedCol = this.props.showPartitionedColumn ?
+      (<td>{isPartitioned ? 'Yes' : 'No'}</td>) :
+      null;
     return (
       <tr>
         <td>
@@ -172,6 +189,7 @@
         </td>
         <td>{dataSize}</td>
         <td>{docCount} {tombStoneWarning}</td>
+        {partitionedCol}
         {this.getExtensionColumns(item)}
 
         <td className="database-actions">
@@ -202,46 +220,123 @@
   );
 };
 
-const RightDatabasesHeader = () => {
-  return (
-    <div className="header-right right-db-header flex-layout flex-row">
-      <JumpToDatabaseWidget loadOptions={Actions.fetchAllDbsWithKey} />
-      <AddDatabaseWidget />
-    </div>
-  );
-};
+class RightDatabasesHeader extends React.Component {
+
+  constructor(props) {
+    super(props);
+    this.state = this.getStoreState();
+  }
+
+  getStoreState () {
+    return {
+      showPartitionedOption: databasesStore.isPartitionedDatabasesAvailable()
+    };
+  }
+
+  componentDidMount() {
+    databasesStore.on('change', this.onChange, this);
+  }
+
+  componentWillUnmount() {
+    databasesStore.off('change', this.onChange, this);
+  }
+
+  onChange () {
+    this.setState(this.getStoreState());
+  }
+
+  render() {
+    return (
+      <div className="header-right right-db-header flex-layout flex-row">
+        <JumpToDatabaseWidget loadOptions={Actions.fetchAllDbsWithKey} />
+        <AddDatabaseWidget showPartitionedOption={this.state.showPartitionedOption}/>
+      </div>
+    );
+  }
+}
 
 class AddDatabaseWidget extends React.Component {
-  state = {
-    isPromptVisible: false,
-    databaseName: ''
+  static defaultProps = {
+    showPartitionedOption: false
   };
 
-  onTrayToggle = () => {
+  static propTypes = {
+    showPartitionedOption: PropTypes.bool.isRequired
+  };
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      isPromptVisible: false,
+      databaseName: '',
+      partitionedSelected: false
+    };
+
+    this.onTrayToggle = this.onTrayToggle.bind(this);
+    this.closeTray = this.closeTray.bind(this);
+    this.focusInput = this.focusInput.bind(this);
+    this.onKeyUpInInput = this.onKeyUpInInput.bind(this);
+    this.onChange = this.onChange.bind(this);
+    this.onAddDatabase = this.onAddDatabase.bind(this);
+    this.onTogglePartitioned = this.onTogglePartitioned.bind(this);
+  }
+
+  onTrayToggle () {
     this.setState({isPromptVisible: !this.state.isPromptVisible});
-  };
+  }
 
-  closeTray = () => {
+  closeTray () {
     this.setState({isPromptVisible: false});
-  };
+  }
 
-  focusInput = () => {
+  focusInput () {
     this.newDbName.focus();
-  };
+  }
 
-  onKeyUpInInput = (e) => {
+  onKeyUpInInput (e) {
     if (e.which === 13) {
       this.onAddDatabase();
     }
-  };
+  }
 
-  onChange = (e) => {
+  onChange (e) {
     this.setState({databaseName: e.target.value});
-  };
+  }
 
-  onAddDatabase = () => {
-    Actions.createNewDatabase(this.state.databaseName);
-  };
+  onAddDatabase () {
+    const partitioned = this.props.showPartitionedOption ?
+      this.state.partitionedSelected :
+      undefined;
+
+    Actions.createNewDatabase(
+      this.state.databaseName,
+      partitioned
+    );
+  }
+
+  onTogglePartitioned() {
+    this.setState({ partitionedSelected: !this.state.partitionedSelected });
+  }
+
+  partitionedCheckobx() {
+    if (!this.props.showPartitionedOption) {
+      return null;
+    }
+    return (
+      <Fragment>
+        <br/>
+        <label style={{margin: '10px 10px 0px 0px'}}>
+          <input
+            id="js-partitioned-db"
+            type="checkbox"
+            checked={this.state.partitionedSelected}
+            onChange={this.onTogglePartitioned}
+            style={{margin: '0px 10px 0px 0px'}} />
+          Partitioned
+        </label>
+      </Fragment>
+    );
+  }
 
   render() {
     return (
@@ -265,6 +360,7 @@
             placeholder="Name of database"
           />
           <a className="btn" id="js-create-database" onClick={this.onAddDatabase}>Create</a>
+          { this.partitionedCheckobx() }
         </TrayContents>
       </div>
     );
diff --git a/app/addons/databases/resources.js b/app/addons/databases/resources.js
index ae379c4..4685322 100644
--- a/app/addons/databases/resources.js
+++ b/app/addons/databases/resources.js
@@ -20,6 +20,12 @@
 
 Databases.Model = FauxtonAPI.Model.extend({
 
+  partitioned: false,
+
+  setPartitioned: function (partitioned) {
+    this.partitioned = partitioned;
+  },
+
   documentation: function () {
     return FauxtonAPI.constants.DOC_URLS.ALL_DBS;
   },
@@ -56,6 +62,9 @@
     } else if (context === "app") {
       return "/database/" + this.safeID();
     }
+    if (this.partitioned) {
+      return Helpers.getServerUrl("/" + this.safeID()) + '?partitioned=true';
+    }
     return Helpers.getServerUrl("/" + this.safeID());
 
   },
diff --git a/app/addons/databases/stores.js b/app/addons/databases/stores.js
index 6884669..00befb0 100644
--- a/app/addons/databases/stores.js
+++ b/app/addons/databases/stores.js
@@ -32,6 +32,8 @@
     this._databaseDetails = [];
     this._failedDbs = [];
     this._fullDbList = [];
+
+    this._partitionedDatabasesAvailable = false;
   },
 
   getPage: function () {
@@ -54,11 +56,13 @@
     this._promptVisible = promptVisible;
   },
 
-  obtainNewDatabaseModel: function (databaseName) {
-    return new Database({
+  obtainNewDatabaseModel: function (databaseName, partitioned) {
+    const dbModel = new Database({
       id: databaseName,
       name: databaseName
     });
+    dbModel.setPartitioned(partitioned);
+    return dbModel;
   },
 
   doesDatabaseExist: function (databaseName) {
@@ -95,10 +99,15 @@
       dataSize: Helpers.formatSize(dataSize),
       docCount: details.doc_count,
       docDelCount: details.doc_del_count,
-      showTombstoneWarning: details.doc_del_count > details.doc_count
+      showTombstoneWarning: details.doc_del_count > details.doc_count,
+      isPartitioned: details.props && details.props.partitioned === true
     };
   },
 
+  isPartitionedDatabasesAvailable: function () {
+    return this._partitionedDatabasesAvailable;
+  },
+
   dispatch: function (action) {
     switch (action.type) {
       case ActionTypes.DATABASES_SETPAGE:
@@ -125,6 +134,10 @@
         this.setLoading(false);
         break;
 
+      case ActionTypes.DATABASES_PARTITIONED_DB_AVAILABLE:
+        this._partitionedDatabasesAvailable = action.options.available;
+        break;
+
       default:
         return;
     }