basic filter and update table
Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/b0541e10 Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/b0541e10 Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/b0541e10 Branch: refs/heads/new-replication Commit: b0541e103dcaab48e27092445c7438ac2b9822fa Parents: 22732ae Author: Garren Smith <garren.sm...@gmail.com> Authored: Wed Sep 21 17:18:29 2016 +0200 Committer: Garren Smith <garren.sm...@gmail.com> Committed: Wed Sep 21 17:18:29 2016 +0200 ---------------------------------------------------------------------- .../components/react-components.react.jsx | 47 ++--- app/addons/replication/actions.js | 28 ++- app/addons/replication/actiontypes.js | 4 +- app/addons/replication/api.js | 38 ++++ .../replication/assets/less/replication.less | 57 ++++++ app/addons/replication/components/activity.js | 178 +++++++++++++++++++ app/addons/replication/components/info.js | 26 --- app/addons/replication/components/tabs.js | 20 +-- app/addons/replication/controller.js | 19 +- app/addons/replication/route.js | 9 +- app/addons/replication/stores.js | 76 +++++--- 11 files changed, 401 insertions(+), 101 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b0541e10/app/addons/components/react-components.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/components/react-components.react.jsx b/app/addons/components/react-components.react.jsx index 42e162a..94a2854 100644 --- a/app/addons/components/react-components.react.jsx +++ b/app/addons/components/react-components.react.jsx @@ -1594,6 +1594,7 @@ const TabElement = ({selected, text, onChange, iconClass}) => { ); }; + TabElement.propTypes = { selected: React.PropTypes.bool.isRequired, text: React.PropTypes.string.isRequired, @@ -1611,32 +1612,32 @@ const TabElementWrapper = ({children}) => { export default { - BadgeList: BadgeList, - Badge: Badge, - BulkActionComponent: BulkActionComponent, - ConfirmButton: ConfirmButton, - ToggleHeaderButton: ToggleHeaderButton, - StyledSelect: StyledSelect, - CodeEditorPanel: CodeEditorPanel, - CodeEditor: CodeEditor, - StringEditModal: StringEditModal, - ZenModeOverlay: ZenModeOverlay, - Beautify: Beautify, - PaddedBorderedBox: PaddedBorderedBox, - Document: Document, - LoadLines: LoadLines, - MenuDropDown: MenuDropDown, - TrayContents: TrayContents, - TrayWrapper: TrayWrapper, - connectToStores: connectToStores, - ApiBarController: ApiBarController, + BadgeList, + Badge, + BulkActionComponent, + ConfirmButton, + ToggleHeaderButton, + StyledSelect, + CodeEditorPanel, + CodeEditor, + StringEditModal, + ZenModeOverlay, + Beautify, + PaddedBorderedBox, + Document, + LoadLines, + MenuDropDown, + TrayContents, + TrayWrapper, + connectToStores, + ApiBarController, + DeleteDatabaseModal, + TabElement, + TabElementWrapper, renderMenuDropDown: function (el, opts) { ReactDOM.render(<MenuDropDown icon="fonticon-vertical-ellipsis" links={opts.links} />, el); }, removeMenuDropDown: function (el) { ReactDOM.unmountComponentAtNode(el); - }, - DeleteDatabaseModal: DeleteDatabaseModal, - TabElement: TabElement, - TabElementWrapper: TabElementWrapper + } }; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b0541e10/app/addons/replication/actions.js ---------------------------------------------------------------------- diff --git a/app/addons/replication/actions.js b/app/addons/replication/actions.js index 5bbece0..fe90603 100644 --- a/app/addons/replication/actions.js +++ b/app/addons/replication/actions.js @@ -13,7 +13,7 @@ import app from '../../app'; import FauxtonAPI from '../../core/api'; import ActionTypes from './actiontypes'; import Helpers from './helpers'; -import {createReplicationDoc} from './api'; +import {createReplicationDoc, fetchReplicationDocs} from './api'; function initReplicator (sourceDatabase) { @@ -90,14 +90,10 @@ function clearReplicationForm () { const tabUrls = { 'new replication': '/replication', - 'all replication': '/replication/info', - 'active replications': '/replication/info/active', - 'errors': '/replication/info/errors', - 'completed replications': '/replication/info/completed' + 'activity': '/replication/activity', }; function changeSelectedTab (tab) { - console.log('change', tab); FauxtonAPI.navigate(tabUrls[tab], { replace: true }); FauxtonAPI.dispatch({ type: ActionTypes.REPLICATION_CHANGE_TAB, @@ -105,11 +101,29 @@ function changeSelectedTab (tab) { }); } +const getReplicationActivity = () => { + fetchReplicationDocs().then(docs => { + FauxtonAPI.dispatch({ + type: ActionTypes.REPLICATION_STATUS, + options: docs + }); + }); +}; + +const filterDocs = (filter) => { + FauxtonAPI.dispatch({ + type: ActionTypes.REPLICATION_FILTER_DOCS, + options: filter + }); +}; + export default { initReplicator, replicate, updateFormField, clearReplicationForm, - changeSelectedTab + changeSelectedTab, + getReplicationActivity, + filterDocs }; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b0541e10/app/addons/replication/actiontypes.js ---------------------------------------------------------------------- diff --git a/app/addons/replication/actiontypes.js b/app/addons/replication/actiontypes.js index 192d096..2d0d89b 100644 --- a/app/addons/replication/actiontypes.js +++ b/app/addons/replication/actiontypes.js @@ -18,6 +18,8 @@ define([], function () { REPLICATION_UPDATE_FORM_FIELD: 'REPLICATION_UPDATE_FORM_FIELD', REPLICATION_CLEAR_FORM: 'REPLICATION_CLEAR_FORM', REPLICATION_STARTING: 'REPLICATION_STARTING', - REPLICATION_CHANGE_TAB: 'REPLICATION_CHANGE_TAB' + REPLICATION_CHANGE_TAB: 'REPLICATION_CHANGE_TAB', + REPLICATION_STATUS: 'REPLICATION_STATUS', + REPLICATION_FILTER_DOCS: 'REPLICATION_FILTER_DOCS' }; }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b0541e10/app/addons/replication/api.js ---------------------------------------------------------------------- diff --git a/app/addons/replication/api.js b/app/addons/replication/api.js index 10dda1f..824461c 100644 --- a/app/addons/replication/api.js +++ b/app/addons/replication/api.js @@ -125,3 +125,41 @@ export const createReplicationDoc = ({ continuous: continuous(replicationType), }); }; + +const removeSensitiveUrlInfo = (url) => { + const urlObj = new URL(url); + return urlObj.origin + urlObj.pathname; +}; + +const getDocUrl = (doc) => { + let url = doc; + + if (typeof doc === "object") { + url = doc.url; + } + return removeSensitiveUrlInfo(url); +}; + +export const parseReplicationDocs = (rows) => { + return rows.map(row => row.doc).map(doc => { + return { + source: getDocUrl(doc.source), + target: getDocUrl(doc.target), + createTarget: doc.create_target, + continuous: doc.continuous === true ? true : false, + status: doc._replication_state, + statusTime: new Date(doc._replication_state_time) + }; + }); +}; + +export const fetchReplicationDocs = () => { + return $.ajax({ + type: 'GET', + url: '/_replicator/_all_docs?include_docs=true&limit=100', + contentType: 'application/json; charset=utf-8', + dataType: 'json', + }).then((res) => { + return parseReplicationDocs(res.rows.filter(row => row.id.indexOf("_design/_replicator") === -1)); + }); +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b0541e10/app/addons/replication/assets/less/replication.less ---------------------------------------------------------------------- diff --git a/app/addons/replication/assets/less/replication.less b/app/addons/replication/assets/less/replication.less index 4739d7a..ddcb599 100644 --- a/app/addons/replication/assets/less/replication.less +++ b/app/addons/replication/assets/less/replication.less @@ -137,3 +137,60 @@ div.replication-page { .replication-clear-link:hover { text-decoration: none; } + +.replication_activity { + padding: 0 10px 0 10px !important; +} + +.replication_table-row { + font-size: 14px; + height: 50px +} + +td.replication_table-col { + vertical-align: middle; +} + +.replication_table-header-source { + width: 30%; + font-weight: bold; +} + +.replication_table-header-target { + width: 30%; + font-weight: bold; +} + +.replication_table-header-type { + font-weight: bold; +} + +.replication_table-header-status { + font-weight: bold; +} + +.replication_table-header-time { + font-weight: bold; +} + +.replication_table-header-actions { + font-weight: bold; +} + +td.replication-row-status { + text-transform: capitalize; + vertical-align: middle; +} + +.replication_row-status--completed { + color: #5cb85c; +} + +.replication_row-status--error { + color: #d9534f; +} + +.replication_table-header-icon { + margin-left: 6px; + width: 16px; +} http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b0541e10/app/addons/replication/components/activity.js ---------------------------------------------------------------------- diff --git a/app/addons/replication/components/activity.js b/app/addons/replication/components/activity.js new file mode 100644 index 0000000..3f643d4 --- /dev/null +++ b/app/addons/replication/components/activity.js @@ -0,0 +1,178 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +import React from 'react'; +import {Table} from "react-bootstrap"; +import moment from 'moment'; +import app from '../../../app'; + +const formatUrl = (url) => { + if (url.indexOf(window.location.hostname) > -1) { + var urlObj = new URL(url); + const encoded = app.utils.safeURLName(urlObj.pathname.slice(1)); + return ( + <span> + {urlObj.origin} + <a href={`#/database/${encoded}/_all_docs`}>{urlObj.pathname}</a> + </span> + ); + } + + return url; +}; + + +const Row = ({ + source, + target, + type, + status, + statusTime +}) => { + return ( + <tr className="replication_table-row"> + <td className="replication_table-col">{formatUrl(source)}</td> + <td className="replication_table-col">{formatUrl(target)}</td> + <td className="replication_table-col">{type}</td> + <td className={`replication-row-status replication_row-status--${status}`}>{status}</td> + <td className="replication_table-col">{moment(statusTime).format("MMM Do, h:mm a")}</td> + <td className="replication_table-col">Actions...</td> + </tr> + ); +}; + +class ReplicationTable extends React.Component { + constructor (props) { + super(props); + this.state = { + descending: true, + column: 'source' + }; + } + + sort(column, descending, docs) { + const sorted = docs.sort((a, b) => { + if (a[column] < b[column]) { + return -1; + } + + if (a[column] > b[column]) { + return 1; + } + + return 0; + + }); + + if (!descending) { + sorted.reverse(); + } + + return sorted; + } + + renderRows () { + return this.sort(this.state.column, this.state.descending, this.props.docs).map((doc, i) => { + return <Row + key={i} + source={doc.source} + target={doc.target} + type={doc.continuous === true ? 'Continuous' : 'One time'} + status={doc.status} + statusTime={doc.statusTime} + />; + }); + } + + iconDirection (column) { + if (column === this.state.column && !this.state.descending) { + return 'fonticon-up-dir'; + } + + return 'fonticon-down-dir'; + } + + onSort (column) { + return (e) => { + this.setState({ + descending: column === this.state.column ? !this.state.descending : true, + column: column + }); + }; + } + + render () { + return ( + <Table striped> + <thead> + <tr> + <th className="replication_table-header-source" onClick={this.onSort('source')}> + Source + <span className={`replication_table-header-icon ${this.iconDirection('source')}`} /> + </th> + <th className="replication_table-header-target" onClick={this.onSort('target')}> + Target + <span className={`replication_table-header-icon ${this.iconDirection('target')}`} /> + </th> + <th className="replication_table-header-type" onClick={this.onSort('type')}> + Type + <span className={`replication_table-header-icon ${this.iconDirection('type')}`} /> + </th> + <th className="replication_table-header-status" onClick={this.onSort('status')}> + State + <span className={`replication_table-header-icon ${this.iconDirection('status')}`} /> + </th> + <th className="replication_table-header-time" onClick={this.onSort('status')}> + State Time + <span className={`replication_table-header-icon ${this.iconDirection('time')}`} /> + </th> + <th className="replication_table-header-actions"> + Actions + </th> + </tr> + </thead> + <tbody> + {this.renderRows()} + </tbody> + </Table> + ); + } +} + +const ReplicationFilter = ({value, onChange}) => { + return ( + <div className="replication_filter"> + <i className="fonticon-filter" /> + <input type="text" placeholder="Filter replications" value={value} onChange={onChange} /> + </div> + ); +}; + +const ReplicationHeader = ({onFilterChange}) => { + return ( + <div className="replication_activity_header"> + <ReplicationFilter onChange={onFilterChange} /> + </div> + ); +}; + +export default class Activity extends React.Component { + + render () { + const {onChangeFilter, docs, filter} = this.props; + return ( + <div className="replication_activity"> + <ReplicationHeader filter={filter} onFilterChange={onChangeFilter} /> + <ReplicationTable docs={docs} /> + </div> + ); + } +} http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b0541e10/app/addons/replication/components/info.js ---------------------------------------------------------------------- diff --git a/app/addons/replication/components/info.js b/app/addons/replication/components/info.js deleted file mode 100644 index b153759..0000000 --- a/app/addons/replication/components/info.js +++ /dev/null @@ -1,26 +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 React from 'react'; - -export default class ReplicationController extends React.Component { - constructor (props) { - super(props); - this.state = this.getStoreState(); - } - - render () { - const rows = this.getReplicationRows(this.props.docs); - <ReplicationTable> - {rows} - </ReplicationTable> - } -} http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b0541e10/app/addons/replication/components/tabs.js ---------------------------------------------------------------------- diff --git a/app/addons/replication/components/tabs.js b/app/addons/replication/components/tabs.js index 58ac31e..44b63d3 100644 --- a/app/addons/replication/components/tabs.js +++ b/app/addons/replication/components/tabs.js @@ -24,24 +24,12 @@ export default ({selectedTab, onTabClick}) => { <div className="monitoring__tabs-wrapper"> <TabElementWrapper> <TabElement - selected={checked(selectedTab, 'new replication')} - text={'New Replication'} - onChange={e => onTabClick(e.target.value.toLowerCase())} /> - <TabElement - selected={checked(selectedTab, 'all replications')} - text={'All Replications'} - onChange={e => onTabClick(e.target.value.toLowerCase())} /> - <TabElement - selected={checked(selectedTab, 'active replications')} - text={'Active Replications'} - onChange={e => onTabClick(e.target.value.toLowerCase())} /> - <TabElement - selected={checked(selectedTab, 'errors')} - text={'Errors'} + selected={checked(selectedTab, 'activity')} + text={'Activity'} onChange={e => onTabClick(e.target.value.toLowerCase())} /> <TabElement - selected={checked(selectedTab, 'completed replications')} - text={'Completed Replications'} + selected={checked(selectedTab, 'new replication')} + text={'New Replication'} onChange={e => onTabClick(e.target.value.toLowerCase())} /> </TabElementWrapper> </div> http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b0541e10/app/addons/replication/controller.js ---------------------------------------------------------------------- diff --git a/app/addons/replication/controller.js b/app/addons/replication/controller.js index c658229..8c2731e 100644 --- a/app/addons/replication/controller.js +++ b/app/addons/replication/controller.js @@ -20,7 +20,7 @@ import base64 from 'base-64'; import Components from '../components/react-components.react'; import NewReplication from './components/newreplication'; import ReplicationTabs from './components/tabs'; -import ReplicationInfo from './components/info'; +import Activity from './components/activity'; const {LoadLines, ConfirmButton} = Components; @@ -56,13 +56,16 @@ export default class ReplicationController extends React.Component { replicationType: store.getReplicationType(), replicationDocName: store.getReplicationDocName(), submittedNoChange: store.getSubmittedNoChange(), - selectedTab: store.getSelectedTab() + selectedTab: store.getSelectedTab(), + statusDocs: store.getFilteredReplicationStatus(), + statusFilter: store.getStatusFilter() }; } componentDidMount () { - Actions.initReplicator(this.props.sourceDatabase); store.on('change', this.onChange, this); + Actions.initReplicator(this.props.sourceDatabase); + Actions.getReplicationActivity(); } componentWillUnmount () { @@ -78,7 +81,7 @@ export default class ReplicationController extends React.Component { const { replicationSource, replicationTarget, replicationType, replicationDocName, passwordModalVisible, databases, sourceDatabase, remoteSource, remoteTarget, - targetDatabase, selectedTab + targetDatabase, selectedTab, statusDocs, statusFilter } = this.state; if (this.state.selectedTab === 'new replication') { @@ -88,7 +91,7 @@ export default class ReplicationController extends React.Component { }; }; - <NewReplication + return <NewReplication clearReplicationForm={Actions.clearReplicationForm} replicate={Actions.replicate} showPasswordModal={Actions.showPasswordModal} @@ -106,7 +109,11 @@ export default class ReplicationController extends React.Component { />; } - return <ReplicationInfo />; + return <Activity + docs={statusDocs} + filter={statusFilter} + onFilterChange={Actions.filterDocs} + />; } render () { http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b0541e10/app/addons/replication/route.js ---------------------------------------------------------------------- diff --git a/app/addons/replication/route.js b/app/addons/replication/route.js index 5709b0a..44b243a 100644 --- a/app/addons/replication/route.js +++ b/app/addons/replication/route.js @@ -12,12 +12,14 @@ import FauxtonAPI from '../../core/api'; import ReplicationController from './controller'; +import Actions from './actions'; const ReplicationRouteObject = FauxtonAPI.RouteObject.extend({ layout: 'one_pane', routes: { 'replication': 'defaultView', - 'replication/:dbname': 'defaultView' + 'replication/:dbname': 'defaultView', + 'replication/activity': 'activityView' }, selectedHeader: 'Replication', disableLoader: true, @@ -32,6 +34,11 @@ const ReplicationRouteObject = FauxtonAPI.RouteObject.extend({ defaultView: function (databaseName) { const sourceDatabase = databaseName || ''; this.setComponent('#dashboard-content', ReplicationController, {sourceDatabase: sourceDatabase}); + }, + + activityView: function () { + Actions.changeSelectedTab('activity'); + this.setComponent('#dashboard-content', ReplicationController); } }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b0541e10/app/addons/replication/stores.js ---------------------------------------------------------------------- diff --git a/app/addons/replication/stores.js b/app/addons/replication/stores.js index a2a66e8..1dde295 100644 --- a/app/addons/replication/stores.js +++ b/app/addons/replication/stores.js @@ -14,14 +14,14 @@ import FauxtonAPI from '../../core/api'; import ActionTypes from './actiontypes'; import Constants from './constants'; import AccountActionTypes from '../auth/actiontypes'; - +import _ from 'lodash'; const ReplicationStore = FauxtonAPI.Store.extend({ - initialize: function () { + initialize () { this.reset(); }, - reset: function () { + reset () { this._loading = false; this._databases = []; this._authenticated = false; @@ -43,6 +43,9 @@ const ReplicationStore = FauxtonAPI.Store.extend({ this._replicationDocName = ''; this._submittedNoChange = false; this._selectedTab = "new replication"; + this._statusDocs = []; + this._statusFilteredStatusDocs = []; + this._statusFilter = ''; }, getSelectedTab () { @@ -57,56 +60,79 @@ const ReplicationStore = FauxtonAPI.Store.extend({ this._submittedNoChange = false; }, - isLoading: function () { + isLoading () { return this._loading; }, - isAuthenticated: function () { + isAuthenticated () { return this._authenticated; }, - getReplicationSource: function () { + getReplicationSource () { return this._replicationSource; }, - getSourceDatabase: function () { + getSourceDatabase () { return this._sourceDatabase; }, - isLocalSourceDatabaseKnown: function () { + isLocalSourceDatabaseKnown () { return _.contains(this._databases, this._sourceDatabase); }, - isLocalTargetDatabaseKnown: function () { + isLocalTargetDatabaseKnown () { return _.contains(this._databases, this._targetDatabase); }, - getReplicationTarget: function () { + getReplicationTarget () { return this._replicationTarget; }, - getDatabases: function () { + getDatabases () { return this._databases; }, - setDatabases: function (databases) { + setDatabases (databases) { this._databases = databases; }, - getReplicationType: function () { + getReplicationType () { return this._replicationType; }, - getTargetDatabase: function () { + getTargetDatabase () { return this._targetDatabase; }, - getReplicationDocName: function () { + getReplicationDocName () { return this._replicationDocName; }, + setReplicationStatus (docs) { + this._statusDocs = docs; + }, + + getReplicationStatus () { + return this._statusDocs; + }, + + getFilteredReplicationStatus () { + return this._statusDocs.filter(doc => { + return _.values(doc).filter(item => { + return item.toString().match(this._statusFilter); + }).length > 0; + }); + }, + + setStatusFilter (filter) { + this._statusFilter = filter; + }, + + getStatusFilter () { + return this._statusFilter; + }, // to cut down on boilerplate - updateFormField: function (fieldName, value) { + updateFormField (fieldName, value) { // I know this could be done by just adding the _ prefix to the passed field name, I just don't much like relying // on the var names like that... @@ -124,23 +150,23 @@ const ReplicationStore = FauxtonAPI.Store.extend({ this[validFieldMap[fieldName]] = value; }, - getRemoteSource: function () { + getRemoteSource () { return this._remoteSource; }, - getRemoteTarget: function () { + getRemoteTarget () { return this._remoteTarget; }, - isPasswordModalVisible: function () { + isPasswordModalVisible () { return this._isPasswordModalVisible; }, - getPassword: function () { + getPassword () { return this._password; }, - dispatch: function ({type, options}) { + dispatch ({type, options}) { switch (type) { case ActionTypes.INIT_REPLICATION: @@ -178,6 +204,14 @@ const ReplicationStore = FauxtonAPI.Store.extend({ this._selectedTab = options; break; + case ActionTypes.REPLICATION_STATUS: + this.setReplicationStatus(options); + break; + + case ActionTypes.REPLICATION_FILTER_DOCS: + this.filterDocs(options); + break; + case AccountActionTypes.AUTH_SHOW_PASSWORD_MODAL: this._isPasswordModalVisible = true; break;