couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From gar...@apache.org
Subject [2/4] fauxton commit: updated refs/heads/master to ff25441
Date Wed, 16 Nov 2016 11:32:53 GMT
Complete new replication redesign

This adds a new replication page and an activity page for replication


Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/ff25441b
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/ff25441b
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/ff25441b

Branch: refs/heads/master
Commit: ff25441bc63a5d8857ecbc51703e8ed19ec9cfc6
Parents: a50108b
Author: Garren Smith <garren.smith@gmail.com>
Authored: Mon Aug 15 15:44:37 2016 +0200
Committer: Garren Smith <garren.smith@gmail.com>
Committed: Wed Nov 16 13:32:29 2016 +0200

----------------------------------------------------------------------
 app/addons/auth/assets/less/auth.less           |   6 -
 app/addons/auth/components.react.jsx            |  35 +-
 app/addons/components/assets/less/layouts.less  |   7 +
 app/addons/components/assets/less/polling.less  |  24 +
 .../components/assets/less/styled-select.less   |  26 +-
 app/addons/components/components/polling.js     |  20 +
 .../components/components/styledselect.js       |   3 +-
 app/addons/components/layouts.js                |   8 +-
 .../components/react-components.react.jsx       |   5 +-
 .../tests/nightwatch/highlightsidebar.js        |   2 +-
 app/addons/replication/actions.js               | 192 ++++++-
 app/addons/replication/actiontypes.js           |  14 +-
 app/addons/replication/api.js                   | 216 ++++++++
 .../replication/assets/less/replication.less    | 333 ++++++++---
 app/addons/replication/components.react.jsx     | 546 -------------------
 app/addons/replication/components/activity.js   | 453 +++++++++++++++
 app/addons/replication/components/modals.js     | 139 +++++
 .../replication/components/newreplication.js    | 277 ++++++++++
 app/addons/replication/components/options.js    |  97 ++++
 .../replication/components/remoteexample.js     |  50 ++
 app/addons/replication/components/source.js     | 170 ++++++
 app/addons/replication/components/submit.js     |  43 ++
 app/addons/replication/components/target.js     | 209 +++++++
 app/addons/replication/controller.js            | 212 +++++++
 app/addons/replication/helpers.js               |  27 +-
 app/addons/replication/route.js                 |  73 ++-
 app/addons/replication/stores.js                | 234 ++++++--
 app/addons/replication/tests/apiSpec.js         | 170 ++++++
 app/addons/replication/tests/helpersSpec.js     |  41 ++
 .../replication/tests/newreplicationSpec.js     | 225 ++++++++
 .../replication/tests/nightwatch/replication.js | 119 ++--
 .../tests/nightwatch/replicationactivity.js     |  50 ++
 app/addons/replication/tests/replicationSpec.js | 212 -------
 app/addons/replication/tests/storesSpec.js      |  14 +-
 assets/less/fauxton.less                        |   9 +
 i18n.json.default.json                          |   4 +-
 package.json                                    |   3 +-
 test/dev.js                                     |   2 +-
 test/mocha/testUtils.js                         |   1 +
 test/nightwatch_tests/custom-commands/helper.js |  12 +-
 test/test.config.underscore                     |   1 +
 webpack.config.test.js                          |  17 +-
 42 files changed, 3317 insertions(+), 984 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/auth/assets/less/auth.less
----------------------------------------------------------------------
diff --git a/app/addons/auth/assets/less/auth.less b/app/addons/auth/assets/less/auth.less
index 4cf1863..0a0d24d 100644
--- a/app/addons/auth/assets/less/auth.less
+++ b/app/addons/auth/assets/less/auth.less
@@ -29,9 +29,3 @@
     margin-top: 0;
   }
 }
-
-.enter-password-modal {
-  input {
-    width: 100%;
-  }
-}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/auth/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/auth/components.react.jsx b/app/addons/auth/components.react.jsx
index ddbfd8c..9446201 100644
--- a/app/addons/auth/components.react.jsx
+++ b/app/addons/auth/components.react.jsx
@@ -17,10 +17,12 @@ import ReactDOM from "react-dom";
 import AuthStores from "./stores";
 import AuthActions from "./actions";
 import { Modal } from 'react-bootstrap';
+import Components from '../components/react-components.react';
 
 var changePasswordStore = AuthStores.changePasswordStore;
 var createAdminStore = AuthStores.createAdminStore;
 var createAdminSidebarStore = AuthStores.createAdminSidebarStore;
+const {ConfirmButton} = Components;
 
 
 var LoginForm = React.createClass({
@@ -328,21 +330,35 @@ class PasswordModal extends React.Component {
   }
 
   render () {
+    const {visible, onClose, submitBtnLabel, headerTitle, modalMessage} = this.props;
+    if (!this.props.visible) {
+      return null;
+    }
+
     return (
-      <Modal dialogClassName="enter-password-modal" show={this.props.visible} onHide={() => this.props.onClose()}>
+      <Modal dialogClassName="enter-password-modal" show={visible} onHide={() => onClose()}>
         <Modal.Header closeButton={true}>
-          <Modal.Title>Enter Password</Modal.Title>
+          <Modal.Title>{headerTitle}</Modal.Title>
         </Modal.Header>
         <Modal.Body>
-          {this.props.modalMessage}
-          <input type="password" placeholder="Enter your password" autoFocus={true} value={this.state.password}
-            onChange={(e) => this.setState({ password: e.target.value })} onKeyPress={this.onKeyPress} />
+          {modalMessage}
+          <input
+            style={{width: "385px"}}
+            type="password"
+            className="password-modal-input"
+            placeholder="Enter your password"
+            autoFocus={true}
+            value={this.state.password}
+            onChange={(e) => this.setState({ password: e.target.value })}
+            onKeyPress={this.onKeyPress}
+            />
         </Modal.Body>
         <Modal.Footer>
-          <a className="cancel-link" onClick={() => this.props.onClose()}>Cancel</a>
-          <button onClick={this.authenticate} className="btn btn-success save">
-            Continue Replication
-          </button>
+          <a className="cancel-link" onClick={() => onClose()}>Cancel</a>
+          <ConfirmButton
+            text={submitBtnLabel}
+            onClick={this.authenticate}
+          />
         </Modal.Footer>
       </Modal>
     );
@@ -356,6 +372,7 @@ PasswordModal.propTypes = {
   submitBtnLabel: React.PropTypes.string
 };
 PasswordModal.defaultProps = {
+  headerTitle: "Enter Password",
   visible: false,
   modalMessage: '',
   onClose: AuthActions.hidePasswordModal,

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/components/assets/less/layouts.less
----------------------------------------------------------------------
diff --git a/app/addons/components/assets/less/layouts.less b/app/addons/components/assets/less/layouts.less
index 86cc23c..adfe63a 100644
--- a/app/addons/components/assets/less/layouts.less
+++ b/app/addons/components/assets/less/layouts.less
@@ -12,3 +12,10 @@
 .template {
   height: 100%;
 }
+
+.right-header-flex {
+  display: flex;
+  align-items: center;
+  flex-direction: row;
+  height: 100%;
+}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/components/assets/less/polling.less
----------------------------------------------------------------------
diff --git a/app/addons/components/assets/less/polling.less b/app/addons/components/assets/less/polling.less
index 36657fa..19b0446 100644
--- a/app/addons/components/assets/less/polling.less
+++ b/app/addons/components/assets/less/polling.less
@@ -37,3 +37,27 @@
 .faux__polling-info-slider {
   cursor: pointer;
 }
+
+div.faux__refresh-btn {
+  border-left: 1px solid #ccc;
+  line-height: 40px;
+  padding: 12px 13px;
+  flex: 0 0 auto;
+}
+
+.faux__refresh-icon {
+  margin-right: 4px;
+}
+
+.faux__refresh-link:visited,
+.faux__refresh-link:focus,
+.faux__refresh-link {
+  color: #666;
+  font-size: 14px;
+  text-decoration: none;
+}
+
+.faux__refresh-link:hover {
+  text-decoration: none;
+  color: #AF2D24;
+}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/components/assets/less/styled-select.less
----------------------------------------------------------------------
diff --git a/app/addons/components/assets/less/styled-select.less b/app/addons/components/assets/less/styled-select.less
index 11ba404..1227527 100644
--- a/app/addons/components/assets/less/styled-select.less
+++ b/app/addons/components/assets/less/styled-select.less
@@ -28,6 +28,7 @@
   text-indent: 4px;
   height: 46px;
   padding-right: 35px;
+  color: #333;
 }
 
 .styled-select select:-moz-focusring {
@@ -39,9 +40,32 @@
   display: none;
 }
 
-.styled-select i {
+.styled-select-icon {
+  pointer-events: none;
   position: absolute;
   right: 10px;
   top: 12px;
+  color: #333;
+}
+
+.styled-select-hover-icon {
   pointer-events: none;
+  position: absolute;
+  right: 10px;
+  top: 12px;
+  color: #E73D34;
+  display: none;
+  z-index: 30;
+}
+
+//this is a litte css trick to create a hover effect for the triangle. We can't use the normal hover event because we
+//need to set pointer-events to none so that a click on the triangle is actually a click on the select dropdown
+.styled-select:hover {
+  .styled-select-hover-icon {
+    display: inline;
+  }
+
+  .styled-select-icon {
+    display: none;
+  }
 }

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/components/components/polling.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/polling.js b/app/addons/components/components/polling.js
index 2afead6..af9ced1 100644
--- a/app/addons/components/components/polling.js
+++ b/app/addons/components/components/polling.js
@@ -128,3 +128,23 @@ Polling.propTypes = {
   stepSize: React.PropTypes.number.isRequired,
   onPoll: React.PropTypes.func.isRequired,
 };
+
+export const RefreshBtn = ({refresh}) =>
+  <div className="faux__refresh-btn">
+    <a
+      className="faux__refresh-link"
+      href="#"
+      data-bypass="true"
+      onClick={e => {
+        e.preventDefault();
+        refresh();
+      }}
+    >
+      <i className="faux__refresh-icon fonticon-arrows-cw"></i>
+      Refresh
+    </a>
+  </div>;
+
+RefreshBtn.propTypes = {
+  refresh: React.PropTypes.func.isRequired
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/components/components/styledselect.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/styledselect.js b/app/addons/components/components/styledselect.js
index 85a6ff4..fc1c27c 100644
--- a/app/addons/components/components/styledselect.js
+++ b/app/addons/components/components/styledselect.js
@@ -23,7 +23,8 @@ export const StyledSelect = React.createClass({
     return (
       <div className="styled-select">
         <label htmlFor={this.props.selectId}>
-          <i className="fonticon-down-dir"></i>
+          <i className="fonticon-down-dir styled-select-icon"></i>
+          <i className="fonticon-down-dir styled-select-hover-icon"></i>
           <select
             value={this.props.selectValue}
             id={this.props.selectId}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/components/layouts.js
----------------------------------------------------------------------
diff --git a/app/addons/components/layouts.js b/app/addons/components/layouts.js
index 3bc9c23..736f60b 100644
--- a/app/addons/components/layouts.js
+++ b/app/addons/components/layouts.js
@@ -53,7 +53,9 @@ export const OnePaneHeader = ({showApiUrl, docURL, endpoint, crumbs, children})
           <Breadcrumbs crumbs={crumbs}/>
         </div>
         <div id='right-header'>
-          {children}
+          <div className="right-header-flex">
+            {children}
+          </div>
         </div>
         {showApiUrl ? <ApiBarWrapper docURL={docURL} endpoint={endpoint} /> : null}
         <div id='notification-center-btn'>
@@ -70,8 +72,8 @@ OnePaneHeader.defaultProps = {
 };
 
 OnePaneHeader.propTypes = {
-  docURL: React.PropTypes.string.isRequired,
-  endpoint: React.PropTypes.string.isRequired,
+  docURL: React.PropTypes.string,
+  endpoint: React.PropTypes.string,
 };
 
 export const OnePaneContent = ({children}) => {

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/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 ec1886f..6c8fdcd 100644
--- a/app/addons/components/react-components.react.jsx
+++ b/app/addons/components/react-components.react.jsx
@@ -28,7 +28,7 @@ import {TrayContents, TrayWrapper, connectToStores} from './components/tray';
 import {ApiBarController} from './components/apibar';
 import {DeleteDatabaseModal} from './components/deletedatabasemodal';
 import {TabElement, TabElementWrapper} from './components/tabelement';
-import {Polling} from './components/polling';
+import {Polling, RefreshBtn} from './components/polling';
 
 export default {
   BadgeList,
@@ -53,5 +53,6 @@ export default {
   ApiBarController,
   DeleteDatabaseModal,
   TabElement,
-  TabElementWrapper
+  TabElementWrapper,
+  RefreshBtn
 };

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/fauxton/tests/nightwatch/highlightsidebar.js
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/tests/nightwatch/highlightsidebar.js b/app/addons/fauxton/tests/nightwatch/highlightsidebar.js
index 015253a..def0e23 100644
--- a/app/addons/fauxton/tests/nightwatch/highlightsidebar.js
+++ b/app/addons/fauxton/tests/nightwatch/highlightsidebar.js
@@ -23,7 +23,7 @@ module.exports = {
       .waitForElementPresent('.add-new-database-btn', waitTime, false)
       .click('a[href="#/replication"]')
       .pause(1000)
-      .waitForElementVisible('#replicate', waitTime, false)
+      .waitForElementVisible('.replication__activity_header-btn', waitTime, false)
       .assert.cssClassPresent('li[data-nav-name="Replication"]', 'active')
     .end();
   }

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/replication/actions.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/actions.js b/app/addons/replication/actions.js
index 72e1909..b40dabb 100644
--- a/app/addons/replication/actions.js
+++ b/app/addons/replication/actions.js
@@ -13,14 +13,16 @@ import app from '../../app';
 import FauxtonAPI from '../../core/api';
 import ActionTypes from './actiontypes';
 import Helpers from './helpers';
+import Constants from './constants';
+import {createReplicationDoc, fetchReplicationDocs, decodeFullUrl} from './api';
 
 
-function initReplicator (sourceDatabase) {
-  if (sourceDatabase) {
+function initReplicator (localSource) {
+  if (localSource) {
     FauxtonAPI.dispatch({
       type: ActionTypes.INIT_REPLICATION,
       options: {
-        sourceDatabase: sourceDatabase
+        localSource: localSource
       }
     });
   }
@@ -39,20 +41,26 @@ function initReplicator (sourceDatabase) {
 }
 
 function replicate (params) {
+  const replicationDoc = createReplicationDoc(params);
+
   const promise = $.ajax({
     url: window.location.origin + '/_replicator',
     contentType: 'application/json',
     type: 'POST',
     dataType: 'json',
-    data: JSON.stringify(params)
+    data: JSON.stringify(replicationDoc)
   });
 
-  const source = Helpers.getDatabaseLabel(params.source);
-  const target = Helpers.getDatabaseLabel(params.target);
+  const source = Helpers.getDatabaseLabel(replicationDoc.source);
+  const target = Helpers.getDatabaseLabel(replicationDoc.target);
+
+  FauxtonAPI.dispatch({
+    type: ActionTypes.REPLICATION_STARTING,
+  });
 
   promise.then(() => {
     FauxtonAPI.addNotification({
-      msg: 'Replication from <code>' + source + '</code> to <code>' + target + '</code> has begun.',
+      msg: `Replication from <code>${decodeURIComponent(source)}</code> to <code>${decodeURIComponent(target)}</code> has been scheduled.`,
       type: 'success',
       escape: false,
       clear: true
@@ -81,10 +89,178 @@ function clearReplicationForm () {
   FauxtonAPI.dispatch({ type: ActionTypes.REPLICATION_CLEAR_FORM });
 }
 
+const getReplicationActivity = () => {
+  FauxtonAPI.dispatch({
+      type: ActionTypes.REPLICATION_FETCHING_STATUS,
+  });
+
+  fetchReplicationDocs().then(docs => {
+    FauxtonAPI.dispatch({
+      type: ActionTypes.REPLICATION_STATUS,
+      options: docs
+    });
+  });
+};
+
+const filterDocs = (filter) => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.REPLICATION_FILTER_DOCS,
+    options: filter
+  });
+};
+
+const selectAllDocs = () => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.REPLICATION_TOGGLE_ALL_DOCS
+  });
+};
+
+const selectDoc = (id) => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.REPLICATION_TOGGLE_DOC,
+    options: id
+  });
+};
+
+const clearSelectedDocs = () => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.REPLICATION_CLEAR_SELECTED_DOCS
+  });
+};
+
+const deleteDocs = (docs) => {
+  const bulkDocs = docs.map(({raw: doc}) => {
+    doc._deleted = true;
+    return doc;
+  });
+
+  FauxtonAPI.addNotification({
+    msg: `Deleting doc${bulkDocs.length > 1 ? 's' : ''}.`,
+    type: 'success',
+    escape: false,
+    clear: true
+  });
+
+  $.ajax({
+    url: app.host + '/_replicator/_bulk_docs',
+    contentType: 'application/json',
+    dataType: 'json',
+    method: 'POST',
+    data: JSON.stringify({docs: bulkDocs})
+  }).then(() => {
+    let msg = 'The selected documents have been deleted.';
+    if (docs.length === 1) {
+      msg = `Document <code>${docs[0]._id}</code> has been deleted`;
+    }
+
+    FauxtonAPI.addNotification({
+      msg: msg,
+      type: 'success',
+      escape: false,
+      clear: true
+    });
+    clearSelectedDocs();
+    getReplicationActivity();
+  }, (xhr) => {
+    const errorMessage = JSON.parse(xhr.responseText);
+    FauxtonAPI.addNotification({
+      msg: errorMessage.reason,
+      type: 'error',
+      clear: true
+    });
+  });
+};
+
+const getReplicationStateFrom = (id) => {
+  $.ajax({
+    url: `${app.host}/_replicator/${encodeURIComponent(id)}`,
+    contentType: 'application/json',
+    dataType: 'json',
+    method: 'GET'
+  }).then((doc) => {
+    const stateDoc = {
+      replicationDocName: doc._id,
+      replicationType: doc.continuous ? Constants.REPLICATION_TYPE.CONTINUOUS : Constants.REPLICATION_TYPE.ONE_TIME,
+    };
+
+    const sourceUrl = _.isObject(doc.source) ? doc.source.url : doc.source;
+    const targetUrl = _.isObject(doc.target) ? doc.target.url : doc.target;
+
+    if (sourceUrl.indexOf(window.location.hostname) > -1) {
+      const url = new URL(sourceUrl);
+      stateDoc.replicationSource = Constants.REPLICATION_SOURCE.LOCAL;
+      stateDoc.localSource = decodeURIComponent(url.pathname.slice(1));
+    } else {
+      stateDoc.replicationSource = Constants.REPLICATION_SOURCE.REMOTE;
+      stateDoc.remoteSource = decodeFullUrl(sourceUrl);
+    }
+
+    if (targetUrl.indexOf(window.location.hostname) > -1) {
+      const url = new URL(targetUrl);
+      stateDoc.replicationTarget = Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE;
+      stateDoc.localTarget = decodeURIComponent(url.pathname.slice(1));
+    } else {
+      stateDoc.replicationTarget = Constants.REPLICATION_TARGET.EXISTING_REMOTE_DATABASE;
+      stateDoc.remoteTarget = decodeFullUrl(targetUrl);
+    }
+
+    FauxtonAPI.dispatch({
+      type: ActionTypes.REPLICATION_SET_STATE_FROM_DOC,
+      options: stateDoc
+    });
+
+  }, (xhr) => {
+    const errorMessage = JSON.parse(xhr.responseText);
+    FauxtonAPI.addNotification({
+      msg: errorMessage.reason,
+      type: 'error',
+      clear: true
+    });
+  });
+};
+
+const showConflictModal = () => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.REPLICATION_SHOW_CONFLICT_MODAL
+  });
+};
+
+const hideConflictModal = () => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.REPLICATION_HIDE_CONFLICT_MODAL
+  });
+};
+
+const updateUsernameAndPassword = (username, password) => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.REPLICATION_USERNAME_PASSWORD,
+    options: {
+      username,
+      password
+    }
+  });
+};
+
+const changeActivitySort = (sort) => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.REPLICATION_CHANGE_ACTIVITY_SORT,
+    options: sort
+  });
+};
 
 export default {
   initReplicator,
   replicate,
   updateFormField,
-  clearReplicationForm
+  clearReplicationForm,
+  getReplicationActivity,
+  filterDocs,
+  selectAllDocs,
+  selectDoc,
+  deleteDocs,
+  getReplicationStateFrom,
+  showConflictModal,
+  hideConflictModal,
+  changeActivitySort,
+  clearSelectedDocs
 };

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/replication/actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/actiontypes.js b/app/addons/replication/actiontypes.js
index 87e689e..fbe389d 100644
--- a/app/addons/replication/actiontypes.js
+++ b/app/addons/replication/actiontypes.js
@@ -16,6 +16,18 @@ define([], function () {
     CHANGE_REPLICATION_SOURCE: 'CHANGE_REPLICATION_SOURCE',
     REPLICATION_DATABASES_LOADED: 'REPLICATION_DATABASES_LOADED',
     REPLICATION_UPDATE_FORM_FIELD: 'REPLICATION_UPDATE_FORM_FIELD',
-    REPLICATION_CLEAR_FORM: 'REPLICATION_CLEAR_FORM'
+    REPLICATION_CLEAR_FORM: 'REPLICATION_CLEAR_FORM',
+    REPLICATION_STARTING: 'REPLICATION_STARTING',
+    REPLICATION_STATUS: 'REPLICATION_STATUS',
+    REPLICATION_FETCHING_STATUS: 'REPLICATION_FETCHING_STATUS',
+    REPLICATION_FILTER_DOCS: 'REPLICATION_FILTER_DOCS',
+    REPLICATION_TOGGLE_ALL_DOCS: 'REPLICATION_TOGGLE_ALL_DOCS',
+    REPLICATION_TOGGLE_DOC: 'REPLICATION_TOGGLE_DOC',
+    REPLICATION_DELETE_DOCS: 'REPLICATION_DELETE_DOCS',
+    REPLICATION_SET_STATE_FROM_DOC: 'REPLICATION_SET_STATE_FROM_DOC',
+    REPLICATION_SHOW_CONFLICT_MODAL: 'REPLICATION_SHOW_CONFLICT_MODAL',
+    REPLICATION_HIDE_CONFLICT_MODAL: 'REPLICATION_HIDE_CONFLICT_MODAL',
+    REPLICATION_CHANGE_ACTIVITY_SORT: 'REPLICATION_CHANGE_ACTIVITY_SORT',
+    REPLICATION_CLEAR_SELECTED_DOCS: 'REPLICATION_CLEAR_SELECTED_DOCS'
   };
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/replication/api.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/api.js b/app/addons/replication/api.js
new file mode 100644
index 0000000..26dd4fd
--- /dev/null
+++ b/app/addons/replication/api.js
@@ -0,0 +1,216 @@
+// 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 Constants from './constants';
+import app from '../../app';
+import FauxtonAPI from '../../core/api';
+import base64 from 'base-64';
+import _ from 'lodash';
+
+export const encodeFullUrl = (fullUrl) => {
+  if (!fullUrl) {return '';}
+  const url = new URL(fullUrl);
+  if (url.username && url.password) {
+    return `${url.protocol}//${url.username}:${url.password}@${url.hostname}/${encodeURIComponent(url.pathname.slice(1))}`;
+  }
+  return `${url.origin}/${encodeURIComponent(url.pathname.slice(1))}`;
+};
+
+export const decodeFullUrl = (fullUrl) => {
+  if (!fullUrl) {return '';}
+  const url = new URL(fullUrl);
+  if (url.username && url.password) {
+    return `${url.protocol}//${url.username}:${url.password}@${url.hostname}/${decodeURIComponent(url.pathname.slice(1))}`;
+  }
+
+  return `${url.origin}/${decodeURIComponent(url.pathname.slice(1))}`;
+};
+
+export const getUsername = () => {
+  return app.session.get('userCtx').name;
+};
+
+export const getAuthHeaders = (username, password) => {
+  return {
+    'Authorization': 'Basic ' + base64.encode(username + ':' + password)
+  };
+};
+
+export const getSource = ({replicationSource, localSource, remoteSource, username, password}) => {
+  if (replicationSource === Constants.REPLICATION_SOURCE.LOCAL) {
+    return {
+      headers: getAuthHeaders(username, password),
+      url: `${window.location.origin}/${encodeURIComponent(localSource)}`
+    };
+  } else {
+    return encodeFullUrl(remoteSource);
+  }
+};
+
+export const getTarget = ({replicationTarget, localTarget, remoteTarget, replicationSource, username, password}) => {
+  let target = encodeFullUrl(remoteTarget);
+  const encodedLocalTarget = encodeURIComponent(localTarget);
+  const headers = getAuthHeaders(username, password);
+
+  if (replicationTarget === Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE) {
+    target = {
+      headers: headers,
+      url: `${window.location.origin}/${encodedLocalTarget}`
+    };
+  } else if (replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE) {
+
+    // check to see if we really need to send headers here or can just do the ELSE clause in all scenarioe
+    if (replicationSource === Constants.REPLICATION_SOURCE.LOCAL) {
+      target = {
+        headers: headers,
+        url: `${window.location.origin}/${encodedLocalTarget}`
+      };
+    } else {
+      const port = window.location.port === '' ? '' : ':' + window.location.port;
+      target = `${window.location.protocol}//${username}:${password}@${window.location.hostname}${port}/${encodedLocalTarget}`;
+    }
+  }
+
+  return target;
+};
+
+export const createTarget = (replicationTarget) => {
+  if (_.contains([
+    Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE,
+    Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE],
+    replicationTarget)) {
+    return true;
+  }
+
+  return false;
+};
+
+export const continuous = (replicationType) => {
+  if (replicationType === Constants.REPLICATION_TYPE.CONTINUOUS) {
+    return true;
+  }
+
+  return false;
+};
+
+export const addDocIdAndRev = (docId, _rev, doc) => {
+  if (docId) {
+    doc._id = docId;
+  }
+
+  if (_rev) {
+    doc._rev = _rev;
+  }
+
+  return doc;
+};
+
+export const createReplicationDoc = ({
+  replicationTarget,
+  replicationSource,
+  replicationType,
+  replicationDocName,
+  password,
+  localTarget,
+  localSource,
+  remoteTarget,
+  remoteSource,
+  _rev
+}) => {
+  const username = getUsername();
+  return addDocIdAndRev(replicationDocName, _rev, {
+    user_ctx: {
+      name: username,
+      roles: ['_admin', '_reader', '_writer']
+    },
+    source: getSource({
+      replicationSource,
+      localSource,
+      remoteSource,
+      username,
+      password
+    }),
+    target: getTarget({
+      replicationTarget,
+      replicationSource,
+      remoteTarget,
+      localTarget,
+      username,
+      password
+    }),
+    create_target: createTarget(replicationTarget),
+    continuous: continuous(replicationType),
+  });
+};
+
+const removeSensitiveUrlInfo = (url) => {
+  const urlObj = new URL(url);
+  return `${urlObj.origin}/${decodeURIComponent(urlObj.pathname.slice(1))}`;
+};
+
+export 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 {
+      _id: doc._id,
+      _rev: doc._rev,
+      selected: false, //use this field for bulk delete in the ui
+      source: getDocUrl(doc.source),
+      target: getDocUrl(doc.target),
+      createTarget: doc.create_target,
+      continuous: doc.continuous === true ? true : false,
+      status: doc._replication_state,
+      errorMsg: doc._replication_state_reason ? doc._replication_state_reason : '',
+      statusTime: new Date(doc._replication_state_time),
+      url: `#/database/_replicator/${app.utils.getSafeIdForDoc(doc._id)}`,
+      raw: doc
+    };
+  });
+};
+
+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));
+  });
+};
+
+export const checkReplicationDocID = (docId) => {
+  const promise = FauxtonAPI.Deferred();
+  $.ajax({
+    type: 'GET',
+    url: `/_replicator/${docId}`,
+    contentType: 'application/json; charset=utf-8',
+    dataType: 'json',
+  }).then((res) => {
+    promise.resolve(true);
+  }, function (xhr) {
+    if (xhr.statusText === "Object Not Found") {
+      promise.resolve(false);
+      return;
+    }
+    promise.resolve(true);
+  });
+  return promise;
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/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 4e97de0..8d3fe5f 100644
--- a/app/addons/replication/assets/less/replication.less
+++ b/app/addons/replication/assets/less/replication.less
@@ -13,16 +13,37 @@
 @import "../../../../../assets/less/variables.less";
 @import "../../../../../assets/less/mixins.less";
 
-.replication-page {
+div.replication__page {
+  padding-top: 25px !important;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.replication__section {
+  display: flex;
+  flex-flow: row wrap;
+  justify-content: flex-start;
+}
+
+.replication__seperator {
+  margin: 6px 0 15px;
+  width: 100%;
+}
+
+.replication__input-label {
+  padding-right: 15px;
+  width: 160px;
+  text-align: right;
+  margin-top: 12px;
   font-size: 14px;
+  margin-right: 10px;
+}
 
-  input, select {
-    font-size: 14px;
-  }
-  input {
-    width: 246px;
-  }
+.replication__input-select {
+  width: 540px;
   select {
+    font-size: 14px;
     width: 246px;
     margin-bottom: 10px;
     background-color: white;
@@ -31,82 +52,262 @@
   .styled-select {
     width: 250px;
   }
+}
 
-  .span3 {
-    text-align: right;
-    margin-top: 12px;
-  }
-  .remote-connection-details {
-    margin: 15px 0;
-  }
-  .connection-url {
-    width: 100%;
-  }
-  .buttons-row {
-    margin-top: 10px;
-    a {
-      padding: 12px;
+.replication__input-react-select {
+  font-size: 14px;
+
+  .Select div.Select-control {
+    padding: 6px;
+    border: 1px solid #cccccc;
+    width: 246px;
+
+    .Select-value, .Select-placeholder {
+      padding: 6px 15px 6px 10px;
     }
-  }
-  .typeahead {
-    width: 100%;
-  }
 
-  hr {
-    margin: 6px 0 15px;
-  }
-  .section-header {
-    font-weight: bold;
-    font-size: 14pt;
+    input {
+      margin-left: -6px;
+    }
+
+    .Select-arrow-zone {
+      padding: 0;
+      width: 18px;
+      color: black;
+    }
   }
 }
 
-#dashboard-content .replication-page {
-  padding-top: 25px;
+.replication__remote-connection-url[type="text"] {
+  font-size: 14px;
+  width: 100%;
+  color: #333;
 }
 
-.connection-url-example {
+.replication__remote-connection-url-text {
   font-size: 9pt;
   color: #999999;
   margin-bottom: 8px;
 }
 
-.custom-id-field {
+.replication__new-input[type="text"] {
+  width: 248px;
+  font-size: 14px;
+  color: #333;
+}
+
+.replication__doc-name {
   position: relative;
   width: 250px;
 
-  span.fonticon {
-    cursor: pointer;
-    position: absolute;
-    right: 6px;
-    top: 8px;
-    font-size: 11px;
-    padding: 8px;
-    color: #999999;
-    .transition(all 0.25s linear);
-    &:hover {
-      color: #333333;
-    }
-    input {
-      padding-right: 32px;
-    }
-  }
 }
 
+.replication__doc-name-icon {
+  cursor: pointer;
+  position: absolute;
+  right: 6px;
+  top: 8px;
+  font-size: 11px;
+  padding: 8px;
+  color: #333;
+  .transition(all 0.25s linear);
+}
 
-body .Select div.Select-control {
-  padding: 6px;
-  border: 1px solid #cccccc;
-  width: 246px;
-  .Select-value, .Select-placeholder {
-    padding: 6px 10px;
-  }
-  input {
-    margin-left: -6px;
-  }
-  .Select-arrow-zone {
-    padding: 0;
-    width: 18px;
-    color: black;
-  }
+.replication__doc-name-icon:hover {
+  color: #E73D34;
+}
+
+//use attribute selector to strengthen this to override bootstraps default font size for inputs
+.replication__doc-name-input[type="text"] {
+  padding-right: 32px;
+  font-size: 14px;
+  width: 248px;
+  color: #333;
+}
+
+.replication__button-row {
+  margin-top: 10px;
+  width: 378px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.replication__clear-link {
+  padding: 12px;
+  font-size: 14px;
+  padding: 12px 0 12px 24px;
+}
+
+.replication__clear-link:focus,
+.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--selected {
+  color: @hoverRed;
+}
+
+.replication__table-header-source {
+  width: 30%;
+  font-weight: bold;
+  cursor: pointer;
+}
+
+.replication__table-header-target {
+  width: 30%;
+  font-weight: bold;
+  cursor: pointer;
+}
+
+.replication__table-header-type {
+  font-weight: bold;
+  width: 9%;
+  cursor: pointer;
+}
+
+.replication__table-header-status {
+  font-weight: bold;
+  width: 9%;
+  cursor: pointer;
+}
+
+.replication__table-header-time {
+  font-weight: bold;
+  width: 13%;
+  cursor: pointer;
+}
+
+.replication__table-header-actions {
+  font-weight: bold;
+  width: 13%;
+}
+
+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;
+}
+
+.replication__activity_header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.replication__activity_header-btn {
+  height: 42px;
+}
+
+a.replication__activity_header-btn:active,
+a.replication__activity_header-btn:visited {
+  color: #fff;
+}
+
+.replication__table-bulk-select {
+  width: 68px;
+}
+
+.replication__bulk-select-wrapper {
+  display: flex;
+}
+
+.replication__bulk-select-header {
+  width: 30px;
+  border: 1px solid #aaa;
+}
+
+input.replication__bulk-select-input[type="checkbox"] {
+  margin: 8px;
+}
+
+.replication__bulk-select-trash {
+  color: #505050;
+  border: 1px solid #aaa;
+  background-color: rgba(0, 0, 0, 0);
+  margin-left: 3px;
+  padding-left: 8px
+}
+
+.replication__row-actions-list {
+  margin: 0px;
+  text-decoration: none;
+}
+
+.replication__row-list {
+  display: inline;
+}
+
+.replication__row-btn {
+  font-size: 16px;
+  text-decoration: none;
+  cursor: pointer;
+  padding-right: 8px;
+  padding-left: 8px;
+}
+
+.replication__row-btn:visited,
+.replication__row-btn:hover {
+  text-decoration: none;
+}
+
+.replication__row-btn--warning {
+  color: #E73D34;
+}
+
+.replication__filter-icon {
+  padding-right: 8px;
+}
+
+input.replication__filter-input[type="text"] {
+  font-size: 14px;
+  border: 0;
+  padding: 10px;
+  margin-bottom: 0;
+}
+
+.replication__error-cancel,
+.replication__error-continue {
+  background-color: #0082BF;
+}
+
+.replication__error-cancel:hover,
+.replication__error-continue:hover {
+  background-color: #e73d34;
+}
+
+td.replication__empty-row {
+  text-align: center;
+}
+
+.replication__remote_icon_help {
+  color: #E33F3B
+}
+
+.replication__remote_icon_help:hover {
+  color: #af2d24;
 }

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/replication/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/replication/components.react.jsx b/app/addons/replication/components.react.jsx
deleted file mode 100644
index 2d4542e..0000000
--- a/app/addons/replication/components.react.jsx
+++ /dev/null
@@ -1,546 +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 FauxtonAPI from '../../core/api';
-import React from 'react';
-import Stores from './stores';
-import Actions from './actions';
-import Constants from './constants';
-import Helpers from './helpers';
-import Components from '../components/react-components.react';
-import base64 from 'base-64';
-import AuthActions from '../auth/actions';
-import AuthComponents from '../auth/components.react';
-import ReactSelect from 'react-select';
-
-const store = Stores.replicationStore;
-const LoadLines = Components.LoadLines;
-const StyledSelect = Components.StyledSelect;
-const ConfirmButton = Components.ConfirmButton;
-const PasswordModal = AuthComponents.PasswordModal;
-
-
-class ReplicationController extends React.Component {
-  constructor (props) {
-    super(props);
-    this.state = this.getStoreState();
-    this.submit = this.submit.bind(this);
-    this.clear = this.clear.bind(this);
-    this.showPasswordModal = this.showPasswordModal.bind(this);
-  }
-
-  getStoreState () {
-    return {
-      loading: store.isLoading(),
-      databases: store.getDatabases(),
-      authenticated: store.isAuthenticated(),
-      password: store.getPassword(),
-
-      // source fields
-      replicationSource: store.getReplicationSource(),
-      sourceDatabase: store.getSourceDatabase(),
-      localSourceDatabaseKnown: store.isLocalSourceDatabaseKnown(),
-      remoteSource: store.getRemoteSource(),
-
-      // target fields
-      replicationTarget: store.getReplicationTarget(),
-      targetDatabase: store.getTargetDatabase(),
-      localTargetDatabaseKnown: store.isLocalTargetDatabaseKnown(),
-      remoteTarget: store.getRemoteTarget(),
-
-      // other
-      passwordModalVisible: store.isPasswordModalVisible(),
-      replicationType: store.getReplicationType(),
-      replicationDocName: store.getReplicationDocName()
-    };
-  }
-
-  componentDidMount () {
-    store.on('change', this.onChange, this);
-  }
-
-  componentWillUnmount () {
-    store.off('change', this.onChange);
-  }
-
-  onChange () {
-    this.setState(this.getStoreState());
-  }
-
-  clear (e) {
-    e.preventDefault();
-    Actions.clearReplicationForm();
-  }
-
-  showPasswordModal () {
-    const { replicationSource, replicationTarget } = this.state;
-
-    const hasLocalSourceOrTarget = (replicationSource === Constants.REPLICATION_SOURCE.LOCAL ||
-      replicationTarget === Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE ||
-      replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE);
-
-    // if the user is authenticated, or if NEITHER the source nor target are local, just submit. The password
-    // modal isn't necessary
-    if (!hasLocalSourceOrTarget || this.state.authenticated) {
-      this.submit();
-      return;
-    }
-    AuthActions.showPasswordModal();
-  }
-
-  getUsername () {
-    return app.session.get('userCtx').name;
-  }
-
-  getAuthHeaders () {
-    const username = this.getUsername();
-    return {
-      'Authorization': 'Basic ' + base64.encode(username + ':' + this.state.password)
-    };
-  }
-
-  submit () {
-    const { replicationTarget, replicationType, replicationDocName} = this.state;
-
-    if (!this.validate()) {
-      return;
-    }
-
-    const params = {
-      source: this.getSource(),
-      target: this.getTarget()
-    };
-
-    if (_.contains([Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE, Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE], replicationTarget)) {
-      params.create_target = true;
-    }
-    if (replicationType === Constants.REPLICATION_TYPE.CONTINUOUS) {
-      params.continuous = true;
-    }
-
-    if (replicationDocName) {
-      params._id = this.state.replicationDocName;
-    }
-
-    // POSTing to the _replicator DB requires auth
-    const user = FauxtonAPI.session.user();
-    const userName = _.isNull(user) ? '' : FauxtonAPI.session.user().name;
-    params.user_ctx = {
-      name: userName,
-      roles: ['_admin', '_reader', '_writer']
-    };
-
-    Actions.replicate(params);
-  }
-
-  getSource () {
-    const { replicationSource, sourceDatabase, remoteSource } = this.state;
-    if (replicationSource === Constants.REPLICATION_SOURCE.LOCAL) {
-      return {
-        headers: this.getAuthHeaders(),
-        url: window.location.origin + '/' + sourceDatabase
-      };
-    } else {
-      return remoteSource;
-    }
-  }
-
-  getTarget () {
-    const { replicationTarget, targetDatabase, remoteTarget, replicationSource, password } = this.state;
-
-    let target;
-    if (replicationTarget === Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE) {
-      target = {
-        headers: this.getAuthHeaders(),
-        url: window.location.origin + '/' + targetDatabase
-      };
-    } else if (replicationTarget === Constants.REPLICATION_TARGET.EXISTING_REMOTE_DATABASE) {
-      target = remoteTarget;
-    } else if (replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE) {
-
-      // check to see if we really need to send headers here or can just do the ELSE clause in all scenarioe
-      if (replicationSource === Constants.REPLICATION_SOURCE.LOCAL) {
-        target = {
-          headers: this.getAuthHeaders(),
-          url: window.location.origin + '/' + targetDatabase
-        };
-      } else {
-        const port = window.location.port === '' ? '' : ':' + window.location.port;
-        target = window.location.protocol + '//' + this.getUsername() + ':' + password + '@'
-          + window.location.hostname + port + '/' + targetDatabase;
-      }
-    } else {
-      target = remoteTarget;
-    }
-
-    return target;
-  }
-
-  validate () {
-    const { replicationTarget, targetDatabase, databases } = this.state;
-
-    if (replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE && _.contains(databases, targetDatabase)) {
-      FauxtonAPI.addNotification({
-        msg: 'The <code>' + targetDatabase + '</code> database already exists locally. Please enter another database name.',
-        type: 'error',
-        escape: false,
-        clear: true
-      });
-      return false;
-    }
-    if (replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE ||
-        replicationTarget === Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE) {
-      let error = '';
-      if (/\s/.test(targetDatabase)) {
-        error = 'The target database may not contain any spaces.';
-      } else if (/^_/.test(targetDatabase)) {
-        error = 'The target database may not start with an underscore.';
-      }
-
-      if (error) {
-        FauxtonAPI.addNotification({
-          msg: error,
-          type: 'error',
-          escape: false,
-          clear: true
-        });
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-  render () {
-    const { loading, replicationSource, replicationTarget, replicationType, replicationDocName, passwordModalVisible,
-      localSourceDatabaseKnown, databases, localTargetDatabaseKnown, sourceDatabase, remoteSource, remoteTarget,
-      targetDatabase } = this.state;
-
-    if (loading) {
-      return (
-        <LoadLines />
-      );
-    }
-
-    let confirmButtonEnabled = true;
-    if (!replicationSource || !replicationTarget) {
-      confirmButtonEnabled = false;
-    }
-    if (replicationSource === Constants.REPLICATION_SOURCE.LOCAL && !localSourceDatabaseKnown) {
-      confirmButtonEnabled = false;
-    }
-    if (replicationTarget === Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE && !localTargetDatabaseKnown) {
-      confirmButtonEnabled = false;
-    }
-
-    return (
-      <div className="replication-page">
-        <div className="row">
-          <div className="span3">
-            Replication Source:
-          </div>
-          <div className="span7">
-            <ReplicationSource
-              value={replicationSource}
-              onChange={(repSource) => Actions.updateFormField('replicationSource', repSource)}/>
-          </div>
-        </div>
-
-        {replicationSource ?
-          <ReplicationSourceRow
-            replicationSource={replicationSource}
-            databases={databases}
-            sourceDatabase={sourceDatabase}
-            remoteSource={remoteSource}
-            onChange={(val) => Actions.updateFormField('remoteSource', val)}
-          /> : null}
-
-        <hr size="1"/>
-
-        <div className="row">
-          <div className="span3">
-            Replication Target:
-          </div>
-          <div className="span7">
-            <ReplicationTarget
-              value={replicationTarget}
-              onChange={(repTarget) => Actions.updateFormField('replicationTarget', repTarget)}/>
-          </div>
-        </div>
-        {replicationTarget ?
-          <ReplicationTargetRow
-            remoteTarget={remoteTarget}
-            replicationTarget={replicationTarget}
-            databases={databases}
-            targetDatabase={targetDatabase}
-          /> : null}
-
-        <hr size="1"/>
-
-        <div className="row">
-          <div className="span3">
-            Replication Type:
-          </div>
-          <div className="span7">
-            <ReplicationType
-              value={replicationType}
-              onChange={(repType) => Actions.updateFormField('replicationType', repType)}/>
-          </div>
-        </div>
-
-        <div className="row">
-          <div className="span3">
-            Replication Document:
-          </div>
-          <div className="span7">
-            <div className="custom-id-field">
-              <span className="fonticon fonticon-cancel" title="Clear field"
-                onClick={(e) => Actions.updateFormField('replicationDocName', '')} />
-              <input type="text" placeholder="Custom, new ID (optional)" value={replicationDocName}
-                onChange={(e) => Actions.updateFormField('replicationDocName', e.target.value)}/>
-            </div>
-          </div>
-        </div>
-
-        <div className="row buttons-row">
-          <div className="span3">
-          </div>
-          <div className="span7">
-            <ConfirmButton id="replicate" text="Start Replication" onClick={this.showPasswordModal} disabled={!confirmButtonEnabled}/>
-            <a href="#" data-bypass="true" onClick={this.clear}>Clear</a>
-          </div>
-        </div>
-
-        <PasswordModal
-          visible={passwordModalVisible}
-          modalMessage={<p>Replication requires authentication.</p>}
-          submitBtnLabel="Continue Replication"
-          onSuccess={this.submit} />
-      </div>
-    );
-  }
-}
-
-
-class ReplicationSourceRow extends React.Component {
-  render () {
-    const { replicationSource, databases, sourceDatabase, remoteSource, onChange} = this.props;
-
-    if (replicationSource === Constants.REPLICATION_SOURCE.LOCAL) {
-      return (
-        <div className="replication-source-name-row row">
-          <div className="span3">
-            Source Name:
-          </div>
-          <div className="span7">
-            <ReactSelect
-              name="source-name"
-              value={sourceDatabase}
-              placeholder="Database name"
-              options={Helpers.getReactSelectOptions(databases)}
-              clearable={false}
-              onChange={(selected) => Actions.updateFormField('sourceDatabase', selected.value)} />
-          </div>
-        </div>
-      );
-    }
-
-    return (
-      <div>
-        <div className="row">
-          <div className="span3">Database URL:</div>
-          <div className="span7">
-            <input type="text" className="connection-url" placeholder="https://" value={remoteSource}
-              onChange={(e) => onChange(e.target.value)} />
-            <div className="connection-url-example">e.g. https://$REMOTE_USERNAME:$REMOTE_PASSWORD@$REMOTE_SERVER/$DATABASE</div>
-          </div>
-        </div>
-      </div>
-    );
-  }
-}
-ReplicationSourceRow.propTypes = {
-  replicationSource: React.PropTypes.string.isRequired,
-  databases: React.PropTypes.array.isRequired,
-  sourceDatabase: React.PropTypes.string.isRequired,
-  remoteSource: React.PropTypes.string.isRequired,
-  onChange: React.PropTypes.func.isRequired
-};
-
-
-class ReplicationSource extends React.Component {
-  getOptions () {
-    const options = [
-      { value: '', label: 'Select source' },
-      { value: Constants.REPLICATION_SOURCE.LOCAL, label: 'Local database' },
-      { value: Constants.REPLICATION_SOURCE.REMOTE, label: 'Remote database' }
-    ];
-    return options.map((option) => {
-      return (
-        <option value={option.value} key={option.value}>{option.label}</option>
-      );
-    });
-  }
-
-  render () {
-    return (
-      <StyledSelect
-        selectContent={this.getOptions()}
-        selectChange={(e) => this.props.onChange(e.target.value)}
-        selectId="replication-source"
-        selectValue={this.props.value} />
-    );
-  }
-}
-ReplicationSource.propTypes = {
-  value: React.PropTypes.string.isRequired,
-  onChange: React.PropTypes.func.isRequired
-};
-
-
-class ReplicationTarget extends React.Component {
-  getOptions () {
-    const options = [
-      { value: '', label: 'Select target' },
-      { value: Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE, label: 'Existing local database' },
-      { value: Constants.REPLICATION_TARGET.EXISTING_REMOTE_DATABASE, label: 'Existing remote database' },
-      { value: Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE, label: 'New local database' },
-      { value: Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE, label: 'New remote database' }
-    ];
-    return options.map((option) => {
-      return (
-        <option value={option.value} key={option.value}>{option.label}</option>
-      );
-    });
-  }
-
-  render () {
-    return (
-      <StyledSelect
-        selectContent={this.getOptions()}
-        selectChange={(e) => this.props.onChange(e.target.value)}
-        selectId="replication-target"
-        selectValue={this.props.value} />
-    );
-  }
-}
-
-ReplicationTarget.propTypes = {
-  value: React.PropTypes.string.isRequired,
-  onChange: React.PropTypes.func.isRequired
-};
-
-
-class ReplicationType extends React.Component {
-  getOptions () {
-    const options = [
-      { value: Constants.REPLICATION_TYPE.ONE_TIME, label: 'One time' },
-      { value: Constants.REPLICATION_TYPE.CONTINUOUS, label: 'Continuous' }
-    ];
-    return _.map(options, function (option) {
-      return (
-        <option value={option.value} key={option.value}>{option.label}</option>
-      );
-    });
-  }
-
-  render () {
-    return (
-      <StyledSelect
-        selectContent={this.getOptions()}
-        selectChange={(e) => this.props.onChange(e.target.value)}
-        selectId="replication-target"
-        selectValue={this.props.value} />
-    );
-  }
-}
-ReplicationType.propTypes = {
-  value: React.PropTypes.string.isRequired,
-  onChange: React.PropTypes.func.isRequired
-};
-
-
-class ReplicationTargetRow extends React.Component {
-  update (value) {
-    Actions.updateFormField('remoteTarget', value);
-  }
-
-  render () {
-    const { replicationTarget, remoteTarget, targetDatabase, databases } = this.props;
-
-    let targetLabel = 'Target Name:';
-    let field = null;
-    let remoteHelpText = 'https://$USERNAME:$PASSWORD@server.com/$DATABASE';
-
-    // new and existing remote DBs show a URL field
-    if (replicationTarget === Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE ||
-        replicationTarget === Constants.REPLICATION_TARGET.EXISTING_REMOTE_DATABASE) {
-      targetLabel = 'Database URL';
-      remoteHelpText = 'https://$REMOTE_USERNAME:$REMOTE_PASSWORD@$REMOTE_SERVER/$DATABASE';
-
-      field = (
-        <div>
-          <input type="text" className="connection-url" placeholder="https://" value={remoteTarget}
-            onChange={(e) => Actions.updateFormField('remoteTarget', e.target.value)} />
-          <div className="connection-url-example">e.g. {remoteHelpText}</div>
-        </div>
-      );
-
-    // new local databases have a freeform text field
-    } else if (replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE) {
-      field = (
-        <input type="text" className="new-local-db" placeholder="Database name" value={targetDatabase}
-          onChange={(e) => Actions.updateFormField('targetDatabase', e.target.value)} />
-      );
-
-    // existing local databases have a typeahead field
-    } else {
-      field = (
-        <ReactSelect
-          value={targetDatabase}
-          options={Helpers.getReactSelectOptions(databases)}
-          placeholder="Database name"
-          clearable={false}
-          onChange={(selected) => Actions.updateFormField('targetDatabase', selected.value)} />
-      );
-    }
-
-    if (replicationTarget === Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE ||
-        replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE) {
-      targetLabel = 'New Database:';
-    }
-
-    return (
-      <div className="replication-target-name-row row">
-        <div className="span3">{targetLabel}</div>
-        <div className="span7">
-          {field}
-        </div>
-      </div>
-    );
-  }
-}
-ReplicationTargetRow.propTypes = {
-  remoteTarget: React.PropTypes.string.isRequired,
-  replicationTarget: React.PropTypes.string.isRequired,
-  databases: React.PropTypes.array.isRequired,
-  targetDatabase: React.PropTypes.string.isRequired
-};
-
-
-export default {
-  ReplicationController,
-  ReplicationSource,
-  ReplicationTarget,
-  ReplicationType,
-  ReplicationTargetRow
-};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/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..be8fe24
--- /dev/null
+++ b/app/addons/replication/components/activity.js
@@ -0,0 +1,453 @@
+// 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';
+import {DeleteModal, ErrorModal} from './modals';
+
+const formatUrl = (url) => {
+  const urlObj = new URL(url);
+  const encoded = encodeURIComponent(urlObj.pathname.slice(1));
+
+  if (url.indexOf(window.location.hostname) > -1) {
+    return (
+      <span>
+        {urlObj.origin + '/'}
+        <a href={`#/database/${encoded}/_all_docs`}>{urlObj.pathname.slice(1)}</a>
+      </span>
+    );
+  }
+
+  return `${urlObj.origin}${urlObj.pathname}`;
+};
+
+class RowActions extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = {
+      modalVisible: false,
+    };
+  }
+
+  showModal () {
+    this.setState({modalVisible: true});
+  }
+
+  closeModal () {
+    this.setState({modalVisible: false});
+  }
+
+  getErrorIcon () {
+    if (!this.props.error) {
+      return null;
+    }
+    return (
+      <li className="replication__row-list">
+        <a
+          data-bypass="true"
+          className="replication__row-btn replication__row-btn--warning icon-exclamation-sign"
+          onClick={this.showModal.bind(this)}
+          title="View error message">
+        </a>
+        <ErrorModal
+          onClick={this.closeModal.bind(this)}
+          onClose={this.closeModal.bind(this)}
+          errorMsg={this.props.errorMsg}
+          visible={this.state.modalVisible}
+        />
+      </li>
+    );
+  }
+
+  render () {
+    const {_id, url, deleteDocs} = this.props;
+    const errorIcon = this.getErrorIcon();
+    return (
+      <ul className="replication__row-actions-list">
+        <li className="replication__row-list">
+          <a
+            href={`#replication/id/${encodeURIComponent(_id)}`}
+            className="replication__row-btn icon-wrench"
+            title={`Edit replication`}
+            data-bypass="true"
+            >
+          </a>
+        </li>
+        <li className="replication__row-list">
+        <a
+          className="replication__row-btn fonticon-document"
+          title={`Edit replication document`}
+          href={url}
+          data-bypass="true"
+          >
+        </a>
+        </li>
+        <li className="replication__row-list">
+        <a
+          className="replication__row-btn icon-trash"
+          title={`Delete document ${_id}`}
+          onClick={() => deleteDocs(_id)}>
+        </a>
+        </li>
+        {errorIcon}
+      </ul>
+    );
+
+  }
+};
+
+RowActions.propTypes = {
+  _id: React.PropTypes.string.isRequired,
+  url: React.PropTypes.string.isRequired,
+  error: React.PropTypes.bool.isRequired,
+  errorMsg: React.PropTypes.string.isRequired,
+  deleteDocs: React.PropTypes.func.isRequired
+};
+
+const Row = ({
+  _id,
+  source,
+  target,
+  type,
+  status,
+  statusTime,
+  url,
+  selected,
+  selectDoc,
+  errorMsg,
+  deleteDocs
+}) => {
+  const momentTime = moment(statusTime);
+  const formattedTime = momentTime.isValid() ? momentTime.format("MMM Do, h:mm a") : '';
+
+  return (
+    <tr className="replication__table-row">
+      <td className="replication__table-col"><input checked={selected} type="checkbox" onChange={() => selectDoc(_id)} /> </td>
+      <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">{formattedTime}</td>
+      <td className="replication__table-col">
+        <RowActions
+          deleteDocs={deleteDocs}
+          _id={_id}
+          url={url}
+          error={status === "error"}
+          errorMsg={errorMsg}
+          />
+      </td>
+    </tr>
+
+  );
+};
+
+Row.propTypes = {
+  _id: React.PropTypes.string.isRequired,
+  source: React.PropTypes.string.isRequired,
+  target: React.PropTypes.string.isRequired,
+  type: React.PropTypes.string.isRequired,
+  status: React.PropTypes.string,
+  url: React.PropTypes.string.isRequired,
+  statusTime: React.PropTypes.object.isRequired,
+  selected: React.PropTypes.bool.isRequired,
+  selectDoc: React.PropTypes.func.isRequired,
+  errorMsg: React.PropTypes.string.isRequired,
+  deleteDocs: React.PropTypes.func.isRequired
+};
+
+const BulkSelectHeader = ({isSelected, deleteDocs, someDocsSelected, onCheck}) => {
+  const trash = someDocsSelected ?
+    <button
+      onClick={() => deleteDocs()}
+      className="replication__bulk-select-trash fonticon fonticon-trash"
+      title="Delete all selected">
+    </button> : null;
+
+  return (
+    <div className="replication__bulk-select-wrapper">
+      <div className="replication__bulk-select-header">
+        <input className="replication__bulk-select-input" checked={isSelected} type="checkbox" onChange={onCheck} />
+      </div>
+    {trash}
+    </div>
+  );
+};
+
+BulkSelectHeader.propTypes = {
+  isSelected: React.PropTypes.bool.isRequired,
+  someDocsSelected: React.PropTypes.bool.isRequired,
+  onCheck: React.PropTypes.func.isRequired,
+  deleteDocs: React.PropTypes.func.isRequired
+};
+
+const EmptyRow = () =>
+  <tr>
+    <td colSpan="7" className="replication__empty-row">
+      There is no replicator-db activity or history to display.
+    </td>
+  </tr>;
+
+class ReplicationTable extends React.Component {
+  constructor (props) {
+    super(props);
+  }
+
+  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 () {
+    if (this.props.docs.length === 0) {
+      return <EmptyRow />;
+    }
+
+    return this.sort(this.props.column, this.props.descending, this.props.docs).map((doc, i) => {
+      return <Row
+        key={i}
+        _id={doc._id}
+        selected={doc.selected}
+        selectDoc={this.props.selectDoc}
+        source={doc.source}
+        target={doc.target}
+        type={doc.continuous === true ? 'Continuous' : 'One time'}
+        status={doc.status}
+        statusTime={doc.statusTime}
+        url={doc.url}
+        deleteDocs={this.props.deleteDocs}
+        errorMsg={doc.errorMsg}
+        doc={doc}
+      />;
+    });
+  }
+
+  iconDirection (column) {
+    if (column === this.props.column && !this.props.descending) {
+      return 'fonticon-up-dir';
+    }
+
+    return 'fonticon-down-dir';
+  }
+
+  onSort (column) {
+    return (e) => {
+      this.props.changeSort({
+        descending: column === this.props.column ? !this.props.descending : true,
+        column
+      });
+    };
+  }
+
+  isSelected (header) {
+    if (header === this.props.column) {
+      return 'replication__table--selected';
+    }
+
+    return '';
+  }
+
+  render () {
+    return (
+      <Table striped>
+        <thead>
+          <tr>
+            <th className="replication__table-bulk-select">
+              <BulkSelectHeader
+                isSelected={this.props.allDocsSelected}
+                onCheck={this.props.selectAllDocs}
+                someDocsSelected={this.props.someDocsSelected}
+                deleteDocs={this.props.deleteDocs}
+                />
+            </th>
+            <th className="replication__table-header-source" onClick={this.onSort('source')}>
+              Source
+              <span className={`replication__table-header-icon ${this.iconDirection('source')} ${this.isSelected('source')}`} />
+            </th>
+            <th className="replication__table-header-target" onClick={this.onSort('target')}>
+              Target
+              <span className={`replication__table-header-icon ${this.iconDirection('target')} ${this.isSelected('target')}`} />
+            </th>
+            <th className="replication__table-header-type" onClick={this.onSort('continuous')}>
+              Type
+              <span className={`replication__table-header-icon ${this.iconDirection('continuous')} ${this.isSelected('continuous')}`} />
+            </th>
+            <th className="replication__table-header-status" onClick={this.onSort('status')}>
+              State
+              <span className={`replication__table-header-icon ${this.iconDirection('status')} ${this.isSelected('status')}`} />
+            </th>
+            <th className="replication__table-header-time" onClick={this.onSort('statusTime')}>
+              State Time
+              <span className={`replication__table-header-icon ${this.iconDirection('statusTime')} ${this.isSelected('statusTime')}`} />
+            </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="replication__filter-icon fonticon-filter" />
+      <input
+        type="text"
+        placeholder="Filter replications"
+        className="replication__filter-input"
+        value={value}
+        onChange={(e) => {onChange(e.target.value);}}
+      />
+    </div>
+  );
+};
+
+ReplicationFilter.propTypes = {
+  value: React.PropTypes.string.isRequired,
+  onChange: React.PropTypes.func.isRequired
+};
+
+const ReplicationHeader = ({filter, onFilterChange}) => {
+  return (
+    <div className="replication__activity_header">
+      <div></div>
+      <ReplicationFilter value={filter} onChange={onFilterChange} />
+      <a href="#/replication/_create" className="btn save replication__activity_header-btn btn-success">
+        <i className="icon fonticon-plus-circled"></i>
+        New Replication
+      </a>
+    </div>
+  );
+};
+
+ReplicationHeader.propTypes = {
+  filter: React.PropTypes.string.isRequired,
+  onFilterChange: React.PropTypes.func.isRequired
+};
+
+export default class Activity extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = {
+      modalVisible: false,
+      unconfirmedDeleteDocId: null
+    };
+  }
+
+  closeModal () {
+    this.setState({
+      modalVisible: false,
+      unconfirmedDeleteDocId: null
+    });
+  }
+
+  showModal (doc) {
+    this.setState({
+      modalVisible: true,
+      unconfirmedDeleteDocId: doc
+    });
+  }
+
+  confirmDeleteDocs () {
+    let docs = [];
+    if (this.state.unconfirmedDeleteDocId) {
+      const doc = this.props.docs.find(doc => doc._id === this.state.unconfirmedDeleteDocId);
+      docs.push(doc);
+    } else {
+      docs = this.props.docs.filter(doc => doc.selected);
+    }
+
+    this.props.deleteDocs(docs);
+    this.closeModal();
+  }
+
+  numDocsSelected () {
+    return this.props.docs.filter(doc => doc.selected).length;
+  }
+
+  render () {
+    const {
+      onFilterChange,
+      activitySort,
+      changeActivitySort,
+      docs,
+      filter,
+      selectAllDocs,
+      someDocsSelected,
+      allDocsSelected,
+      selectDoc
+    } = this.props;
+
+    const {modalVisible} = this.state;
+    return (
+      <div className="replication__activity">
+        <ReplicationHeader
+          filter={filter}
+          onFilterChange={onFilterChange}
+        />
+        <ReplicationTable
+          someDocsSelected={someDocsSelected}
+          allDocsSelected={allDocsSelected}
+          selectAllDocs={selectAllDocs}
+          docs={docs}
+          selectDoc={selectDoc}
+          deleteDocs={this.showModal.bind(this)}
+          descending={activitySort.descending}
+          column={activitySort.column}
+          changeSort={changeActivitySort}
+        />
+        <DeleteModal
+          multipleDocs={this.numDocsSelected()}
+          visible={modalVisible}
+          onClose={this.closeModal.bind(this)}
+          onClick={this.confirmDeleteDocs.bind(this)}
+          />
+      </div>
+    );
+  }
+}
+
+Activity.propTypes = {
+  docs: React.PropTypes.array.isRequired,
+  filter: React.PropTypes.string.isRequired,
+  selectAllDocs: React.PropTypes.func.isRequired,
+  allDocsSelected: React.PropTypes.bool.isRequired,
+  someDocsSelected: React.PropTypes.bool.isRequired,
+  selectDoc: React.PropTypes.func.isRequired,
+  onFilterChange: React.PropTypes.func.isRequired,
+  deleteDocs: React.PropTypes.func.isRequired,
+  activitySort: React.PropTypes.object.isRequired,
+  changeActivitySort: React.PropTypes.func.isRequired
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/replication/components/modals.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/components/modals.js b/app/addons/replication/components/modals.js
new file mode 100644
index 0000000..c02f31b
--- /dev/null
+++ b/app/addons/replication/components/modals.js
@@ -0,0 +1,139 @@
+// 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 {Modal} from 'react-bootstrap';
+import Components from '../../components/react-components.react';
+
+const {ConfirmButton} = Components;
+
+
+export const DeleteModal = ({
+  visible,
+  onClose,
+  onClick,
+  multipleDocs
+}) => {
+
+  if (!visible) {
+    return null;
+  }
+
+  let header = "You are deleting a replication document.";
+
+  if (multipleDocs > 1) {
+    header = `You are deleting ${multipleDocs} replication documents.`;
+  }
+
+  return (
+    <Modal dialogClassName="replication_delete-doc-modal" show={visible} onHide={() => onClose()}>
+      <Modal.Header closeButton={true}>
+        <Modal.Title>Verify Deletion</Modal.Title>
+      </Modal.Header>
+      <Modal.Body>
+        <p>{header}</p>
+        <p>
+          Deleting a replication document stops continuous replication
+          and incomplete one-time replication, but does not affect replicated documents.
+        </p>
+        <p>
+          Replication jobs that do not have replication documents do not appear in Replicator DB Activity.
+        </p>
+      </Modal.Body>
+      <Modal.Footer>
+        <a className="cancel-link" onClick={onClose}>Cancel</a>
+        <ConfirmButton
+          text={"Delete Document"}
+          onClick={onClick}
+        />
+      </Modal.Footer>
+    </Modal>
+  );
+};
+
+DeleteModal.propTypes = {
+  visible: React.PropTypes.bool.isRequired,
+  onClick: React.PropTypes.func.isRequired,
+  onClose: React.PropTypes.func.isRequired,
+  multipleDocs: React.PropTypes.number.isRequired
+};
+
+export const ErrorModal = ({visible, onClose, errorMsg, onClick}) => {
+
+  if (!visible) {
+    return null;
+  }
+
+  return (
+    <Modal dialogClassName="replication__error-doc-modal" show={visible} onHide={() => onClose()}>
+      <Modal.Header closeButton={true}>
+        <Modal.Title>Replication Error</Modal.Title>
+      </Modal.Header>
+      <Modal.Body>
+        <p>
+          {errorMsg}
+        </p>
+      </Modal.Body>
+      <Modal.Footer>
+      </Modal.Footer>
+    </Modal>
+  );
+};
+
+ErrorModal.propTypes = {
+  visible: React.PropTypes.bool.isRequired,
+  onClick: React.PropTypes.func.isRequired,
+  onClose: React.PropTypes.func.isRequired,
+  errorMsg: React.PropTypes.string.isRequired
+};
+
+export const ConflictModal = ({visible, docId, onClose, onClick}) => {
+
+  if (!visible) {
+    return null;
+  }
+
+  return (
+    <Modal dialogClassName="replication__error-doc-modal" show={visible} onHide={() => onClose()}>
+      <Modal.Header closeButton={true}>
+        <Modal.Title>Fix Document Conflict</Modal.Title>
+      </Modal.Header>
+      <Modal.Body>
+        <p>
+          A replication document with ID <code>{docId}</code> already exists.
+        </p>
+        <p>
+          You can overwrite the existing document, or change the new replication job’s document ID.
+        </p>
+        <p>
+          If you overwrite the existing document, any replication job currently using the replication document will stop,
+          and that job will not appear in Replicator DB Activity. Replicated documents will not be affected.
+        </p>
+      </Modal.Body>
+      <Modal.Footer>
+        <button onClick={onClose} className="btn replication__error-cancel">
+          Change Document ID
+        </button>
+        <button onClick={onClick} className="btn replication__error-continue">
+          <i className="icon icon-eraser"></i>
+          Overwrite Existing Document
+        </button>
+      </Modal.Footer>
+    </Modal>
+  );
+};
+
+ConflictModal.propTypes = {
+  visible: React.PropTypes.bool.isRequired,
+  onClick: React.PropTypes.func.isRequired,
+  onClose: React.PropTypes.func.isRequired,
+  docId: React.PropTypes.string.isRequired
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/replication/components/newreplication.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/components/newreplication.js b/app/addons/replication/components/newreplication.js
new file mode 100644
index 0000000..e274fa7
--- /dev/null
+++ b/app/addons/replication/components/newreplication.js
@@ -0,0 +1,277 @@
+// 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';
+import FauxtonAPI from '../../../core/api';
+import {ReplicationSource} from './source';
+import {ReplicationTarget} from './target';
+import {ReplicationOptions} from './options';
+import {ReplicationSubmit} from './submit';
+import AuthComponents from '../../auth/components.react';
+import Constants from '../constants';
+import {ConflictModal} from './modals';
+import {isEmpty} from 'lodash';
+
+const {PasswordModal} = AuthComponents;
+
+export default class NewReplicationController extends React.Component {
+  constructor (props) {
+    super(props);
+    this.submit = this.submit.bind(this);
+    this.clear = this.clear.bind(this);
+    this.showPasswordModal = this.showPasswordModal.bind(this);
+    this.runReplicationChecks = this.runReplicationChecks.bind(this);
+  }
+
+  clear (e) {
+    e.preventDefault();
+    this.props.clearReplicationForm();
+  }
+
+  showPasswordModal () {
+    this.props.hideConflictModal();
+    const { replicationSource, replicationTarget } = this.props;
+
+    const hasLocalSourceOrTarget = (replicationSource === Constants.REPLICATION_SOURCE.LOCAL ||
+      replicationTarget === Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE ||
+      replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE);
+
+    // if the user is authenticated, or if NEITHER the source nor target are local, just submit. The password
+    // modal isn't necessary or if couchdb is in admin party mode
+    if (!hasLocalSourceOrTarget || this.props.authenticated || FauxtonAPI.session.isAdminParty()) {
+      this.submit(this.props.username, this.props.password);
+      return;
+    }
+
+    this.props.showPasswordModal();
+  }
+
+  checkReplicationDocID () {
+    const {showConflictModal, replicationDocName, checkReplicationDocID} = this.props;
+    checkReplicationDocID(replicationDocName).then(existingDoc => {
+      if (existingDoc) {
+        showConflictModal();
+        return;
+      }
+
+      this.showPasswordModal();
+    });
+  }
+
+  runReplicationChecks () {
+    const {replicationDocName} = this.props;
+    if (!this.validate()) {
+      return;
+    }
+    if (replicationDocName) {
+      this.checkReplicationDocID();
+      return;
+    }
+
+    this.showPasswordModal();
+  }
+
+  validate () {
+    const {
+      remoteTarget,
+      remoteSource,
+      replicationTarget,
+      replicationSource,
+      localTarget,
+      localSource,
+      databases
+    } = this.props;
+
+    if (replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE && _.contains(databases, localTarget)) {
+      FauxtonAPI.addNotification({
+        msg: 'The <code>' + localTarget + '</code> database already exists locally. Please enter another database name.',
+        type: 'error',
+        escape: false,
+        clear: true
+      });
+      return false;
+    }
+    if (replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE ||
+        replicationTarget === Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE) {
+      let error = '';
+      if (/\s/.test(localTarget)) {
+        error = 'The target database may not contain any spaces.';
+      } else if (/^_/.test(localTarget)) {
+        error = 'The target database may not start with an underscore.';
+      }
+
+      if (error) {
+        FauxtonAPI.addNotification({
+          msg: error,
+          type: 'error',
+          escape: false,
+          clear: true
+        });
+        return false;
+      }
+    }
+
+    //check that source and target are not the same. They can trigger a false positive if they are ""
+    if ((remoteTarget === remoteSource && !isEmpty(remoteTarget))
+      || (localSource === localTarget && !isEmpty(localSource))) {
+        FauxtonAPI.addNotification({
+          msg: 'Cannot replicate a database to itself',
+          type: 'error',
+          escape: false,
+          clear: true
+        });
+
+        return false;
+    }
+
+    return true;
+  }
+
+  submit (username, password) {
+    const {
+      replicationTarget,
+      replicationSource,
+      replicationType,
+      replicationDocName,
+      remoteTarget,
+      remoteSource,
+      localTarget,
+      localSource
+    } = this.props;
+
+    let _rev;
+    if (replicationDocName) {
+      const doc = this.props.docs.find(doc => doc._id === replicationDocName);
+      if (doc) {
+        _rev = doc._rev;
+      }
+    }
+
+    this.props.replicate({
+      replicationTarget,
+      replicationSource,
+      replicationType,
+      replicationDocName,
+      username,
+      password,
+      localTarget: localTarget,
+      localSource: localSource,
+      remoteTarget,
+      remoteSource,
+      _rev
+    });
+  }
+
+  confirmButtonEnabled () {
+    const {
+      remoteSource,
+      localSourceKnown,
+      replicationSource,
+      replicationTarget,
+      localTargetKnown,
+      localTarget,
+      submittedNoChange,
+    } = this.props;
+
+    if (submittedNoChange) {
+      return false;
+    }
+
+    if (!replicationSource || !replicationTarget) {
+      return false;
+    }
+
+    if (replicationSource === Constants.REPLICATION_SOURCE.LOCAL && !localSourceKnown) {
+      return false;
+    }
+    if (replicationTarget === Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE && !localTargetKnown) {
+      return false;
+    }
+
+    if (replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE && !localTarget) {
+      return false;
+    }
+
+    if (replicationSource === Constants.REPLICATION_SOURCE.REMOTE && remoteSource === "") {
+      return false;
+    }
+
+    return true;
+  }
+
+  render () {
+   const {
+     replicationSource,
+      replicationTarget,
+      replicationType,
+      replicationDocName,
+      passwordModalVisible,
+      conflictModalVisible,
+      databases,
+      localSource,
+      remoteSource,
+      remoteTarget,
+      localTarget,
+      updateFormField,
+      clearReplicationForm
+    } = this.props;
+
+    return (
+      <div>
+        <ReplicationSource
+          replicationSource={replicationSource}
+          localSource={localSource}
+          databases={databases}
+          remoteSource={remoteSource}
+          onSourceSelect={updateFormField('replicationSource')}
+          onRemoteSourceChange={updateFormField('remoteSource')}
+          onLocalSourceChange={updateFormField('localSource')}
+        />
+        <hr className="replication__seperator" size="1"/>
+        <ReplicationTarget
+          replicationTarget={replicationTarget}
+          onTargetChange={updateFormField('replicationTarget')}
+          databases={databases}
+          localTarget={localTarget}
+          remoteTarget={remoteTarget}
+          onRemoteTargetChange={updateFormField('remoteTarget')}
+          onLocalTargetChange={updateFormField('localTarget')}
+        />
+        <hr className="replication__seperator" size="1"/>
+        <ReplicationOptions
+          replicationType={replicationType}
+          replicationDocName={replicationDocName}
+          onDocChange={updateFormField('replicationDocName')}
+          onTypeChange={updateFormField('replicationType')}
+        />
+        <ReplicationSubmit
+          disabled={!this.confirmButtonEnabled()}
+          onClick={this.runReplicationChecks}
+          onClear={clearReplicationForm}
+        />
+        <PasswordModal
+          visible={passwordModalVisible}
+          modalMessage={<p>{app.i18n.en_US['replication-password-modal-text']}</p>}
+          submitBtnLabel="Start Replication"
+          headerTitle={app.i18n.en_US['replication-password-modal-header']}
+          onSuccess={this.submit} />
+        <ConflictModal
+          visible={conflictModalVisible}
+          onClick={this.showPasswordModal}
+          onClose={this.props.hideConflictModal}
+          docId={replicationDocName}
+          />
+      </div>
+    );
+  }
+}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/replication/components/options.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/components/options.js b/app/addons/replication/components/options.js
new file mode 100644
index 0000000..acb2501
--- /dev/null
+++ b/app/addons/replication/components/options.js
@@ -0,0 +1,97 @@
+// 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 Constants from '../constants';
+import Components from '../../components/react-components.react';
+import ReactSelect from 'react-select';
+
+const { StyledSelect } = Components;
+
+const getReplicationTypeOptions = () => {
+  return [
+    { value: Constants.REPLICATION_TYPE.ONE_TIME, label: 'One time' },
+    { value: Constants.REPLICATION_TYPE.CONTINUOUS, label: 'Continuous' }
+  ].map(option => <option value={option.value} key={option.value}>{option.label}</option>);
+};
+
+const ReplicationType = ({value, onChange}) => {
+  return (
+    <div className="replication__section">
+      <div className="replication__input-label">
+        Replication Type:
+      </div>
+      <div className="replication__input-select">
+        <StyledSelect
+          selectContent={getReplicationTypeOptions()}
+          selectChange={(e) => onChange(e.target.value)}
+          selectId="replication-target"
+          selectValue={value} />
+      </div>
+    </div>
+  );
+};
+
+ReplicationType.propTypes = {
+  value: React.PropTypes.string.isRequired,
+  onChange: React.PropTypes.func.isRequired
+};
+
+const ReplicationDoc = ({value, onChange}) =>
+<div className="replication__section">
+  <div className="replication__input-label">
+    Replication Document:
+  </div>
+  <div className="replication__doc-name">
+    <span className="fonticon fonticon-cancel replication__doc-name-icon" title="Clear field"
+      onClick={(e) => onChange('')} />
+    <input
+      type="text"
+      className="replication__doc-name-input"
+      placeholder="Custom ID (optional)"
+      value={value}
+      onChange={(e) => onChange(e.target.value)}
+    />
+  </div>
+</div>;
+
+ReplicationDoc.propTypes = {
+  value: React.PropTypes.string.isRequired,
+  onChange: React.PropTypes.func.isRequired
+};
+
+export class ReplicationOptions extends React.Component {
+
+  render () {
+    const {replicationType, replicationDocName, onDocChange, onTypeChange} = this.props;
+
+    return (
+      <div>
+        <ReplicationType
+          onChange={onTypeChange}
+          value={replicationType}
+        />
+        <ReplicationDoc
+          onChange={onDocChange}
+          value={replicationDocName}
+        />
+      </div>
+    );
+  }
+
+}
+
+ReplicationOptions.propTypes = {
+  replicationDocName: React.PropTypes.string.isRequired,
+  replicationType: React.PropTypes.string.isRequired,
+  onDocChange: React.PropTypes.func.isRequired,
+  onTypeChange: React.PropTypes.func.isRequired
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/replication/components/remoteexample.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/components/remoteexample.js b/app/addons/replication/components/remoteexample.js
new file mode 100644
index 0000000..1992b32
--- /dev/null
+++ b/app/addons/replication/components/remoteexample.js
@@ -0,0 +1,50 @@
+// 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 {OverlayTrigger, Tooltip} from 'react-bootstrap';
+
+const tooltipExisting = (
+  <Tooltip id="tooltip">
+    <p>
+      If you know the credentials for the remote account, you can use that remote username and password.
+    </p>
+    <p>
+      If a remote database granted permissions to your local account, you can use the local-account username and password.
+    </p>
+    <p>
+      If the remote database granted permissions to "everybody," you do not need to enter a username and password.
+    </p>
+  </Tooltip>
+);
+
+const tooltipNew = (
+  <Tooltip id="tooltip">
+    Enter the username and password of the remote account.
+  </Tooltip>
+);
+
+const RemoteExample = ({newRemote}) => {
+  const newRemoteText = newRemote ? 'If a "new" database already exists, data will replicate into that existing database.' : null;
+  return (
+    <div
+      className="replication__remote-connection-url-text">
+      https://$USERNAME:$PASSWORD@$REMOTE_SERVER/$DATABASE
+      &nbsp;
+      <OverlayTrigger placement="right" overlay={newRemote ? tooltipNew : tooltipExisting}>
+        <i className="replication__remote_icon_help icon icon-question-sign"/>
+      </OverlayTrigger>
+      <p>{newRemoteText}</p>
+    </div>
+  );
+};
+
+export default RemoteExample;


Mime
View raw message