couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From michel...@apache.org
Subject fauxton commit: updated refs/heads/master to df5010b
Date Tue, 14 Apr 2015 18:03:02 GMT
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: michellephung@gmail.com <michellephung@gmail.com>
Authored: Fri Mar 6 18:57:02 2015 -0500
Committer: michellephung@gmail.com <michellephung@gmail.com>
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": "fake1@fake.net",
+      "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": "fake2@fake2.net",
+      "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": "fake3@fake3.net",
+      "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;


Mime
View raw message