[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;
}