This is an automated email from the ASF dual-hosted git repository. amaranhao pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git
The following commit(s) were added to refs/heads/master by this push: new 55d3118 [partitioned-dbs] Support create and list databases (#1125) 55d3118 is described below commit 55d31186d1a89ddc63575adcca006e8d93906e64 Author: Antonio Maranhao <30349380+antonio-maran...@users.noreply.github.com> AuthorDate: Mon Sep 24 13:24:40 2018 -0400 [partitioned-dbs] Support create and list databases (#1125) * Support create and list partitioned databases --- app/addons/databases/__tests__/components.test.js | 79 ++++++++--- .../databases/__tests__/databasepagination.test.js | 2 +- app/addons/databases/actions.js | 31 +++- app/addons/databases/actiontypes.js | 4 +- app/addons/databases/base.js | 20 +++ app/addons/databases/components.js | 158 +++++++++++++++++---- app/addons/databases/resources.js | 9 ++ app/addons/databases/stores.js | 19 ++- 8 files changed, 263 insertions(+), 59 deletions(-) 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 React from "react"; 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 @@ describe('DatabaseTable', () => { 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 @@ describe('DatabaseTable', () => { 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 @@ describe('DatabaseTable', () => { 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 @@ describe('DatabaseTable', () => { 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 app from "../../app"; 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 @@ export default { }); }, - 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 @@ export default { } }); - 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 @@ export default { 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 @@ export default { }); 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 @@ export default { 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 @@ Databases.initialize = function () { 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 FauxtonAPI from "../../core/api"; 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 @@ class DatabasesController extends React.Component { return { dbList: databasesStore.getDbList(), loading: databasesStore.isLoading(), - showDeleteDatabaseModal: deleteDbModalStore.getShowDeleteDatabaseModal() + showDeleteDatabaseModal: deleteDbModalStore.getShowDeleteDatabaseModal(), + showPartitionedColumn: databasesStore.isPartitionedDatabasesAvailable() }; }; @@ -63,7 +64,8 @@ class DatabasesController extends React.Component { <DatabaseTable showDeleteDatabaseModal={this.state.showDeleteDatabaseModal} dbList={dbList} - loading={loading} /> + loading={loading} + showPartitionedColumn={this.state.showPartitionedColumn} /> ); } } @@ -73,12 +75,13 @@ class DatabaseTable extends React.Component { 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 @@ class DatabaseTable extends React.Component { <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 DatabaseTable extends React.Component { 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 @@ class DatabaseRow extends React.Component { 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 @@ class DatabaseRow extends React.Component { </tr> ); } - + const partitionedCol = this.props.showPartitionedColumn ? + (<td>{isPartitioned ? 'Yes' : 'No'}</td>) : + null; return ( <tr> <td> @@ -172,6 +189,7 @@ class DatabaseRow extends React.Component { </td> <td>{dataSize}</td> <td>{docCount} {tombStoneWarning}</td> + {partitionedCol} {this.getExtensionColumns(item)} <td className="database-actions"> @@ -202,46 +220,123 @@ const GraveyardInfo = ({docCount, docDelCount}) => { ); }; -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 = () => { - this.setState({isPromptVisible: !this.state.isPromptVisible}); + static propTypes = { + showPartitionedOption: PropTypes.bool.isRequired }; - closeTray = () => { + 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 () { 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 @@ class AddDatabaseWidget extends React.Component { 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.DocLimit = 100; 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 @@ Databases.Model = FauxtonAPI.Model.extend({ } 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 @@ const DatabasesStoreConstructor = FauxtonAPI.Store.extend({ this._databaseDetails = []; this._failedDbs = []; this._fullDbList = []; + + this._partitionedDatabasesAvailable = false; }, getPage: function () { @@ -54,11 +56,13 @@ const DatabasesStoreConstructor = FauxtonAPI.Store.extend({ 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 @@ const DatabasesStoreConstructor = FauxtonAPI.Store.extend({ 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 @@ const DatabasesStoreConstructor = FauxtonAPI.Store.extend({ this.setLoading(false); break; + case ActionTypes.DATABASES_PARTITIONED_DB_AVAILABLE: + this._partitionedDatabasesAvailable = action.options.available; + break; + default: return; }