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: "{this.props.searchTerm}"</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: "{this.props.searchTerm}"</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 -};