This is an automated email from the ASF dual-hosted git repository.

garren pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git


The following commit(s) were added to refs/heads/master by this push:
     new b425e60  Convert activetasks to redux (#1141)
b425e60 is described below

commit b425e60d02e131bad18bdb6a5999e8411df7189d
Author: garren smith <garren.sm...@gmail.com>
AuthorDate: Mon Oct 15 21:29:03 2018 +0200

    Convert activetasks to redux (#1141)
    
    * Convert activetasks to redux
    
    * remove unused function
    
    * fix failing test
---
 .../activetasks/__tests__/components.test.js       |  80 ++--
 .../__tests__/fakeActiveTaskResponse.js            |   3 +-
 app/addons/activetasks/__tests__/reducer.test.js   | 106 +++++
 app/addons/activetasks/__tests__/stores.test.js    |  90 ----
 app/addons/activetasks/actions.js                  | 119 +++--
 app/addons/activetasks/{base.js => api.js}         |  16 +-
 app/addons/activetasks/base.js                     |   5 +
 app/addons/activetasks/components.js               | 481 ---------------------
 app/addons/activetasks/components/controller.js    |  59 +++
 app/addons/activetasks/components/filtertabs.js    |  75 ++++
 app/addons/activetasks/components/polling.js       |  48 ++
 app/addons/activetasks/components/table.js         |  44 ++
 app/addons/activetasks/components/tablebody.js     |  70 +++
 .../activetasks/components/tablebodycontents.js    | 117 +++++
 app/addons/activetasks/components/tableheader.js   |  86 ++++
 app/addons/activetasks/container.js                |  57 +++
 app/addons/activetasks/layout.js                   |   9 +-
 app/addons/activetasks/reducers.js                 | 141 ++++++
 app/addons/activetasks/resources.js                |  46 --
 app/addons/activetasks/routes.js                   |   9 +-
 app/addons/activetasks/stores.js                   | 223 ----------
 21 files changed, 921 insertions(+), 963 deletions(-)

diff --git a/app/addons/activetasks/__tests__/components.test.js 
b/app/addons/activetasks/__tests__/components.test.js
index b2994a1..e871eb8 100644
--- a/app/addons/activetasks/__tests__/components.test.js
+++ b/app/addons/activetasks/__tests__/components.test.js
@@ -9,43 +9,20 @@
 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 // License for the specific language governing permissions and limitations 
under
 // the License.
-import ActiveTasks from "../resources";
-import Components from "../components";
-import Stores from "../stores";
-import fakedResponse from "./fakeActiveTaskResponse";
+import TableHeader from '../components/tableheader';
+import FilterTabs from '../components/filtertabs';
 import React from "react";
 import ReactDOM from "react-dom";
-import Actions from "../actions";
 import utils from "../../../../test/mocha/testUtils";
 import {mount} from 'enzyme';
 import sinon from "sinon";
 const assert = utils.assert;
-var restore = utils.restore;
-var activeTasksStore = Stores.activeTasksStore;
-var activeTasksCollection = new ActiveTasks.AllTasks({});
-activeTasksCollection.parse(fakedResponse);
 
 describe('Active Tasks -- Components', () => {
 
   describe('Active Tasks Table (Components)', () => {
-    let table;
-
-    beforeEach(() => {
-      activeTasksStore.initAfterFetching(activeTasksCollection.table, 
activeTasksCollection);
-      table = mount(<Components.ActiveTasksController />);
-    });
-
-    afterEach(() => {
-      restore(window.confirm);
-    });
-
     describe('Active Tasks Filter tray', () => {
 
-      afterEach(() => {
-        restore(Actions.switchTab);
-        restore(Actions.setSearchTerm);
-      });
-
       const radioTexts = [
         'Replication',
         'Database Compaction',
@@ -54,44 +31,57 @@ describe('Active Tasks -- Components', () => {
       ];
 
       it('should trigger change to radio buttons', () => {
-
         radioTexts.forEach((text) => {
-          let spy = sinon.spy(Actions, 'switchTab');
+          let spy = sinon.spy();
+          const tabs = mount(
+            <FilterTabs
+              onRadioClick={spy}
+              selectedRadio={"All Tasks"}
+              radioNames={radioTexts}
+            />
+          );
 
-          table.find(`input[value="${text}"]`).simulate('change');
+          tabs.find(`input[value="${text}"]`).simulate('change');
           assert.ok(spy.calledOnce);
-
-          spy.restore();
         });
       });
 
       it('should trigger change to search term', () => {
-        const spy = sinon.spy(Actions, 'setSearchTerm');
-        table.find('.searchbox').simulate('change', {target: {value: 
'searching'}});
+        const spy = sinon.spy();
+        const tabs = mount(
+          <FilterTabs
+            onSearch={spy}
+            selectedRadio={"All Tasks"}
+          />
+        );
+        tabs.find('.searchbox').simulate('change', {target: {value: 
'searching'}});
         assert.ok(spy.calledOnce);
       });
     });
 
     describe('Active Tasks Table Headers', () => {
-      var headerNames = [
+      const tableTexts = [
         'type',
         'database',
-        'started-on',
-        'updated-on',
-        'pid',
-        'progress'
+        'started-on'
       ];
 
-      afterEach(() => {
-        restore(Actions.sortByColumnHeader);
-      });
+      it('should trigger change to radio buttons', () => {
+
+        tableTexts.forEach((text) => {
+          let spy = sinon.spy();
+          const table = mount(
+            <table>
+              <TableHeader
+                onTableHeaderClick={spy}
+                headerIsAscending={true}
+                sortByHeader={"All Tasks"}
+              />
+            </table>
+          );
 
-      it('should trigger change to which header to sort by', () => {
-        headerNames.forEach(header => {
-          let spy = sinon.spy(Actions, 'sortByColumnHeader');
-          table.find('#' + header).simulate('change');
+          table.find(`.${text}`).simulate('click');
           assert.ok(spy.calledOnce);
-          spy.restore();
         });
       });
     });
diff --git a/app/addons/activetasks/__tests__/fakeActiveTaskResponse.js 
b/app/addons/activetasks/__tests__/fakeActiveTaskResponse.js
index 823b7f2..bf60a47 100644
--- a/app/addons/activetasks/__tests__/fakeActiveTaskResponse.js
+++ b/app/addons/activetasks/__tests__/fakeActiveTaskResponse.js
@@ -9,7 +9,7 @@
 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 // License for the specific language governing permissions and limitations 
under
 // the License.
-var fakeData = [
+export default [
   {
     "user": "okra",
     "updated_on": 10,
@@ -116,4 +116,3 @@ var fakeData = [
     "started_on": 1426602505
   },
 ];
-export default fakeData;
diff --git a/app/addons/activetasks/__tests__/reducer.test.js 
b/app/addons/activetasks/__tests__/reducer.test.js
new file mode 100644
index 0000000..28aba0c
--- /dev/null
+++ b/app/addons/activetasks/__tests__/reducer.test.js
@@ -0,0 +1,106 @@
+// 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.
+import reducer from '../reducers';
+import ActionTypes from '../actiontypes';
+import fakedResponse from "./fakeActiveTaskResponse";
+import utils from "../../../../test/mocha/testUtils";
+const assert = utils.assert;
+
+describe('Active Tasks -- Stores', () => {
+  let initState;
+
+  beforeEach(() => {
+    initState = reducer(undefined, {
+      type: ActionTypes.ACTIVE_TASKS_FETCH_AND_SET,
+      options: fakedResponse
+    });
+  });
+
+  describe('Active Task Stores - Filter Tab Tray', () => {
+
+    it('should filter the table correctly, by radio -- All Tasks', () => {
+      const state = reducer(initState, {
+        type: ActionTypes.ACTIVE_TASKS_SWITCH_TAB,
+        options: 'all_tasks'
+      });
+      assert.ok(state.tasks.length > 0);
+      assert.deepEqual(state.tasks.length, state.filteredTasks.length);
+    });
+
+    it('should filter the table correctly, by radio', () => {
+      const state = reducer(initState, {
+        type: ActionTypes.ACTIVE_TASKS_SWITCH_TAB,
+        options: 'replication'
+      });
+
+      //parse table and check that it only contains objects with type: 
Replication
+      assert.ok(state.filteredTasks.length > 0);
+      state.filteredTasks.forEach(task => {
+        assert.deepEqual(task.type, 'replication');
+        assert.deepEqual(task.type, state.selectedRadio);
+      });
+    });
+
+    it.only('should search the table correctly', () => {
+      var searchTerm = 'base';
+      const state = reducer(initState, {
+        type: ActionTypes.ACTIVE_TASKS_SET_SEARCH_TERM,
+        options: searchTerm
+      });
+
+      const fakeFilteredTable = [
+        { user: 'information'},
+        { user: 'ooo'}
+      ];
+
+      assert.equal(fakeFilteredTable[0].user, state.filteredTasks[0].user);
+      assert.equal(fakeFilteredTable[1].user, state.filteredTasks[1].user);
+    });
+  });
+
+  describe('Active Task Stores - Table Header Sort - Select 
Ascending/Descending', () => {
+
+    it('should set header as ascending, on default', () => {
+      const state = reducer(initState, {
+        type: ActionTypes.ACTIVE_TASKS_SWITCH_TAB,
+        options: 'all_tasks'
+      });
+      assert.ok(state.headerIsAscending);
+    });
+
+    it('should set header as descending, if same header is selected again', () 
=> {
+      const state = reducer(initState, {
+        type: ActionTypes.ACTIVE_TASKS_SORT_BY_COLUMN_HEADER,
+        options: 'sameHeader'
+      });
+      assert.ok(state.headerIsAscending);
+
+      const state2 = reducer(state, {
+        type: ActionTypes.ACTIVE_TASKS_SORT_BY_COLUMN_HEADER,
+        options: 'sameHeader'
+      });
+      assert.notOk(state2.headerIsAscending);
+
+      const state3 = reducer(state2, {
+        type: ActionTypes.ACTIVE_TASKS_SORT_BY_COLUMN_HEADER,
+        options: 'sameHeader'
+      });
+      assert.ok(state3.headerIsAscending);
+
+      const state4 = reducer(state3, {
+        type: ActionTypes.ACTIVE_TASKS_SORT_BY_COLUMN_HEADER,
+        options: 'differentHeader'
+      });
+      assert.ok(state4.headerIsAscending);
+    });
+  });
+});
diff --git a/app/addons/activetasks/__tests__/stores.test.js 
b/app/addons/activetasks/__tests__/stores.test.js
deleted file mode 100644
index c6f67d1..0000000
--- a/app/addons/activetasks/__tests__/stores.test.js
+++ /dev/null
@@ -1,90 +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.
-import ActiveTasks from "../resources";
-import Stores from "../stores";
-import fakedResponse from "./fakeActiveTaskResponse";
-import utils from "../../../../test/mocha/testUtils";
-const assert = utils.assert;
-
-var activeTasksStore = Stores.activeTasksStore;
-var activeTasksCollection = new ActiveTasks.AllTasks();
-activeTasksCollection.parse(fakedResponse);
-
-describe('Active Tasks -- Stores', function () {
-  beforeEach(function () {
-    activeTasksStore.initAfterFetching(activeTasksCollection.table, 
activeTasksCollection);
-  });
-
-  describe('Active Task Stores - Filter Tab Tray', function () {
-    var fakeFilteredTable;
-
-    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, (activeTask) => {
-        assert.ok(activeTasksStore.passesRadioFilter(activeTask));
-        assert.deepEqual(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);
-
-      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());
-    });
-
-    it('should set header as descending, if same header is selected again', 
function () {
-      activeTasksStore._prevSortbyHeader = 'sameHeader';
-      activeTasksStore._sortByHeader = 'sameHeader';
-      activeTasksStore.toggleHeaderIsAscending();
-      assert.notOk(activeTasksStore.getHeaderIsAscending());
-    });
-
-    it('should set header as ascending, if different header is selected', 
function () {
-      activeTasksStore._sortByHeader = 'differentHeader';
-      activeTasksStore._prevSortbyHeader = 'sameHeader';
-      activeTasksStore.toggleHeaderIsAscending();
-      assert.ok(activeTasksStore.getHeaderIsAscending());
-    });
-  });
-});
diff --git a/app/addons/activetasks/actions.js 
b/app/addons/activetasks/actions.js
index e6213cd..60d6488 100644
--- a/app/addons/activetasks/actions.js
+++ b/app/addons/activetasks/actions.js
@@ -9,72 +9,69 @@
 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 // License for the specific language governing permissions and limitations 
under
 // the License.
-import FauxtonAPI from "../../core/api";
+import FauxtonAPI from '../../core/api';
 import ActionTypes from "./actiontypes";
+import fetchActiveTasks from './api';
 
-export default {
+export const setActiveTaskIsLoading = (boolean) => {
+  return {
+    type: ActionTypes.ACTIVE_TASKS_SET_IS_LOADING,
+    options: boolean
+  };
+};
 
-  init: function (activeTasks) {
-    this.fetchAndSetActiveTasks(activeTasks.table, activeTasks);
-    FauxtonAPI.when(activeTasks.fetch()).then(function () {
-      this.fetchAndSetActiveTasks(activeTasks.table, activeTasks);
-      this.setActiveTaskIsLoading(false);
-    }.bind(this));
-  },
+export const setActiveTasks = (tasks) => {
+  return {
+    type: ActionTypes.ACTIVE_TASKS_FETCH_AND_SET,
+    options: tasks
+  };
+};
 
-  fetchAndSetActiveTasks: function (collection, backboneCollection) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.ACTIVE_TASKS_FETCH_AND_SET,
-      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
-      }
-    });
-  },
-  setActiveTaskIsLoading: function (boolean) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.ACTIVE_TASKS_SET_IS_LOADING,
-      options: boolean
+export const init = () => (dispatch) => {
+  dispatch(setActiveTaskIsLoading(true));
+  fetchActiveTasks()
+    .then(tasks => {
+      dispatch(setActiveTaskIsLoading(false));
+      dispatch(setActiveTasks(tasks));
+    })
+    .catch(error => {
+      FauxtonAPI.addNotification({
+        msg: `Fetching active tasks failed: ${error}`,
+        type: 'error'
+      });
     });
-  },
-  runPollingUpdate (collection) {
-    collection.pollingFetch().then(() => {
-      FauxtonAPI.dispatch({
-        type: ActionTypes.ACTIVE_TASKS_POLLING_COLLECTION,
-        options: collection.table
+};
+
+export const switchTab = (tab) => {
+  return {
+    type: ActionTypes.ACTIVE_TASKS_SWITCH_TAB,
+    options: tab
+  };
+};
+
+export const setSearchTerm = (searchTerm) => {
+  return {
+    type: ActionTypes.ACTIVE_TASKS_SET_SEARCH_TERM,
+    options: searchTerm
+  };
+};
+
+export const sortByColumnHeader = (columnName) => {
+  return {
+    type: ActionTypes.ACTIVE_TASKS_SORT_BY_COLUMN_HEADER,
+    options: columnName
+  };
+};
+
+export const runPollingUpdate = () => (dispatch) => {
+  fetchActiveTasks()
+    .then(tasks => {
+      dispatch(setActiveTasks(tasks));
+    })
+    .catch(error => {
+      FauxtonAPI.addNotification({
+        msg: `Fetching active tasks failed: ${error}`,
+        type: 'error'
       });
     });
-  }
 };
diff --git a/app/addons/activetasks/base.js b/app/addons/activetasks/api.js
similarity index 65%
copy from app/addons/activetasks/base.js
copy to app/addons/activetasks/api.js
index 62a2728..161c341 100644
--- a/app/addons/activetasks/base.js
+++ b/app/addons/activetasks/api.js
@@ -10,12 +10,16 @@
 // License for the specific language governing permissions and limitations 
under
 // the License.
 
-import FauxtonAPI from "../../core/api";
-import Activetasks from "./routes";
-import "./assets/less/activetasks.less";
+import Helpers from "../../helpers";
+import { get } from '../../core/ajax';
 
-Activetasks.initialize = function () {
-  FauxtonAPI.addHeaderLink({title: 'Active Tasks', icon: 
'fonticon-activetasks', href: '#/activetasks'});
+export default () => {
+  return get(Helpers.getServerUrl('/_active_tasks'))
+    .then(tasks => {
+      if (tasks.error) {
+        throw new Error(tasks.reason);
+      }
+      return tasks;
+    });
 };
 
-export default Activetasks;
diff --git a/app/addons/activetasks/base.js b/app/addons/activetasks/base.js
index 62a2728..f51cf89 100644
--- a/app/addons/activetasks/base.js
+++ b/app/addons/activetasks/base.js
@@ -13,9 +13,14 @@
 import FauxtonAPI from "../../core/api";
 import Activetasks from "./routes";
 import "./assets/less/activetasks.less";
+import reducers from './reducers';
 
 Activetasks.initialize = function () {
   FauxtonAPI.addHeaderLink({title: 'Active Tasks', icon: 
'fonticon-activetasks', href: '#/activetasks'});
 };
 
+FauxtonAPI.addReducers({
+  activetasks: reducers
+});
+
 export default Activetasks;
diff --git a/app/addons/activetasks/components.js 
b/app/addons/activetasks/components.js
deleted file mode 100644
index fa02ebb..0000000
--- a/app/addons/activetasks/components.js
+++ /dev/null
@@ -1,481 +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.
-
-import app from "../../app";
-import React from "react";
-import ReactDOM from "react-dom";
-import Stores from "./stores";
-import Resources from "./resources";
-import Actions from "./actions";
-import Components from "../components/react-components";
-import Helpers from '../../helpers';
-
-const {TabElement, TabElementWrapper, Polling} = Components;
-
-const activeTasksStore = Stores.activeTasksStore;
-
-export class ActiveTasksController extends React.Component {
-  getStoreState = () => {
-    return {
-      collection: activeTasksStore.getCollection(),
-      searchTerm: activeTasksStore.getSearchTerm(),
-      selectedRadio: activeTasksStore.getSelectedRadio(),
-
-      sortByHeader: activeTasksStore.getSortByHeader(),
-      headerIsAscending: activeTasksStore.getHeaderIsAscending()
-
-    };
-  };
-
-  componentDidMount() {
-    Actions.init(new Resources.AllTasks());
-    activeTasksStore.on('change', this.onChange, this);
-  }
-
-  componentWillUnmount() {
-    activeTasksStore.off('change', this.onChange, this);
-  }
-
-  onChange = () => {
-    this.setState(this.getStoreState());
-  };
-
-  setNewSearchTerm = (searchTerm) => {
-    Actions.setSearchTerm(searchTerm);
-  };
-
-  switchTab = (newRadioButton) => {  //tabs buttons
-    Actions.switchTab(newRadioButton);
-  };
-
-  tableHeaderOnClick = (headerClicked) => {
-    Actions.sortByColumnHeader(headerClicked);
-  };
-
-  state = this.getStoreState();
-
-  render() {
-    const {collection, searchTerm, selectedRadio, sortByHeader, 
headerIsAscending} = this.state;
-
-    const setSearchTerm = this.setNewSearchTerm;
-    const onTableHeaderClick = this.tableHeaderOnClick;
-
-    return (
-      <div id="active-tasks-page" className="scrollable">
-        <div className="inner">
-          <ActiveTasksFilterTabs
-            searchTerm={searchTerm}
-            selectedRadio={selectedRadio}
-            onSearch={setSearchTerm}
-            onRadioClick={this.switchTab}/>
-          <ActiveTaskTable
-            collection={collection}
-            searchTerm={searchTerm}
-            selectedRadio={selectedRadio}
-            onTableHeaderClick={onTableHeaderClick}
-            sortByHeader={sortByHeader}
-            headerIsAscending={headerIsAscending} />
-        </div>
-      </div>
-    );
-  }
-}
-
-class ActiveTasksFilterTabs extends React.Component {
-  static defaultProps = {
-    radioNames : [
-      'All Tasks',
-      'Replication',
-      'Database Compaction',
-      'Indexer',
-      'View Compaction'
-    ]
-  };
-
-  checked = (radioName) => {
-    return this.props.selectedRadio === radioName;
-  };
-
-  onRadioClick = (e) => {
-    var radioName = e.target.value;
-    this.props.onRadioClick(radioName);
-  };
-
-  createFilterTabs = () => {
-    return (
-      this.props.radioNames.map((radioName, i) => {
-        const checked = this.checked(radioName);
-
-        return (
-          <TabElement
-            key={i}
-            selected={checked}
-            text={radioName}
-            onChange={this.onRadioClick} />
-        );
-      })
-    );
-  };
-
-  searchTermChange = (e) => {
-    var searchTerm = e.target.value;
-    this.props.onSearch(searchTerm);
-  };
-
-  render() {
-    const filterTabs = this.createFilterTabs();
-    return (
-      <TabElementWrapper>
-        {filterTabs}
-        <li className="component-tab-list-element">
-          <input
-            id="active-tasks-search-box"
-            className="searchbox"
-            type="text"
-            name="search"
-            placeholder="Search for databases..."
-            value={this.props.searchTerm}
-            onChange={this.searchTermChange} />
-        </li>
-      </TabElementWrapper>
-    );
-  }
-}
-
-class ActiveTaskTable extends React.Component {
-  render() {
-    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 id="active-tasks-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>
-    );
-  }
-}
-
-class ActiveTasksTableHeader extends React.Component {
-  static defaultProps = {
-    headerNames : [
-      ['type', 'Type'],
-      ['database', 'Database'],
-      ['started-on', 'Started on'],
-      ['updated-on', 'Updated on'],
-      ['pid', 'PID'],
-      ['progress', 'Status']
-    ]
-  };
-
-  createTableHeadingFields = () => {
-    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() {
-    return (
-      <thead>
-        <tr>{this.createTableHeadingFields()}</tr>
-      </thead>
-    );
-  }
-}
-
-class TableHeader extends React.Component {
-  arrow = () => {
-    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 = (e) => {
-    var headerSelected = e.target.value;
-    this.props.onTableHeaderClick(headerSelected);
-  };
-
-  render() {
-    var arrow = this.arrow();
-    var th_class = 'header-field ' + this.props.headerName;
-
-    return (
-      <td className={th_class + " tableheader"} value={this.props.headerName}>
-        <input
-          type="radio"
-          name="header-field"
-          id={this.props.headerName}
-          value={this.props.headerName}
-          className="header-field radio"
-          onChange={this.onTableHeaderClick} />
-        <label
-          className="header-field label-text active-tasks-header noselect"
-          htmlFor={this.props.headerName}>
-          {this.props.displayName} {arrow}
-        </label>
-      </td>
-    );
-  }
-}
-
-class ActiveTasksTableBody extends React.Component {
-  getStoreState = () => {
-    return {
-      filteredTable: activeTasksStore.getFilteredTable(this.props.collection)
-    };
-  };
-
-  UNSAFE_componentWillReceiveProps() {
-    this.setState({
-      filteredTable: activeTasksStore.getFilteredTable(this.props.collection)
-    });
-  }
-
-  createRows = () => {
-    var isThereASearchTerm = this.props.searchTerm.trim() === "";
-
-    if (this.state.filteredTable.length === 0) {
-      return isThereASearchTerm ? this.noActiveTasks() : 
this.noActiveTasksMatchFilter();
-    }
-
-    return _.map(this.state.filteredTable, (item, key) => {
-      return <ActiveTaskTableBodyContents key={key} item={item} />;
-    });
-  };
-
-  noActiveTasks = () => {
-    var type = this.props.selectedRadio;
-    if (type === "All Tasks") {
-      type = "";
-    }
-
-    return (
-      <tr className="no-matching-database-on-search">
-        <td  colSpan="6">No active {type} tasks.</td>
-      </tr>
-    );
-  };
-
-  noActiveTasksMatchFilter = () => {
-    var type = this.props.selectedRadio;
-    if (type === "All Tasks") {
-      type = "";
-    }
-
-    return (
-      <tr className="no-matching-database-on-search">
-        <td colSpan="6">No active {type} tasks match with filter: 
&quot;{this.props.searchTerm}&quot;</td>
-      </tr>
-    );
-  };
-
-  state = this.getStoreState();
-
-  render() {
-    return (
-      <tbody className="js-tasks-go-here">
-        {this.createRows()}
-      </tbody>
-    );
-  }
-}
-
-class ActiveTaskTableBodyContents extends React.Component {
-  getInfo = (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 = (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() {
-    var rowData =  this.getInfo(this.props.item);
-    var objectFieldMsg = this.multilineMessage(rowData.objectField, 
'to-from-database');
-    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>
-    );
-  }
-}
-
-export class ActiveTasksPollingWidgetController extends React.Component {
-  getStoreState = () => {
-    return {
-      collection:  activeTasksStore.getBackboneCollection()
-    };
-  };
-
-  componentDidMount() {
-    activeTasksStore.on('change', this.onChange, this);
-  }
-
-  componentWillUnmount() {
-    activeTasksStore.off('change', this.onChange, this);
-  }
-
-  onChange = () => {
-    this.setState(this.getStoreState());
-  };
-
-  runPollingUpdate = () => {
-    Actions.runPollingUpdate(this.state.collection);
-  };
-
-  getPluralForLabel = () => {
-    return this.state.pollingInterval === "1" ? '' : 's';
-  };
-
-  state = this.getStoreState();
-
-  render() {
-    let activePollingClass = "active-tasks__polling-wrapper";
-    if (Helpers.isIE1X()) {
-      activePollingClass += " " + activePollingClass + "--ie1X";
-    }
-    return (
-      <div className={activePollingClass}>
-        <Polling
-          min={1}
-          max={30}
-          stepSize={1}
-          startValue={15}
-          valueUnits={"second"}
-          onPoll={this.runPollingUpdate}
-        />
-      </div>
-    );
-  }
-}
-
-var activeTasksHelpers = {
-  getTimeInfo (timeStamp) {
-    var timeMessage = [
-      app.helpers.formatDate(timeStamp),
-      app.helpers.getDateFromNow(timeStamp)
-    ];
-    return timeMessage;
-  },
-
-  getDatabaseFieldMessage (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 (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, 'changes_done')) {
-      progressMessage.push(item.changes_done + ' Changes done.');
-    }
-
-    return progressMessage;
-  },
-
-  getSourceSequence (item) {
-    return item.source_seq;
-  }
-
-};
-
-export default {
-  ActiveTasksController: ActiveTasksController,
-  ActiveTasksFilterTabs: ActiveTasksFilterTabs,
-
-  ActiveTaskTable: ActiveTaskTable,
-  ActiveTasksTableHeader: ActiveTasksTableHeader,
-  TableHeader: TableHeader,
-  ActiveTasksTableBody: ActiveTasksTableBody,
-  ActiveTaskTableBodyContents: ActiveTaskTableBodyContents,
-
-  ActiveTasksPollingWidgetController: ActiveTasksPollingWidgetController
-};
diff --git a/app/addons/activetasks/components/controller.js 
b/app/addons/activetasks/components/controller.js
new file mode 100644
index 0000000..ec8a841
--- /dev/null
+++ b/app/addons/activetasks/components/controller.js
@@ -0,0 +1,59 @@
+// 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.
+import React from 'react';
+import ActiveTasksFilterTabs from './filtertabs';
+import ActiveTaskTable from './table';
+
+export default class ActiveTasksController extends React.Component {
+  componentDidMount() {
+    this.props.init();
+  }
+
+  setNewSearchTerm = (searchTerm) => {
+    this.props.setSearchTerm(searchTerm);
+  };
+
+  switchTab = (newRadioButton) => {  //tabs buttons
+    this.props.switchTab(newRadioButton);
+  };
+
+  tableHeaderOnClick = (headerClicked) => {
+    this.props.sortByColumnHeader(headerClicked);
+  };
+
+  render() {
+    const {isLoading, tasks, searchTerm, selectedRadio, sortByHeader, 
headerIsAscending} = this.props;
+
+    const setSearchTerm = this.setNewSearchTerm;
+    const onTableHeaderClick = this.tableHeaderOnClick;
+
+    return (
+      <div id="active-tasks-page" className="scrollable">
+        <div className="inner">
+          <ActiveTasksFilterTabs
+            searchTerm={searchTerm}
+            selectedRadio={selectedRadio}
+            onSearch={setSearchTerm}
+            onRadioClick={this.switchTab}/>
+          <ActiveTaskTable
+            isLoading={isLoading}
+            tasks={tasks}
+            searchTerm={searchTerm}
+            selectedRadio={selectedRadio}
+            onTableHeaderClick={onTableHeaderClick}
+            sortByHeader={sortByHeader}
+            headerIsAscending={headerIsAscending} />
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/activetasks/components/filtertabs.js 
b/app/addons/activetasks/components/filtertabs.js
new file mode 100644
index 0000000..8d5410c
--- /dev/null
+++ b/app/addons/activetasks/components/filtertabs.js
@@ -0,0 +1,75 @@
+// 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.
+import React from 'react';
+import Components from "../../components/react-components";
+const {TabElement, TabElementWrapper} = Components;
+
+export default class ActiveTasksFilterTabs extends React.Component {
+  static defaultProps = {
+    radioNames : [
+      'All Tasks',
+      'Replication',
+      'Database Compaction',
+      'Indexer',
+      'View Compaction'
+    ]
+  };
+
+  checked = (radioName) => {
+    return this.props.selectedRadio === radioName;
+  };
+
+  onRadioClick = (e) => {
+    var radioName = e.target.value;
+    this.props.onRadioClick(radioName);
+  };
+
+  createFilterTabs = () => {
+    return (
+      this.props.radioNames.map((radioName, i) => {
+        const checked = this.checked(radioName);
+
+        return (
+          <TabElement
+            key={i}
+            selected={checked}
+            text={radioName}
+            onChange={this.onRadioClick} />
+        );
+      })
+    );
+  };
+
+  searchTermChange = (e) => {
+    var searchTerm = e.target.value;
+    this.props.onSearch(searchTerm);
+  };
+
+  render() {
+    const filterTabs = this.createFilterTabs();
+    return (
+      <TabElementWrapper>
+        {filterTabs}
+        <li className="component-tab-list-element">
+          <input
+            id="active-tasks-search-box"
+            className="searchbox"
+            type="text"
+            name="search"
+            placeholder="Search for databases..."
+            value={this.props.searchTerm}
+            onChange={this.searchTermChange} />
+        </li>
+      </TabElementWrapper>
+    );
+  }
+}
diff --git a/app/addons/activetasks/components/polling.js 
b/app/addons/activetasks/components/polling.js
new file mode 100644
index 0000000..6e3d52e
--- /dev/null
+++ b/app/addons/activetasks/components/polling.js
@@ -0,0 +1,48 @@
+// 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.
+
+import React from 'react';
+import Helpers from '../../../helpers';
+import Components from "../../components/react-components";
+const { Polling } = Components;
+
+export default class ActiveTasksPollingWidgetController extends 
React.Component {
+
+  runPollingUpdate = () => {
+    this.props.runPollingUpdate();
+  };
+
+  getPluralForLabel = () => {
+    return this.state.pollingInterval === "1" ? '' : 's';
+  };
+
+
+  render() {
+    let activePollingClass = "active-tasks__polling-wrapper";
+    if (Helpers.isIE1X()) {
+      activePollingClass += " " + activePollingClass + "--ie1X";
+    }
+    return (
+      <div className={activePollingClass}>
+        <Polling
+          min={1}
+          max={30}
+          stepSize={1}
+          startValue={15}
+          valueUnits={"second"}
+          onPoll={this.runPollingUpdate}
+        />
+      </div>
+    );
+  }
+}
+
diff --git a/app/addons/activetasks/components/table.js 
b/app/addons/activetasks/components/table.js
new file mode 100644
index 0000000..b006090
--- /dev/null
+++ b/app/addons/activetasks/components/table.js
@@ -0,0 +1,44 @@
+// 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.
+import React from 'react';
+import ActiveTasksTableBody from './tablebody';
+import ActiveTasksTableHeader from './tableheader';
+
+export default class ActiveTaskTable extends React.Component {
+  render() {
+    const {
+      tasks,
+      selectedRadio,
+      searchTerm,
+      sortByHeader,
+      onTableHeaderClick,
+      headerIsAscending,
+      isLoading
+    } = this.props;
+
+    return (
+      <div id="dashboard-lower-content">
+        <table id="active-tasks-table" className="table table-bordered 
table-striped active-tasks">
+          <ActiveTasksTableHeader
+            onTableHeaderClick={onTableHeaderClick}
+            sortByHeader={sortByHeader}
+            headerIsAscending={headerIsAscending}/>
+          <ActiveTasksTableBody
+            tasks={tasks}
+            selectedRadio={selectedRadio}
+            isLoading={isLoading}
+            searchTerm={searchTerm}/>
+        </table>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/activetasks/components/tablebody.js 
b/app/addons/activetasks/components/tablebody.js
new file mode 100644
index 0000000..bb73880
--- /dev/null
+++ b/app/addons/activetasks/components/tablebody.js
@@ -0,0 +1,70 @@
+// 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.
+
+import React from 'react';
+import ActiveTaskTableBodyContents from './tablebodycontents';
+
+export default class ActiveTasksTableBody extends React.Component {
+  createRows = () => {
+    var isThereASearchTerm = this.props.searchTerm.trim() === "";
+
+    if (this.props.tasks.length === 0 || this.props.isLoading) {
+      return isThereASearchTerm ? this.noActiveTasks() : 
this.noActiveTasksMatchFilter();
+    }
+
+    return this.props.tasks.map((item, key) => {
+      return <ActiveTaskTableBodyContents key={key} item={item} />;
+    });
+  };
+
+  getType = () => {
+    const type = this.props.selectedRadio;
+    if (type === "All Tasks") {
+      return "";
+    }
+
+    return type;
+  }
+
+  noActiveTasks = () => {
+    const type = this.getType();
+    let msg = <td  colSpan="6">No active {type} tasks.</td>;
+
+    if (this.props.isLoading) {
+      msg = <td  colSpan="6">Loading tasks.</td>;
+    }
+
+    return (
+      <tr className="no-matching-database-on-search">
+        {msg}
+      </tr>
+    );
+  };
+
+  noActiveTasksMatchFilter = () => {
+    const type = this.getType();
+
+    return (
+      <tr className="no-matching-database-on-search">
+        <td colSpan="6">No active {type} tasks match with filter: 
&quot;{this.props.searchTerm}&quot;</td>
+      </tr>
+    );
+  };
+
+  render() {
+    return (
+      <tbody className="js-tasks-go-here">
+        {this.createRows()}
+      </tbody>
+    );
+  }
+}
diff --git a/app/addons/activetasks/components/tablebodycontents.js 
b/app/addons/activetasks/components/tablebodycontents.js
new file mode 100644
index 0000000..e8c6e2b
--- /dev/null
+++ b/app/addons/activetasks/components/tablebodycontents.js
@@ -0,0 +1,117 @@
+// 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.
+
+import React from 'react';
+import app from '../../../app';
+
+const activeTasksHelpers = {
+  getTimeInfo (timeStamp) {
+    var timeMessage = [
+      app.helpers.formatDate(timeStamp),
+      app.helpers.getDateFromNow(timeStamp)
+    ];
+    return timeMessage;
+  },
+
+  getDatabaseFieldMessage (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 (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, 'changes_done')) {
+      progressMessage.push(item.changes_done + ' Changes done.');
+    }
+
+    return progressMessage;
+  },
+
+  getSourceSequence (item) {
+    return item.source_seq;
+  }
+
+};
+
+export default class ActiveTaskTableBodyContents extends React.Component {
+  getInfo = (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 = (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() {
+    var rowData =  this.getInfo(this.props.item);
+    var objectFieldMsg = this.multilineMessage(rowData.objectField, 
'to-from-database');
+    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>
+    );
+  }
+}
diff --git a/app/addons/activetasks/components/tableheader.js 
b/app/addons/activetasks/components/tableheader.js
new file mode 100644
index 0000000..2b85cc5
--- /dev/null
+++ b/app/addons/activetasks/components/tableheader.js
@@ -0,0 +1,86 @@
+// 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.
+
+import React from 'react';
+
+export class TableHeader extends React.Component {
+
+  constructor (props) {
+    super(props);
+    this.onTableHeaderClick = this.onTableHeaderClick.bind(this);
+  }
+
+
+  arrow = () => {
+    const sortBy = this.props.sortByHeader;
+    const currentName = this.props.headerName;
+    const headerIsAscending = this.props.headerIsAscending;
+    const arrow = headerIsAscending ? 'icon icon-caret-up' : 'icon 
icon-caret-down';
+
+    if (sortBy === currentName) {
+      return <i className={arrow}></i>;
+    }
+  };
+
+  onTableHeaderClick = () => {
+    this.props.onTableHeaderClick(this.props.headerName);
+  };
+
+  render() {
+    return (
+      <td className={`header-field ${this.props.headerName} tableheader`} 
onClick={this.onTableHeaderClick}>
+        <label className="header-field label-text active-tasks-header 
noselect">
+          {this.props.displayName} {this.arrow()}
+        </label>
+      </td>
+    );
+  }
+}
+
+export default class ActiveTasksTableHeader extends React.Component {
+  static defaultProps = {
+    headerNames : [
+      ['type', 'Type'],
+      ['database', 'Database'],
+      ['started-on', 'Started on'],
+      ['updated-on', 'Updated on'],
+      ['pid', 'PID'],
+      ['progress', 'Status']
+    ]
+  };
+
+  createTableHeadingFields = () => {
+    const {
+      onTableHeaderClick,
+      sortByHeader,
+      headerIsAscending
+    } = this.props;
+
+    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() {
+    return (
+      <thead>
+        <tr>{this.createTableHeadingFields()}</tr>
+      </thead>
+    );
+  }
+}
diff --git a/app/addons/activetasks/container.js 
b/app/addons/activetasks/container.js
new file mode 100644
index 0000000..5e2f126
--- /dev/null
+++ b/app/addons/activetasks/container.js
@@ -0,0 +1,57 @@
+// 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.
+
+import { connect } from 'react-redux';
+import Controller from './layout';
+import {
+  init,
+  changePollingInterval,
+  setSearchTerm,
+  sortByColumnHeader,
+  switchTab,
+  runPollingUpdate
+} from './actions';
+import {
+  getTasks,
+  getHeaderIsAscending,
+  getSelectedRadio,
+  getSortByHeader,
+  getSearchTerm,
+  getIsLoading
+} from './reducers';
+
+const mapStateToProps = ({activetasks}) => {
+  return {
+    tasks: getTasks(activetasks),
+    headerIsAscending: getHeaderIsAscending(activetasks),
+    selectedRadio: getSelectedRadio(activetasks),
+    sortByHeader: getSortByHeader(activetasks),
+    searchTerm: getSearchTerm(activetasks),
+    isLoading: getIsLoading(activetasks)
+  };
+};
+
+const mapDispatchToProps = (dispatch) => {
+  return {
+    init: () => dispatch(init()),
+    changePollingInterval: (interval) => 
dispatch(changePollingInterval(interval)),
+    setSearchTerm: (term) => dispatch(setSearchTerm(term)),
+    sortByColumnHeader: (column) => dispatch(sortByColumnHeader(column)),
+    switchTab: (tab) => dispatch(switchTab(tab)),
+    runPollingUpdate: () => dispatch(runPollingUpdate())
+  };
+};
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(Controller);
diff --git a/app/addons/activetasks/layout.js b/app/addons/activetasks/layout.js
index b6b88d2..e126237 100644
--- a/app/addons/activetasks/layout.js
+++ b/app/addons/activetasks/layout.js
@@ -14,13 +14,14 @@ import React from 'react';
 import Helpers from "../../helpers";
 import FauxtonAPI from "../../core/api";
 import {OnePane, OnePaneHeader, OnePaneContent} from '../components/layouts';
-import {ActiveTasksController, ActiveTasksPollingWidgetController} from 
"./components";
+import ActiveTasksController from "./components/controller";
+import ActiveTasksPollingWidgetController from './components/polling';
 
 const crumbs = [
   {'name': 'Active Tasks'}
 ];
 
-export const ActiveTasksLayout = () => {
+export const ActiveTasksLayout = (props) => {
   return (
     <OnePane>
       <OnePaneHeader
@@ -28,10 +29,10 @@ export const ActiveTasksLayout = () => {
         docURL={FauxtonAPI.constants.DOC_URLS.ACTIVE_TASKS}
         endpoint={Helpers.getApiUrl('/_active_tasks')}
       >
-        <ActiveTasksPollingWidgetController />
+        <ActiveTasksPollingWidgetController {...props} />
       </OnePaneHeader>
       <OnePaneContent>
-        <ActiveTasksController />
+        <ActiveTasksController {...props} />
       </OnePaneContent>
     </OnePane>
   );
diff --git a/app/addons/activetasks/reducers.js 
b/app/addons/activetasks/reducers.js
new file mode 100644
index 0000000..bba4bf4
--- /dev/null
+++ b/app/addons/activetasks/reducers.js
@@ -0,0 +1,141 @@
+// 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.
+import ActionTypes from "./actiontypes";
+import { has, sortBy, isUndefined } from 'lodash';
+
+const initialState = {
+  isLoading: false,
+  sortByHeader: 'started-on',
+  headerIsAscending: true,
+  selectedRadio: 'All Tasks',
+  searchTerm: '',
+  tasks: [],
+  filteredTasks: []
+};
+
+const sortTasksByColumnHeader = (colName, tasks, headerIsAscending) => {
+  var sorted = sortBy(tasks, (item) => {
+    var variable = colName;
+
+    if (isUndefined(item[variable])) {
+      variable = 'source';
+    }
+    return item[variable];
+  });
+
+  if (!headerIsAscending) {
+    return sorted.reverse();
+  }
+
+  return sorted;
+};
+
+
+const setSearchTerm = (state, searchTerm) => {
+  const filteredTasks = filterTasks(searchTerm, state.selectedRadio, 
state.sortbyHeader, state.tasks, state.headerIsAscending);
+  return {
+    ...state,
+    filteredTasks,
+    searchTerm
+  };
+};
+
+const passesSearchFilter = (item, searchTerm) => {
+  const regex = new RegExp(searchTerm, 'g');
+  let 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);
+};
+
+const passesRadioFilter = (item, selectedRadio) => {
+  const fixedSelectedRadio = selectedRadio.toLowerCase().replace(' ', '_');
+  return item.type ===  fixedSelectedRadio ||  fixedSelectedRadio === 
'all_tasks';
+};
+
+const filterTasks = (searchTerm, selectedRadio, sortByHeader, tasks, 
headerIsAscending) => {
+  const filtered = tasks.filter(task => {
+    return passesRadioFilter(task, selectedRadio) && passesSearchFilter(task, 
searchTerm);
+  });
+
+  return sortTasksByColumnHeader(sortByHeader, filtered, headerIsAscending);
+};
+
+const setHeaderIsAscending = (prevSortbyHeader, sortByHeader, 
headerIsAscending) => {
+  if (prevSortbyHeader === sortByHeader) {
+    return !headerIsAscending;
+  }
+
+  return true;
+};
+
+export default (state = initialState, {type, options}) => {
+  switch (type) {
+    case ActionTypes.ACTIVE_TASKS_FETCH_AND_SET:
+      return {
+        ...state,
+        tasks: options,
+        filteredTasks: filterTasks(state.searchTerm, state.selectedRadio, 
state.sortByHeader, options, state.headerIsAscending)
+      };
+
+    case ActionTypes.ACTIVE_TASKS_SWITCH_TAB:
+      const filteredTasks = filterTasks(state.searchTerm, options, 
state.sortByHeader, state.tasks, headerIsAscending);
+      return {
+        ...state,
+        selectedRadio: options,
+        filteredTasks
+      };
+
+    case ActionTypes.ACTIVE_TASKS_CHANGE_POLLING_INTERVAL:
+      return {
+        ...state,
+        pollingIntervalSeconds: options
+      };
+
+    case ActionTypes.ACTIVE_TASKS_SET_SEARCH_TERM:
+      return setSearchTerm(state, options);
+
+    case ActionTypes.ACTIVE_TASKS_SORT_BY_COLUMN_HEADER:
+      const prevSortbyHeader = state.sortByHeader;
+      const sortByHeader = options;
+      const headerIsAscending = setHeaderIsAscending(prevSortbyHeader, 
sortByHeader, state.headerIsAscending);
+      const headerFilteredTasks = filterTasks(state.searchTerm, 
state.selectedRadio, sortByHeader, state.tasks, headerIsAscending);
+      return {
+        ...state,
+        sortByHeader,
+        headerIsAscending,
+        filteredTasks: headerFilteredTasks
+      };
+
+    case ActionTypes.ACTIVE_TASKS_SET_IS_LOADING:
+      return {
+        ...state,
+        isLoading: options
+      };
+  }
+  return state;
+};
+
+export const getTasks = (state) => state.filteredTasks;
+export const getHeaderIsAscending = (state) => state.headerIsAscending;
+export const getIsLoading = (state) => state.isLoading;
+export const getSelectedRadio = (state) => state.selectedRadio;
+export const getSortByHeader = (state) => state.sortByHeader;
+export const getSearchTerm = (state) => state.searchTerm;
diff --git a/app/addons/activetasks/resources.js 
b/app/addons/activetasks/resources.js
deleted file mode 100644
index 13ca545..0000000
--- a/app/addons/activetasks/resources.js
+++ /dev/null
@@ -1,46 +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.
-
-import Helpers from "../../helpers";
-import Actions from "./actions";
-var Active = {};
-
-Active.AllTasks = Backbone.Collection.extend({
-
-  url: function () {
-    return Helpers.getServerUrl('/_active_tasks');
-  },
-
-  pollingFetch: function () { //still need this for the polling
-    Actions.setActiveTaskIsLoading(true);
-    return this.fetch({reset: true, parse: true});
-  },
-
-  parse: function (resp) {
-    //no more backbone models, collection is converted into an array of objects
-    Actions.setActiveTaskIsLoading(false);
-    var collectionTable = [];
-
-    _.each(resp, (item) => {
-      collectionTable.push(item);
-    });
-
-    //collection is an array of objects
-    this.table = collectionTable;
-    return resp;
-  },
-
-  table: []
-
-});
-
-export default Active;
diff --git a/app/addons/activetasks/routes.js b/app/addons/activetasks/routes.js
index e45cc9b..2d51fc4 100644
--- a/app/addons/activetasks/routes.js
+++ b/app/addons/activetasks/routes.js
@@ -12,8 +12,7 @@
 
 import React from 'react';
 import FauxtonAPI from "../../core/api";
-import ActiveTasksResources from "./resources";
-import Layout from './layout';
+import Layout from './container';
 
 var ActiveTasksRouteObject = FauxtonAPI.RouteObject.extend({
   selectedHeader: 'Active Tasks',
@@ -30,6 +29,6 @@ var ActiveTasksRouteObject = FauxtonAPI.RouteObject.extend({
   }
 });
 
-ActiveTasksResources.RouteObjects = [ActiveTasksRouteObject];
-
-export default ActiveTasksResources;
+export default {
+  RouteObjects: [ActiveTasksRouteObject]
+};
diff --git a/app/addons/activetasks/stores.js b/app/addons/activetasks/stores.js
deleted file mode 100644
index 656ddc8..0000000
--- a/app/addons/activetasks/stores.js
+++ /dev/null
@@ -1,223 +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.
-
-import FauxtonAPI from "../../core/api";
-import ActionTypes from "./actiontypes";
-
-var ActiveTasksStore = FauxtonAPI.Store.extend({
-  initialize () {
-    this._prevSortbyHeader = 'started-on';
-    this._headerIsAscending = true;
-    this._selectedRadio = 'All Tasks';
-    this._sortByHeader = 'started-on';
-    this._searchTerm = '';
-    this._pollingIntervalSeconds = 5;
-
-  },
-
-  initAfterFetching (collectionTable, backboneCollection) {
-    this._collection = collectionTable;
-    this.sortCollectionByColumnHeader(this._sortByHeader);
-    this._backboneCollection = backboneCollection;
-    this.setIsLoading(true, new Date());
-  },
-
-  isLoading () {
-    return this._isLoading;
-  },
-
-  setIsLoading (bool, time) {
-    if (bool) {
-      this._startTimeForLoading = time;
-      this._isLoading = true;
-    } else {
-      var stoptime = time;
-      var responseTime = stoptime - this._startTimeForLoading;
-      if (responseTime < 800) {
-        setTimeout(function () {
-          this._isLoading = false;
-          this.triggerChange();
-        }.bind(this), 800);  //stop after 800ms for smooth animation
-      } else {
-        this._isLoading = false;
-      }
-    }
-  },
-
-  getSelectedRadio () {
-    return this._selectedRadio;
-  },
-
-  setSelectedRadio (selectedRadio) {
-    this._selectedRadio = selectedRadio;
-  },
-
-  setCollectionFromPolling (collection) {
-    this.setCollection(collection);
-    this.sortCollectionByColumnHeader(this._prevSortbyHeader, false);
-  },
-
-  setCollection (collection) {
-    this._collection = collection;
-  },
-
-  getCollection () {
-    return this._collection;
-  },
-
-  getBackboneCollection () {
-    return this._backboneCollection;
-  },
-
-  setSearchTerm (searchTerm) {
-    this._searchTerm = searchTerm;
-  },
-
-  getSearchTerm () {
-    return this._searchTerm;
-  },
-
-  getSortByHeader () {
-    return this._sortByHeader;
-  },
-
-  setSortByHeader (header) {
-    this._sortByHeader = header;
-  },
-
-  getHeaderIsAscending () {
-    return this._headerIsAscending;
-  },
-
-  toggleHeaderIsAscending () {
-    if (this._prevSortbyHeader === this._sortByHeader) {
-      this._headerIsAscending = !this._headerIsAscending;
-      return;
-    }
-
-    this._headerIsAscending = true;
-  },
-
-  sortCollectionByColumnHeader (colName) {
-    var collectionTable = this._collection;
-
-    var sorted = _.sortBy(collectionTable, (item) => {
-      var variable = colName;
-
-      if (_.isUndefined(item[variable])) {
-        variable = 'source';
-      }
-      return item[variable];
-    });
-
-    this._prevSortbyHeader = colName;
-    this._collection = sorted;
-  },
-
-  getFilteredTable () {
-    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 (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 (item) {
-    var selectedRadio = this._selectedRadio.toLowerCase().replace(' ', '_');
-    return item.type ===  selectedRadio ||  selectedRadio === 'all_tasks';
-  },
-
-  dispatch (action) {
-    switch (action.type) {
-
-      case ActionTypes.ACTIVE_TASKS_FETCH_AND_SET:
-        this.initAfterFetching(action.options.collectionTable, 
action.options.backboneCollection);
-        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;
-
-      case ActionTypes.ACTIVE_TASKS_SET_IS_LOADING:
-        this.setIsLoading(action.options, new Date());
-        this.triggerChange();
-        break;
-
-      case ActionTypes.ACTIVE_TASKS_POLLING_COLLECTION:
-        this.setCollectionFromPolling(action.options);
-        this.triggerChange();
-        break;
-
-      default:
-        return;
-    }
-  }
-});
-
-var activeTasksStore = new ActiveTasksStore();
-activeTasksStore.dispatchToken = 
FauxtonAPI.dispatcher.register(activeTasksStore.dispatch.bind(activeTasksStore));
-
-export default {
-  activeTasksStore: activeTasksStore
-};

Reply via email to