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 <[email protected]> Authored: Thu Apr 9 16:35:39 2015 +0200 Committer: sebastianrothbucher <[email protected]> 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>  + <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}>«</a> + </li> + {rangeItems} + <li className={(page < totalPages ? null : "disabled")}> + <a href={prefix + Math.min(page + 1, totalPages) + suffix}>»</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); + }); + + }); + +}); +
