Repository: couchdb-fauxton Updated Branches: refs/heads/master df5010b3c -> a34579ad8
Changes auto-update option added This PR adds a "Auto-update changes list" option on the Changes page, within the Filters tab. When enabled, it will automatically update the page with whatever changes occur on the database. Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/a34579ad Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/a34579ad Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/a34579ad Branch: refs/heads/master Commit: a34579ad82476849de2dcaaad3a64b3cfef64d87 Parents: df5010b Author: Ben Keen <[email protected]> Authored: Thu Apr 2 12:10:25 2015 -0700 Committer: Ben Keen <[email protected]> Committed: Tue Apr 14 13:12:40 2015 -0700 ---------------------------------------------------------------------- app/addons/documents/assets/less/changes.less | 17 ++++- app/addons/documents/changes/actions.js | 67 ++++++++++++++++--- app/addons/documents/changes/actiontypes.js | 6 +- .../documents/changes/components.react.jsx | 33 +++++++--- app/addons/documents/changes/stores.js | 69 ++++++++++++++++---- app/addons/documents/helpers.js | 6 ++ app/addons/documents/routes-documents.js | 10 +-- .../tests/changes.componentsSpec.react.jsx | 41 ++++++------ .../documents/tests/changes.storesSpec.js | 32 +++++---- .../documents/tests/nightwatch/changes.js | 31 +++++++++ assets/less/animations.less | 18 ++++- assets/less/variables.less | 1 + 12 files changed, 256 insertions(+), 75 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a34579ad/app/addons/documents/assets/less/changes.less ---------------------------------------------------------------------- diff --git a/app/addons/documents/assets/less/changes.less b/app/addons/documents/assets/less/changes.less index 37fcad1..9c883c8 100644 --- a/app/addons/documents/assets/less/changes.less +++ b/app/addons/documents/assets/less/changes.less @@ -70,7 +70,7 @@ .keyframes(slideDownChangesFilter, { opacity: 0; - height: 0px; + height: 0; }, { opacity: 1; @@ -83,7 +83,7 @@ }, { opacity: 0; - height: 0px; + height: 0; }); .toggle-changes-filter-enter { @@ -106,3 +106,16 @@ margin-left: 20px; } +.changes-polling { + float: right; + input { + margin: 0 8px 1px; + } + label { + display: inline-block; + } +} + +.new-change-row { + -webkit-animation: slideDown 1.5s both, highlight-element 2.0s 1; +} http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a34579ad/app/addons/documents/changes/actions.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/changes/actions.js b/app/addons/documents/changes/actions.js index d94da66..447833c 100644 --- a/app/addons/documents/changes/actions.js +++ b/app/addons/documents/changes/actions.js @@ -13,9 +13,16 @@ define([ 'app', 'api', - 'addons/documents/changes/actiontypes' + 'addons/documents/changes/actiontypes', + 'addons/documents/changes/stores', + 'addons/documents/helpers' ], -function (app, FauxtonAPI, ActionTypes) { +function (app, FauxtonAPI, ActionTypes, Stores, Helpers) { + + var changesStore = Stores.changesStore; + var pollingTimeout = 60000; + var currentRequest; + return { toggleTabVisibility: function () { @@ -38,18 +45,60 @@ function (app, FauxtonAPI, ActionTypes) { }); }, - setChanges: function (options) { + initChanges: function (options) { FauxtonAPI.dispatch({ - type: ActionTypes.SET_CHANGES, + type: ActionTypes.INIT_CHANGES, options: options }); + currentRequest = null; + this.getLatestChanges(); + }, + + getLatestChanges: function () { + var params = { + limit: 100 + }; + + // after the first request for the changes list has been made, switch to longpoll + if (currentRequest) { + params.since = changesStore.getLastSeqNum(); + params.timeout = pollingTimeout; + params.feed = 'longpoll'; + } + + var query = $.param(params); + var db = app.utils.safeURLName(changesStore.getDatabaseName()); + currentRequest = $.get('/' + db + '/_changes?' + query); + currentRequest.then(_.bind(this.updateChanges, this)); }, - fetchChanges: function (options) { - var changes = options.changes; - changes.fetch().then(function () { - this.setChanges(options); - }.bind(this)); + updateChanges: function (resp) { + var json = JSON.parse(resp); + + // only bother updating the list of changes if the seq num has changed + var latestSeqNum = Helpers.getSeqNum(json.last_seq); + if (latestSeqNum !== changesStore.getLastSeqNum()) { + FauxtonAPI.dispatch({ + type: ActionTypes.UPDATE_CHANGES, + changes: json.results, + seqNum: latestSeqNum + }); + } + + if (changesStore.pollingEnabled()) { + this.getLatestChanges(); + } + }, + + togglePolling: function () { + FauxtonAPI.dispatch({ type: ActionTypes.TOGGLE_CHANGES_POLLING }); + + // the user just enabled polling. Start 'er up + if (changesStore.pollingEnabled()) { + this.getLatestChanges(); + } else { + currentRequest.abort(); + } } }; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a34579ad/app/addons/documents/changes/actiontypes.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/changes/actiontypes.js b/app/addons/documents/changes/actiontypes.js index 7f91ae6..edca4b9 100644 --- a/app/addons/documents/changes/actiontypes.js +++ b/app/addons/documents/changes/actiontypes.js @@ -12,11 +12,13 @@ define([], function () { return { - SET_CHANGES: 'SET_CHANGES', + INIT_CHANGES: 'INIT_CHANGES', + UPDATE_CHANGES: 'UPDATE_CHANGES', TOGGLE_CHANGES_TAB_VISIBILITY: 'TOGGLE_CHANGES_TAB_VISIBILITY', ADD_CHANGES_FILTER_ITEM: 'ADD_CHANGES_FILTER_ITEM', REMOVE_CHANGES_FILTER_ITEM: 'REMOVE_CHANGES_FILTER_ITEM', UPDATE_CHANGES_FILTER: 'UPDATE_CHANGES_FILTER', - TOGGLE_CHANGES_CODE_VISIBILITY: 'TOGGLE_CHANGES_CODE_VISIBILITY' + TOGGLE_CHANGES_CODE_VISIBILITY: 'TOGGLE_CHANGES_CODE_VISIBILITY', + TOGGLE_CHANGES_POLLING: 'TOGGLE_CHANGES_POLLING' }; }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a34579ad/app/addons/documents/changes/components.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/documents/changes/components.react.jsx b/app/addons/documents/changes/components.react.jsx index 69ca6f7..c440e71 100644 --- a/app/addons/documents/changes/components.react.jsx +++ b/app/addons/documents/changes/components.react.jsx @@ -54,7 +54,7 @@ define([ render: function () { var tabContent = ''; if (this.state.showTabContent) { - tabContent = <ChangesFilter key="changesFilterSection" />; + tabContent = <ChangesTabContent key="changesFilterSection" />; } return ( @@ -90,10 +90,11 @@ define([ }); - var ChangesFilter = React.createClass({ + var ChangesTabContent = React.createClass({ getStoreState: function () { return { - filters: changesStore.getFilters() + filters: changesStore.getFilters(), + pollingEnabled: changesStore.pollingEnabled() }; }, @@ -134,15 +135,30 @@ define([ return changesStore.hasFilter(filter); }, + togglePolling: function () { + Actions.togglePolling(); + }, + render: function () { return ( <div className="tab-content"> <div className="tab-pane active" ref="filterTab"> <div className="changes-header js-filter"> + <div className="changes-polling"> + <input + type="checkbox" + id="changes-toggle-polling" + checked={this.state.pollingEnabled} + onChange={this.togglePolling} + /> + <label htmlFor="changes-toggle-polling">Auto-update changes list</label> + </div> <AddFilterForm tooltip={this.props.tooltip} filter={this.state.filter} addFilter={this.addFilter} hasFilter={this.hasFilter} /> <ul className="filter-list">{this.getFilters()}</ul> </div> + <div className="changes-auto-update"> + </div> </div> </div> ); @@ -308,7 +324,8 @@ define([ getRows: function () { return _.map(this.state.changes, function (change) { - return <ChangeRow change={change} key={change.id} databaseName={this.state.databaseName} />; + var key = change.id + '-' + change.seq; + return <ChangeRow change={change} key={key} databaseName={this.state.databaseName} />; }, this); }, @@ -358,10 +375,11 @@ define([ }, render: function () { - var jsonBtnClasses = "btn btn-small " + (this.state.codeVisible ? 'btn-secondary' : 'btn-primary'); + var jsonBtnClasses = 'btn btn-small' + (this.state.codeVisible ? ' btn-secondary' : ' btn-primary'); + var wrapperClass = 'change-wrapper' + (this.props.change.isNew ? ' new-change-row' : ''); return ( - <div className="change-wrapper"> + <div className={wrapperClass}> <div className="change-box" data-id={this.props.change.id}> <div className="row-fluid"> <div className="span2">seq</div> @@ -432,10 +450,9 @@ define([ React.unmountComponentAtNode(el); }, - // exposed for testing purposes only ChangesHeaderController: ChangesHeaderController, ChangesHeaderTab: ChangesHeaderTab, - ChangesFilter: ChangesFilter, + ChangesTabContent: ChangesTabContent, ChangesController: ChangesController, ChangeRow: ChangeRow }; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a34579ad/app/addons/documents/changes/stores.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/changes/stores.js b/app/addons/documents/changes/stores.js index 301e8c0..397854b 100644 --- a/app/addons/documents/changes/stores.js +++ b/app/addons/documents/changes/stores.js @@ -12,8 +12,9 @@ define([ 'api', - 'addons/documents/changes/actiontypes' -], function (FauxtonAPI, ActionTypes) { + 'addons/documents/changes/actiontypes', + 'addons/documents/helpers' +], function (FauxtonAPI, ActionTypes, Helpers) { var ChangesStore = FauxtonAPI.Store.extend({ @@ -28,20 +29,41 @@ define([ this._databaseName = ''; this._maxChangesListed = 100; this._showingSubset = false; + this._pollingEnabled = false; + this._lastSequenceNum = null; }, - setChanges: function (options) { - this._filters = options.filters; + initChanges: function (options) { + this.reset(); this._databaseName = options.databaseName; - this._changes = _.map(options.changes.models, function (change) { + }, + + updateChanges: function (seqNum, changes) { + + // make a note of the most recent sequence number. This is used for a point of reference for polling for new changes + this._lastSequenceNum = seqNum; + + // mark any additional changes that come after first page load as "new" so we can add a nice highlight effect + // when the new row is rendered + var firstBatch = this._changes.length === 0; + _.each(this._changes, function (change) { + change.isNew = false; + }); + + var newChanges = _.map(changes, function (change) { + var seq = Helpers.getSeqNum(change.seq); return { - id: change.get('id'), - seq: change.get('seq'), - deleted: change.get('deleted') ? change.get('deleted') : false, - changes: change.get('changes'), - doc: change.get('doc') // only populated with ?include_docs=true + id: change.id, + seq: seq, + deleted: _.has(change, 'deleted') ? change.deleted : false, + changes: change.changes, + doc: change.doc, // only populated with ?include_docs=true + isNew: !firstBatch }; }); + + // add the new changes to the start of the list + this._changes = newChanges.concat(this._changes); }, getChanges: function () { @@ -102,10 +124,29 @@ define([ this._maxChangesListed = num; }, + togglePolling: function () { + this._pollingEnabled = !this._pollingEnabled; + + // if polling was just enabled, reset the last sequence num to 'now' so only future changes will appear + this._lastSequenceNum = 'now'; + }, + + pollingEnabled: function () { + return this._pollingEnabled; + }, + + getLastSeqNum: function () { + return this._lastSequenceNum; + }, + dispatch: function (action) { switch (action.type) { - case ActionTypes.SET_CHANGES: - this.setChanges(action.options); + case ActionTypes.INIT_CHANGES: + this.initChanges(action.options); + this.triggerChange(); + break; + case ActionTypes.UPDATE_CHANGES: + this.updateChanges(action.seqNum, action.changes); this.triggerChange(); break; case ActionTypes.TOGGLE_CHANGES_TAB_VISIBILITY: @@ -120,6 +161,10 @@ define([ this.removeFilter(action.filter); this.triggerChange(); break; + case ActionTypes.TOGGLE_CHANGES_POLLING: + this.togglePolling(); + this.triggerChange(); + break; } } }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a34579ad/app/addons/documents/helpers.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/helpers.js b/app/addons/documents/helpers.js index 9197bf8..e04e3b4 100644 --- a/app/addons/documents/helpers.js +++ b/app/addons/documents/helpers.js @@ -33,5 +33,11 @@ define([ return previousPage; }; + + // sequence info is an array in couchdb2 with two indexes. On couch 1.x, it's just a string / number + Helpers.getSeqNum = function (val) { + return _.isArray(val) ? val[1] : val; + }; + return Helpers; }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a34579ad/app/addons/documents/routes-documents.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/routes-documents.js b/app/addons/documents/routes-documents.js index 652025e..695d991 100644 --- a/app/addons/documents/routes-documents.js +++ b/app/addons/documents/routes-documents.js @@ -160,17 +160,11 @@ function (app, FauxtonAPI, BaseRoute, Documents, Changes, ChangesActions, DocEdi }, changes: function () { - var docParams = app.getParams(); - this.database.buildChanges(docParams); - - ChangesActions.fetchChanges({ - changes: this.database.changes, - filters: [], + ChangesActions.initChanges({ databaseName: this.database.id }); - - this.setComponent("#dashboard-lower-content", Changes.ChangesController); this.setComponent('#dashboard-upper-content', Changes.ChangesHeaderController); + this.setComponent("#dashboard-lower-content", Changes.ChangesController); this.footer && this.footer.remove(); this.toolsView && this.toolsView.remove(); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a34579ad/app/addons/documents/tests/changes.componentsSpec.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/changes.componentsSpec.react.jsx b/app/addons/documents/tests/changes.componentsSpec.react.jsx index ecfb8bb..ecf3f35 100644 --- a/app/addons/documents/tests/changes.componentsSpec.react.jsx +++ b/app/addons/documents/tests/changes.componentsSpec.react.jsx @@ -78,12 +78,12 @@ define([ }); - describe('ChangesFilter', function () { + describe('ChangesTabContent', function () { var container, changesFilterEl; beforeEach(function () { container = document.createElement('div'); - changesFilterEl = TestUtils.renderIntoDocument(<Changes.ChangesFilter />, container); + changesFilterEl = TestUtils.renderIntoDocument(<Changes.ChangesTabContent />, container); }); afterEach(function () { @@ -195,24 +195,25 @@ define([ describe('ChangesController', function () { var containerEl, headerEl, $headerEl, changesEl, $changesEl; - var changesCollection = new Backbone.Collection([ + var results = [ { id: 'doc_1', seq: 4, deleted: false, changes: { code: 'here' } }, { id: 'doc_2', seq: 1, deleted: false, changes: { code: 'here' } }, { id: 'doc_3', seq: 6, deleted: true, changes: { code: 'here' } }, { id: 'doc_4', seq: 7, deleted: false, changes: { code: 'here' } }, { id: 'doc_5', seq: 1, deleted: true, changes: { code: 'here' } } - ]); + ]; + var changesResponse = JSON.stringify({ + last_seq: 123, + 'results': results + }); beforeEach(function () { - Actions.setChanges({ - changes: changesCollection, - filters: [], - databaseName: 'testDatabase' - }); + Actions.initChanges({ databaseName: 'testDatabase' }); headerEl = TestUtils.renderIntoDocument(<Changes.ChangesHeaderController />, containerEl); $headerEl = $(headerEl.getDOMNode()); changesEl = TestUtils.renderIntoDocument(<Changes.ChangesController />, containerEl); $changesEl = $(changesEl.getDOMNode()); + Actions.updateChanges(changesResponse); }); afterEach(function () { @@ -222,7 +223,7 @@ define([ it('should list the right number of changes', function () { - assert.equal(changesCollection.length, $changesEl.find('.change-box').length); + assert.equal(results.length, $changesEl.find('.change-box').length); }); @@ -293,20 +294,22 @@ define([ beforeEach(function () { - // to keep the test speedy, override the default value (1000) - Stores.changesStore.setMaxChanges(maxChanges); - var changes = []; _.times(maxChanges + 10, function (i) { - changes.push(new Backbone.Model({ id: 'doc_' + i, seq: 1, changes: { code: 'here' } })); + changes.push({ id: 'doc_' + i, seq: 1, changes: { code: 'here' } }); }); - var changesCollection = new Backbone.Collection(changes); - Actions.setChanges({ - changes: changesCollection, - filters: [], - databaseName: 'test' + var response = JSON.stringify({ + last_seq: 1, + results: changes }); + + Actions.initChanges({ databaseName: 'test' }); + + // to keep the test speedy, override the default value (1000) + Stores.changesStore.setMaxChanges(maxChanges); + + Actions.updateChanges(response); changesEl = TestUtils.renderIntoDocument(<Changes.ChangesController />, containerEl); }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a34579ad/app/addons/documents/tests/changes.storesSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/changes.storesSpec.js b/app/addons/documents/tests/changes.storesSpec.js index 36b313e..fedfb13 100644 --- a/app/addons/documents/tests/changes.storesSpec.js +++ b/app/addons/documents/tests/changes.storesSpec.js @@ -23,8 +23,6 @@ define([ describe('ChangesStore', function () { - var collection = new Backbone.Collection(); - afterEach(function () { Stores.changesStore.reset(); }); @@ -67,10 +65,9 @@ define([ assert.ok(Stores.changesStore.hasFilter(filter) === true); }); - it('getDatabaseName() returns database name', function () { var dbName = 'hoopoes'; - Stores.changesStore.setChanges({ databaseName: dbName, changes: collection }); + Stores.changesStore.initChanges({ databaseName: dbName }); assert.equal(Stores.changesStore.getDatabaseName(), dbName); Stores.changesStore.reset(); @@ -79,23 +76,32 @@ define([ it("getChanges() should return a subset if there are a lot of changes", function () { - // to keep the test speedy, override the default max value + // to keep the test speedy, we override the default max value var maxChanges = 10; - Stores.changesStore.setMaxChanges(maxChanges); - var changes = []; _.times(maxChanges + 10, function (i) { - changes.push(new Backbone.Model({ id: 'doc_' + i, seq: 1, changes: { } })); - }); - var changesCollection = new Backbone.Collection(changes); - Stores.changesStore.setChanges({ - changes: changesCollection, - databaseName: "test" + changes.push({ id: 'doc_' + i, seq: 1, changes: {}}); }); + Stores.changesStore.initChanges({ databaseName: "test" }); + Stores.changesStore.setMaxChanges(maxChanges); + + var seqNum = 123; + Stores.changesStore.updateChanges(seqNum, changes); var results = Stores.changesStore.getChanges(); assert.equal(maxChanges, results.length); }); + + it("tracks last sequence number", function () { + assert.equal(null, Stores.changesStore.getLastSeqNum()); + + var seqNum = 123; + Stores.changesStore.updateChanges(seqNum, []); + + // confirm it's been stored + assert.equal(seqNum, Stores.changesStore.getLastSeqNum()); + }); + }); }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a34579ad/app/addons/documents/tests/nightwatch/changes.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/changes.js b/app/addons/documents/tests/nightwatch/changes.js index 49c1527..41000d4 100644 --- a/app/addons/documents/tests/nightwatch/changes.js +++ b/app/addons/documents/tests/nightwatch/changes.js @@ -11,6 +11,7 @@ // the License. module.exports = { + 'Does not display the Select-all-button': function (client) { var waitTime = client.globals.maxWaitTime, newDatabaseName = client.globals.testDatabaseName, @@ -42,5 +43,35 @@ module.exports = { .click('.js-doc-link') .waitForElementPresent('#doc-editor-actions-panel', waitTime, false) .end(); + }, + + 'Check auto-update feature': function (client) { + var waitTime = client.globals.maxWaitTime, + newDatabaseName = client.globals.testDatabaseName, + newDocName = 'totally-new-doc', + baseUrl = client.globals.test_settings.launch_url; + + client + .loginToGUI() + + // create a single document + .createDocument('doc_1', newDatabaseName) + + // go to the changes page and enable the auto-update feature + .url(baseUrl + '/#/database/' + newDatabaseName + '/_changes') + + .clickWhenVisible('#db-views-tabs-nav a', waitTime, false) + .waitForElementPresent('#changes-toggle-polling', waitTime, false) + .clickWhenVisible('#changes-toggle-polling', waitTime, false) + + // now add a new item behind the scenes with nano. Before it's added, confirm it's not already in the page + .waitForElementNotPresent('.change-box[data-id="' + newDocName + '"]', waitTime, false) + .createDocument(newDocName, newDatabaseName) + + .waitForElementPresent('.change-box[data-id="' + newDocName + '"]', waitTime, false) + + // we win! + .end(); } + }; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a34579ad/assets/less/animations.less ---------------------------------------------------------------------- diff --git a/assets/less/animations.less b/assets/less/animations.less index a4fc7d9..9ad0a3c 100644 --- a/assets/less/animations.less +++ b/assets/less/animations.less @@ -48,7 +48,7 @@ /* a generic slide-up/down effect that looks smooth for items with unknown heights */ .keyframes(slideDown, { opacity: 0; - max-height: 0px; + max-height: 0; }, { opacity: 1; @@ -60,6 +60,20 @@ opacity: 1; }, { - max-height: 0px; + max-height: 0; opacity: 0; }); + +.highlight { + -webkit-animation: highlight-element 2.5s 1; + -moz-animation: highlight-element 2.5s 1; +} + +@-webkit-keyframes highlight-element { + 0% { background-color: @highlightEffectColor; } + 100% { background-color: #f1f1f1; } +} +@-moz-keyframes highlight-element { + 0% { background-color: @highlightEffectColor; } + 100% { background-color: #f1f1f1; } +} http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a34579ad/assets/less/variables.less ---------------------------------------------------------------------- diff --git a/assets/less/variables.less b/assets/less/variables.less index 45dfc0f..be45dd4 100644 --- a/assets/less/variables.less +++ b/assets/less/variables.less @@ -102,6 +102,7 @@ /* animation */ @transitionSpeed: .25s; @transitionEaseType: linear; +@highlightEffectColor: #bbbbbb; /* breakpoints */ @collapsedNavWidth: 64px;
