couchdb-commits mailing list archives

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


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

Branch: refs/heads/master
Commit: a50108b0ba59d3b24543c552485be64b8a009d68
Parents: b245c6d
Author: Ben Keen <ben.keen@gmail.com>
Authored: Fri Jun 3 11:58:29 2016 +0000
Committer: Garren Smith <garren.smith@gmail.com>
Committed: Wed Nov 16 13:32:29 2016 +0200

----------------------------------------------------------------------
 app/addons/auth/actions.js                      | 184 ++++---
 app/addons/auth/actiontypes.js                  |   6 +-
 app/addons/auth/assets/less/auth.less           |   6 +
 app/addons/auth/components.react.jsx            |  72 ++-
 .../tests/nightwatch/highlightsidebar.js        |   2 +-
 app/addons/replication/actions.js               |  90 +++
 app/addons/replication/actiontypes.js           |  21 +
 .../replication/assets/less/replication.less    | 243 +++------
 app/addons/replication/base.js                  |  14 +-
 app/addons/replication/components.react.jsx     | 546 +++++++++++++++++++
 app/addons/replication/constants.js             |  34 ++
 app/addons/replication/helpers.js               |  17 +
 app/addons/replication/resources.js             |  63 ---
 app/addons/replication/route.js                 |  50 +-
 app/addons/replication/stores.js                | 188 +++++++
 app/addons/replication/templates/form.html      |  75 ---
 app/addons/replication/templates/progress.html  |  22 -
 .../replication/tests/nightwatch/replication.js | 129 +++++
 app/addons/replication/tests/replicationSpec.js | 212 +++++++
 app/addons/replication/tests/storesSpec.js      |  59 ++
 app/addons/replication/views.js                 | 343 ------------
 package.json                                    |   1 +
 .../custom-commands/checkForDocumentCreated.js  |   1 +
 test/nightwatch_tests/custom-commands/helper.js |  10 +-
 24 files changed, 1606 insertions(+), 782 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/auth/actions.js
----------------------------------------------------------------------
diff --git a/app/addons/auth/actions.js b/app/addons/auth/actions.js
index 104f057..9375c7b 100644
--- a/app/addons/auth/actions.js
+++ b/app/addons/auth/actions.js
@@ -28,88 +28,130 @@ var errorHandler = function (xhr, type, msg) {
 };
 
 
-export default {
+function login (username, password, urlBack) {
+  var promise = FauxtonAPI.session.login(username, password);
+
+  promise.then(() => {
+    FauxtonAPI.addNotification({ msg: FauxtonAPI.session.messages.loggedIn });
+    if (urlBack) {
+      return FauxtonAPI.navigate(urlBack);
+    }
+    FauxtonAPI.navigate('/');
+  }, errorHandler);
+}
+
+function changePassword (password, passwordConfirm) {
+  var nodes = nodesStore.getNodes();
+  var promise = FauxtonAPI.session.changePassword(password, passwordConfirm, nodes[0].node);
+
+  promise.then(() => {
+    FauxtonAPI.addNotification({ msg: FauxtonAPI.session.messages.changePassword });
+    FauxtonAPI.dispatch({ type: ActionTypes.AUTH_CLEAR_CHANGE_PWD_FIELDS });
+  }, errorHandler);
+}
+
+function updateChangePasswordField (value) {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.AUTH_UPDATE_CHANGE_PWD_FIELD,
+    value: value
+  });
+}
+
+function updateChangePasswordConfirmField (value) {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.AUTH_UPDATE_CHANGE_PWD_CONFIRM_FIELD,
+    value: value
+  });
+}
 
-  login: function (username, password, urlBack) {
-    var promise = FauxtonAPI.session.login(username, password);
+function createAdmin (username, password, loginAfter) {
+  var nodes = nodesStore.getNodes();
+  var promise = FauxtonAPI.session.createAdmin(username, password, loginAfter, nodes[0].node);
 
-    promise.then(function () {
-      FauxtonAPI.addNotification({ msg: FauxtonAPI.session.messages.loggedIn });
-      if (urlBack) {
-        return FauxtonAPI.navigate(urlBack);
-      }
+  promise.then(() => {
+    FauxtonAPI.addNotification({ msg: FauxtonAPI.session.messages.adminCreated });
+    if (loginAfter) {
       FauxtonAPI.navigate('/');
+    } else {
+      FauxtonAPI.dispatch({ type: ActionTypes.AUTH_CLEAR_CREATE_ADMIN_FIELDS });
+    }
+  }, (xhr, type, msg) => {
+    msg = xhr;
+    if (arguments.length === 3) {
+      msg = xhr.responseJSON.reason;
+    }
+    errorHandler(FauxtonAPI.session.messages.adminCreationFailedPrefix + ' ' + msg);
+  });
+}
+
+// simple authentication method - does nothing other than check creds
+function authenticate (username, password, onSuccess) {
+  $.ajax({
+    cache: false,
+    type: 'POST',
+    url: '/_session',
+    dataType: 'json',
+    data: { name: username, password: password }
+  }).then(() => {
+    FauxtonAPI.dispatch({
+      type: ActionTypes.AUTH_CREDS_VALID,
+      options: { username: username, password: password }
     });
-    promise.fail(errorHandler);
-  },
-
-  changePassword: function (password, passwordConfirm) {
-    var nodes = nodesStore.getNodes();
-    var promise = FauxtonAPI.session.changePassword(password, passwordConfirm, nodes[0].node);
-
-    promise.done(function () {
-      FauxtonAPI.addNotification({ msg: FauxtonAPI.session.messages.changePassword });
-      FauxtonAPI.dispatch({ type: ActionTypes.AUTH_CLEAR_CHANGE_PWD_FIELDS });
+    hidePasswordModal();
+    onSuccess(username, password);
+  }, () => {
+    FauxtonAPI.addNotification({
+      msg: 'Your password is incorrect.',
+      type: 'error',
+      clear: true
     });
-
-    promise.fail(errorHandler);
-  },
-
-  updateChangePasswordField: function (value) {
     FauxtonAPI.dispatch({
-      type: ActionTypes.AUTH_UPDATE_CHANGE_PWD_FIELD,
-      value: value
+      type: ActionTypes.AUTH_CREDS_INVALID,
+      options: { username: username, password: password }
     });
-  },
+  });
+}
 
-  updateChangePasswordConfirmField: function (value) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.AUTH_UPDATE_CHANGE_PWD_CONFIRM_FIELD,
-      value: value
-    });
-  },
-
-  createAdmin: function (username, password, loginAfter) {
-    var nodes = nodesStore.getNodes();
-    var promise = FauxtonAPI.session.createAdmin(username, password, loginAfter, nodes[0].node);
-
-    promise.then(function () {
-      FauxtonAPI.addNotification({ msg: FauxtonAPI.session.messages.adminCreated });
-      if (loginAfter) {
-        FauxtonAPI.navigate('/');
-      } else {
-        FauxtonAPI.dispatch({ type: ActionTypes.AUTH_CLEAR_CREATE_ADMIN_FIELDS });
-      }
-    });
+function updateCreateAdminUsername (value) {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.AUTH_UPDATE_CREATE_ADMIN_USERNAME_FIELD,
+    value: value
+  });
+}
 
-    promise.fail(function (xhr, type, msg) {
-      msg = xhr;
-      if (arguments.length === 3) {
-        msg = xhr.responseJSON.reason;
-      }
-      errorHandler(FauxtonAPI.session.messages.adminCreationFailedPrefix + ' ' + msg);
-    });
-  },
+function updateCreateAdminPassword (value) {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.AUTH_UPDATE_CREATE_ADMIN_PWD_FIELD,
+    value: value
+  });
+}
 
-  updateCreateAdminUsername: function (value) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.AUTH_UPDATE_CREATE_ADMIN_USERNAME_FIELD,
-      value: value
-    });
-  },
+function selectPage (page) {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.AUTH_SELECT_PAGE,
+    page: page
+  });
+}
 
-  updateCreateAdminPassword: function (value) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.AUTH_UPDATE_CREATE_ADMIN_PWD_FIELD,
-      value: value
-    });
-  },
+function showPasswordModal () {
+  FauxtonAPI.dispatch({ type: ActionTypes.AUTH_SHOW_PASSWORD_MODAL });
+}
+
+function hidePasswordModal () {
+  FauxtonAPI.dispatch({ type: ActionTypes.AUTH_HIDE_PASSWORD_MODAL });
+}
 
-  selectPage: function (page) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.AUTH_SELECT_PAGE,
-      page: page
-    });
-  }
 
+export default {
+  login,
+  changePassword,
+  updateChangePasswordField,
+  updateChangePasswordConfirmField,
+  createAdmin,
+  authenticate,
+  updateCreateAdminUsername,
+  updateCreateAdminPassword,
+  selectPage,
+  showPasswordModal,
+  hidePasswordModal
 };

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/auth/actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/auth/actiontypes.js b/app/addons/auth/actiontypes.js
index af3d02a..937113a 100644
--- a/app/addons/auth/actiontypes.js
+++ b/app/addons/auth/actiontypes.js
@@ -17,5 +17,9 @@ export default {
   AUTH_CLEAR_CREATE_ADMIN_FIELDS: 'AUTH_CLEAR_CREATE_ADMIN_FIELDS',
   AUTH_UPDATE_CREATE_ADMIN_USERNAME_FIELD: 'AUTH_UPDATE_CREATE_ADMIN_USERNAME_FIELD',
   AUTH_UPDATE_CREATE_ADMIN_PWD_FIELD: 'AUTH_UPDATE_CREATE_ADMIN_PWD_FIELD',
-  AUTH_SELECT_PAGE: 'AUTH_SELECT_PAGE'
+  AUTH_SELECT_PAGE: 'AUTH_SELECT_PAGE',
+  AUTH_CREDS_VALID: 'AUTH_CREDS_VALID',
+  AUTH_CREDS_INVALID: 'AUTH_CREDS_INVALID',
+  AUTH_SHOW_PASSWORD_MODAL: 'AUTH_SHOW_PASSWORD_MODAL',
+  AUTH_HIDE_PASSWORD_MODAL: 'AUTH_HIDE_PASSWORD_MODAL'
 };

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

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/auth/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/auth/components.react.jsx b/app/addons/auth/components.react.jsx
index fda85de..ddbfd8c 100644
--- a/app/addons/auth/components.react.jsx
+++ b/app/addons/auth/components.react.jsx
@@ -16,6 +16,7 @@ import React from "react";
 import ReactDOM from "react-dom";
 import AuthStores from "./stores";
 import AuthActions from "./actions";
+import { Modal } from 'react-bootstrap';
 
 var changePasswordStore = AuthStores.changePasswordStore;
 var createAdminStore = AuthStores.createAdminStore;
@@ -302,9 +303,72 @@ var CreateAdminSidebar = React.createClass({
   }
 });
 
+
+class PasswordModal extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = {
+      password: ''
+    };
+    this.authenticate = this.authenticate.bind(this);
+    this.onKeyPress = this.onKeyPress.bind(this);
+  }
+
+  // clicking <Enter> should submit the form
+  onKeyPress (e) {
+    if (e.key === 'Enter') {
+      this.authenticate();
+    }
+  }
+
+  // default authentication function. This can be overridden via props if you want to do something different
+  authenticate () {
+    const username = app.session.get('userCtx').name; // yuck. But simplest for now until logging in publishes the user data
+    this.props.onSubmit(username, this.state.password, this.props.onSuccess);
+  }
+
+  render () {
+    return (
+      <Modal dialogClassName="enter-password-modal" show={this.props.visible} onHide={() => this.props.onClose()}>
+        <Modal.Header closeButton={true}>
+          <Modal.Title>Enter Password</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} />
+        </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>
+        </Modal.Footer>
+      </Modal>
+    );
+  }
+}
+PasswordModal.propTypes = {
+  visible: React.PropTypes.bool.isRequired,
+  modalMessage: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element]),
+  onSubmit: React.PropTypes.func.isRequired,
+  onClose: React.PropTypes.func.isRequired,
+  submitBtnLabel: React.PropTypes.string
+};
+PasswordModal.defaultProps = {
+  visible: false,
+  modalMessage: '',
+  onClose: AuthActions.hidePasswordModal,
+  onSubmit: AuthActions.authenticate,
+  onSuccess: () => {},
+  submitBtnLabel: 'Continue'
+};
+
+
 export default {
-  LoginForm: LoginForm,
-  ChangePasswordForm: ChangePasswordForm,
-  CreateAdminForm: CreateAdminForm,
-  CreateAdminSidebar: CreateAdminSidebar
+  LoginForm,
+  ChangePasswordForm,
+  CreateAdminForm,
+  CreateAdminSidebar,
+  PasswordModal
 };

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/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 770837d..015253a 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('#replication', waitTime, false)
+      .waitForElementVisible('#replicate', waitTime, false)
       .assert.cssClassPresent('li[data-nav-name="Replication"]', 'active')
     .end();
   }

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/replication/actions.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/actions.js b/app/addons/replication/actions.js
new file mode 100644
index 0000000..72e1909
--- /dev/null
+++ b/app/addons/replication/actions.js
@@ -0,0 +1,90 @@
+// 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 ActionTypes from './actiontypes';
+import Helpers from './helpers';
+
+
+function initReplicator (sourceDatabase) {
+  if (sourceDatabase) {
+    FauxtonAPI.dispatch({
+      type: ActionTypes.INIT_REPLICATION,
+      options: {
+        sourceDatabase: sourceDatabase
+      }
+    });
+  }
+  $.ajax({
+    url: app.host + '/_all_dbs',
+    contentType: 'application/json',
+    dataType: 'json'
+  }).then((databases) => {
+    FauxtonAPI.dispatch({
+      type: ActionTypes.REPLICATION_DATABASES_LOADED,
+      options: {
+        databases: databases
+      }
+    });
+  });
+}
+
+function replicate (params) {
+  const promise = $.ajax({
+    url: window.location.origin + '/_replicator',
+    contentType: 'application/json',
+    type: 'POST',
+    dataType: 'json',
+    data: JSON.stringify(params)
+  });
+
+  const source = Helpers.getDatabaseLabel(params.source);
+  const target = Helpers.getDatabaseLabel(params.target);
+
+  promise.then(() => {
+    FauxtonAPI.addNotification({
+      msg: 'Replication from <code>' + source + '</code> to <code>' + target + '</code> has begun.',
+      type: 'success',
+      escape: false,
+      clear: true
+    });
+  }, (xhr) => {
+    const errorMessage = JSON.parse(xhr.responseText);
+    FauxtonAPI.addNotification({
+      msg: errorMessage.reason,
+      type: 'error',
+      clear: true
+    });
+  });
+}
+
+function updateFormField (fieldName, value) {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.REPLICATION_UPDATE_FORM_FIELD,
+    options: {
+      fieldName: fieldName,
+      value: value
+    }
+  });
+}
+
+function clearReplicationForm () {
+  FauxtonAPI.dispatch({ type: ActionTypes.REPLICATION_CLEAR_FORM });
+}
+
+
+export default {
+  initReplicator,
+  replicate,
+  updateFormField,
+  clearReplicationForm
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/replication/actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/actiontypes.js b/app/addons/replication/actiontypes.js
new file mode 100644
index 0000000..87e689e
--- /dev/null
+++ b/app/addons/replication/actiontypes.js
@@ -0,0 +1,21 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+define([], function () {
+  return {
+    INIT_REPLICATION: 'INIT_REPLICATION',
+    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'
+  };
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/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 d917885..4e97de0 100644
--- a/app/addons/replication/assets/less/replication.less
+++ b/app/addons/replication/assets/less/replication.less
@@ -11,187 +11,102 @@
 // the License.
 
 @import "../../../../../assets/less/variables.less";
+@import "../../../../../assets/less/mixins.less";
 
-#replication {
-  position: relative;
-  max-width: none;
-  width: auto;
+.replication-page {
+  font-size: 14px;
 
-  .form_set {
-    width: 350px;
-    display: inline-block;
-    border: 1px solid @greyBrownLighter;
-    padding: 15px 10px 0;
-    margin-bottom: 20px;
-    &.middle {
-      width: 100px;
-      border: none;
-      position: relative;
-      height: 100px;
-      margin: 0;
-    }
-    input, select {
-      margin: 0 0 16px 5px;
-      height: 40px;
-      width: 318px;
-    }
-    .btn-group {
-      margin: 0 0 16px 5px;
-      .btn {
-        padding: 10px 57px;
-      }
-    }
-    &.local {
-      .local_option {
-        display: block;
-      }
-      .remote_option {
-        display: none;
-      }
-      .local-btn {
-        background-color: @brandPrimary;
-        color: #fff;
-      }
-      .remote-btn {
-        background-color: #f5f5f5;
-        color: @fontGrey;
-      }
-    }
-    .local_option {
-      display: none;
-    }
-    .remote-btn {
-      background-color: @brandPrimary;
-      color: #fff;
-    }
+  input, select {
+    font-size: 14px;
+  }
+  input {
+    width: 246px;
+  }
+  select {
+    width: 246px;
+    margin-bottom: 10px;
+    background-color: white;
+    border: 1px solid #cccccc;
+  }
+  .styled-select {
+    width: 250px;
   }
 
-
-  .options {
-    position: relative;
-    &:after {
-      content: '';
-      display: block;
-      position: absolute;
-      right: -16px;
-      top: 9px;
-      width: 0;
-      height: 0;
-      border-left: 5px solid transparent;
-      border-right: 5px solid transparent;
-      border-bottom: 5px solid black;
-      border-top: none;
-    }
-    &.off {
-      &:after {
-      content: '';
-      display: block;
-      position: absolute;
-      right: -16px;
-      top: 9px;
-      width: 0;
-      height: 0;
-      border-left: 5px solid transparent;
-      border-right: 5px solid transparent;
-      border-bottom: none;
-      border-top: 5px solid black;
-      }
-    }
+  .span3 {
+    text-align: right;
+    margin-top: 12px;
   }
-  .control-group {
-    label {
-      float: left;
-      min-height: 30px;
-      vertical-align: top;
-      padding-right: 5px;
-      min-width: 130px;
-      padding-left: 0px;
-    }
-    input[type=radio],
-    input[type=checkbox] {
-      margin: 0 0 2px 0;
+  .remote-connection-details {
+    margin: 15px 0;
+  }
+  .connection-url {
+    width: 100%;
+  }
+  .buttons-row {
+    margin-top: 10px;
+    a {
+      padding: 12px;
     }
   }
+  .typeahead {
+    width: 100%;
+  }
 
-  .circle {
-    z-index: 0;
-    position: absolute;
-    top: 20px;
-    left: 15px;
-
-    &:after {
-      width: 70px;
-      height: 70px;
-      content: '';
-      display: block;
-      position: relative;
-      margin: 0 auto;
-      border: 1px solid @greyBrownLighter;
-      -webkit-border-radius: 40px;
-      -moz-border-radius: 40px;
-      border-radius:40px;
-    }
+  hr {
+    margin: 6px 0 15px;
+  }
+  .section-header {
+    font-weight: bold;
+    font-size: 14pt;
   }
-  .swap {
-    text-decoration: none;
-    z-index: 30;
+}
+
+#dashboard-content .replication-page {
+  padding-top: 25px;
+}
+
+.connection-url-example {
+  font-size: 9pt;
+  color: #999999;
+  margin-bottom: 8px;
+}
+
+.custom-id-field {
+  position: relative;
+  width: 250px;
+
+  span.fonticon {
     cursor: pointer;
     position: absolute;
-    font-size: 40px;
-    width: 27px;
-    height: 12px;
-    top: 31px;
-    left: 30px;
+    right: 6px;
+    top: 8px;
+    font-size: 11px;
+    padding: 8px;
+    color: #999999;
+    .transition(all 0.25s linear);
     &:hover {
-      color: @greyBrownLighter;
+      color: #333333;
+    }
+    input {
+      padding-right: 32px;
     }
   }
 }
 
-#replicationStatus {
-  &.showHeader {
-    li.header {
-      display: block;
-      border: none;
-    }
-    ul {
-      border:1px solid @greyBrownLighter;
-    }
+
+body .Select div.Select-control {
+  padding: 6px;
+  border: 1px solid #cccccc;
+  width: 246px;
+  .Select-value, .Select-placeholder {
+    padding: 6px 10px;
   }
-  li.header {
-    display: none;
+  input {
+    margin-left: -6px;
   }
-  ul {
-    margin: 0;
-    li {
-      .progress,
-      p {
-        margin: 0px;
-        vertical-align: bottom;
-        &.break {
-          -ms-word-break: break-all;
-          word-break: break-all;
-
-          /* Non standard for webkit */
-          word-break: break-word;
-          -webkit-hyphens: auto;
-          -moz-hyphens: auto;
-          hyphens: auto;
-        }
-      }
-      padding: 10px 10px;
-      margin: 0;
-      list-style: none;
-      border-top: 1px solid @greyBrownLighter;
-      div.bar {
-        font-size: 16px;
-        line-height: 30px;
-      }
-    }
+  .Select-arrow-zone {
+    padding: 0;
+    width: 18px;
+    color: black;
   }
 }
-
-.task-cancel-button {
-    padding: 4px 12px;
-    margin-bottom: 3px;
-}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/replication/base.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/base.js b/app/addons/replication/base.js
index 352d6b0..cdb5b6b 100644
--- a/app/addons/replication/base.js
+++ b/app/addons/replication/base.js
@@ -10,17 +10,21 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import app from "../../app";
-import FauxtonAPI from "../../core/api";
-import replication from "./route";
-import "./assets/less/replication.less";
+import app from '../../app';
+import FauxtonAPI from '../../core/api';
+import replication from './route';
+import './assets/less/replication.less';
+
 replication.initialize = function () {
   FauxtonAPI.addHeaderLink({ title: 'Replication', href: '#/replication', icon: 'fonticon-replicate' });
 };
 
 FauxtonAPI.registerUrls('replication', {
-  app: function (db) {
+  app: (db) => {
     return '#/replication/' + db;
+  },
+  api: () => {
+    return window.location.origin + '/_replicator';
   }
 });
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/replication/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/replication/components.react.jsx b/app/addons/replication/components.react.jsx
new file mode 100644
index 0000000..2d4542e
--- /dev/null
+++ b/app/addons/replication/components.react.jsx
@@ -0,0 +1,546 @@
+// 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/a50108b0/app/addons/replication/constants.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/constants.js b/app/addons/replication/constants.js
new file mode 100644
index 0000000..eb5459f
--- /dev/null
+++ b/app/addons/replication/constants.js
@@ -0,0 +1,34 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+define([], function () {
+
+  return {
+    REPLICATION_SOURCE: {
+      LOCAL: 'REPLICATION_SOURCE_LOCAL',
+      REMOTE: 'REPLICATION_SOURCE_REMOTE'
+    },
+
+    REPLICATION_TARGET: {
+      EXISTING_LOCAL_DATABASE: 'REPLICATION_TARGET_EXISTING_LOCAL_DATABASE',
+      EXISTING_REMOTE_DATABASE: 'REPLICATION_TARGET_EXISTING_REMOTE_DATABASE',
+      NEW_LOCAL_DATABASE: 'REPLICATION_TARGET_NEW_LOCAL_DATABASE',
+      NEW_REMOTE_DATABASE: 'REPLICATION_TARGET_NEW_REMOTE_DATABASE'
+    },
+
+    REPLICATION_TYPE: {
+      ONE_TIME: 'REPLICATION_TYPE_ONE_TIME',
+      CONTINUOUS: 'REPLICATION_TYPE_CONTINUOUS'
+    }
+  };
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/replication/helpers.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/helpers.js b/app/addons/replication/helpers.js
new file mode 100644
index 0000000..fd82b45
--- /dev/null
+++ b/app/addons/replication/helpers.js
@@ -0,0 +1,17 @@
+
+function getDatabaseLabel (db) {
+  let dbString = (_.isString(db)) ? db.trim().replace(/\/$/, '') : db.url;
+  const matches = dbString.match(/[^\/]+$/, '');
+  return matches[0];
+}
+
+function getReactSelectOptions (list) {
+  return _.map(list, (item) => {
+    return { value: item, label: item };
+  });
+}
+
+export default {
+  getDatabaseLabel,
+  getReactSelectOptions
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/replication/resources.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/resources.js b/app/addons/replication/resources.js
deleted file mode 100644
index 7402435..0000000
--- a/app/addons/replication/resources.js
+++ /dev/null
@@ -1,63 +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";
-var Replication = {};
-
-// these are probably dupes from the database modules. I'm going to keep them separate for now
-Replication.DBModel = Backbone.Model.extend({
-  label: function () {
-    // for autocomplete
-    return this.get('name');
-  }
-});
-
-Replication.DBList = Backbone.Collection.extend({
-  model: Replication.DBModel,
-  url: function () {
-    return app.host + '/_all_dbs';
-  },
-  parse: function (resp) {
-    // TODO: pagination!
-    return _.map(resp, function (database) {
-      return {
-        id: database,
-        name: database
-      };
-    });
-  }
-});
-
-Replication.Task = Backbone.Model.extend({});
-
-Replication.Tasks = Backbone.Collection.extend({
-  model: Replication.Task,
-  url: function () {
-    return app.host + '/_active_tasks';
-  },
-  parse: function (resp) {
-    //only want replication tasks to return
-    return _.filter(resp, function (task) {
-      return task.type === 'replication';
-    });
-  }
-});
-
-Replication.Replicate = Backbone.Model.extend({
-  documentation: FauxtonAPI.constants.DOC_URLS.REPLICATION,
-  url: function () {
-    return window.location.origin + '/_replicate';
-  }
-});
-
-export default Replication;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/replication/route.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/route.js b/app/addons/replication/route.js
index 858ad6f..6947045 100644
--- a/app/addons/replication/route.js
+++ b/app/addons/replication/route.js
@@ -10,48 +10,34 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import app from "../../app";
-import FauxtonAPI from "../../core/api";
-import Replication from "./resources";
-import Views from "./views";
-var RepRouteObject = FauxtonAPI.RouteObject.extend({
+import app from '../../app';
+import FauxtonAPI from '../../core/api';
+import Actions from './actions';
+import Components from './components.react';
+
+
+var ReplicationRouteObject = FauxtonAPI.RouteObject.extend({
   layout: 'one_pane',
   routes: {
-    "replication": 'defaultView',
-    "replication/:dbname": 'defaultView'
+    'replication': 'defaultView',
+    'replication/:dbname': 'defaultView'
   },
   selectedHeader: 'Replication',
   apiUrl: function () {
-    return [this.replication.url(), this.replication.documentation];
+    return [FauxtonAPI.urls('replication', 'api'), FauxtonAPI.constants.DOC_URLS.REPLICATION];
   },
   crumbs: [
-    { "name": 'Replicate changes from: ' }
+    { name: 'Replication', link: 'replication' }
   ],
-  defaultView: function (dbname) {
-    var isAdmin = FauxtonAPI.session.isAdmin();
-
-    this.tasks = [];
-    this.databases = new Replication.DBList({});
-    this.replication = new Replication.Replicate({});
-
-    if (isAdmin) {
-      this.tasks = new Replication.Tasks({ id: 'ReplicationTasks' });
-      this.setView('#dashboard-content', new Views.ReplicationFormForAdmins({
-        selectedDB: dbname || '',
-        collection: this.databases,
-        status: this.tasks
-      }));
-      return;
-    }
-    this.setView('#dashboard-content', new Views.ReplicationForm({
-      selectedDB: dbname || '',
-      collection: this.databases,
-      status: this.tasks
-    }));
+  roles: ['fx_loggedIn'],
+  defaultView: function (databaseName) {
+    const sourceDatabase = databaseName || '';
+    Actions.initReplicator(sourceDatabase);
+    this.setComponent('#dashboard-content', Components.ReplicationController);
   }
 });
 
-
-Replication.RouteObjects = [RepRouteObject];
+var Replication = {};
+Replication.RouteObjects = [ReplicationRouteObject];
 
 export default Replication;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/replication/stores.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/stores.js b/app/addons/replication/stores.js
new file mode 100644
index 0000000..2da7c61
--- /dev/null
+++ b/app/addons/replication/stores.js
@@ -0,0 +1,188 @@
+// 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 ActionTypes from './actiontypes';
+import Constants from './constants';
+import AccountActionTypes from '../auth/actiontypes';
+
+
+const ReplicationStore = FauxtonAPI.Store.extend({
+  initialize: function () {
+    this.reset();
+  },
+
+  reset: function () {
+    this._loading = false;
+    this._databases = [];
+    this._authenticated = false;
+    this._password = '';
+
+    // source fields
+    this._replicationSource = '';
+    this._sourceDatabase = '';
+    this._remoteSource = '';
+
+    // target fields
+    this._replicationTarget = '';
+    this._targetDatabase = '';
+    this._remoteTarget = '';
+
+    // other
+    this._isPasswordModalVisible = false;
+    this._replicationType = Constants.REPLICATION_TYPE.ONE_TIME;
+    this._replicationDocName = '';
+  },
+
+  isLoading: function () {
+    return this._loading;
+  },
+
+  isAuthenticated: function () {
+    return this._authenticated;
+  },
+
+  getReplicationSource: function () {
+    return this._replicationSource;
+  },
+
+  getSourceDatabase: function () {
+    return this._sourceDatabase;
+  },
+
+  isLocalSourceDatabaseKnown: function () {
+    return _.contains(this._databases, this._sourceDatabase);
+  },
+
+  isLocalTargetDatabaseKnown: function () {
+    return _.contains(this._databases, this._targetDatabase);
+  },
+
+  getReplicationTarget: function () {
+    return this._replicationTarget;
+  },
+
+  getDatabases: function () {
+    return this._databases;
+  },
+
+  setDatabases: function (databases) {
+    this._databases = databases;
+  },
+
+  getReplicationType: function () {
+    return this._replicationType;
+  },
+
+  getTargetDatabase: function () {
+    return this._targetDatabase;
+  },
+
+  getReplicationDocName: function () {
+    return this._replicationDocName;
+  },
+
+  // to cut down on boilerplate
+  updateFormField: function (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...
+    var validFieldMap = {
+      remoteSource: '_remoteSource',
+      remoteTarget: '_remoteTarget',
+      targetDatabase: '_targetDatabase',
+      replicationType: '_replicationType',
+      replicationDocName: '_replicationDocName',
+      replicationSource: '_replicationSource',
+      replicationTarget: '_replicationTarget',
+      sourceDatabase: '_sourceDatabase'
+    };
+
+    this[validFieldMap[fieldName]] = value;
+  },
+
+  getRemoteSource: function () {
+    return this._remoteSource;
+  },
+
+  getRemoteTarget: function () {
+    return this._remoteTarget;
+  },
+
+  isPasswordModalVisible: function () {
+    return this._isPasswordModalVisible;
+  },
+
+  getPassword: function () {
+    return this._password;
+  },
+
+  dispatch: function (action) {
+    switch (action.type) {
+
+      case ActionTypes.INIT_REPLICATION:
+        this._loading = true;
+        this._sourceDatabase = action.options.sourceDatabase;
+
+        if (this._sourceDatabase) {
+          this._replicationSource = Constants.REPLICATION_SOURCE.LOCAL;
+          this._remoteSource = '';
+          this._replicationTarget = '';
+          this._targetDatabase = '';
+          this._remoteTarget = '';
+        }
+      break;
+
+      case ActionTypes.REPLICATION_DATABASES_LOADED:
+        this.setDatabases(action.options.databases);
+        this._loading = false;
+      break;
+
+      case ActionTypes.REPLICATION_UPDATE_FORM_FIELD:
+        this.updateFormField(action.options.fieldName, action.options.value);
+      break;
+
+      case ActionTypes.REPLICATION_CLEAR_FORM:
+        this.reset();
+      break;
+
+      case AccountActionTypes.AUTH_SHOW_PASSWORD_MODAL:
+        this._isPasswordModalVisible = true;
+      break;
+
+      case AccountActionTypes.AUTH_HIDE_PASSWORD_MODAL:
+        this._isPasswordModalVisible = false;
+      break;
+
+      case AccountActionTypes.AUTH_CREDS_VALID:
+        this._authenticated = true;
+        this._password = action.options.password;
+      break;
+
+      case AccountActionTypes.AUTH_CREDS_INVALID:
+        this._authenticated = false;
+      break;
+
+      default:
+      return;
+    }
+
+    this.triggerChange();
+  }
+});
+
+const replicationStore = new ReplicationStore();
+replicationStore.dispatchToken = FauxtonAPI.dispatcher.register(replicationStore.dispatch);
+
+export default {
+  replicationStore
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/replication/templates/form.html
----------------------------------------------------------------------
diff --git a/app/addons/replication/templates/form.html b/app/addons/replication/templates/form.html
deleted file mode 100644
index b5bc63d..0000000
--- a/app/addons/replication/templates/form.html
+++ /dev/null
@@ -1,75 +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.
-*/ %>
-
-<form id="replication" class="form-horizontal">
-  <div class="from form_set local">
-    <div class="btn-group">
-      <button class="btn local-btn" type="button" value="local">Local</button>
-      <button class="btn remote-btn" type="button" value="remote">Remote</button>
-    </div>
-
-    <div class="from_local local_option">
-      <select id="from_name" name="source">
-        <% _.each( databases, function ( db, i ) { %>
-           <option value="<%- db.name %>" <% if (selectedDB == db.name) {%>selected<%}%> ><%- db.name %></option>
-        <% }); %>
-      </select>
-    </div>
-    <div class="from_to_remote remote_option">
-      <input type="text" id="from_url" name="source" size="30" value="http://">
-    </div>
-  </div>
-
-  <div class="form_set middle">
-    <span class="circle"></span>
-    <a href="#" title="Switch Target and Source" class="swap">
-      <span class="fonticon-swap-arrows"></span>
-    </a>
-  </div>
-
-  <div class="to form_set local">
-    <div class="btn-group">
-      <button class="btn local-btn" type="button" value="local">Local</button>
-      <button class="btn remote-btn" type="button" value="remote">Remote</button>
-    </div>
-    <div class="to_local local_option">
-      <input type="text" id="to_name" name="target" size="30" placeholder="database name">
-    </div>
-
-    <div class="to_remote remote_option">
-      <input type="text" id="to_url" name="target" size="30" value="http://">
-    </div>
-  </div>
-
-	<div class="actions">
-		<div class="control-group">
-			<label for="continuous">
-				<input type="checkbox" name="continuous" value="true" id="continuous">
-				Continuous
-			</label>
-
-			<label for="createTarget">
-				<input type="checkbox" name="create_target" value="true" id="createTarget">
-				Create Target <a class="help-link" data-bypass="true" href="<%-getDocUrl('REPLICATION')%>" target="_blank"><i class="icon-question-sign" rel="tooltip" title="Create the target database"></i></a>
-			</label>
-		</div>
-
-		<button class="btn btn-success save" type="submit">
-          <i class="icon fonticon-ok-circled"></i>
-          Replicate
-        </button>
-	</div>
-</form>
-
-<div id="replicationStatus"></div>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/replication/templates/progress.html
----------------------------------------------------------------------
diff --git a/app/addons/replication/templates/progress.html b/app/addons/replication/templates/progress.html
deleted file mode 100644
index 20ba471..0000000
--- a/app/addons/replication/templates/progress.html
+++ /dev/null
@@ -1,22 +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.
-*/ %>
-<p class="span6 break">Replicating <strong><%-source%></strong> to <strong><%-target%></strong></p>
-
-<div class="span4 progress progress-striped active">
-  <div class="bar" style="width: <%=progress || 0%>%;"><%=progress || "0"%>%</div>
-</div>
-
-<span class="span1">
-	<button class="cancel btn btn-danger btn-large delete task-cancel-button" data-source="<%-source%>"  data-rep-id="<%-repid%>" data-continuous="<%-continuous%>" data-target="<%-target%>">Cancel</button>
-</span>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/replication/tests/nightwatch/replication.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/tests/nightwatch/replication.js b/app/addons/replication/tests/nightwatch/replication.js
new file mode 100644
index 0000000..d70356a
--- /dev/null
+++ b/app/addons/replication/tests/nightwatch/replication.js
@@ -0,0 +1,129 @@
+// 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.
+
+
+
+const helpers = require('../../../../../test/nightwatch_tests/helpers/helpers.js');
+const newDatabaseName1 = 'fauxton-selenium-tests-replication1';
+const newDatabaseName2 = 'fauxton-selenium-tests-replication2';
+const replicatedDBName = 'replicated-db';
+const docName1 = 'doc-name1';
+const docName2 = 'doc-name2';
+const pwd = 'testerpass';
+const longWaitTime = 120000;
+
+const destroyDBs = (client, done) => {
+  var nano = helpers.getNanoInstance(client.globals.test_settings.db_url);
+  nano.db.destroy(newDatabaseName1, () => {
+    nano.db.destroy(newDatabaseName2, () => {
+      nano.db.destroy(replicatedDBName, () => {
+        done();
+      });
+    });
+  });
+};
+
+module.exports = {
+  before: destroyDBs, // just in case the test failed on prev execution
+  after: destroyDBs,
+
+  'Replicates existing local db to new local db' : function (client) {
+    var waitTime = client.globals.maxWaitTime,
+        baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .createDatabase(newDatabaseName1)
+      .checkForDatabaseCreated(newDatabaseName1, longWaitTime)
+      .createDocument(docName1, newDatabaseName1)
+      .loginToGUI()
+      .url(baseUrl + '/#replication')
+      .waitForElementPresent('button#replicate', waitTime, true)
+      .waitForElementPresent('#replication-source', waitTime, true)
+
+      // select LOCAL as the source
+      .click('#replication-source')
+      .click('#replication-source option[value="REPLICATION_SOURCE_LOCAL"]')
+      .waitForElementPresent('.replication-source-name-row', waitTime, true)
+
+      // enter our source DB
+      .setValue('.replication-source-name-row .Select-input input', [newDatabaseName1])
+      .keys(['\uE015', '\uE015', '\uE006'])
+
+      // enter a new target name
+      .click('#replication-target')
+      .click('option[value="REPLICATION_TARGET_NEW_LOCAL_DATABASE"]')
+      .setValue('.new-local-db', replicatedDBName)
+
+      .click('#replicate')
+
+      .waitForElementPresent('.enter-password-modal', waitTime, true)
+      .setValue('.enter-password-modal input[type="password"]', pwd)
+      .click('.enter-password-modal button.save')
+      .waitForElementNotPresent('.enter-password-modal', waitTime, true)
+
+      // now check the database was created
+      .checkForDatabaseCreated(replicatedDBName, longWaitTime)
+
+      // lastly, check the doc was replicated as well
+      .checkForDocumentCreated(docName1, longWaitTime, replicatedDBName)
+      .end();
+  },
+
+
+  'Replicates existing local db to existing local db' : function (client) {
+    var waitTime = client.globals.maxWaitTime,
+      baseUrl = client.globals.test_settings.launch_url;
+
+    client
+
+      // create two databases, each with a single (different) doc
+      .createDatabase(newDatabaseName1)
+      .checkForDatabaseCreated(newDatabaseName1, longWaitTime)
+      .createDocument(docName1, newDatabaseName1)
+      .createDatabase(newDatabaseName2)
+      .checkForDatabaseCreated(newDatabaseName2, longWaitTime)
+      .createDocument(docName2, newDatabaseName2)
+
+      // now login and fill in the replication form
+      .loginToGUI()
+      .url(baseUrl + '/#replication')
+      .waitForElementPresent('button#replicate', waitTime, true)
+      .waitForElementPresent('#replication-source', waitTime, true)
+
+      // select the LOCAL db as the source
+      .click('#replication-source')
+      .click('#replication-source option[value="REPLICATION_SOURCE_LOCAL"]')
+      .waitForElementPresent('.replication-source-name-row', waitTime, true)
+      .setValue('.replication-source-name-row .Select-input input', [newDatabaseName1])
+      .keys(['\uE015', '\uE015', '\uE006'])
+
+      // select existing local as the target
+      .click('#replication-target')
+      .click('#replication-target option[value="REPLICATION_TARGET_EXISTING_LOCAL_DATABASE"]')
+      .setValue('.replication-target-name-row .Select-input input', [newDatabaseName2])
+      .keys(['\uE015', '\uE015', '\uE006'])
+
+      .getAttribute('#replicate', 'disabled', function (result) {
+        // confirm it's not disabled
+        this.assert.equal(result.value, null);
+      })
+      .click('#replicate')
+
+      .waitForElementPresent('.enter-password-modal', waitTime, true)
+      .setValue('.enter-password-modal input[type="password"]', pwd)
+      .click('.enter-password-modal button.save')
+
+      // now check the target database contains the doc from the original db
+      .checkForDocumentCreated(docName1, longWaitTime, newDatabaseName2)
+      .end();
+  }
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/replication/tests/replicationSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/tests/replicationSpec.js b/app/addons/replication/tests/replicationSpec.js
new file mode 100644
index 0000000..4664c4e
--- /dev/null
+++ b/app/addons/replication/tests/replicationSpec.js
@@ -0,0 +1,212 @@
+// 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 ReactDOM from 'react-dom';
+import FauxtonAPI from '../../../core/api';
+import TestUtils from 'react-addons-test-utils';
+import utils from '../../../../test/mocha/testUtils';
+import Components from '../components.react';
+import Constants from '../constants';
+import Actions from '../actions';
+import ActionTypes from '../actiontypes';
+import Stores from '../stores';
+
+const assert = utils.assert;
+const store = Stores.replicationStore;
+
+
+const updateField = function (fieldName, value) {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.REPLICATION_UPDATE_FORM_FIELD,
+    options: {
+      fieldName: fieldName,
+      value: value
+    }
+  });
+};
+
+
+describe('Replication', () => {
+
+  describe('ReplicationTargetRow', () => {
+    let el, container;
+
+    beforeEach(() => {
+      container = document.createElement('div');
+    });
+
+    afterEach(() => {
+      ReactDOM.unmountComponentAtNode(container);
+      store.reset();
+    });
+
+    it('new remote replication target shows a URL field', () => {
+      el = TestUtils.renderIntoDocument(
+        <Components.ReplicationTargetRow
+          remoteTarget="remotetarget"
+          replicationTarget={Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE}
+          databases={['one', 'two']}
+          targetDatabase=""
+        />,
+        container
+      );
+      assert.equal($(ReactDOM.findDOMNode(el)).find('input.connection-url').length, 1);
+    });
+
+    it('existing remote replication target also shows a URL field', () => {
+      el = TestUtils.renderIntoDocument(
+        <Components.ReplicationTargetRow
+          remoteTarget="remotetarget"
+          replicationTarget={Constants.REPLICATION_TARGET.EXISTING_REMOTE_DATABASE}
+          databases={['one', 'two']}
+          targetDatabase=""
+        />,
+        container
+      );
+      assert.equal($(ReactDOM.findDOMNode(el)).find('input.connection-url').length, 1);
+    });
+
+    it('new local database fields have simple textfield', () => {
+      el = TestUtils.renderIntoDocument(
+        <Components.ReplicationTargetRow
+          remoteTarget="remotetarget"
+          replicationTarget={Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE}
+          databases={['one', 'two']}
+          targetDatabase=""
+        />,
+        container
+      );
+      assert.equal($(ReactDOM.findDOMNode(el)).find('input.connection-url').length, 0);
+      assert.equal($(ReactDOM.findDOMNode(el)).find('input.new-local-db').length, 1);
+    });
+
+    it('existing local databases fields have typeahead field', () => {
+      el = TestUtils.renderIntoDocument(
+        <Components.ReplicationTargetRow
+          remoteTarget="remotetarget"
+          replicationTarget={Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE}
+          databases={['one', 'two']}
+          targetDatabase=""
+        />,
+        container
+      );
+      assert.equal($(ReactDOM.findDOMNode(el)).find('input.connection-url').length, 0);
+      assert.equal($(ReactDOM.findDOMNode(el)).find('input.new-local-db').length, 0);
+
+      // (the typeahead field has a search icon)
+      assert.equal($(ReactDOM.findDOMNode(el)).find('.Select--single').length, 1);
+    });
+
+  });
+
+
+  describe('ReplicationController', () => {
+
+    describe('Replicate button', () => {
+      let el, container;
+
+      beforeEach(() => {
+        container = document.createElement('div');
+      });
+
+      afterEach(() => {
+        ReactDOM.unmountComponentAtNode(container);
+        store.reset();
+      });
+
+      it('shows loading spinner until databases loaded', () => {
+        el = TestUtils.renderIntoDocument(<Components.ReplicationController/>, container);
+        Actions.initReplicator('sourcedb');
+        assert.ok($(ReactDOM.findDOMNode(el)).hasClass('loading-lines'));
+
+        FauxtonAPI.dispatch({
+          type: ActionTypes.REPLICATION_DATABASES_LOADED,
+          options: { databases: ['one', 'two', 'three'] }
+        });
+        assert.notOk($(ReactDOM.findDOMNode(el)).hasClass('loading-lines'));
+      });
+
+      it('disabled by default', () => {
+        el = TestUtils.renderIntoDocument(<Components.ReplicationController/>, container);
+        Actions.initReplicator('sourcedb');
+        FauxtonAPI.dispatch({
+          type: ActionTypes.REPLICATION_DATABASES_LOADED,
+          options: { databases: ['one', 'two', 'three'] }
+        });
+        assert.ok($(ReactDOM.findDOMNode(el)).find('#replicate').is(':disabled'));
+      });
+
+      it('enabled when all fields entered', () => {
+        el = TestUtils.renderIntoDocument(<Components.ReplicationController/>, container);
+        Actions.initReplicator('sourcedb');
+        FauxtonAPI.dispatch({
+          type: ActionTypes.REPLICATION_DATABASES_LOADED,
+          options: { databases: ['one', 'two', 'three'] }
+        });
+
+        updateField('replicationSource', Constants.REPLICATION_SOURCE.LOCAL);
+        updateField('sourceDatabase', 'one');
+        updateField('replicationTarget', Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE);
+        updateField('targetDatabase', 'two');
+
+        assert.notOk($(ReactDOM.findDOMNode(el)).find('#replicate').is(':disabled'));
+      });
+
+      it('disabled when missing replication source', () => {
+        el = TestUtils.renderIntoDocument(<Components.ReplicationController/>, container);
+        Actions.initReplicator('sourcedb');
+        FauxtonAPI.dispatch({
+          type: ActionTypes.REPLICATION_DATABASES_LOADED,
+          options: { databases: ['one', 'two', 'three'] }
+        });
+
+        updateField('replicationTarget', Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE);
+        updateField('targetDatabase', 'two');
+
+        assert.ok($(ReactDOM.findDOMNode(el)).find('#replicate').is(':disabled'));
+      });
+
+      it('disabled when source is local, but not in known list of dbs', () => {
+        el = TestUtils.renderIntoDocument(<Components.ReplicationController/>, container);
+        Actions.initReplicator('sourcedb');
+        FauxtonAPI.dispatch({
+          type: ActionTypes.REPLICATION_DATABASES_LOADED,
+          options: { databases: ['one', 'two', 'three'] }
+        });
+
+        updateField('replicationSource', Constants.REPLICATION_SOURCE.LOCAL);
+        updateField('sourceDatabase', 'unknown-source-db');
+        updateField('replicationTarget', Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE);
+        updateField('targetDatabase', 'two');
+
+        assert.ok($(ReactDOM.findDOMNode(el)).find('#replicate').is(':disabled'));
+      });
+
+      it('disabled when target is local, but not in known list of dbs', () => {
+        el = TestUtils.renderIntoDocument(<Components.ReplicationController/>, container);
+        Actions.initReplicator('sourcedb');
+        FauxtonAPI.dispatch({
+          type: ActionTypes.REPLICATION_DATABASES_LOADED,
+          options: { databases: ['one', 'two', 'three'] }
+        });
+
+        updateField('replicationSource', Constants.REPLICATION_SOURCE.LOCAL);
+        updateField('sourceDatabase', 'one');
+        updateField('replicationTarget', Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE);
+        updateField('targetDatabase', 'unknown-target-db');
+
+        assert.ok($(ReactDOM.findDOMNode(el)).find('#replicate').is(':disabled'));
+      });
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/replication/tests/storesSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/tests/storesSpec.js b/app/addons/replication/tests/storesSpec.js
new file mode 100644
index 0000000..04be3df
--- /dev/null
+++ b/app/addons/replication/tests/storesSpec.js
@@ -0,0 +1,59 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+import utils from '../../../../test/mocha/testUtils';
+import Stores from '../stores';
+import Constants from '../constants';
+
+const assert = utils.assert;
+const store = Stores.replicationStore;
+
+describe('Databases Store', function () {
+
+  afterEach(function () {
+    store.reset();
+  });
+
+  it('confirm updateFormField updates all fields', function () {
+    assert.equal(store.getRemoteSource(), '');
+    store.updateFormField('remoteSource', 'SOURCE');
+    assert.equal(store.getRemoteSource(), 'SOURCE');
+
+    assert.equal(store.getRemoteTarget(), '');
+    store.updateFormField('remoteTarget', 'TARGET');
+    assert.equal(store.getRemoteTarget(), 'TARGET');
+
+    assert.equal(store.getTargetDatabase(), '');
+    store.updateFormField('targetDatabase', 'db');
+    assert.equal(store.getTargetDatabase(), 'db');
+
+    assert.equal(store.getReplicationType(), Constants.REPLICATION_TYPE.ONE_TIME);
+    store.updateFormField('replicationType', Constants.REPLICATION_TYPE.CONTINUOUS);
+    assert.equal(store.getReplicationType(), Constants.REPLICATION_TYPE.CONTINUOUS);
+
+    assert.equal(store.getReplicationDocName(), '');
+    store.updateFormField('replicationDocName', 'doc-name');
+    assert.equal(store.getReplicationDocName(), 'doc-name');
+
+    assert.equal(store.getReplicationSource(), '');
+    store.updateFormField('replicationSource', 'rsource');
+    assert.equal(store.getReplicationSource(), 'rsource');
+
+    assert.equal(store.getReplicationTarget(), '');
+    store.updateFormField('replicationTarget', 'rtarget');
+    assert.equal(store.getReplicationTarget(), 'rtarget');
+
+    assert.equal(store.getSourceDatabase(), '');
+    store.updateFormField('sourceDatabase', 'source-db');
+    assert.equal(store.getSourceDatabase(), 'source-db');
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/app/addons/replication/views.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/views.js b/app/addons/replication/views.js
deleted file mode 100644
index cef6629..0000000
--- a/app/addons/replication/views.js
+++ /dev/null
@@ -1,343 +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 Components from "../fauxton/components";
-import Replication from "./resources";
-var View = {},
-  Events = {},
-  pollingInfo = {
-    rate: 5,
-    intervalId: null
-  };
-
-_.extend(Events, Backbone.Events);
-
-// NOTES: http://wiki.apache.org/couchdb/Replication
-
-// Replication form view is huge
-// -----------------------------------
-// afterRender: autocomplete on the target input field
-// beforeRender:  add the status table
-// disableFields:  disable non active fields on submit
-// enableFields:  enable field when radio btns are clicked
-// establish:  get the DB list for autocomplete
-// formValidation:  make sure fields aren't empty
-// showProgress:  make a call to active_tasks model and show only replication types.  Poll every 5 seconds. (make this it's own view)
-// startReplication:  saves to the model, starts replication
-// submit:  form submit handler
-// swapFields:  change to and from target
-// toggleAdvancedOptions:  toggle advanced
-
-View.ReplicationFormForAdmins = FauxtonAPI.View.extend({
-  template: 'addons/replication/templates/form',
-  events:  {
-    'submit #replication': 'validate',
-    'click .btn-group .btn': 'showFields',
-    'click .swap': 'swapFields',
-    'click .options': 'toggleAdvancedOptions'
-  },
-
-  initialize: function (options) {
-    this.status = options.status;
-    this.selectedDB = options.selectedDB;
-    this.newRepModel = new Replication.Replicate({});
-  },
-
-  afterRender: function () {
-    this.dbSearchTypeahead = new Components.DbSearchTypeahead({
-      dbLimit: 30,
-      el: 'input#to_name'
-    });
-
-    this.dbSearchTypeahead.render();
-  },
-
-  beforeRender: function () {
-    this.insertView('#replicationStatus', new View.ReplicationListForAdmins({
-      collection: this.status
-    }));
-  },
-
-  cleanup: function () {
-    clearInterval(pollingInfo.intervalId);
-  },
-
-  enableFields: function () {
-    this.$el.find('input', 'select').attr('disabled', false);
-  },
-
-  disableFields: function () {
-    this.$el.find('input:hidden', 'select:hidden').attr('disabled', true);
-  },
-
-  showFields: function (e) {
-    var $currentTarget = this.$(e.currentTarget),
-    targetVal = $currentTarget.val();
-
-    if (targetVal === 'local') {
-      $currentTarget.parents('.form_set').addClass('local');
-      return;
-    }
-
-    $currentTarget.parents('.form_set').removeClass('local');
-  },
-
-  establish: function () {
-    return [this.collection.fetch(), this.status.fetch()];
-  },
-
-  validate: function (e) {
-    e.preventDefault();
-    if (this.formValidation()) {
-      FauxtonAPI.addNotification({
-        msg: 'Please enter every field.',
-        type: 'error',
-        clear: true
-      });
-      return;
-
-    } else if (this.$('input#to_name').is(':visible') && !this.$('input[name=create_target]').is(':checked')) {
-      var alreadyExists = this.collection.where({
-        "name": this.$('input#to_name').val()
-      });
-      if (alreadyExists.length === 0) {
-        FauxtonAPI.addNotification({
-          msg: 'This database doesn\'t exist. Check create target if you want to create it.',
-          type: 'error',
-          clear: true
-        });
-        return;
-      }
-    }
-
-    this.submit(e);
-  },
-
-  formValidation: function () {
-    var $remote = this.$el.find('input:visible'),
-        error = false;
-    _.each($remote, function (item) {
-      if (item.value === 'http://' || item.value === '') {
-        error = true;
-      }
-    });
-    return error;
-  },
-
-  serialize: function () {
-    return {
-      databases:  this.collection.toJSON(),
-      selectedDB: this.selectedDB
-    };
-  },
-
-  startReplication: function (json) {
-    var that = this;
-    this.newRepModel.save(json, {
-      success: function (resp) {
-        FauxtonAPI.addNotification({
-          msg: 'Replication from ' + resp.get('source') + ' to ' + resp.get('target') + ' has begun.',
-          type: 'success',
-          clear: true
-        });
-        that.updateButtonText(false);
-        Events.trigger('update:tasks');
-      },
-      error: function (model, xhr, options) {
-        var errorMessage = JSON.parse(xhr.responseText);
-        FauxtonAPI.addNotification({
-          msg: errorMessage.reason,
-          type: 'error',
-          clear: true
-        });
-        that.updateButtonText(false);
-      }
-    });
-    this.enableFields();
-  },
-
-  updateButtonText: function (wait) {
-    var $button = this.$('#replication button[type=submit]');
-    if (wait) {
-      $button.text('Starting replication...').attr('disabled', true);
-    } else {
-      $button.text('Replication').attr('disabled', false);
-    }
-  },
-
-  submit: function (e) {
-    this.disableFields();
-    var formJSON = {};
-    _.map(this.$(e.currentTarget).serializeArray(), function (formData) {
-      if (formData.value !== '') {
-        formJSON[formData.name] = (formData.value === "true" ? true : formData.value.replace(/\s/g, '').toLowerCase());
-      }
-    });
-
-    this.updateButtonText(true);
-    this.startReplication(formJSON);
-  },
-
-  swapFields: function (e) {
-    // WALL O' VARIABLES
-    var $fromSelect = this.$('#from_name'),
-        $toSelect = this.$('#to_name'),
-        $toInput = this.$('#to_url'),
-        $fromInput = this.$('#from_url'),
-        fromSelectVal = $fromSelect.val(),
-        fromInputVal = $fromInput.val(),
-        toSelectVal = $toSelect.val(),
-        toInputVal = $toInput.val();
-
-    $fromSelect.val(toSelectVal);
-    $toSelect.val(fromSelectVal);
-
-    $fromInput.val(toInputVal);
-    $toInput.val(fromInputVal);
-
-    // prevent other click handlers from running
-    return false;
-  }
-});
-
-View.ReplicationForm = View.ReplicationFormForAdmins.extend({
-  template: 'addons/replication/templates/form',
-
-  events: {
-    'submit #replication': 'validate',
-    'click .btn-group .btn': 'showFields',
-    'click .swap': 'swapFields',
-    'click .options': 'toggleAdvancedOptions'
-  },
-
-  initialize: function (options) {
-    this.selectedDB = options.selectedDB;
-    this.newRepModel = new Replication.Replicate({});
-  },
-
-  beforeRender: function () {},
-
-  establish: function () {
-    return [this.collection.fetch()];
-  }
-});
-
-View.ReplicationListForAdmins = FauxtonAPI.View.extend({
-  tagName: 'ul',
-
-  initialize: function () {
-    Events.bind('update:tasks', this.establish, this);
-    this.listenTo(this.collection, 'reset', this.render);
-    this.$el.prepend('<li class="header"><h4>Active Replication Tasks</h4></li>');
-  },
-
-  establish: function () {
-    return [this.collection.fetch({ reset: true })];
-  },
-
-  setPolling: function () {
-    var that = this;
-    this.cleanup();
-    pollingInfo.intervalId = setInterval(function () {
-      that.establish();
-    }, pollingInfo.rate * 1000);
-  },
-
-  cleanup: function () {
-    Events.unbind('update:tasks');
-    clearInterval(pollingInfo.intervalId);
-  },
-
-  beforeRender: function () {
-    this.collection.forEach(function (item) {
-      this.insertView(new View.replicationItem({
-        model: item
-      }));
-    }, this);
-  },
-
-  showHeader: function () {
-    this.$el.parent()
-      .toggleClass('showHeader', this.collection.length > 0);
-  },
-
-  afterRender: function () {
-    this.showHeader();
-    this.setPolling();
-  }
-});
-
-//make this a table row item.
-View.replicationItem = FauxtonAPI.View.extend({
-  tagName: 'li',
-  className: 'row',
-  template: 'addons/replication/templates/progress',
-  events: {
-    'click .cancel': 'cancelReplication'
-  },
-
-  initialize: function () {
-    this.newRepModel = new Replication.Replicate({});
-  },
-
-  establish: function () {
-    return [this.model.fetch()];
-  },
-
-  cancelReplication: function (e) {
-    // need to pass "cancel": true with source & target
-    var $currentTarget = this.$(e.currentTarget),
-        repID = $currentTarget.attr('data-rep-id');
-
-    this.newRepModel.save({
-      "replication_id": repID,
-      "cancel": true
-    },
-    {
-      success: function (model, xhr, options) {
-        FauxtonAPI.addNotification({
-          msg: 'Replication stopped.',
-          type: 'success',
-          clear: true
-        });
-      },
-      error: function (model, xhr, options) {
-        var errorMessage = JSON.parse(xhr.responseText);
-        FauxtonAPI.addNotification({
-          msg: errorMessage.reason,
-          type: 'error',
-          clear: true
-        });
-      }
-    });
-  },
-
-  afterRender: function () {
-    if (this.model.get('continuous')) {
-      this.$el.addClass('continuous');
-    }
-  },
-
-  serialize: function () {
-    return {
-      progress:  this.model.get('progress'),
-      target: this.model.get('target'),
-      source: this.model.get('source'),
-      continuous: this.model.get('continuous'),
-      repid: this.model.get('replication_id')
-    };
-  }
-});
-
-export default View;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/package.json
----------------------------------------------------------------------
diff --git a/package.json b/package.json
index 6d0cc2f..a7e44e4 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
     "babel-register": "^6.4.3",
     "backbone": "^1.1.0",
     "backbone.layoutmanager": "^0.9.5",
+    "base-64": "^0.1.0",
     "brace": "^0.7.0",
     "chai": "^3.5.0",
     "clean-css": "^3.4.9",

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a50108b0/test/nightwatch_tests/custom-commands/checkForDocumentCreated.js
----------------------------------------------------------------------
diff --git a/test/nightwatch_tests/custom-commands/checkForDocumentCreated.js b/test/nightwatch_tests/custom-commands/checkForDocumentCreated.js
index d6b156e..87d6b4e 100644
--- a/test/nightwatch_tests/custom-commands/checkForDocumentCreated.js
+++ b/test/nightwatch_tests/custom-commands/checkForDocumentCreated.js
@@ -38,6 +38,7 @@ CheckForDocumentCreated.prototype.command = function (doc, timeout, db) {
 
   const url = [couchUrl, db, doc].join('/');
 
+  console.log('checking this doc exists: ', url);
   checkForDocumentCreated(url, timeout, () => {
     this.emit('complete');
   });


Mime
View raw message