Repository: couchdb-fauxton Updated Branches: refs/heads/master 5a66c7362 -> df5010b3c
Active Tasks in ReactJS Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/df5010b3 Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/df5010b3 Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/df5010b3 Branch: refs/heads/master Commit: df5010b3c2798e69f692ff180516b4628270e09b Parents: 5a66c73 Author: [email protected] <[email protected]> Authored: Fri Mar 6 18:57:02 2015 -0500 Committer: [email protected] <[email protected]> Committed: Tue Apr 14 14:01:53 2015 -0400 ---------------------------------------------------------------------- app/addons/activetasks/actions.js | 69 +++ app/addons/activetasks/actiontypes.js | 21 + .../activetasks/assets/less/activetasks.less | 160 ++++- app/addons/activetasks/components.react.jsx | 598 +++++++++++++++++++ app/addons/activetasks/resources.js | 56 +- app/addons/activetasks/routes.js | 54 +- app/addons/activetasks/stores.js | 217 +++++++ .../activetasks/templates/tab_header.html | 33 - app/addons/activetasks/templates/table.html | 36 -- .../activetasks/templates/tabledetail.html | 32 - app/addons/activetasks/templates/tabs.html | 28 - .../tests/activetasks.componentsSpec.react.jsx | 118 ++++ .../activetasks/tests/activetasks.storesSpec.js | 178 ++++++ .../activetasks/tests/fakeActiveTaskResponse.js | 121 ++++ app/addons/activetasks/tests/viewsSpec.js | 132 ---- app/addons/activetasks/views.js | 237 -------- app/helpers.js | 5 +- 17 files changed, 1514 insertions(+), 581 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/actions.js ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/actions.js b/app/addons/activetasks/actions.js new file mode 100644 index 0000000..74fae94 --- /dev/null +++ b/app/addons/activetasks/actions.js @@ -0,0 +1,69 @@ +// 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/activetasks/actiontypes', + 'addons/activetasks/resources' +], +function (FauxtonAPI, ActionTypes, Resources) { + return { + fetchAndSetActiveTasks: function (options) { + var activeTasks = options; + + FauxtonAPI.when(activeTasks.fetch()).then(function () { + this.init(activeTasks.table, activeTasks); + }.bind(this)); + }, + + init: function (collection, backboneCollection) { + FauxtonAPI.dispatch({ + type: ActionTypes.ACTIVE_TASKS_INIT, + options: { + collectionTable: collection, + backboneCollection: backboneCollection + } + }); + }, + changePollingInterval: function (interval) { + FauxtonAPI.dispatch({ + type: ActionTypes.ACTIVE_TASKS_CHANGE_POLLING_INTERVAL, + options: interval + }); + }, + switchTab: function (tab) { + FauxtonAPI.dispatch({ + type: ActionTypes.ACTIVE_TASKS_SWITCH_TAB, + options: tab + }); + }, + setCollection: function (collection) { + FauxtonAPI.dispatch({ + type: ActionTypes.ACTIVE_TASKS_SET_COLLECTION, + options: collection + }); + }, + setSearchTerm: function (searchTerm) { + FauxtonAPI.dispatch({ + type: ActionTypes.ACTIVE_TASKS_SET_SEARCH_TERM, + options: searchTerm + }); + }, + sortByColumnHeader: function (columnName) { + FauxtonAPI.dispatch({ + type: ActionTypes.ACTIVE_TASKS_SORT_BY_COLUMN_HEADER, + options: { + columnName: columnName + } + }); + } + }; +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/actiontypes.js ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/actiontypes.js b/app/addons/activetasks/actiontypes.js new file mode 100644 index 0000000..8cd2838 --- /dev/null +++ b/app/addons/activetasks/actiontypes.js @@ -0,0 +1,21 @@ +// 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 { + ACTIVE_TASKS_CHANGE_POLLING_INTERVAL: 'ACTIVE_TASKS_CHANGE_POLLING_INTERVAL', + ACTIVE_TASKS_SWITCH_TAB: 'ACTIVE_TASKS_SWITCH_TAB', + ACTIVE_TASKS_SET_COLLECTION: 'ACTIVE_TASKS_SET_COLLECTION', + ACTIVE_TASKS_SET_SEARCH_TERM: 'ACTIVE_TASKS_SET_SEARCH_TERM', + ACTIVE_TASKS_SORT_BY_COLUMN_HEADER: 'ACTIVE_TASKS_SORT_BY_COLUMN_HEADER', + ACTIVE_TASKS_INIT: 'ACTIVE_TASKS_INIT' + }; +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/assets/less/activetasks.less ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/assets/less/activetasks.less b/app/addons/activetasks/assets/less/activetasks.less index d11f8f7..1116671 100644 --- a/app/addons/activetasks/assets/less/activetasks.less +++ b/app/addons/activetasks/assets/less/activetasks.less @@ -15,32 +15,33 @@ cursor: pointer; } -.activetasks-header .task-search-database { +.task-search-database { margin: 20px; } .active-tasks th { + &.type { width: 10%; } &.database { - width: 23%; + width: 30%; } - &.started { - width: 17%; + &.started_on { + width: 13%; } - &.updated { - width: 17%; + &.updated_on { + width: 13%; } &.pid { width: 10%; } - &.status { + &.progress { width: 23%; } } @@ -63,3 +64,148 @@ } } +.no-matching-database-on-search { + color: #e33f3b; +} + +p.multiline-active-tasks-message { + margin: 0; + + &.time:nth-child(2) { + color: #888; + } +} + +#dashboard-upper-content { + padding-right: 20px; +} + +.active-tasks.dashboard-upper-menu { + left: 220px; + + .closeMenu & { + left: 64px; + } +} + +.dashboard-lower-menu { + padding-top: 90px; + padding-left: 20px; +} + +input[type="text"].searchbox { + width: 200px; + height: 40px; + float: right; + margin:0px; +} + +#toggle-filter-tab:hover { + color: white; + background-color: #e33f3b; +} + +.filter-tray.toggleFilterTray-enter { // starting css + overflow: hidden; + padding-top: 0px; + max-height: 0px; +} + // animate opening +.filter-tray.toggleFilterTray-enter.toggleFilterTray-enter-active { + max-height: 300px; + height: auto; + transition: max-height 0.5s linear; +} + +.filter-tray { // final css + height: auto; + max-height: 300px; + overflow: hidden; +} + // animate closing +.filter-tray.toggleFilterTray-leave.toggleFilterTray-leave-active { + max-height: 0; + padding-top: 0px; + transition: max-height 0.5s linear; +} + +.filter-checkboxes { + float: left; + height: auto; + width: auto; + margin-top: 10px; +} + +.active-tasks-one-checkbox { + input { + vertical-align: middle; + margin-right: 5px; + margin-bottom: 3px; + } + margin-right: 10px; + display: inline-block; +} + +.active-tasks-checkbox-label { + display: inline-block; + color: #666; +} + +@media (max-width: 940px) { + .filter-checkboxes li { + display: inline-block; + text-align: left; + width: 175px; + } +} + +.filter-checkboxes-form { + margin:0px; +} + +.polling-interval-widget { + + width: 250px; + margin-right: 20px; + margin-top: 10px; + color: #666; + + li { + list-style-type: none; + } + + .polling-interval-time-label { + display: inline-block; + float: right; + margin-right: 0px; + cursor: default; + } + + #pollingRange { + width: 250px; + } +} + +.header-field { + &.radio { + display: none; + } + + &.label-text { + display: block; + font-weight: bold; + padding:10px; + margin: 0px; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + + .table th& { + padding: 0px; + margin: 0px; + } +} http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/components.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/components.react.jsx b/app/addons/activetasks/components.react.jsx new file mode 100644 index 0000000..235b25d --- /dev/null +++ b/app/addons/activetasks/components.react.jsx @@ -0,0 +1,598 @@ +// 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/activetasks/stores', + 'addons/activetasks/resources', + 'addons/activetasks/actions' +], function (app, FauxtonAPI, React, Stores, Resources, Actions) { + + var activeTasksStore = Stores.activeTasksStore; + var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; + + var ActiveTasksController = React.createClass({ + + getStoreState: function () { + return { + collection: activeTasksStore.getCollection(), + searchTerm: activeTasksStore.getSearchTerm(), + selectedRadio: activeTasksStore.getSelectedRadio(), + + sortByHeader: activeTasksStore.getSortByHeader(), + headerIsAscending: activeTasksStore.getHeaderIsAscending(), + + setPolling: activeTasksStore.setPolling, + clearPolling: activeTasksStore.clearPolling, + }; + }, + + getInitialState: function () { + return this.getStoreState(); + }, + + componentDidMount: function () { + this.state.setPolling(); + activeTasksStore.on('change', this.onChange, this); + }, + + componentWillUnmount: function () { + this.state.clearPolling(); + activeTasksStore.off('change', this.onChange, this); + }, + + onChange: function () { + this.setState(this.getStoreState()); + }, + + setNewSearchTerm: function (searchTerm) { + Actions.setSearchTerm(searchTerm); + }, + + switchTab: function (newRadioButton) { //radio buttons + Actions.switchTab(newRadioButton); + }, + + tableHeaderOnClick: function (headerClicked) { + Actions.sortByColumnHeader(headerClicked); + }, + + render: function () { + var collection = this.state.collection; + var searchTerm = this.state.searchTerm; + var selectedRadio = this.state.selectedRadio; + var sortByHeader = this.state.sortByHeader; + var headerIsAscending = this.state.headerIsAscending; + + var setSearchTerm = this.setNewSearchTerm; + var onTableHeaderClick = this.tableHeaderOnClick; + + if (collection.length === 0 ) { + return (<div className="active-tasks"><tr><td><p> No active tasks. </p></td></tr></div>); + } + return ( + <div className="scrollable"> + <div className="inner"> + <ActiveTasksFilter + searchTerm={searchTerm} + selectedRadio={selectedRadio} + onSearch={setSearchTerm} + onRadioClick={this.switchTab}/> + <ActiveTaskTable + collection={collection} + searchTerm={searchTerm} + selectedRadio={selectedRadio} + onTableHeaderClick={onTableHeaderClick} + sortByHeader={sortByHeader} + headerIsAscending={headerIsAscending} /> + </div> + </div> + ); + } + }); + + var ActiveTasksFilter = React.createClass({ + getStoreState: function () { + return { + isFilterTrayVisible: false + }; + }, + + getInitialState: function () { + return this.getStoreState(); + }, + + toggleFilterTray: function () { + this.setState({ + isFilterTrayVisible : !this.state.isFilterTrayVisible + }); + }, + + render: function () { + var filterTray = ''; + + if (this.state.isFilterTrayVisible) { + filterTray = <ActiveTasksFilterTray + key="filter-tray" + selectedRadio={this.props.selectedRadio} + onSearch={this.props.onSearch} + onRadioClick={this.props.onRadioClick} />; + } + + return ( + <div id="dashboard-upper-content"> + <div className="dashboard-upper-menu active-tasks"> + <ActiveTasksFilterTab onClick={this.toggleFilterTray} /> + </div> + <ReactCSSTransitionGroup + className="dashboard-lower-menu" + transitionName="toggleFilterTray" + component="div" > + {filterTray} + </ReactCSSTransitionGroup> + </div> + ); + } + }); + + var ActiveTasksFilterTab = React.createClass({ + render: function () { + return ( + <ul className="nav nav-tabs" id="db-views-tabs-nav"> + <li> + <a id="toggle-filter-tab" + className="toggle-filter-tab" + data-bypass="true" + data-toggle="button" + onClick={this.props.onClick}> + <i className="fonticon fonticon-plus"></i> + Filter + </a> + </li> + </ul>); + } + }); + + var ActiveTasksFilterTray = React.createClass({ + searchTermChange: function (e) { + var searchTerm = e.target.value; + this.props.onSearch(searchTerm); + }, + + render: function () { + return ( + <div className="filter-tray"> + <ActiveTasksFilterTrayCheckBoxes + onRadioClick={this.props.onRadioClick} + selectedRadio={this.props.selectedRadio} /> + <input + className="searchbox" + type="text" + name="search" + placeholder="Search for databases..." + value={this.props.searchTerm} + onChange={this.searchTermChange} /> + </div> + ); + } + }); + + var ActiveTasksFilterTrayCheckBoxes = React.createClass({ + getDefaultProps: function () { + return { + radioNames : [ + 'All Tasks', + 'Replication', + 'Database Compaction', + 'Indexer', + 'View Compaction' + ] + }; + }, + + checked: function (radioName) { + return this.props.selectedRadio === radioName; + }, + + onRadioClick: function (e) { + var radioName = e.target.value; + this.props.onRadioClick(radioName); + }, + + createCheckboxes: function () { + return ( + this.props.radioNames.map(function (radioName) { + var checked = this.checked(radioName); + var id = radioName.replace(' ', '-'); + var radioClassName = "radio-" + id; + var radioClick = this.onRadioClick; + + return ( + <li className="active-tasks-one-checkbox" key={radioName + "li"}> + <input + id={id} + type="radio" + key ={radioName} + name="radio-button-active-task-filter-tray" + value={radioName} + checked={checked} + onChange={radioClick} /> + <label htmlFor={id} className="active-tasks-checkbox-label"> + {radioName} + </label> + </li> + ); + }.bind(this)) + ); + }, + + render: function () { + var filterCheckboxes = this.createCheckboxes(); + return ( + <ul className="filter-checkboxes"> + <form className="filter-checkboxes-form"> + {filterCheckboxes} + </form> + </ul> + ); + } + }); + + var ActiveTaskTable = React.createClass({ + render: function () { + var collection = this.props.collection; + var selectedRadio = this.props.selectedRadio; + var searchTerm = this.props.searchTerm; + var sortByHeader = this.props.sortByHeader; + var onTableHeaderClick = this.props.onTableHeaderClick; + var headerIsAscending = this.props.headerIsAscending; + + return ( + <div id="dashboard-lower-content"> + <table className="table table-bordered table-striped active-tasks"> + <ActiveTasksTableHeader + onTableHeaderClick={onTableHeaderClick} + sortByHeader={sortByHeader} + headerIsAscending={headerIsAscending}/> + <ActiveTasksTableBody + collection={collection} + selectedRadio={selectedRadio} + searchTerm={searchTerm}/> + </table> + </div> + ); + } + }); + + var ActiveTasksTableHeader = React.createClass({ + getDefaultProps: function () { + return { + headerNames : [ + ['type', 'Type'], + ['database', 'Database'], + ['started_on', 'Started On'], + ['updated_on', 'Updated On'], + ['pid', 'PID'], + ['progress', 'Status'] + ] + }; + }, + + createTableHeadingFields: function () { + var onTableHeaderClick = this.props.onTableHeaderClick; + var sortByHeader = this.props.sortByHeader; + var headerIsAscending = this.props.headerIsAscending; + return ( + this.props.headerNames.map(function (header) { + return ( + <TableHeader + headerName={header[0]} + displayName={header[1]} + key={header[0]} + onTableHeaderClick={onTableHeaderClick} + sortByHeader={sortByHeader} + headerIsAscending={headerIsAscending} /> + ); + }) + ); + }, + + render: function () { + var tableHeadingFields = this.createTableHeadingFields(); + return ( + <thead> + <tr>{tableHeadingFields}</tr> + </thead> + ); + } + }); + + var TableHeader = React.createClass({ + arrow: function () { + var sortBy = this.props.sortByHeader; + var currentName = this.props.headerName; + var headerIsAscending = this.props.headerIsAscending; + var arrow = headerIsAscending ? 'icon icon-caret-up' : 'icon icon-caret-down'; + + if (sortBy === currentName) { + return <i className={arrow}></i>; + } + }, + + onTableHeaderClick: function (e) { + var headerSelected = e.target.value; + this.props.onTableHeaderClick(headerSelected); + }, + + render: function () { + var arrow = this.arrow(); + var th_class = 'header-field ' + this.props.headerName; + + return ( + <input + type="radio" + name="header-field" + id={this.props.headerName} + value={this.props.headerName} + className="header-field radio" + onChange={this.onTableHeaderClick}> + <th className={th_class} value={this.props.headerName}> + <label + className="header-field label-text" + htmlFor={this.props.headerName}> + {this.props.displayName} {arrow} + </label> + </th> + </input> + ); + } + }); + + var ActiveTasksTableBody = React.createClass({ + + getStoreState: function () { + return { + filteredTable: activeTasksStore.getFilteredTable(this.props.collection) + }; + }, + + getInitialState: function () { + return this.getStoreState(); + }, + + componentWillReceiveProps: function (nextProps) { + this.setState({ + filteredTable: activeTasksStore.getFilteredTable(this.props.collection) + }); + }, + + createRows: function () { + var isThereASearchTerm = this.props.searchTerm.trim() === ""; + + if (this.state.filteredTable.length === 0) { + return isThereASearchTerm ? this.noActiveTasks() : this.noActiveTasksMatchFilter(); + } + + return _.map(this.state.filteredTable, function (item, iteration) { + return <ActiveTaskTableBodyContents key={Math.random()} item={item} />; + }); + }, + + noActiveTasks: function () { + return ( + <tr className="no-matching-database-on-search"> + <td colSpan="6">No active {this.props.selectedRadio} tasks.</td> + </tr> + ); + }, + + noActiveTasksMatchFilter: function () { + return ( + <tr className="no-matching-database-on-search"> + <td colSpan="6">No active {this.props.selectedRadio} tasks match with filter: "{this.props.searchTerm}".</td> + </tr> + ); + }, + + render: function () { + var tableBody = this.createRows(); + return ( + <tbody className="js-tasks-go-here"> + {tableBody} + </tbody> + ); + } + }); + + var ActiveTaskTableBodyContents = React.createClass({ + getInfo: function (item) { + return { + type : item.type, + objectField: activeTasksHelpers.getDatabaseFieldMessage(item), + started_on: activeTasksHelpers.getTimeInfo(item.started_on), + updated_on: activeTasksHelpers.getTimeInfo(item.updated_on), + pid: item.pid.replace(/[<>]/g, ''), + progress: activeTasksHelpers.getProgressMessage(item), + }; + }, + + multilineMessage: function (messageArray, optionalClassName) { + + if (!optionalClassName) { + optionalClassName = ''; + } + var cssClasses = 'multiline-active-tasks-message ' + optionalClassName; + + return messageArray.map(function (msgLine, iterator) { + return <p key={iterator} className={cssClasses}>{msgLine}</p>; + }); + }, + + render: function () { + var rowData = this.getInfo(this.props.item); + var objectFieldMsg = this.multilineMessage(rowData.objectField); + var startedOnMsg = this.multilineMessage(rowData.started_on, 'time'); + var updatedOnMsg = this.multilineMessage(rowData.updated_on, 'time'); + var progressMsg = this.multilineMessage(rowData.progress); + + return ( + <tr> + <td>{rowData.type}</td> + <td>{objectFieldMsg}</td> + <td>{startedOnMsg}</td> + <td>{updatedOnMsg}</td> + <td>{rowData.pid}</td> + <td>{progressMsg}</td> + </tr> + ); + } + }); + + var ActiveTasksPollingWidgetController = React.createClass({ + + getStoreState: function () { + return { + pollingInterval: activeTasksStore.getPollingInterval() + }; + }, + + getInitialState: function () { + return this.getStoreState(); + }, + + componentDidMount: function () { + activeTasksStore.on('change', this.onChange, this); + }, + + onChange: function () { + if (this.isMounted()) { + this.setState(this.getStoreState()); + } + }, + + pollingIntervalChange: function (event) { + Actions.changePollingInterval(event.target.value); + }, + + getPluralForLabel: function () { + return this.state.pollingInterval === "1" ? '' : 's'; + }, + + createPollingWidget: function () { + var pollingInterval = this.state.pollingInterval; + var s = this.getPluralForLabel(); + var onChangeHandle = this.pollingIntervalChange; + + return ( + <ul className="polling-interval-widget"> + <li className="polling-interval-name">Polling interval + <label className="polling-interval-time-label" htmlFor="pollingRange"> + <span>{pollingInterval}</span> second{s} + </label> + </li> + <li> + <input + id="pollingRange" + type="range" + min="1" + max="30" + step="1" + value={pollingInterval} + onChange={onChangeHandle}/> + </li> + </ul> + ); + }, + + render: function () { + var pollingWidget = this.createPollingWidget(); + + return <div>{pollingWidget}</div>; + } + }); + + var activeTasksHelpers = { + getTimeInfo: function (timeStamp) { + var timeMessage = [ + app.helpers.formatDate(timeStamp), + app.helpers.getDateFromNow(timeStamp) + ]; + return timeMessage; + }, + + getDatabaseFieldMessage: function (item) { + var type = item.type; + var databaseFieldMessage = []; + + if (type === 'replication') { + databaseFieldMessage.push('From: ' + item.source); + databaseFieldMessage.push('To: ' + item.target); + } else if (type === 'indexer') { + databaseFieldMessage.push(item.database); + databaseFieldMessage.push('(View: ' + item.design_document + ')'); + } else { + databaseFieldMessage.push(item.database); + } + + return databaseFieldMessage; + }, + + getProgressMessage: function (item) { + var progressMessage = []; + var type = item.type; + + if (_.has(item, 'progress')) { + progressMessage.push('Progress: ' + item.progress + '%'); + } + + if (type === 'indexer') { + progressMessage.push( + 'Processed ' + item.changes_done + ' of ' + item.total_changes + ' changes.' + ); + } else if (type === 'replication') { + progressMessage.push(item.docs_written + ' docs written.'); + + if (_.has(item, 'changes_pending')) { + progressMessage.push(item.changes_pending + ' pending changes.'); + } + } + + if (_.has(item, 'source_seq')) { + progressMessage.push('Current source sequence: ' + item.source_seq + '. '); + } + + if (_.has(item, 'changes_done')) { + progressMessage.push(item.changes_done + ' Changes done.'); + } + + return progressMessage; + } + }; + + return { + ActiveTasksController: ActiveTasksController, + ActiveTasksFilter: ActiveTasksFilter, + ActiveTasksFilterTab: ActiveTasksFilterTab, + ActiveTasksFilterTray: ActiveTasksFilterTray, + + ActiveTaskTable: ActiveTaskTable, + ActiveTasksTableHeader: ActiveTasksTableHeader, + TableHeader: TableHeader, + ActiveTasksTableBody: ActiveTasksTableBody, + ActiveTaskTableBodyContents: ActiveTaskTableBodyContents, + + ActiveTasksPollingWidgetController: ActiveTasksPollingWidgetController + }; + +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/resources.js ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/resources.js b/app/addons/activetasks/resources.js index ebb4dd6..292521c 100644 --- a/app/addons/activetasks/resources.js +++ b/app/addons/activetasks/resources.js @@ -16,54 +16,34 @@ define([ ], function (app, FauxtonAPI) { - app.taskSortBy = 'type'; - var Active = {}; - Active.events = {}; - _.extend(Active.events, Backbone.Events); - - Active.Task = Backbone.Model.extend({ - idAttribute: "pid" - }); - Active.AllTasks = Backbone.Collection.extend({ - model: Active.Task, - sortByColumn: function (colName) { - app.taskSortBy = colName; - this.sort(); + url: function () { + return app.host + '/_active_tasks'; }, - comparator: function (item) { - var value = app.taskSortBy, - values; - - if (value.indexOf(',') !== -1) { - values = value.split(','); - _.each(values, function (val) { - if (item.get(val)) { - value = val; - } - }); - } - return item.get(value); + pollingFetch: function () { //still need this for the polling + this.fetch({reset: true, parse: true}); + return this; }, - documentation: FauxtonAPI.constants.DOC_URLS.ACTIVE_TASKS, + parse: function (resp) { + //no more backbone models, collection is converted into an array of objects + var collectionTable = []; - url: function (context) { - if (context === 'apiurl') { - return window.location.origin + '/_active_tasks'; - } else { - return app.host + '/_active_tasks'; - } - } - }); + _.each(resp, function (item) { + collectionTable.push(item); + }); + + //collection is an array of objects + this.table = collectionTable; + return resp; + }, + + table: [] - Active.Search = Backbone.Model.extend({ - filterDatabase: null, - filterType: 'all' }); return Active; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/routes.js ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/routes.js b/app/addons/activetasks/routes.js index 5044091..0340100 100644 --- a/app/addons/activetasks/routes.js +++ b/app/addons/activetasks/routes.js @@ -14,60 +14,40 @@ define([ 'app', 'api', 'addons/activetasks/resources', - 'addons/activetasks/views' + 'addons/activetasks/components.react', + 'addons/activetasks/actions' ], -function (app, FauxtonAPI, Activetasks, Views) { +function (app, FauxtonAPI, ActiveTasksResources, ActiveTasksComponents, Actions) { var ActiveTasksRouteObject = FauxtonAPI.RouteObject.extend({ - layout: 'with_tabs_sidebar', - + selectedHeader: 'Active Tasks', + layout: 'one_pane', routes: { - 'activetasks/:id': 'defaultView', - 'activetasks': 'defaultView' - }, - - events: { - 'route:changeFilter': 'changeFilter' + 'activetasks/:id': 'showActiveTasks', + 'activetasks': 'showActiveTasks' }, - - selectedHeader: 'Active Tasks', - crumbs: [ {'name': 'Active tasks', 'link': 'activetasks'} ], - apiUrl: function () { - return [this.allTasks.url('apiurl'), this.allTasks.documentation]; + var apiurl = window.location.origin + '/_active_tasks'; + return [ apiurl, FauxtonAPI.constants.DOC_URLS.ACTIVE_TASKS]; }, - roles: ['_admin'], - initialize: function () { - this.allTasks = new Activetasks.AllTasks(); - this.search = new Activetasks.Search(); - }, - - defaultView: function () { - this.setView('#dashboard-lower-content', new Views.View({ - collection: this.allTasks, - currentView: 'all', - searchModel: this.search - })); - - this.setView('#sidebar-content', new Views.TabMenu({})); - - this.headerView = this.setView('#dashboard-upper-content', new Views.TabHeader({ - searchModel: this.search - })); + this.allTasks = new ActiveTasksResources.AllTasks(); }, + showActiveTasks: function () { + Actions.fetchAndSetActiveTasks(this.allTasks); + Actions.changePollingInterval(5); - changeFilter: function (filterType) { - this.search.set('filterType', filterType); + this.setComponent('#dashboard-content', ActiveTasksComponents.ActiveTasksController); + this.setComponent('#right-header', ActiveTasksComponents.ActiveTasksPollingWidgetController); } }); - Activetasks.RouteObjects = [ActiveTasksRouteObject]; + ActiveTasksResources.RouteObjects = [ActiveTasksRouteObject]; - return Activetasks; + return ActiveTasksResources; }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/stores.js ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/stores.js b/app/addons/activetasks/stores.js new file mode 100644 index 0000000..4c61a52 --- /dev/null +++ b/app/addons/activetasks/stores.js @@ -0,0 +1,217 @@ +// 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/activetasks/actiontypes' +], function (app, FauxtonAPI, ActionTypes) { + + var ActiveTasksStore = FauxtonAPI.Store.extend({ + + init: function (collectionTable, backboneCollection) { + this._prevSortbyHeader = 'started_on'; + this._headerIsAscending = true; + this._selectedRadio = 'All Tasks'; + this._sortByHeader = 'started_on'; + this._searchTerm = ''; + this._collection = collectionTable; + this._pollingIntervalSeconds = 5; + this.sortCollectionByColumnHeader(this._sortByHeader); + this._backboneCollection = backboneCollection; + }, + + getSelectedRadio: function () { + return this._selectedRadio; + }, + + setSelectedRadio: function (selectedRadio) { + this._selectedRadio = selectedRadio; + }, + + getPollingInterval: function () { + return this._pollingIntervalSeconds; + }, + + setPollingInterval: function (pollingInterval) { + this._pollingIntervalSeconds = pollingInterval; + }, + + setPolling: function () { + clearInterval(this.getIntervalID()); + var id = setInterval(function () { + this._backboneCollection.pollingFetch(); + this.setCollection(this._backboneCollection.table); + this.sortCollectionByColumnHeader(this._prevSortbyHeader, false); + this.triggerChange(); + }.bind(this), this.getPollingInterval() * 1000); + + this.setIntervalID(id); + }, + + clearPolling: function () { + clearInterval(this.getIntervalID()); + }, + + getIntervalID: function () { + return this._intervalID; + }, + + setIntervalID: function (id) { + this._intervalID = id; + }, + + setCollection: function (collection) { + this._collection = collection; + }, + + getCollection: function () { + return this._collection; + }, + + setSearchTerm: function (searchTerm) { + this._searchTerm = searchTerm; + }, + + getSearchTerm: function () { + return this._searchTerm; + }, + + getSortByHeader: function () { + return this._sortByHeader; + }, + + setSortByHeader: function (header) { + this._sortByHeader = header; + }, + + getHeaderIsAscending: function () { + return this._headerIsAscending; + }, + + toggleHeaderIsAscending: function () { + if (this._prevSortbyHeader === this._sortByHeader) { + this._headerIsAscending = !this._headerIsAscending; + } + }, + + sortCollectionByColumnHeader: function (colName) { + var collectionTable = this._collection; + + var sorted = _.sortBy(collectionTable, function (item) { + var variable = colName; + + if (_.isUndefined(item[variable])) { + variable = 'source'; + } + return item[variable]; + }); + + this._prevSortbyHeader = colName; + this._collection = sorted; + }, + + getFilteredTable: function (collection) { + var table = []; + + //sort the table here + this.sortCollectionByColumnHeader(this._sortByHeader); + + //insert all matches into table + this._collection.map(function (item) { + var passesRadioFilter = this.passesRadioFilter(item); + var passesSearchFilter = this.passesSearchFilter(item); + + if (passesRadioFilter && passesSearchFilter) { + table.push(item); + } + }.bind(this)); + + // reverse if descending + if (!this._headerIsAscending) { + table.reverse(); + } + + return table; + }, + + passesSearchFilter: function (item) { + var searchTerm = this._searchTerm; + var regex = new RegExp(searchTerm, 'g'); + + var itemDatabasesTerm = ''; + if (_.has(item, 'database')) { + itemDatabasesTerm += item.database; + } + if (_.has(item, 'source')) { + itemDatabasesTerm += item.source; + } + if (_.has(item, 'target')) { + itemDatabasesTerm += item.target; + } + + return regex.test(itemDatabasesTerm); + }, + + passesRadioFilter: function (item) { + var selectedRadio = this._selectedRadio.toLowerCase().replace(' ', '_') ; + return item.type === selectedRadio || selectedRadio === 'all_tasks'; + }, + + dispatch: function (action) { + switch (action.type) { + + case ActionTypes.ACTIVE_TASKS_INIT: + this.init(action.options.collectionTable, action.options.backboneCollection); + break; + + case ActionTypes.ACTIVE_TASKS_CHANGE_POLLING_INTERVAL: + this.setPollingInterval(action.options); + this.setPolling(); + this.triggerChange(); + break; + + case ActionTypes.ACTIVE_TASKS_SWITCH_TAB: + this.setSelectedRadio(action.options); + this.triggerChange(); + break; + + case ActionTypes.ACTIVE_TASKS_SET_COLLECTION: + this.setCollection(action.options); + this.triggerChange(); + break; + + case ActionTypes.ACTIVE_TASKS_SET_SEARCH_TERM: + this.setSearchTerm(action.options); + this.triggerChange(); + break; + + case ActionTypes.ACTIVE_TASKS_SORT_BY_COLUMN_HEADER: + this.toggleHeaderIsAscending(); + this.setSortByHeader(action.options.columnName); + this.sortCollectionByColumnHeader(action.options.columnName); + this.triggerChange(); + break; + + default: + return; + } + } + }); + + var activeTasksStore = new ActiveTasksStore(); + activeTasksStore.dispatchToken = FauxtonAPI.dispatcher.register(activeTasksStore.dispatch.bind(activeTasksStore)); + return { + activeTasksStore: activeTasksStore + }; + +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/templates/tab_header.html ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/templates/tab_header.html b/app/addons/activetasks/templates/tab_header.html deleted file mode 100644 index f6aa3d9..0000000 --- a/app/addons/activetasks/templates/tab_header.html +++ /dev/null @@ -1,33 +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="dashboard-upper-menu"> - <ul class="nav nav-tabs" id="db-views-tabs-nav"> - <li> - <a class="js-toggle-filter" href="#filter" data-bypass="true" data-toggle="tab"> - <i class="fonticon fonticon-plus"></i>Filter - </a> - </li> - </ul> -</div> - -<div class="tab-content"> - <div class="tab-pane" id="query"> - <div class="activetasks-header"> - <div class="pull-right"> - <input class="task-search-database" type="text" name="search" placeholder="Search for databases..."> - </div> - </div> - </div> -</div> http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/templates/table.html ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/templates/table.html b/app/addons/activetasks/templates/table.html deleted file mode 100644 index 92264dd..0000000 --- a/app/addons/activetasks/templates/table.html +++ /dev/null @@ -1,36 +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. -*/%> - -<% if (collection.length === 0) { %> - <tr> - <td> - <p>No tasks.</p> - </td> - </tr> -<% } else { %> - - <thead> - <tr> - <th class="type" data-type="type">Type</th> - <th class="database" data-type="source,target,database">Database</th> - <th class="started" data-type="started_on">Started on</th> - <th class="updated" data-type="updated_on">Last updated on</th> - <th class="pid" data-type="pid">PID</th> - <th class="status" data-type="progress">Status</th> - </tr> - </thead> - - <tbody class="js-tasks-go-here"> - </tbody> -<% } %> http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/templates/tabledetail.html ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/templates/tabledetail.html b/app/addons/activetasks/templates/tabledetail.html deleted file mode 100644 index ca4c766..0000000 --- a/app/addons/activetasks/templates/tabledetail.html +++ /dev/null @@ -1,32 +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> - <%= model.get("type")%> -</td> -<td> - <%= objectField %> -</td> -<td> - <%= formatDate(model.get("started_on")) %> -</td> -<td> - <%= formatDate(model.get("updated_on")) %> -</td> -<td> - <%= model.get("pid")%> -</td> -<td> - <p><%=progress%> </p> -</td> http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/templates/tabs.html ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/templates/tabs.html b/app/addons/activetasks/templates/tabs.html deleted file mode 100644 index 8bffa4d..0000000 --- a/app/addons/activetasks/templates/tabs.html +++ /dev/null @@ -1,28 +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. -*/%> - -<ul class="task-tabs nav nav-list"> - <% for (var filter in filters) { %> - <li data-type="<%=filter%>"> - <a><%=filters[filter]%></a> - </li> - <% } %> -</ul> -<ul class="nav nav-list views polling-interval"> - <li class="nav-header">Polling interval</li> - <li> - <input id="pollingRange" type="range" min="1" max="30" step="1" value="5"> - <label for="pollingRange"><span>5</span> second(s)</label> - </li> -</ul> http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/tests/activetasks.componentsSpec.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/tests/activetasks.componentsSpec.react.jsx b/app/addons/activetasks/tests/activetasks.componentsSpec.react.jsx new file mode 100644 index 0000000..41aa08a --- /dev/null +++ b/app/addons/activetasks/tests/activetasks.componentsSpec.react.jsx @@ -0,0 +1,118 @@ +// 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/activetasks/resources', + 'addons/activetasks/components.react', + 'addons/activetasks/stores', + 'addons/activetasks/tests/fakeActiveTaskResponse', + 'react', + 'addons/activetasks/actions', + 'testUtils' +], function (FauxtonAPI, ActiveTasks, Components, Stores, fakedResponse, React, Actions, testUtils) { + var assert = testUtils.assert; + var TestUtils = React.addons.TestUtils; + var activeTasksStore = Stores.activeTasksStore; + var activeTasksCollection = new ActiveTasks.AllTasks({}); + activeTasksCollection.parse(fakedResponse); + + describe('Active Tasks -- Components', function () { + + describe('Active Tasks Polling (Components)', function () { + var pollingWidgetDiv, pollingWidget; + + beforeEach(function () { + pollingWidgetDiv = document.createElement('div'); + pollingWidget = TestUtils.renderIntoDocument( + React.createElement(Components.ActiveTasksPollingWidgetController, null), pollingWidgetDiv + ); + }); + + afterEach(function () { + React.unmountComponentAtNode(pollingWidgetDiv); + }); + + it('should trigger update polling interval', function () { + var spy = sinon.spy(Actions, 'changePollingInterval'); + var rangeNode = TestUtils.findRenderedDOMComponentWithTag(pollingWidget, 'input'); + var time = '9'; + + TestUtils.Simulate.change(rangeNode, {target: {value: time}}); + assert.ok(spy.calledOnce); + }); + }); + + describe('Active Tasks Table (Components)', function () { + var table, tableDiv, spy, filterTab; + + beforeEach(function () { + tableDiv = document.createElement('div'); + activeTasksStore.init(activeTasksCollection.table, activeTasksCollection); + table = TestUtils.renderIntoDocument(React.createElement(Components.ActiveTasksController, null), tableDiv); + + // open filter tray + filterTab = TestUtils.findRenderedDOMComponentWithClass(table, 'toggle-filter-tab'); + TestUtils.Simulate.click(filterTab); + }); + + afterEach(function () { + spy.restore(); + React.unmountComponentAtNode(tableDiv); + window.confirm.restore && window.confirm.restore(); + }); + + describe('Active Tasks Filter tray', function () { + var radioIDs = [ + 'Replication', + 'Database-Compaction', + 'Indexer', + 'View-Compaction' + ]; + + it('should trigger change to radio buttons', function () { + _.each(radioIDs, function (radioID) { + spy = sinon.spy(Actions, 'switchTab'); + TestUtils.Simulate.change($(table.getDOMNode()).find('#' + radioID)[0]); + assert.ok(spy.calledOnce); + spy.restore(); + }); + }); + + it('should trigger change to search term', function () { + spy = sinon.spy(Actions, 'setSearchTerm'); + TestUtils.Simulate.change($(table.getDOMNode()).find('.searchbox')[0], {target: {value: 'searching'}}); + assert.ok(spy.calledOnce); + }); + }); + + describe('Active Tasks Table Headers', function () { + var headerNames = [ + 'type', + 'database', + 'started_on', + 'updated_on', + 'pid', + 'progress' + ]; + + it('should trigger change to which header to sort by', function () { + _.each(headerNames, function (header) { + spy = sinon.spy(Actions, 'sortByColumnHeader'); + TestUtils.Simulate.change($(table.getDOMNode()).find('#' + header)[0]); + assert.ok(spy.calledOnce); + spy.restore(); + }); + }); + }); + }); + }); +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/tests/activetasks.storesSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/tests/activetasks.storesSpec.js b/app/addons/activetasks/tests/activetasks.storesSpec.js new file mode 100644 index 0000000..462d94d --- /dev/null +++ b/app/addons/activetasks/tests/activetasks.storesSpec.js @@ -0,0 +1,178 @@ +// 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/activetasks/resources', + 'addons/activetasks/components.react', + 'addons/activetasks/stores', + 'addons/activetasks/tests/fakeActiveTaskResponse', + 'react', + 'testUtils' +], function (FauxtonAPI, ActiveTasks, Components, Stores, fakedResponse, React, testUtils) { + var assert = testUtils.assert; + var TestUtils = React.addons.TestUtils; + + var activeTasksStore = Stores.activeTasksStore; + var activeTasksCollection = new ActiveTasks.AllTasks(); + activeTasksCollection.parse(fakedResponse); + + describe('Active Tasks -- Stores', function () { + var spy; + + beforeEach(function () { + activeTasksStore.init(activeTasksCollection.table, activeTasksCollection); + this.clock = sinon.useFakeTimers(); + }); + + afterEach(function () { + testUtils.restore(spy); + testUtils.restore(this.clock); + }); + + describe('Active Task Stores - Polling', function () { + var pollingWidgetDiv, pollingWidget; + + beforeEach(function () { + activeTasksStore.init(activeTasksCollection.table, activeTasksCollection); + this.clock = sinon.useFakeTimers(); + }); + + afterEach(function () { + testUtils.restore(spy); + testUtils.restore(this.clock); + }); + + it('should poll at the min time', function () { + spy = sinon.spy(activeTasksStore, 'getPollingInterval'); + var minTime = 1; + activeTasksStore.setPollingInterval(minTime); + activeTasksStore.setPolling(); + assert.ok(spy.calledOnce); + + setInterval(spy, minTime * 1000); + this.clock.tick(minTime * 1000); + assert.ok(spy.calledTwice); + + this.clock.tick(minTime * 1000); + assert.ok(spy.calledThrice); + }); + + it('should poll at the max time', function () { + spy = sinon.spy(activeTasksStore, 'getPollingInterval'); + + var maxTime = 30; + activeTasksStore.setPollingInterval(maxTime); + activeTasksStore.setPolling(); + assert.ok(spy.calledOnce); + + setInterval(spy, maxTime * 1000); + this.clock.tick(maxTime * 1000); + assert.ok(spy.calledTwice); + + this.clock.tick(maxTime * 1000); + assert.ok(spy.calledThrice); + }); + + it('should poll at a mid time', function () { + spy = sinon.spy(activeTasksStore, 'getPollingInterval'); + + var midtime = 15; + activeTasksStore.setPollingInterval(midtime); + activeTasksStore.setPolling(); + assert.ok(spy.calledOnce); + + setInterval(spy, midtime * 1000); + this.clock.tick(midtime * 1000); + assert.ok(spy.calledTwice); + + this.clock.tick(midtime * 1000); + assert.ok(spy.calledThrice); + }); + + it('should clear interval each time', function () { + var spy = sinon.spy(window, 'clearInterval'); + activeTasksStore.setPolling(); + assert.ok(spy.calledOnce); + }); + }); + + describe('Active Task Stores - Filter Tab Tray', function () { + var fakeFilteredTable, storeFilteredtable; + function sort (a, b, sortBy) { //sorts array by objects with key 'sortBy', with default started_on + if (_.isUndefined(sortBy)) { + sortBy = 'started_on'; + } + return b[sortBy] - a[sortBy]; + } + + afterEach(function () { + fakeFilteredTable = []; + }); + + it('should filter the table correctly, by radio -- All Tasks', function () { + activeTasksStore.setSelectedRadio('all_tasks'); + //parse table and check that it only contains objects any type + var table = activeTasksStore.getFilteredTable(activeTasksStore._collection); + assert.ok(activeTasksStore._collection.length, table.length); + }); + + it('should filter the table correctly, by radio', function () { + activeTasksStore.setSelectedRadio('replication'); + var storeFilteredtable = activeTasksStore.getFilteredTable(activeTasksStore._collection); + + //parse table and check that it only contains objects with type: Replication + _.each(storeFilteredtable, function (activeTask) { + assert.ok(activeTasksStore.passesRadioFilter(activeTask)); + assert.ok(activeTask.type === activeTasksStore.getSelectedRadio()); + }); + }); + + it('should search the table correctly', function () { + activeTasksStore.setSelectedRadio('all_tasks'); + var searchTerm = 'base'; + activeTasksStore.setSearchTerm(searchTerm); + var storeGeneratedTable = activeTasksStore.getFilteredTable(activeTasksStore._collection); + var regEx = new RegExp(searchTerm); + + fakeFilteredTable = [ + { user: 'information'}, + { user: 'ooo'} + ]; + + assert.equal(fakeFilteredTable[0].user, storeGeneratedTable[0].user); + assert.equal(fakeFilteredTable[1].user, storeGeneratedTable[1].user); + }); + }); + + describe('Active Task Stores - Table Header Sort - Select Ascending/Descending', function () { + + it('should set header as ascending, on default', function () { + activeTasksStore.setSelectedRadio('all_tasks'); + activeTasksStore._headerIsAscending = true; + assert.ok(activeTasksStore.getHeaderIsAscending() === true); + }); + + it('should set header as descending, if same header is selected again', function () { + activeTasksStore._prevSortbyHeader = 'sameHeader'; + activeTasksStore._sortByHeader = 'sameHeader'; + activeTasksStore.toggleHeaderIsAscending(); + assert.ok(activeTasksStore.getHeaderIsAscending() === false); + }); + + it('should set header as ascending, if different header is selected', function () { + activeTasksStore._sortByHeader = 'differentHeader'; + activeTasksStore.toggleHeaderIsAscending(); + assert.ok(activeTasksStore.getHeaderIsAscending() === true); + }); + }); + }); +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/tests/fakeActiveTaskResponse.js ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/tests/fakeActiveTaskResponse.js b/app/addons/activetasks/tests/fakeActiveTaskResponse.js new file mode 100644 index 0000000..687e566 --- /dev/null +++ b/app/addons/activetasks/tests/fakeActiveTaskResponse.js @@ -0,0 +1,121 @@ +// 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 () { + var fakeData = [ + { + "user": "okra", + "updated_on": 10, + "type": "replication", + "target": "https://okra.fake.com/okra_rep/", + "doc_write_failures": 0, + "doc_id": "rep_1", + "continuous": true, + "checkpointed_source_seq": "3-g1AAAAEkeJyFzkEOgjAQBdAJkHgOPUBDQS1dyVVmaE0l0CaIa72Z3qyOwahscPMn-Zm8_A4AMpca2BhqwmBrQzIXfQj-7E7eiqYLF4N-FN6OHf8mCHSIMbYuwbTnYiUllUbJl7H-GNUSQTUnXd8KTEqR467Qc0UtKT7jhBsfhu5fCa3SFR3nUvlfekzS76a9Vmi37RPEPVpb", + "checkpoint_interval": 5000, + "changes_pending": null, + "pid": "<0.10487.5819>", + "node": "[email protected]", + "docs_read": 0, + "docs_written": 0, + "missing_revisions_found": 0, + "replication_id": "07d9a41f181ce11b93ba1855ca7+continuous+create_target", + "revisions_checked": 0, + "source": "https://okra.fake.com/okra/", + "source_seq": 0, + "started_on": 9 + }, + { + "user": "ooo", + "updated_on": 8, + "type": "indexer", + "node": "[email protected]", + "pid": "<0.10541.4469>", + "changes_done": 145, + "database": "shards/00000000-3fffffff/ooo/fakedatabase.1234567890abc", + "design_document": "_design/superpurple", + "progress": 0, + "started_on": 7, + "total_changes": 22942 + }, + { + "updated_on": 6, + "node": "[email protected]", + "pid": "<0.1152.4474>", + "changes_done": 144001, + "database": "shards/00000000-3fffffff/global_changes.1398718618", + "progress": 66, + "started_on": 5, + "total_changes": 218052, + "type": "database_compaction" + }, + { + "user": "information", + "updated_on": 4, + "type": "replication", + "target": "https://information.com/somedatabase/", + "doc_write_failures": 0, + "doc_id": "c0ffb663f75cd940aadb4a4eaa", + "continuous": true, + "checkpointed_source_seq": 58717, + "checkpoint_interval": 3600000, + "changes_pending": 0, + "pid": "<0.11612.3684>", + "node": "fake.fake.com", + "docs_read": 108, + "docs_written": 108, + "missing_revisions_found": 108, + "replication_id": "a546c13951c6bd4f2d187d388+continuous", + "revisions_checked": 1024, + "source": "http://software:*****@123.123.123:5984/somedatabase/", + "source_seq": 58717, + "started_on": 3 + }, + { + "user": "abc", + "updated_on": 2, + "type": "replication", + "target": "https://fake.com/db_abc/", + "doc_write_failures": 0, + "doc_id": "replication_2014", + "continuous": true, + "checkpointed_source_seq": "2-g1AAAAEXeJzLYWBgYMlgTyrNSS3QS87JL01JzCvRy0styQGqY0pkSLL___9_VgZTImMuUIA9JSUx1cTIAE2_IS79SQ5AMqkexQiLNAPzNAsjYp2QxwIkGRqAFNCU_SBjGMDGGFokp6WZJ6IZY4TfmAMQY_4jjDE2SDE3TzHJAgBp5FSv", + "checkpoint_interval": 5000, + "changes_pending": 0, + "pid": "<0.14029.1733>", + "node": "node.node.node..net", + "docs_read": 0, + "docs_written": 0, + "missing_revisions_found": 0, + "replication_id": "33af566bab6a58aee04e+continuous", + "revisions_checked": 7, + "source": "https://fake.fake123.com/db_abc/", + "source_seq": "2-g1AAAAEXeJzLYWBgYMlgS3QS87JL01JzCvRy0styQGqY0pkSLL___9_VgZTImMuUIA9JSUx1cTIAE2_IS79SQ5AMqkexQiLNAPzNAsjYp2QxwIkGRqAFNCU_SBjGMDGGFokp6WZJ6IZY4TfmAMQY_4jjDE2SDE3TzHJAgBp5FSv", + "started_on": 1 + }, + { + "view": 5, + "user": "treeman", + "updated_on": 1426614009, + "type": "view_compaction", + "total_changes": 1108953, + "node": "treeman.net", + "pid": "<0.19668.4045>", + "changes_done": 430000, + "database": "shards/c0000000-ffffffff/treecompany/fake.1234567890", + "design_document": "_design/trunk", + "phase": "view", + "progress": 38, + "started_on": 1426602505 + }, + ]; + return fakeData; +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/tests/viewsSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/tests/viewsSpec.js b/app/addons/activetasks/tests/viewsSpec.js deleted file mode 100644 index 6f28f5b..0000000 --- a/app/addons/activetasks/tests/viewsSpec.js +++ /dev/null @@ -1,132 +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([ - 'api', - 'addons/activetasks/views', - 'addons/activetasks/resources', - 'addons/activetasks/routes', - 'testUtils' -], function (FauxtonAPI, Views, Activetasks, RouteObject, testUtils) { - var assert = testUtils.assert, - ViewSandbox = testUtils.ViewSandbox; - - describe("TabMenu", function () { - var tabMenu; - - beforeEach(function () { - tabMenu = new Views.TabMenu({}); - }); - - describe("on change polling rate", function () { - var viewSandbox; - beforeEach(function (done) { - viewSandbox = new ViewSandbox(); - viewSandbox.renderView(tabMenu, done); - }); - - afterEach(function () { - viewSandbox.remove(); - }); - - it("Should set polling rate", function () { - var $range = tabMenu.$('#pollingRange'); - $range.val(15); - $range.trigger('input'); - - assert.equal(tabMenu.$('span').text(), 15); - }); - - it("Should clearInterval", function () { - var $range = tabMenu.$('#pollingRange'); - var clearIntervalMock = sinon.spy(window, 'clearInterval'); - $range.trigger('input'); - - assert.ok(clearIntervalMock.calledOnce); - }); - - it("Should trigger update:poll event", function () { - var spy = sinon.spy(); - Views.Events.on('update:poll', spy); - var $range = tabMenu.$('#pollingRange'); - $range.trigger('input'); - - assert.ok(spy.calledOnce); - }); - - it("should set the correct active tab", function () { - var $rep = tabMenu.$('li[data-type="replication"]'); - $rep.click(); - assert.ok($rep.hasClass('active')); - }); - }); - - describe('on request by type', function () { - var viewSandbox, mainView, tabHeader, searchModel; - - beforeEach(function (done) { - searchModel = new Activetasks.Search(); - - tabHeader = new Views.TabHeader({ - searchModel: searchModel - }); - mainView = new Views.View({ - collection: new Activetasks.AllTasks(), - currentView: "all", - searchModel: searchModel - }); - - viewSandbox = new ViewSandbox(); - viewSandbox.renderView(tabHeader, function () { - viewSandbox.renderView(mainView, done); - }); - }); - - afterEach(function () { - viewSandbox.remove(); - }); - - it("should set the filter 'database' for the main-view", function () { - var $rep = tabHeader.$("input").val("registry").trigger("keyup"); - assert.equal("registry", mainView.searchModel.get('filterDatabase')); - }); - }); - - }); - - describe('DataSection', function () { - var viewSandbox, mainView; - beforeEach(function (done) { - mainView = new Views.View({ - collection: new Activetasks.AllTasks(), - currentView: "all", - searchModel: new Activetasks.Search() - }); - - viewSandbox = new ViewSandbox(); - viewSandbox.renderView(mainView, done); - }); - - afterEach(function () { - viewSandbox.remove(); - }); - - describe('#setPolling', function () { - - it('Should set polling interval', function () { - var spy = sinon.spy(window, 'setInterval'); - mainView.setPolling(); - assert.ok(spy.calledOnce); - }); - - }); - }); -}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/addons/activetasks/views.js ---------------------------------------------------------------------- diff --git a/app/addons/activetasks/views.js b/app/addons/activetasks/views.js deleted file mode 100644 index dadb9e0..0000000 --- a/app/addons/activetasks/views.js +++ /dev/null @@ -1,237 +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', - 'api', - 'addons/activetasks/resources' -], - -function (app, FauxtonAPI, ActiveTasks) { - - var Views = {}, - Events = {}, - pollingInfo = { - rate: '5', - intervalId: null - }; - - Views.Events = _.extend(Events, Backbone.Events); - - Views.View = FauxtonAPI.View.extend({ - tagName: 'table', - className: 'table table-bordered table-striped active-tasks', - template: 'addons/activetasks/templates/table', - - events: { - 'click th': 'sortByType' - }, - - initialize: function () { - this.listenTo(this.searchModel, 'change', this.render); - this.listenTo(this.collection, 'reset', this.render); - }, - - beforeRender: function () { - this.filterAndInsertView(); - }, - - filterAndInsertView: function () { - var database = this.searchModel.get('filterDatabase'), - filter = this.searchModel.get('filterType'), - databaseRegex = new RegExp(database, 'g'); - - this.removeView('.js-tasks-go-here'); - - this.collection.forEach(function (item) { - - if (filter && filter !== 'all' && item.get('type') !== filter) { - return; - } - - if (database && - !databaseRegex.test(item.get('source')) && - !databaseRegex.test(item.get('target')) && - !databaseRegex.test(item.get('database'))) { - return; - } - - var view = new Views.TableDetail({ - model: item - }); - this.insertView('.js-tasks-go-here', view); - }, this); - }, - - afterRender: function () { - Events.bind('update:poll', this.setPolling, this); - this.setPolling(); - }, - - establish: function () { - return [this.collection.fetch()]; - }, - - serialize: function () { - return { - currentView: this.currentView, - collection: this.collection - }; - }, - - sortByType: function (e) { - var currentTarget = e.currentTarget, - datatype = this.$(currentTarget).attr('data-type'); - - this.collection.sortByColumn(datatype); - this.render(); - }, - - setPolling: function () { - var collection = this.collection; - - clearInterval(pollingInfo.intervalId); - pollingInfo.intervalId = setInterval(function () { - collection.fetch({reset: true}); - }, pollingInfo.rate * 1000); - }, - - cleanup: function () { - clearInterval(pollingInfo.intervalId); - } - }); - - Views.TabMenu = FauxtonAPI.View.extend({ - tagName: 'nav', - className: 'sidenav', - template: 'addons/activetasks/templates/tabs', - - events: { - 'click .task-tabs li': 'requestByType', - 'input #pollingRange': 'changePollInterval', - 'change #pollingRange': 'changePollInterval' - }, - - serialize: function () { - return { - filters: { - 'all': 'All tasks', - 'replication': 'Replication', - 'database_compaction': 'Database Compaction', - 'indexer': 'Indexer', - 'view_compaction': 'View Compaction' - } - }; - }, - - afterRender: function () { - this.$('.task-tabs').find('li').eq(0).addClass('active'); - }, - - changePollInterval: function (e) { - var range = this.$(e.currentTarget).val(); - this.$('label[for="pollingRange"] span').text(range); - pollingInfo.rate = range; - clearInterval(pollingInfo.intervalId); - Events.trigger('update:poll'); - }, - - cleanup: function () { - clearInterval(pollingInfo.intervalId); - }, - - requestByType: function (e) { - var currentTarget = e.currentTarget, - filter = this.$(currentTarget).attr('data-type'); - - this.$('.task-tabs').find('li').removeClass('active'); - this.$(currentTarget).addClass('active'); - - FauxtonAPI.triggerRouteEvent('changeFilter', filter); - } - }); - - Views.TableDetail = FauxtonAPI.View.extend({ - tagName: 'tr', - template: 'addons/activetasks/templates/tabledetail', - - initialize: function () { - this.type = this.model.get('type'); - }, - - getObject: function () { - var objectField = this.model.get('database'); - if (this.type === 'replication') { - objectField = this.model.get('source') + ' to ' + this.model.get('target'); - } else if (this.type === 'indexer') { - objectField = this.model.get('database') + ' (View: ' + this.model.get('design_document') + ')'; - } - return objectField; - }, - - getProgress: function () { - var progress = ''; - if (this.type === 'indexer') { - progress = 'Processed ' + this.model.get('changes_done') + ' of ' + this.model.get('total_changes') + ' changes. '; - } else if (this.type === 'replication') { - progress = this.model.get('docs_written') + ' docs written. '; - if (!_.isUndefined(this.model.get('changes_pending'))) { - progress += this.model.get('changes_pending') + ' pending changes. '; - } - } - if (!_.isUndefined(this.model.get('source_seq'))) { - progress += 'Current source sequence: ' + this.model.get('source_seq') + '. '; - } - if (!_.isUndefined(this.model.get('changes_done'))) { - progress += this.model.get('changes_done') + ' Changes done. '; - } - if (!_.isUndefined(this.model.get('progress'))) { - progress += 'Progress: ' + this.model.get('progress') + '% '; - } - - return progress; - }, - - serialize: function () { - return { - model: this.model, - objectField: this.getObject(), - progress: this.getProgress() - }; - } - }); - - Views.TabHeader = FauxtonAPI.View.extend({ - template: 'addons/activetasks/templates/tab_header', - - events: { - 'keyup input': 'searchDb', - 'click .js-toggle-filter': 'toggleQuery' - }, - - toggleQuery: function () { - $('#dashboard-content').scrollTop(0); - this.$('#query').toggle('slow'); - }, - - searchDb: function (event) { - event.preventDefault(); - - var $search = this.$('input[name="search"]'), - database = $search.val(); - - this.searchModel.set('filterDatabase', database); - } - }); - - return Views; -}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/df5010b3/app/helpers.js ---------------------------------------------------------------------- diff --git a/app/helpers.js b/app/helpers.js index 478ca89..53b1096 100644 --- a/app/helpers.js +++ b/app/helpers.js @@ -55,7 +55,10 @@ function (constants, utils, d3, moment) { }; Helpers.formatDate = function (timestamp) { - return moment(timestamp, 'X').format('MMM Do, h:m:ss a'); + return moment(timestamp, 'X').format('MMM Do, h:mm:ss a'); + }; + Helpers.getDateFromNow = function (timestamp) { + return moment(timestamp, 'X').fromNow(); }; return Helpers;
