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 <ben.k...@gmail.com> Authored: Wed Mar 16 14:06:33 2016 -0700 Committer: Garren Smith <garren.sm...@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:$passw...@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;