You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ga...@apache.org on 2016/09/21 15:18:54 UTC
[03/29] fauxton commit: updated refs/heads/new-replication to b0541e1
Replication page update
Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/cc37a7b4
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/cc37a7b4
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/cc37a7b4
Branch: refs/heads/new-replication
Commit: cc37a7b406fa425ddbbf95e9c81c50ac04a4c97b
Parents: a97831a
Author: Ben Keen <be...@gmail.com>
Authored: Wed Mar 16 14:06:33 2016 -0700
Committer: Garren Smith <ga...@gmail.com>
Committed: Wed Sep 14 17:22: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 ++-
.../components/react-components.react.jsx | 18 +-
.../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 | 131 +++++
app/addons/replication/tests/replicationSpec.js | 212 ++++++-
app/addons/replication/tests/storesSpec.js | 59 ++
app/addons/replication/views.js | 343 ------------
22 files changed, 1593 insertions(+), 803 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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 d1dcbf1..42e162a 100644
--- a/app/addons/components/react-components.react.jsx
+++ b/app/addons/components/react-components.react.jsx
@@ -1,3 +1,4 @@
+
// 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
@@ -227,7 +228,14 @@ var StyledSelect = React.createClass({
propTypes: {
selectValue: React.PropTypes.string.isRequired,
selectId: React.PropTypes.string.isRequired,
- selectChange: React.PropTypes.func.isRequired
+ selectChange: React.PropTypes.func.isRequired,
+ autoFocus: React.PropTypes.bool
+ },
+
+ getDefaultProps: function () {
+ return {
+ autoFocus: false
+ };
},
render: function () {
@@ -240,6 +248,7 @@ var StyledSelect = React.createClass({
id={this.props.selectId}
className={this.props.selectValue}
onChange={this.props.selectChange}
+ autoFocus={this.props.autoFocus}
>
{this.props.selectContent}
</select>
@@ -1117,7 +1126,7 @@ const ConfirmButton = React.createClass({
buttonType: React.PropTypes.string,
'data-id': React.PropTypes.string,
onClick: React.PropTypes.func,
- disabled: React.PropTypes.bool,
+ disabled: React.PropTypes.bool
},
getDefaultProps: function () {
@@ -1128,7 +1137,8 @@ const ConfirmButton = React.createClass({
buttonType: 'btn-success',
style: {},
'data-id': null,
- onClick: function () { }
+ onClick: function () { },
+ disabled: false
};
},
@@ -1152,6 +1162,7 @@ const ConfirmButton = React.createClass({
className={'btn save ' + buttonType}
id={id}
style={style}
+ disabled={disabled}
>
{this.getIcon()}
{text}
@@ -1160,6 +1171,7 @@ const ConfirmButton = React.createClass({
}
});
+
var MenuDropDown = React.createClass({
getDefaultProps: function () {
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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/cc37a7b4/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..54cb8f8
--- /dev/null
+++ b/app/addons/replication/tests/nightwatch/replication.js
@@ -0,0 +1,131 @@
+// 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 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)
+ .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('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')
+
+ // now check the database was created
+ .checkForDatabaseCreated(newDatabaseName1, waitTime, true)
+
+ // lastly, check the doc was replicated as well
+ .url(baseUrl + '/' + newDatabaseName1 + '/' + docName1)
+ .waitForElementVisible('html', waitTime, false)
+ .getText('html', function (result) {
+ const data = result.value,
+ createdDocIsPresent = data.indexOf(docName1);
+
+ this.verify.ok(createdDocIsPresent > 0, 'Checking doc exists.');
+ })
+ .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)
+ .createDocument(docName1, newDatabaseName1)
+ .createDatabase(newDatabaseName2)
+ .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('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('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, waitTime, newDatabaseName2)
+ .end();
+ }
+};
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/cc37a7b4/app/addons/replication/tests/replicationSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/replication/tests/replicationSpec.js b/app/addons/replication/tests/replicationSpec.js
index bae87c1..4664c4e 100644
--- a/app/addons/replication/tests/replicationSpec.js
+++ b/app/addons/replication/tests/replicationSpec.js
@@ -9,30 +9,204 @@
// 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 Replication from "../base";
-import Views from "../views";
-import Resources from "../resources";
-import testUtils from "../../../../test/mocha/testUtils";
-var assert = testUtils.assert,
- ViewSandbox = testUtils.ViewSandbox,
- viewSandbox;
-
-describe('Replication Addon', function () {
- describe('Replication View', function () {
- var view = new Views.ReplicationForm({
- collection: new Replication.DBList()
+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');
});
- beforeEach(function (done) {
- viewSandbox = new ViewSandbox();
- viewSandbox.renderView(view, done);
+
+ afterEach(() => {
+ ReactDOM.unmountComponentAtNode(container);
+ store.reset();
});
- afterEach(function () {
- viewSandbox.remove();
+ 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("should render", function () {
- assert.ok(view.$el.length > 0);
+ 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/cc37a7b4/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/cc37a7b4/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;