Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id 338A4200BD4 for ; Wed, 16 Nov 2016 12:32:56 +0100 (CET) Received: by cust-asf.ponee.io (Postfix) id 32163160B13; Wed, 16 Nov 2016 11:32:56 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id E2112160B03 for ; Wed, 16 Nov 2016 12:32:53 +0100 (CET) Received: (qmail 7086 invoked by uid 500); 16 Nov 2016 11:32:53 -0000 Mailing-List: contact commits-help@couchdb.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@couchdb.apache.org Delivered-To: mailing list commits@couchdb.apache.org Received: (qmail 7026 invoked by uid 99); 16 Nov 2016 11:32:53 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 16 Nov 2016 11:32:53 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id C6900DFC55; Wed, 16 Nov 2016 11:32:52 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit From: garren@apache.org To: commits@couchdb.apache.org Date: Wed, 16 Nov 2016 11:32:53 -0000 Message-Id: In-Reply-To: <5228a67f0cd54c868dd643384ca25cf0@git.apache.org> References: <5228a67f0cd54c868dd643384ca25cf0@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [2/4] fauxton commit: updated refs/heads/master to ff25441 archived-at: Wed, 16 Nov 2016 11:32:56 -0000 Complete new replication redesign This adds a new replication page and an activity page for replication Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/ff25441b Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/ff25441b Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/ff25441b Branch: refs/heads/master Commit: ff25441bc63a5d8857ecbc51703e8ed19ec9cfc6 Parents: a50108b Author: Garren Smith Authored: Mon Aug 15 15:44:37 2016 +0200 Committer: Garren Smith Committed: Wed Nov 16 13:32:29 2016 +0200 ---------------------------------------------------------------------- app/addons/auth/assets/less/auth.less | 6 - app/addons/auth/components.react.jsx | 35 +- app/addons/components/assets/less/layouts.less | 7 + app/addons/components/assets/less/polling.less | 24 + .../components/assets/less/styled-select.less | 26 +- app/addons/components/components/polling.js | 20 + .../components/components/styledselect.js | 3 +- app/addons/components/layouts.js | 8 +- .../components/react-components.react.jsx | 5 +- .../tests/nightwatch/highlightsidebar.js | 2 +- app/addons/replication/actions.js | 192 ++++++- app/addons/replication/actiontypes.js | 14 +- app/addons/replication/api.js | 216 ++++++++ .../replication/assets/less/replication.less | 333 ++++++++--- app/addons/replication/components.react.jsx | 546 ------------------- app/addons/replication/components/activity.js | 453 +++++++++++++++ app/addons/replication/components/modals.js | 139 +++++ .../replication/components/newreplication.js | 277 ++++++++++ app/addons/replication/components/options.js | 97 ++++ .../replication/components/remoteexample.js | 50 ++ app/addons/replication/components/source.js | 170 ++++++ app/addons/replication/components/submit.js | 43 ++ app/addons/replication/components/target.js | 209 +++++++ app/addons/replication/controller.js | 212 +++++++ app/addons/replication/helpers.js | 27 +- app/addons/replication/route.js | 73 ++- app/addons/replication/stores.js | 234 ++++++-- app/addons/replication/tests/apiSpec.js | 170 ++++++ app/addons/replication/tests/helpersSpec.js | 41 ++ .../replication/tests/newreplicationSpec.js | 225 ++++++++ .../replication/tests/nightwatch/replication.js | 119 ++-- .../tests/nightwatch/replicationactivity.js | 50 ++ app/addons/replication/tests/replicationSpec.js | 212 ------- app/addons/replication/tests/storesSpec.js | 14 +- assets/less/fauxton.less | 9 + i18n.json.default.json | 4 +- package.json | 3 +- test/dev.js | 2 +- test/mocha/testUtils.js | 1 + test/nightwatch_tests/custom-commands/helper.js | 12 +- test/test.config.underscore | 1 + webpack.config.test.js | 17 +- 42 files changed, 3317 insertions(+), 984 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/auth/assets/less/auth.less ---------------------------------------------------------------------- diff --git a/app/addons/auth/assets/less/auth.less b/app/addons/auth/assets/less/auth.less index 4cf1863..0a0d24d 100644 --- a/app/addons/auth/assets/less/auth.less +++ b/app/addons/auth/assets/less/auth.less @@ -29,9 +29,3 @@ margin-top: 0; } } - -.enter-password-modal { - input { - width: 100%; - } -} http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/auth/components.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/auth/components.react.jsx b/app/addons/auth/components.react.jsx index ddbfd8c..9446201 100644 --- a/app/addons/auth/components.react.jsx +++ b/app/addons/auth/components.react.jsx @@ -17,10 +17,12 @@ import ReactDOM from "react-dom"; import AuthStores from "./stores"; import AuthActions from "./actions"; import { Modal } from 'react-bootstrap'; +import Components from '../components/react-components.react'; var changePasswordStore = AuthStores.changePasswordStore; var createAdminStore = AuthStores.createAdminStore; var createAdminSidebarStore = AuthStores.createAdminSidebarStore; +const {ConfirmButton} = Components; var LoginForm = React.createClass({ @@ -328,21 +330,35 @@ class PasswordModal extends React.Component { } render () { + const {visible, onClose, submitBtnLabel, headerTitle, modalMessage} = this.props; + if (!this.props.visible) { + return null; + } + return ( - this.props.onClose()}> + onClose()}> - Enter Password + {headerTitle} - {this.props.modalMessage} - this.setState({ password: e.target.value })} onKeyPress={this.onKeyPress} /> + {modalMessage} + this.setState({ password: e.target.value })} + onKeyPress={this.onKeyPress} + /> - this.props.onClose()}>Cancel - + onClose()}>Cancel + ); @@ -356,6 +372,7 @@ PasswordModal.propTypes = { submitBtnLabel: React.PropTypes.string }; PasswordModal.defaultProps = { + headerTitle: "Enter Password", visible: false, modalMessage: '', onClose: AuthActions.hidePasswordModal, http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/components/assets/less/layouts.less ---------------------------------------------------------------------- diff --git a/app/addons/components/assets/less/layouts.less b/app/addons/components/assets/less/layouts.less index 86cc23c..adfe63a 100644 --- a/app/addons/components/assets/less/layouts.less +++ b/app/addons/components/assets/less/layouts.less @@ -12,3 +12,10 @@ .template { height: 100%; } + +.right-header-flex { + display: flex; + align-items: center; + flex-direction: row; + height: 100%; +} http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/components/assets/less/polling.less ---------------------------------------------------------------------- diff --git a/app/addons/components/assets/less/polling.less b/app/addons/components/assets/less/polling.less index 36657fa..19b0446 100644 --- a/app/addons/components/assets/less/polling.less +++ b/app/addons/components/assets/less/polling.less @@ -37,3 +37,27 @@ .faux__polling-info-slider { cursor: pointer; } + +div.faux__refresh-btn { + border-left: 1px solid #ccc; + line-height: 40px; + padding: 12px 13px; + flex: 0 0 auto; +} + +.faux__refresh-icon { + margin-right: 4px; +} + +.faux__refresh-link:visited, +.faux__refresh-link:focus, +.faux__refresh-link { + color: #666; + font-size: 14px; + text-decoration: none; +} + +.faux__refresh-link:hover { + text-decoration: none; + color: #AF2D24; +} http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/components/assets/less/styled-select.less ---------------------------------------------------------------------- diff --git a/app/addons/components/assets/less/styled-select.less b/app/addons/components/assets/less/styled-select.less index 11ba404..1227527 100644 --- a/app/addons/components/assets/less/styled-select.less +++ b/app/addons/components/assets/less/styled-select.less @@ -28,6 +28,7 @@ text-indent: 4px; height: 46px; padding-right: 35px; + color: #333; } .styled-select select:-moz-focusring { @@ -39,9 +40,32 @@ display: none; } -.styled-select i { +.styled-select-icon { + pointer-events: none; position: absolute; right: 10px; top: 12px; + color: #333; +} + +.styled-select-hover-icon { pointer-events: none; + position: absolute; + right: 10px; + top: 12px; + color: #E73D34; + display: none; + z-index: 30; +} + +//this is a litte css trick to create a hover effect for the triangle. We can't use the normal hover event because we +//need to set pointer-events to none so that a click on the triangle is actually a click on the select dropdown +.styled-select:hover { + .styled-select-hover-icon { + display: inline; + } + + .styled-select-icon { + display: none; + } } http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/components/components/polling.js ---------------------------------------------------------------------- diff --git a/app/addons/components/components/polling.js b/app/addons/components/components/polling.js index 2afead6..af9ced1 100644 --- a/app/addons/components/components/polling.js +++ b/app/addons/components/components/polling.js @@ -128,3 +128,23 @@ Polling.propTypes = { stepSize: React.PropTypes.number.isRequired, onPoll: React.PropTypes.func.isRequired, }; + +export const RefreshBtn = ({refresh}) => + ; + +RefreshBtn.propTypes = { + refresh: React.PropTypes.func.isRequired +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/components/components/styledselect.js ---------------------------------------------------------------------- diff --git a/app/addons/components/components/styledselect.js b/app/addons/components/components/styledselect.js index 85a6ff4..fc1c27c 100644 --- a/app/addons/components/components/styledselect.js +++ b/app/addons/components/components/styledselect.js @@ -23,7 +23,8 @@ export const StyledSelect = React.createClass({ return (
- - - -
-
-
-
- - Clear -
-
- - Replication requires authentication.

} - submitBtnLabel="Continue Replication" - onSuccess={this.submit} /> - - ); - } -} - - -class ReplicationSourceRow extends React.Component { - render () { - const { replicationSource, databases, sourceDatabase, remoteSource, onChange} = this.props; - - if (replicationSource === Constants.REPLICATION_SOURCE.LOCAL) { - return ( -
-
- Source Name: -
-
- Actions.updateFormField('sourceDatabase', selected.value)} /> -
-
- ); - } - - return ( -
-
-
Database URL:
-
- onChange(e.target.value)} /> -
e.g. https://$REMOTE_USERNAME:$REMOTE_PASSWORD@$REMOTE_SERVER/$DATABASE
-
-
-
- ); - } -} -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 ( - - ); - }); - } - - render () { - return ( - 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 ( - - ); - }); - } - - render () { - return ( - 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 ( - - ); - }); - } - - render () { - return ( - 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 = ( -
- Actions.updateFormField('remoteTarget', e.target.value)} /> -
e.g. {remoteHelpText}
-
- ); - - // new local databases have a freeform text field - } else if (replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE) { - field = ( - Actions.updateFormField('targetDatabase', e.target.value)} /> - ); - - // existing local databases have a typeahead field - } else { - field = ( - Actions.updateFormField('targetDatabase', selected.value)} /> - ); - } - - if (replicationTarget === Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE || - replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE) { - targetLabel = 'New Database:'; - } - - return ( -
-
{targetLabel}
-
- {field} -
-
- ); - } -} -ReplicationTargetRow.propTypes = { - remoteTarget: React.PropTypes.string.isRequired, - replicationTarget: React.PropTypes.string.isRequired, - databases: React.PropTypes.array.isRequired, - targetDatabase: React.PropTypes.string.isRequired -}; - - -export default { - ReplicationController, - ReplicationSource, - ReplicationTarget, - ReplicationType, - ReplicationTargetRow -}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/replication/components/activity.js ---------------------------------------------------------------------- diff --git a/app/addons/replication/components/activity.js b/app/addons/replication/components/activity.js new file mode 100644 index 0000000..be8fe24 --- /dev/null +++ b/app/addons/replication/components/activity.js @@ -0,0 +1,453 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +import React from 'react'; +import {Table} from "react-bootstrap"; +import moment from 'moment'; +import app from '../../../app'; +import {DeleteModal, ErrorModal} from './modals'; + +const formatUrl = (url) => { + const urlObj = new URL(url); + const encoded = encodeURIComponent(urlObj.pathname.slice(1)); + + if (url.indexOf(window.location.hostname) > -1) { + return ( + + {urlObj.origin + '/'} + {urlObj.pathname.slice(1)} + + ); + } + + return `${urlObj.origin}${urlObj.pathname}`; +}; + +class RowActions extends React.Component { + constructor (props) { + super(props); + this.state = { + modalVisible: false, + }; + } + + showModal () { + this.setState({modalVisible: true}); + } + + closeModal () { + this.setState({modalVisible: false}); + } + + getErrorIcon () { + if (!this.props.error) { + return null; + } + return ( +
  • + + + +
  • + ); + } + + render () { + const {_id, url, deleteDocs} = this.props; + const errorIcon = this.getErrorIcon(); + return ( + + ); + + } +}; + +RowActions.propTypes = { + _id: React.PropTypes.string.isRequired, + url: React.PropTypes.string.isRequired, + error: React.PropTypes.bool.isRequired, + errorMsg: React.PropTypes.string.isRequired, + deleteDocs: React.PropTypes.func.isRequired +}; + +const Row = ({ + _id, + source, + target, + type, + status, + statusTime, + url, + selected, + selectDoc, + errorMsg, + deleteDocs +}) => { + const momentTime = moment(statusTime); + const formattedTime = momentTime.isValid() ? momentTime.format("MMM Do, h:mm a") : ''; + + return ( + + selectDoc(_id)} /> + {formatUrl(source)} + {formatUrl(target)} + {type} + {status} + {formattedTime} + + + + + + ); +}; + +Row.propTypes = { + _id: React.PropTypes.string.isRequired, + source: React.PropTypes.string.isRequired, + target: React.PropTypes.string.isRequired, + type: React.PropTypes.string.isRequired, + status: React.PropTypes.string, + url: React.PropTypes.string.isRequired, + statusTime: React.PropTypes.object.isRequired, + selected: React.PropTypes.bool.isRequired, + selectDoc: React.PropTypes.func.isRequired, + errorMsg: React.PropTypes.string.isRequired, + deleteDocs: React.PropTypes.func.isRequired +}; + +const BulkSelectHeader = ({isSelected, deleteDocs, someDocsSelected, onCheck}) => { + const trash = someDocsSelected ? + : null; + + return ( +
    +
    + +
    + {trash} +
    + ); +}; + +BulkSelectHeader.propTypes = { + isSelected: React.PropTypes.bool.isRequired, + someDocsSelected: React.PropTypes.bool.isRequired, + onCheck: React.PropTypes.func.isRequired, + deleteDocs: React.PropTypes.func.isRequired +}; + +const EmptyRow = () => + + + There is no replicator-db activity or history to display. + + ; + +class ReplicationTable extends React.Component { + constructor (props) { + super(props); + } + + sort(column, descending, docs) { + const sorted = docs.sort((a, b) => { + if (a[column] < b[column]) { + return -1; + } + + if (a[column] > b[column]) { + return 1; + } + + return 0; + + }); + + if (!descending) { + sorted.reverse(); + } + + return sorted; + } + + renderRows () { + if (this.props.docs.length === 0) { + return ; + } + + return this.sort(this.props.column, this.props.descending, this.props.docs).map((doc, i) => { + return ; + }); + } + + iconDirection (column) { + if (column === this.props.column && !this.props.descending) { + return 'fonticon-up-dir'; + } + + return 'fonticon-down-dir'; + } + + onSort (column) { + return (e) => { + this.props.changeSort({ + descending: column === this.props.column ? !this.props.descending : true, + column + }); + }; + } + + isSelected (header) { + if (header === this.props.column) { + return 'replication__table--selected'; + } + + return ''; + } + + render () { + return ( + + + + + + + + + + + + + + {this.renderRows()} + +
    + + + Source + + + Target + + + Type + + + State + + + State Time + + + Actions +
    + ); + } +} + +const ReplicationFilter = ({value, onChange}) => { + return ( +
    + + {onChange(e.target.value);}} + /> +
    + ); +}; + +ReplicationFilter.propTypes = { + value: React.PropTypes.string.isRequired, + onChange: React.PropTypes.func.isRequired +}; + +const ReplicationHeader = ({filter, onFilterChange}) => { + return ( + + ); +}; + +ReplicationHeader.propTypes = { + filter: React.PropTypes.string.isRequired, + onFilterChange: React.PropTypes.func.isRequired +}; + +export default class Activity extends React.Component { + constructor (props) { + super(props); + this.state = { + modalVisible: false, + unconfirmedDeleteDocId: null + }; + } + + closeModal () { + this.setState({ + modalVisible: false, + unconfirmedDeleteDocId: null + }); + } + + showModal (doc) { + this.setState({ + modalVisible: true, + unconfirmedDeleteDocId: doc + }); + } + + confirmDeleteDocs () { + let docs = []; + if (this.state.unconfirmedDeleteDocId) { + const doc = this.props.docs.find(doc => doc._id === this.state.unconfirmedDeleteDocId); + docs.push(doc); + } else { + docs = this.props.docs.filter(doc => doc.selected); + } + + this.props.deleteDocs(docs); + this.closeModal(); + } + + numDocsSelected () { + return this.props.docs.filter(doc => doc.selected).length; + } + + render () { + const { + onFilterChange, + activitySort, + changeActivitySort, + docs, + filter, + selectAllDocs, + someDocsSelected, + allDocsSelected, + selectDoc + } = this.props; + + const {modalVisible} = this.state; + return ( +
    + + + +
    + ); + } +} + +Activity.propTypes = { + docs: React.PropTypes.array.isRequired, + filter: React.PropTypes.string.isRequired, + selectAllDocs: React.PropTypes.func.isRequired, + allDocsSelected: React.PropTypes.bool.isRequired, + someDocsSelected: React.PropTypes.bool.isRequired, + selectDoc: React.PropTypes.func.isRequired, + onFilterChange: React.PropTypes.func.isRequired, + deleteDocs: React.PropTypes.func.isRequired, + activitySort: React.PropTypes.object.isRequired, + changeActivitySort: React.PropTypes.func.isRequired +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/replication/components/modals.js ---------------------------------------------------------------------- diff --git a/app/addons/replication/components/modals.js b/app/addons/replication/components/modals.js new file mode 100644 index 0000000..c02f31b --- /dev/null +++ b/app/addons/replication/components/modals.js @@ -0,0 +1,139 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +import React from 'react'; +import {Modal} from 'react-bootstrap'; +import Components from '../../components/react-components.react'; + +const {ConfirmButton} = Components; + + +export const DeleteModal = ({ + visible, + onClose, + onClick, + multipleDocs +}) => { + + if (!visible) { + return null; + } + + let header = "You are deleting a replication document."; + + if (multipleDocs > 1) { + header = `You are deleting ${multipleDocs} replication documents.`; + } + + return ( + onClose()}> + + Verify Deletion + + +

    {header}

    +

    + Deleting a replication document stops continuous replication + and incomplete one-time replication, but does not affect replicated documents. +

    +

    + Replication jobs that do not have replication documents do not appear in Replicator DB Activity. +

    +
    + + Cancel + + +
    + ); +}; + +DeleteModal.propTypes = { + visible: React.PropTypes.bool.isRequired, + onClick: React.PropTypes.func.isRequired, + onClose: React.PropTypes.func.isRequired, + multipleDocs: React.PropTypes.number.isRequired +}; + +export const ErrorModal = ({visible, onClose, errorMsg, onClick}) => { + + if (!visible) { + return null; + } + + return ( + onClose()}> + + Replication Error + + +

    + {errorMsg} +

    +
    + + +
    + ); +}; + +ErrorModal.propTypes = { + visible: React.PropTypes.bool.isRequired, + onClick: React.PropTypes.func.isRequired, + onClose: React.PropTypes.func.isRequired, + errorMsg: React.PropTypes.string.isRequired +}; + +export const ConflictModal = ({visible, docId, onClose, onClick}) => { + + if (!visible) { + return null; + } + + return ( + onClose()}> + + Fix Document Conflict + + +

    + A replication document with ID {docId} already exists. +

    +

    + You can overwrite the existing document, or change the new replication job’s document ID. +

    +

    + If you overwrite the existing document, any replication job currently using the replication document will stop, + and that job will not appear in Replicator DB Activity. Replicated documents will not be affected. +

    +
    + + + + +
    + ); +}; + +ConflictModal.propTypes = { + visible: React.PropTypes.bool.isRequired, + onClick: React.PropTypes.func.isRequired, + onClose: React.PropTypes.func.isRequired, + docId: React.PropTypes.string.isRequired +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/replication/components/newreplication.js ---------------------------------------------------------------------- diff --git a/app/addons/replication/components/newreplication.js b/app/addons/replication/components/newreplication.js new file mode 100644 index 0000000..e274fa7 --- /dev/null +++ b/app/addons/replication/components/newreplication.js @@ -0,0 +1,277 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import React from 'react'; +import app from '../../../app'; +import FauxtonAPI from '../../../core/api'; +import {ReplicationSource} from './source'; +import {ReplicationTarget} from './target'; +import {ReplicationOptions} from './options'; +import {ReplicationSubmit} from './submit'; +import AuthComponents from '../../auth/components.react'; +import Constants from '../constants'; +import {ConflictModal} from './modals'; +import {isEmpty} from 'lodash'; + +const {PasswordModal} = AuthComponents; + +export default class NewReplicationController extends React.Component { + constructor (props) { + super(props); + this.submit = this.submit.bind(this); + this.clear = this.clear.bind(this); + this.showPasswordModal = this.showPasswordModal.bind(this); + this.runReplicationChecks = this.runReplicationChecks.bind(this); + } + + clear (e) { + e.preventDefault(); + this.props.clearReplicationForm(); + } + + showPasswordModal () { + this.props.hideConflictModal(); + const { replicationSource, replicationTarget } = this.props; + + const hasLocalSourceOrTarget = (replicationSource === Constants.REPLICATION_SOURCE.LOCAL || + replicationTarget === Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE || + replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE); + + // if the user is authenticated, or if NEITHER the source nor target are local, just submit. The password + // modal isn't necessary or if couchdb is in admin party mode + if (!hasLocalSourceOrTarget || this.props.authenticated || FauxtonAPI.session.isAdminParty()) { + this.submit(this.props.username, this.props.password); + return; + } + + this.props.showPasswordModal(); + } + + checkReplicationDocID () { + const {showConflictModal, replicationDocName, checkReplicationDocID} = this.props; + checkReplicationDocID(replicationDocName).then(existingDoc => { + if (existingDoc) { + showConflictModal(); + return; + } + + this.showPasswordModal(); + }); + } + + runReplicationChecks () { + const {replicationDocName} = this.props; + if (!this.validate()) { + return; + } + if (replicationDocName) { + this.checkReplicationDocID(); + return; + } + + this.showPasswordModal(); + } + + validate () { + const { + remoteTarget, + remoteSource, + replicationTarget, + replicationSource, + localTarget, + localSource, + databases + } = this.props; + + if (replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE && _.contains(databases, localTarget)) { + FauxtonAPI.addNotification({ + msg: 'The ' + localTarget + ' database already exists locally. Please enter another database name.', + type: 'error', + escape: false, + clear: true + }); + return false; + } + if (replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE || + replicationTarget === Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE) { + let error = ''; + if (/\s/.test(localTarget)) { + error = 'The target database may not contain any spaces.'; + } else if (/^_/.test(localTarget)) { + error = 'The target database may not start with an underscore.'; + } + + if (error) { + FauxtonAPI.addNotification({ + msg: error, + type: 'error', + escape: false, + clear: true + }); + return false; + } + } + + //check that source and target are not the same. They can trigger a false positive if they are "" + if ((remoteTarget === remoteSource && !isEmpty(remoteTarget)) + || (localSource === localTarget && !isEmpty(localSource))) { + FauxtonAPI.addNotification({ + msg: 'Cannot replicate a database to itself', + type: 'error', + escape: false, + clear: true + }); + + return false; + } + + return true; + } + + submit (username, password) { + const { + replicationTarget, + replicationSource, + replicationType, + replicationDocName, + remoteTarget, + remoteSource, + localTarget, + localSource + } = this.props; + + let _rev; + if (replicationDocName) { + const doc = this.props.docs.find(doc => doc._id === replicationDocName); + if (doc) { + _rev = doc._rev; + } + } + + this.props.replicate({ + replicationTarget, + replicationSource, + replicationType, + replicationDocName, + username, + password, + localTarget: localTarget, + localSource: localSource, + remoteTarget, + remoteSource, + _rev + }); + } + + confirmButtonEnabled () { + const { + remoteSource, + localSourceKnown, + replicationSource, + replicationTarget, + localTargetKnown, + localTarget, + submittedNoChange, + } = this.props; + + if (submittedNoChange) { + return false; + } + + if (!replicationSource || !replicationTarget) { + return false; + } + + if (replicationSource === Constants.REPLICATION_SOURCE.LOCAL && !localSourceKnown) { + return false; + } + if (replicationTarget === Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE && !localTargetKnown) { + return false; + } + + if (replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE && !localTarget) { + return false; + } + + if (replicationSource === Constants.REPLICATION_SOURCE.REMOTE && remoteSource === "") { + return false; + } + + return true; + } + + render () { + const { + replicationSource, + replicationTarget, + replicationType, + replicationDocName, + passwordModalVisible, + conflictModalVisible, + databases, + localSource, + remoteSource, + remoteTarget, + localTarget, + updateFormField, + clearReplicationForm + } = this.props; + + return ( +
    + +
    + +
    + + + {app.i18n.en_US['replication-password-modal-text']}

    } + submitBtnLabel="Start Replication" + headerTitle={app.i18n.en_US['replication-password-modal-header']} + onSuccess={this.submit} /> + +
    + ); + } +} http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/replication/components/options.js ---------------------------------------------------------------------- diff --git a/app/addons/replication/components/options.js b/app/addons/replication/components/options.js new file mode 100644 index 0000000..acb2501 --- /dev/null +++ b/app/addons/replication/components/options.js @@ -0,0 +1,97 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +import React from 'react'; +import Constants from '../constants'; +import Components from '../../components/react-components.react'; +import ReactSelect from 'react-select'; + +const { StyledSelect } = Components; + +const getReplicationTypeOptions = () => { + return [ + { value: Constants.REPLICATION_TYPE.ONE_TIME, label: 'One time' }, + { value: Constants.REPLICATION_TYPE.CONTINUOUS, label: 'Continuous' } + ].map(option => ); +}; + +const ReplicationType = ({value, onChange}) => { + return ( +
    +
    + Replication Type: +
    +
    + onChange(e.target.value)} + selectId="replication-target" + selectValue={value} /> +
    +
    + ); +}; + +ReplicationType.propTypes = { + value: React.PropTypes.string.isRequired, + onChange: React.PropTypes.func.isRequired +}; + +const ReplicationDoc = ({value, onChange}) => +
    +
    + Replication Document: +
    +
    + onChange('')} /> + onChange(e.target.value)} + /> +
    +
    ; + +ReplicationDoc.propTypes = { + value: React.PropTypes.string.isRequired, + onChange: React.PropTypes.func.isRequired +}; + +export class ReplicationOptions extends React.Component { + + render () { + const {replicationType, replicationDocName, onDocChange, onTypeChange} = this.props; + + return ( +
    + + +
    + ); + } + +} + +ReplicationOptions.propTypes = { + replicationDocName: React.PropTypes.string.isRequired, + replicationType: React.PropTypes.string.isRequired, + onDocChange: React.PropTypes.func.isRequired, + onTypeChange: React.PropTypes.func.isRequired +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ff25441b/app/addons/replication/components/remoteexample.js ---------------------------------------------------------------------- diff --git a/app/addons/replication/components/remoteexample.js b/app/addons/replication/components/remoteexample.js new file mode 100644 index 0000000..1992b32 --- /dev/null +++ b/app/addons/replication/components/remoteexample.js @@ -0,0 +1,50 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +import React from 'react'; +import {OverlayTrigger, Tooltip} from 'react-bootstrap'; + +const tooltipExisting = ( + +

    + If you know the credentials for the remote account, you can use that remote username and password. +

    +

    + If a remote database granted permissions to your local account, you can use the local-account username and password. +

    +

    + If the remote database granted permissions to "everybody," you do not need to enter a username and password. +

    +
    +); + +const tooltipNew = ( + + Enter the username and password of the remote account. + +); + +const RemoteExample = ({newRemote}) => { + const newRemoteText = newRemote ? 'If a "new" database already exists, data will replicate into that existing database.' : null; + return ( +
    + https://$USERNAME:$PASSWORD@$REMOTE_SERVER/$DATABASE +   + + + +

    {newRemoteText}

    +
    + ); +}; + +export default RemoteExample;