diff --git a/.travis.yml b/.travis.yml
index 48d54d34e..7eccf52bd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,13 @@
language: node_js
node_js:
- 6
-sudo: required
+sudo: false
services:
- docker
git:
depth: 1
before_script:
- - ./bin/install-docker-travis.sh
- npm run docker:up
- npm install
- docker logs couchdb
@@ -19,6 +18,8 @@ before_script:
- grunt debugDev
- DIST=./dist/debug ./bin/fauxton &
- sleep 30
+ - docker logs couchdb
+ - curl http://127.0.0.1:5984
script:
- travis_retry ./node_modules/.bin/grunt nightwatch
after_script:
diff --git a/app/addons/components/assets/less/styled-select.less b/app/addons/components/assets/less/styled-select.less
index 12275273a..45e94e730 100644
--- a/app/addons/components/assets/less/styled-select.less
+++ b/app/addons/components/assets/less/styled-select.less
@@ -31,6 +31,13 @@
color: #333;
}
+//bug in firefox for text-indent
+@-moz-document url-prefix() {
+ .styled-select select {
+ text-indent: 0px;
+ }
+}
+
.styled-select select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 #000;
diff --git a/app/addons/databases/components.react.jsx b/app/addons/databases/components.react.jsx
index cddc6027e..58cf05301 100644
--- a/app/addons/databases/components.react.jsx
+++ b/app/addons/databases/components.react.jsx
@@ -182,7 +182,7 @@ var DatabaseRow = React.createClass({
+ href={"#/replication/_create/" + encodedId} />
diff --git a/app/addons/replication/__tests__/actions.test.js b/app/addons/replication/__tests__/actions.test.js
new file mode 100644
index 000000000..804aa6bab
--- /dev/null
+++ b/app/addons/replication/__tests__/actions.test.js
@@ -0,0 +1,182 @@
+// 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 {replicate, getReplicationStateFrom, deleteDocs} from '../actions';
+import ActionTypes from '../actiontypes';
+import fetchMock from 'fetch-mock';
+import app from '../../../app';
+import FauxtonAPI from '../../../core/api';
+
+app.session = {
+ get () {
+ return 'test-user-name';
+ }
+};
+
+Object.defineProperty(window.location, 'origin', {
+ writable: true,
+ value: 'http://dev:8000'
+});
+
+const assert = utils.assert;
+
+describe("Replication Actions", () => {
+
+ describe('replicate', () => {
+ afterEach(fetchMock.restore);
+
+ it('creates a new database if it does not exist', (done) => {
+ fetchMock.postOnce('/_replicator', {
+ status: 404,
+ body: {
+ error: "not_found",
+ reason: "Database does not exist."
+ }
+ });
+
+ fetchMock.putOnce('/_replicator', {
+ status: 200,
+ body: {
+ ok: true
+ }
+ });
+
+ const finalPost = fetchMock.postOnce('/_replicator', {
+ status: 200,
+ body: {
+ ok: true
+ }
+ });
+
+ replicate ({
+ localSource: "animaldb",
+ localTarget: "boom123",
+ password: "testerpass",
+ remoteSource: "",
+ remoteTarget: "",
+ replicationDocName: "",
+ replicationSource: "REPLICATION_SOURCE_LOCAL",
+ replicationTarget: "REPLICATION_TARGET_NEW_LOCAL_DATABASE",
+ replicationType: "",
+ username: "tester"
+ });
+
+ //this is not pretty, and might cause some false errors. But its tricky to tell when this test has completed
+ setTimeout(() => {
+ assert.ok(finalPost.called('/_replicator'));
+ done();
+ }, 100);
+ });
+ });
+
+ describe('getReplicationStateFrom', () => {
+ const doc = {
+ "_id": "7dcea9874a8fcb13c6630a1547001559",
+ "_rev": "2-98d29cc74e77b6dc38f5fc0dcec0033c",
+ "user_ctx": {
+ "name": "tester",
+ "roles": [
+ "_admin",
+ "_reader",
+ "_writer"
+ ]
+ },
+ "source": {
+ "headers": {
+ "Authorization": "Basic dGVzdGVyOnRlc3RlcnBhc3M="
+ },
+ "url": "http://dev:8000/animaldb"
+ },
+ "target": {
+ "headers": {
+ "Authorization": "Basic dGVzdGVyOnRlc3RlcnBhc3M="
+ },
+ "url": "http://dev:8000/boom123"
+ },
+ "create_target": true,
+ "continuous": false,
+ "owner": "tester",
+ "_replication_id": "90ff5a45623aa6821a6b0c20f5d3b5e8"
+ };
+
+ const docState = {
+ "replicationDocName": "7dcea9874a8fcb13c6630a1547001559",
+ "replicationType": "REPLICATION_TYPE_ONE_TIME",
+ "replicationSource": "REPLICATION_SOURCE_LOCAL",
+ "localSource": "animaldb",
+ "replicationTarget": "REPLICATION_TARGET_EXISTING_LOCAL_DATABASE",
+ "localTarget": "boom123"
+ };
+
+ it('builds up correct state', (done) => {
+ FauxtonAPI.dispatcher.register(({type, options}) => {
+ if (ActionTypes.REPLICATION_SET_STATE_FROM_DOC === type) {
+ assert.deepEqual(docState, options);
+ setTimeout(done);
+ }
+ });
+
+ fetchMock.getOnce('/_replicator/7dcea9874a8fcb13c6630a1547001559', doc);
+ getReplicationStateFrom(doc._id);
+ });
+ });
+
+ describe('deleteDocs', () => {
+ it('sends bulk doc request', (done) => {
+ const resp = [
+ {
+ "ok": true,
+ "id": "should-fail",
+ "rev": "32-14e8495723c34271ef1391adf83defc2"
+ },
+ {
+ "ok": true,
+ "id": "my-cool-id",
+ "rev": "3-f16f14d11708952b3d787846ef6ef8a9"
+ }
+ ];
+
+ const docs = [
+ {
+ _id: "should-fail",
+ _rev: "31-cdc233eb8a98e3aa3a87cd72f6a86301",
+ raw: {
+ _id: "should-fail",
+ _rev: "31-cdc233eb8a98e3aa3a87cd72f6a86301"
+ },
+ },
+ {
+ _id: "my-cool-id",
+ _rev: "2-da6af558740409e61d563769a8044a68",
+ raw: {
+ _id: "my-cool-id",
+ _rev: "2-da6af558740409e61d563769a8044a68"
+ }
+ }
+ ];
+
+ fetchMock.getOnce('/_scheduler/job', 404);
+ fetchMock.getOnce('/_replicator/_all_docs?include_docs=true&limit=100', {rows: []});
+ fetchMock.postOnce('/_replicator/_bulk_docs', {
+ status: 200,
+ body: resp
+ });
+ deleteDocs(docs);
+
+ FauxtonAPI.dispatcher.register(({type}) => {
+ if (ActionTypes.REPLICATION_CLEAR_SELECTED_DOCS === type) {
+ setTimeout(done);
+ }
+ });
+ });
+ });
+});
diff --git a/app/addons/replication/__tests__/api.tests.js b/app/addons/replication/__tests__/api.tests.js
index 3ebb79273..54721c2f4 100644
--- a/app/addons/replication/__tests__/api.tests.js
+++ b/app/addons/replication/__tests__/api.tests.js
@@ -21,9 +21,12 @@ import {
decodeFullUrl,
getCredentialsFromUrl,
removeCredentialsFromUrl,
- removeSensitiveUrlInfo
+ removeSensitiveUrlInfo,
+ supportNewApi,
+ fetchReplicationDocs
} from '../api';
import Constants from '../constants';
+import fetchMock from 'fetch-mock';
const assert = utils.assert;
@@ -299,6 +302,146 @@ describe('Replication API', () => {
const url = removeCredentialsFromUrl("https://bob:m@:/rley@my-couchdb.com/db");
assert.deepEqual(url, 'https://my-couchdb.com/db');
});
+ });
+
+ describe('supportNewApi', () => {
+ afterEach(() => {
+ fetchMock.restore();
+ });
+
+ it('returns true for support', () => {
+ fetchMock.getOnce('/_scheduler/jobs', 200);
+ return supportNewApi(true)
+ .then(resp => {
+ assert.ok(resp);
+ });
+ });
+
+ it('returns false for no support', () => {
+ fetchMock.getOnce('/_scheduler/jobs', 404);
+ return supportNewApi(true)
+ .then(resp => {
+ assert.notOk(resp);
+ });
+ });
+
+ });
+ describe("fetchReplicationDocs", () => {
+ const _repDocs = {
+ "total_rows":2,
+ "offset":0,
+ "rows":[
+ {
+ "id":"_design/_replicator",
+ "key":"_design/_replicator",
+ "value":{
+ "rev":"1-1390740c4877979dbe8998382876556c"
+ },
+ "doc":{"_id":"_design/_replicator",
+ "_rev":"1-1390740c4877979dbe8998382876556c",
+ "language":"javascript",
+ "validate_doc_update":"\n function(newDoc, oldDoc, userCtx) {\n function reportError(error_msg) {\n log('Error writing document `' + newDoc._id +\n '\\' to the replicator database: ' + error_msg);\n throw({forbidden: error_msg});\n }\n\n function validateEndpoint(endpoint, fieldName) {\n if ((typeof endpoint !== 'string') &&\n ((typeof endpoint !== 'object') || (endpoint === null))) {\n\n reportError('The `' + fieldName + '\\' property must exist' +\n ' and be either a string or an object.');\n }\n\n if (typeof endpoint === 'object') {\n if ((typeof endpoint.url !== 'string') || !endpoint.url) {\n reportError('The url property must exist in the `' +\n fieldName + '\\' field and must be a non-empty string.');\n }\n\n if ((typeof endpoint.auth !== 'undefined') &&\n ((typeof endpoint.auth !== 'object') ||\n endpoint.auth === null)) {\n\n reportError('`' + fieldName +\n '.auth\\' must be a non-null object.');\n }\n\n if ((typeof endpoint.headers !== 'undefined') &&\n ((typeof endpoint.headers !== 'object') ||\n endpoint.headers === null)) {\n\n reportError('`' + fieldName +\n '.headers\\' must be a non-null object.');\n }\n }\n }\n\n var isReplicator = (userCtx.roles.indexOf('_replicator') >= 0);\n var isAdmin = (userCtx.roles.indexOf('_admin') >= 0);\n\n if (oldDoc && !newDoc._deleted && !isReplicator &&\n (oldDoc._replication_state === 'triggered')) {\n reportError('Only the replicator can edit replication documents ' +\n 'that are in the triggered state.');\n }\n\n if (!newDoc._deleted) {\n validateEndpoint(newDoc.source, 'source');\n validateEndpoint(newDoc.target, 'target');\n\n if ((typeof newDoc.create_target !== 'undefined') &&\n (typeof newDoc.create_target !== 'boolean')) {\n\n reportError('The `create_target\\' field must be a boolean.');\n }\n\n if ((typeof newDoc.continuous !== 'undefined') &&\n (typeof newDoc.continuous !== 'boolean')) {\n\n reportError('The `continuous\\' field must be a boolean.');\n }\n\n if ((typeof newDoc.doc_ids !== 'undefined') &&\n !isArray(newDoc.doc_ids)) {\n\n reportError('The `doc_ids\\' field must be an array of strings.');\n }\n\n if ((typeof newDoc.selector !== 'undefined') &&\n (typeof newDoc.selector !== 'object')) {\n\n reportError('The `selector\\' field must be an object.');\n }\n\n if ((typeof newDoc.filter !== 'undefined') &&\n ((typeof newDoc.filter !== 'string') || !newDoc.filter)) {\n\n reportError('The `filter\\' field must be a non-empty string.');\n }\n\n if ((typeof newDoc.doc_ids !== 'undefined') &&\n (typeof newDoc.selector !== 'undefined')) {\n\n reportError('`doc_ids\\' field is incompatible with `selector\\'.');\n }\n\n if ( ((typeof newDoc.doc_ids !== 'undefined') ||\n (typeof newDoc.selector !== 'undefined')) &&\n (typeof newDoc.filter !== 'undefined') ) {\n\n reportError('`filter\\' field is incompatible with `selector\\' and `doc_ids\\'.');\n }\n\n if ((typeof newDoc.query_params !== 'undefined') &&\n ((typeof newDoc.query_params !== 'object') ||\n newDoc.query_params === null)) {\n\n reportError('The `query_params\\' field must be an object.');\n }\n\n if (newDoc.user_ctx) {\n var user_ctx = newDoc.user_ctx;\n\n if ((typeof user_ctx !== 'object') || (user_ctx === null)) {\n reportError('The `user_ctx\\' property must be a ' +\n 'non-null object.');\n }\n\n if (!(user_ctx.name === null ||\n (typeof user_ctx.name === 'undefined') ||\n ((typeof user_ctx.name === 'string') &&\n user_ctx.name.length > 0))) {\n\n reportError('The `user_ctx.name\\' property must be a ' +\n 'non-empty string or null.');\n }\n\n if (!isAdmin && (user_ctx.name !== userCtx.name)) {\n reportError('The given `user_ctx.name\\' is not valid');\n }\n\n if (user_ctx.roles && !isArray(user_ctx.roles)) {\n reportError('The `user_ctx.roles\\' property must be ' +\n 'an array of strings.');\n }\n\n if (!isAdmin && user_ctx.roles) {\n for (var i = 0; i < user_ctx.roles.length; i++) {\n var role = user_ctx.roles[i];\n\n if (typeof role !== 'string' || role.length === 0) {\n reportError('Roles must be non-empty strings.');\n }\n if (userCtx.roles.indexOf(role) === -1) {\n reportError('Invalid role (`' + role +\n '\\') in the `user_ctx\\'');\n }\n }\n }\n } else {\n if (!isAdmin) {\n reportError('The `user_ctx\\' property is missing (it is ' +\n 'optional for admins only).');\n }\n }\n } else {\n if (!isAdmin) {\n if (!oldDoc.user_ctx || (oldDoc.user_ctx.name !== userCtx.name)) {\n reportError('Replication documents can only be deleted by ' +\n 'admins or by the users who created them.');\n }\n }\n }\n }\n"
+ }
+ },
+ {
+ "id":"c94d4839d1897105cb75e1251e0003ea",
+ "key":"c94d4839d1897105cb75e1251e0003ea",
+ "value":{
+ "rev":"3-4559cb522de85ce03bd0e1991025e89a"
+ },
+ "doc":{"_id":"c94d4839d1897105cb75e1251e0003ea",
+ "_rev":"3-4559cb522de85ce03bd0e1991025e89a",
+ "user_ctx":{
+ "name":"tester",
+ "roles":["_admin", "_reader", "_writer"]},
+ "source":{
+ "headers":{
+ "Authorization":"Basic dGVzdGVyOnRlc3RlcnBhc3M="
+ },
+ "url":"http://dev:5984/animaldb"},
+ "target":{
+ "headers":{
+ "Authorization":"Basic dGVzdGVyOnRlc3RlcnBhc3M="},
+ "url":"http://dev:5984/animaldb-clone"
+ },
+ "create_target":true,
+ "continuous":false,
+ "owner":"tester",
+ "_replication_state":"completed",
+ "_replication_state_time":"2017-02-28T12:16:28+00:00",
+ "_replication_id":"0ce2939af29317b5dbe11c15570ddfda",
+ "_replication_stats":{
+ "revisions_checked":14,
+ "missing_revisions_found":14,
+ "docs_read":14,
+ "docs_written":14,
+ "changes_pending":null,
+ "doc_write_failures":0,
+ "checkpointed_source_seq":"15-g1AAAAJDeJyV0N0NgjAQAOAKRnlzBJ3AcKWl9Uk20ZbSEII4gm6im-gmugke1AQJ8aFpck3u50vuakJIVIaGrJqzKSADKrYxPqixECii123bVmWoFidMLGVsqEjYtP0voTcY9f6rzHqFKcglsz5K1imHkcJTnoJVPsqxUy4jxepEioJ7KM0cI7nih9BtkDSlkAif2zjp7qRHJwW9lLNdDkZ6S08nvQZJMsNT4b_d20k_d4oVE1aK6VT1AXTajes"
+ }
+ }
+ }
+ ]};
+
+ const _schedDocs = {
+ "offset": 0,
+ "docs": [
+ {
+ "database":"_replicator",
+ "doc_id":"c94d4839d1897105cb75e1251e0003ea",
+ "id":null,
+ "state":"completed",
+ "error_count":0,
+ "info":{
+ "revisions_checked":0,
+ "missing_revisions_found":0,
+ "docs_read":0,
+ "docs_written":0,
+ "changes_pending":null,
+ "doc_write_failures":0,
+ "checkpointed_source_seq":"56-g1AAAAGweJzLYWBgYMlgTmFQTElKzi9KdUhJMjTQy00tyixJTE_VS87JL01JzCvRy0styQEqZUpkSLL___9_VgZzIm8uUIDd1NIkNSk5LYVBAW6AKXb9aLYY47ElyQFIJtVDLeIBW2ScbGJiYGJKjBloNhnisSmPBUgyNAApoGX7QbaJg21LTDEwNE8zR_aWCVGW4VCFZNkBiGVgr3GALTNLSzQ0T0xEtgyHm7MAbEaMZw"},
+ "last_updated":"2017-03-07T14:46:17Z",
+ "start_time":"2017-03-07T14:46:16Z"
+ }
+ ],
+ "total": 1
+ };
+
+ describe('old api', () => {
+ afterEach(() => {
+ fetchMock.restore();
+ });
+
+ it("returns parsedReplicationDocs", () => {
+ fetchMock.getOnce('/_scheduler/jobs', 404);
+ fetchMock.get('/_replicator/_all_docs?include_docs=true&limit=100', _repDocs);
+ return supportNewApi(true)
+ .then(fetchReplicationDocs)
+ .then(docs => {
+ assert.deepEqual(docs.length, 1);
+ assert.deepEqual(docs[0]._id, "c94d4839d1897105cb75e1251e0003ea");
+ });
+ });
+ });
+
+ describe('new api', () => {
+ afterEach(() => {
+ fetchMock.restore();
+ });
+
+ it("returns parsedReplicationDocs", () => {
+ fetchMock.getOnce('/_scheduler/jobs', 200);
+ fetchMock.get('/_replicator/_all_docs?include_docs=true&limit=100', _repDocs);
+ fetchMock.get('/_scheduler/docs?include_docs=true', _schedDocs);
+ return supportNewApi(true)
+ .then(fetchReplicationDocs)
+ .then(docs => {
+ assert.deepEqual(docs.length, 1);
+ assert.deepEqual(docs[0]._id, "c94d4839d1897105cb75e1251e0003ea");
+ assert.deepEqual(docs[0].stateTime, new Date('2017-03-07T14:46:17'));
+ });
+ });
+ });
});
});
diff --git a/app/addons/replication/tests/storesSpec.js b/app/addons/replication/__tests__/stores.tests.js
similarity index 100%
rename from app/addons/replication/tests/storesSpec.js
rename to app/addons/replication/__tests__/stores.tests.js
diff --git a/app/addons/replication/actions.js b/app/addons/replication/actions.js
index cdf5bfb6d..83abb240e 100644
--- a/app/addons/replication/actions.js
+++ b/app/addons/replication/actions.js
@@ -9,12 +9,20 @@
// 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';
import Constants from './constants';
-import {createReplicationDoc, fetchReplicationDocs, decodeFullUrl} from './api';
+import {
+ supportNewApi,
+ createReplicationDoc,
+ fetchReplicateInfo,
+ fetchReplicationDocs,
+ decodeFullUrl,
+ deleteReplicatesApi,
+ createReplicatorDB
+} from './api';
+import 'whatwg-fetch';
function initReplicator (localSource) {
@@ -26,11 +34,16 @@ function initReplicator (localSource) {
}
});
}
- $.ajax({
- url: app.host + '/_all_dbs',
- contentType: 'application/json',
- dataType: 'json'
- }).then((databases) => {
+
+ fetch('/_all_dbs', {
+ credentials: 'include',
+ headers: {
+ 'Accept': 'application/json; charset=utf-8',
+ 'Content-Type': 'application/json'
+ },
+ })
+ .then(resp => resp.json())
+ .then((databases) => {
FauxtonAPI.dispatch({
type: ActionTypes.REPLICATION_DATABASES_LOADED,
options: {
@@ -40,16 +53,19 @@ function initReplicator (localSource) {
});
}
-function replicate (params) {
+export const replicate = (params) => {
const replicationDoc = createReplicationDoc(params);
- const promise = $.ajax({
- url: window.location.origin + '/_replicator',
- contentType: 'application/json',
- type: 'POST',
- dataType: 'json',
- data: JSON.stringify(replicationDoc)
- });
+ const promise = fetch('/_replicator', {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Accept': 'application/json; charset=utf-8',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(replicationDoc)
+ })
+ .then(res => res.json());
const source = Helpers.getDatabaseLabel(replicationDoc.source);
const target = Helpers.getDatabaseLabel(replicationDoc.target);
@@ -58,22 +74,37 @@ function replicate (params) {
type: ActionTypes.REPLICATION_STARTING,
});
- promise.then(() => {
+ const handleError = (json) => {
FauxtonAPI.addNotification({
- msg: `Replication from ${decodeURIComponent(source)} to ${decodeURIComponent(target)} has been scheduled.`,
- type: 'success',
- escape: false,
+ msg: json.reason,
+ type: 'error',
clear: true
});
- }, (xhr) => {
- const errorMessage = JSON.parse(xhr.responseText);
+ };
+
+ promise.then(json => {
+ if (!json.ok) {
+ throw json;
+ }
+
FauxtonAPI.addNotification({
- msg: errorMessage.reason,
- type: 'error',
+ msg: `Replication from ${decodeURIComponent(source)} to ${decodeURIComponent(target)} has been scheduled.`,
+ type: 'success',
+ escape: false,
clear: true
});
+ })
+ .catch(json => {
+ if (json.error && json.error === "not_found") {
+ return createReplicatorDB().then(() => {
+ return replicate(params);
+ })
+ .catch(handleError);
+ }
+
+ handleError(json);
});
-}
+};
function updateFormField (fieldName, value) {
FauxtonAPI.dispatch({
@@ -89,12 +120,12 @@ function clearReplicationForm () {
FauxtonAPI.dispatch({ type: ActionTypes.REPLICATION_CLEAR_FORM });
}
-const getReplicationActivity = () => {
+const getReplicationActivity = (supportNewApi) => {
FauxtonAPI.dispatch({
type: ActionTypes.REPLICATION_FETCHING_STATUS,
});
- fetchReplicationDocs().then(docs => {
+ fetchReplicationDocs(supportNewApi).then(docs => {
FauxtonAPI.dispatch({
type: ActionTypes.REPLICATION_STATUS,
options: docs
@@ -102,6 +133,27 @@ const getReplicationActivity = () => {
});
};
+const getReplicateActivity = () => {
+ supportNewApi()
+ .then(newApi => {
+ if (!newApi) {
+ return;
+ }
+
+ FauxtonAPI.dispatch({
+ type: ActionTypes.REPLICATION_FETCHING_REPLICATE_STATUS,
+ });
+
+ fetchReplicateInfo()
+ .then(replicateInfo => {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.REPLICATION_REPLICATE_STATUS,
+ options: replicateInfo
+ });
+ });
+ });
+};
+
const filterDocs = (filter) => {
FauxtonAPI.dispatch({
type: ActionTypes.REPLICATION_FILTER_DOCS,
@@ -109,6 +161,13 @@ const filterDocs = (filter) => {
});
};
+const filterReplicate = (filter) => {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.REPLICATION_FILTER_REPLICATE,
+ options: filter
+ });
+};
+
const selectAllDocs = () => {
FauxtonAPI.dispatch({
type: ActionTypes.REPLICATION_TOGGLE_ALL_DOCS
@@ -122,13 +181,32 @@ const selectDoc = (id) => {
});
};
+const selectAllReplicates = () => {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.REPLICATION_TOGGLE_ALL_REPLICATE
+ });
+};
+
+const selectReplicate = (id) => {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.REPLICATION_TOGGLE_REPLICATE,
+ options: id
+ });
+};
+
const clearSelectedDocs = () => {
FauxtonAPI.dispatch({
type: ActionTypes.REPLICATION_CLEAR_SELECTED_DOCS
});
};
-const deleteDocs = (docs) => {
+const clearSelectedReplicates = () => {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.REPLICATION_CLEAR_SELECTED_REPLICATES
+ });
+};
+
+export const deleteDocs = (docs) => {
const bulkDocs = docs.map(({raw: doc}) => {
doc._deleted = true;
return doc;
@@ -141,13 +219,23 @@ const deleteDocs = (docs) => {
clear: true
});
- $.ajax({
- url: app.host + '/_replicator/_bulk_docs',
- contentType: 'application/json',
- dataType: 'json',
+ fetch('/_replicator/_bulk_docs', {
+ credentials: 'include',
+ headers: {
+ 'Accept': 'application/json; charset=utf-8',
+ 'Content-Type': 'application/json'
+ },
method: 'POST',
- data: JSON.stringify({docs: bulkDocs})
- }).then(() => {
+ body: JSON.stringify({docs: bulkDocs})
+ })
+ .then(resp => {
+ if (!resp.ok) {
+ throw resp;
+ }
+ return resp.json();
+ })
+ .then(() => {
+
let msg = 'The selected documents have been deleted.';
if (docs.length === 1) {
msg = `Document ${docs[0]._id} has been deleted`;
@@ -159,8 +247,47 @@ const deleteDocs = (docs) => {
escape: false,
clear: true
});
+
clearSelectedDocs();
getReplicationActivity();
+ })
+ .catch(resp => {
+ resp.json()
+ .then(error => {
+ FauxtonAPI.addNotification({
+ msg: error.reason,
+ type: 'error',
+ clear: true
+ });
+ });
+
+ });
+};
+
+const deleteReplicates = (replicates) => {
+ FauxtonAPI.addNotification({
+ msg: `Deleting _replicate${replicates.length > 1 ? 's' : ''}.`,
+ type: 'success',
+ escape: false,
+ clear: true
+ });
+
+ deleteReplicatesApi(replicates)
+ .then(() => {
+ let msg = 'The selected replications have been deleted.';
+ if (replicates.length === 1) {
+ msg = `Replication ${replicates[0]._id} has been deleted`;
+ }
+
+ clearSelectedReplicates();
+ getReplicateActivity();
+
+ FauxtonAPI.addNotification({
+ msg: msg,
+ type: 'success',
+ escape: false,
+ clear: true
+ });
}, (xhr) => {
const errorMessage = JSON.parse(xhr.responseText);
FauxtonAPI.addNotification({
@@ -171,13 +298,20 @@ const deleteDocs = (docs) => {
});
};
-const getReplicationStateFrom = (id) => {
- $.ajax({
- url: `${app.host}/_replicator/${encodeURIComponent(id)}`,
- contentType: 'application/json',
- dataType: 'json',
+export const getReplicationStateFrom = (id) => {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.REPLICATION_FETCHING_FORM_STATE
+ });
+
+ fetch(`/_replicator/${encodeURIComponent(id)}`, {
+ credentials: 'include',
+ headers: {
+ 'Accept': 'application/json; charset=utf-8',
+ },
method: 'GET'
- }).then((doc) => {
+ })
+ .then(resp => resp.json())
+ .then((doc) => {
const stateDoc = {
replicationDocName: doc._id,
replicationType: doc.continuous ? Constants.REPLICATION_TYPE.CONTINUOUS : Constants.REPLICATION_TYPE.ONE_TIME,
@@ -209,10 +343,10 @@ const getReplicationStateFrom = (id) => {
options: stateDoc
});
- }, (xhr) => {
- const errorMessage = JSON.parse(xhr.responseText);
+ })
+ .catch(error => {
FauxtonAPI.addNotification({
- msg: errorMessage.reason,
+ msg: error.reason,
type: 'error',
clear: true
});
@@ -238,7 +372,28 @@ const changeActivitySort = (sort) => {
});
};
+const changeTabSection = (newSection, url) => {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.REPLICATION_CHANGE_TAB_SECTION,
+ options: newSection
+ });
+
+ if (url) {
+ FauxtonAPI.navigate(url, {trigger: false});
+ }
+};
+
+const checkForNewApi = () => {
+ supportNewApi().then(newApi => {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.REPLICATION_SUPPORT_NEW_API,
+ options: newApi
+ });
+ });
+};
+
export default {
+ checkForNewApi,
initReplicator,
replicate,
updateFormField,
@@ -252,5 +407,11 @@ export default {
showConflictModal,
hideConflictModal,
changeActivitySort,
- clearSelectedDocs
+ clearSelectedDocs,
+ changeTabSection,
+ getReplicateActivity,
+ filterReplicate,
+ selectReplicate,
+ selectAllReplicates,
+ deleteReplicates
};
diff --git a/app/addons/replication/actiontypes.js b/app/addons/replication/actiontypes.js
index f2bb47828..5089ea4ca 100644
--- a/app/addons/replication/actiontypes.js
+++ b/app/addons/replication/actiontypes.js
@@ -27,5 +27,14 @@ export default {
REPLICATION_SHOW_CONFLICT_MODAL: 'REPLICATION_SHOW_CONFLICT_MODAL',
REPLICATION_HIDE_CONFLICT_MODAL: 'REPLICATION_HIDE_CONFLICT_MODAL',
REPLICATION_CHANGE_ACTIVITY_SORT: 'REPLICATION_CHANGE_ACTIVITY_SORT',
- REPLICATION_CLEAR_SELECTED_DOCS: 'REPLICATION_CLEAR_SELECTED_DOCS'
+ REPLICATION_CLEAR_SELECTED_DOCS: 'REPLICATION_CLEAR_SELECTED_DOCS',
+ REPLICATION_CHANGE_TAB_SECTION: 'REPLICATION_CHANGE_TAB_SECTION',
+ REPLICATION_FETCHING_REPLICATE_STATUS: 'REPLICATION_FETCHING_REPLICATE_STATUS',
+ REPLICATION_REPLICATE_STATUS: 'REPLICATION_REPLICATE_STATUS',
+ REPLICATION_SUPPORT_NEW_API: 'REPLICATION_SUPPORT_NEW_API',
+ REPLICATION_FILTER_REPLICATE: 'REPLICATION_FILTER_REPLICATE',
+ REPLICATION_TOGGLE_ALL_REPLICATE: 'REPLICATION_TOGGLE_ALL_REPLICATE',
+ REPLICATION_TOGGLE_REPLICATE: 'REPLICATION_TOGGLE_REPLICATE',
+ REPLICATION_CLEAR_SELECTED_REPLICATES: 'REPLICATION_CLEAR_SELECTED_REPLICATES',
+ REPLICATION_FETCHING_FORM_STATE: 'REPLICATION_FETCHING_FORM_STATE'
};
diff --git a/app/addons/replication/api.js b/app/addons/replication/api.js
index 0bda51786..6f2942323 100644
--- a/app/addons/replication/api.js
+++ b/app/addons/replication/api.js
@@ -16,6 +16,30 @@ import app from '../../app';
import FauxtonAPI from '../../core/api';
import base64 from 'base-64';
import _ from 'lodash';
+import 'whatwg-fetch';
+
+let newApiPromise = null;
+export const supportNewApi = (forceCheck) => {
+ if (!newApiPromise || forceCheck) {
+ newApiPromise = new FauxtonAPI.Promise((resolve) => {
+ fetch('/_scheduler/jobs', {
+ credentials: 'include',
+ headers: {
+ 'Accept': 'application/json; charset=utf-8',
+ }
+ })
+ .then(resp => {
+ if (resp.status > 202) {
+ return resolve(false);
+ }
+
+ resolve(true);
+ });
+ });
+ }
+
+ return newApiPromise;
+};
export const encodeFullUrl = (fullUrl) => {
if (!fullUrl) {return '';}
@@ -208,6 +232,9 @@ export const removeSensitiveUrlInfo = (url) => {
export const getDocUrl = (doc) => {
let url = doc;
+ if (!doc) {
+ return '';
+ }
if (typeof doc === "object") {
url = doc.url;
@@ -228,34 +255,98 @@ export const parseReplicationDocs = (rows) => {
status: doc._replication_state,
errorMsg: doc._replication_state_reason ? doc._replication_state_reason : '',
statusTime: new Date(doc._replication_state_time),
- url: `#/database/_replicator/${app.utils.getSafeIdForDoc(doc._id)}`,
+ startTime: new Date(doc._replication_start_time),
+ url: `#/database/_replicator/${encodeURIComponent(doc._id)}`,
raw: doc
};
});
};
+export const convertState = (state) => {
+ if (state.toLowerCase() === 'error' || state.toLowerCase() === 'crashing') {
+ return 'retrying';
+ }
+
+ return state;
+};
+
+export const combineDocsAndScheduler = (docs, schedulerDocs) => {
+ return docs.map(doc => {
+ const schedule = schedulerDocs.find(s => s.doc_id === doc._id);
+ if (!schedule) {
+ return doc;
+ }
+
+ doc.status = convertState(schedule.state);
+ if (schedule.start_time) {
+ doc.startTime = new Date(schedule.start_time);
+ }
+
+ if (schedule.last_updated) {
+ doc.stateTime = new Date(schedule.last_updated);
+ }
+
+ return doc;
+ });
+};
+
export const fetchReplicationDocs = () => {
- return $.ajax({
- type: 'GET',
- url: '/_replicator/_all_docs?include_docs=true&limit=100',
- contentType: 'application/json; charset=utf-8',
- dataType: 'json',
- }).then((res) => {
- return parseReplicationDocs(res.rows.filter(row => row.id.indexOf("_design/_replicator") === -1));
+ return supportNewApi()
+ .then(newApi => {
+ const docsPromise = fetch('/_replicator/_all_docs?include_docs=true&limit=100', {
+ credentials: 'include',
+ headers: {
+ 'Accept': 'application/json; charset=utf-8',
+ }
+ })
+ .then(res => res.json())
+ .then((res) => {
+ if (res.error) {
+ return [];
+ }
+
+ return parseReplicationDocs(res.rows.filter(row => row.id.indexOf("_design/_replicator") === -1));
+ });
+
+ if (!newApi) {
+ return docsPromise;
+ }
+ const schedulerPromise = fetchSchedulerDocs();
+ return FauxtonAPI.Promise.join(docsPromise, schedulerPromise, (docs, schedulerDocs) => {
+ return combineDocsAndScheduler(docs, schedulerDocs);
+ })
+ .catch(() => {
+ return [];
+ });
+ });
+};
+
+export const fetchSchedulerDocs = () => {
+ return fetch('/_scheduler/docs?include_docs=true', {
+ credentials: 'include',
+ headers: {
+ 'Accept': 'application/json; charset=utf-8',
+ }
+ })
+ .then(res => res.json())
+ .then((res) => {
+ if (res.error) {
+ return [];
+ }
+
+ return res.docs;
});
};
export const checkReplicationDocID = (docId) => {
const promise = FauxtonAPI.Deferred();
- $.ajax({
- type: 'GET',
- url: `/_replicator/${docId}`,
- contentType: 'application/json; charset=utf-8',
- dataType: 'json',
- }).then(() => {
- promise.resolve(true);
- }, function (xhr) {
- if (xhr.statusText === "Object Not Found") {
+ fetch(`/_replicator/${docId}`, {
+ credentials: 'include',
+ headers: {
+ 'Accept': 'application/json; charset=utf-8'
+ },
+ }).then(resp => {
+ if (resp.statusText === "Object Not Found") {
promise.resolve(false);
return;
}
@@ -263,3 +354,83 @@ export const checkReplicationDocID = (docId) => {
});
return promise;
};
+
+export const parseReplicateInfo = (resp) => {
+ return resp.jobs.filter(job => job.database === null).map(job => {
+ return {
+ _id: job.id,
+ source: getDocUrl(job.source.slice(0, job.source.length - 1)),
+ target: getDocUrl(job.target.slice(0, job.target.length - 1)),
+ startTime: new Date(job.start_time),
+ statusTime: new Date(job.last_updated),
+ //making an asumption here that the first element is the latest
+ status: convertState(job.history[0].type),
+ errorMsg: '',
+ selected: false,
+ continuous: /continuous/.test(job.id),
+ raw: job
+ };
+ });
+};
+
+export const fetchReplicateInfo = () => {
+ return supportNewApi()
+ .then(newApi => {
+ if (!newApi) {
+ return [];
+ }
+
+ return fetch('/_scheduler/jobs', {
+ credentials: 'include',
+ headers: {
+ 'Accept': 'application/json; charset=utf-8'
+ },
+ })
+ .then(resp => resp.json())
+ .then(resp => {
+ return parseReplicateInfo(resp);
+ });
+ });
+};
+
+export const deleteReplicatesApi = (replicates) => {
+ const promises = replicates.map(replicate => {
+ const data = {
+ replication_id: replicate._id,
+ cancel: true
+ };
+
+ return fetch('/_replicate', {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Accept': 'application/json; charset=utf-8',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(data)
+ })
+ .then(resp => resp.json());
+ });
+
+ return FauxtonAPI.Promise.all(promises);
+};
+
+export const createReplicatorDB = () => {
+ return fetch('/_replicator', {
+ method: 'PUT',
+ credentials: 'include',
+ headers: {
+ 'Accept': 'application/json; charset=utf-8',
+ }
+ })
+ .then(res => {
+ if (!res.ok) {
+ throw {reason: 'Failed to create the _replicator database.'};
+ }
+
+ return res.json();
+ })
+ .then(() => {
+ return true;
+ });
+};
diff --git a/app/addons/replication/assets/less/replication.less b/app/addons/replication/assets/less/replication.less
index 8d3fe5f0e..828749e7c 100644
--- a/app/addons/replication/assets/less/replication.less
+++ b/app/addons/replication/assets/less/replication.less
@@ -44,23 +44,27 @@ div.replication__page {
width: 540px;
select {
font-size: 14px;
- width: 246px;
+ width: 400px;
margin-bottom: 10px;
background-color: white;
border: 1px solid #cccccc;
}
.styled-select {
- width: 250px;
+ width: 400px;
}
}
.replication__input-react-select {
font-size: 14px;
+ .Select .Select-menu-outer {
+ width: 400px;
+ }
+
.Select div.Select-control {
padding: 6px;
border: 1px solid #cccccc;
- width: 246px;
+ width: 400px;
.Select-value, .Select-placeholder {
padding: 6px 15px 6px 10px;
@@ -80,7 +84,7 @@ div.replication__page {
.replication__remote-connection-url[type="text"] {
font-size: 14px;
- width: 100%;
+ width: 400px;
color: #333;
}
@@ -91,14 +95,14 @@ div.replication__page {
}
.replication__new-input[type="text"] {
- width: 248px;
+ width: 400px;
font-size: 14px;
color: #333;
}
.replication__doc-name {
position: relative;
- width: 250px;
+ width: 400px;
}
@@ -121,7 +125,7 @@ div.replication__page {
.replication__doc-name-input[type="text"] {
padding-right: 32px;
font-size: 14px;
- width: 248px;
+ width: 400px;
color: #333;
}
@@ -270,6 +274,10 @@ input.replication__bulk-select-input[type="checkbox"] {
padding-left: 8px;
}
+.replication__row-btn--no-left-pad {
+ padding-left: 0px;
+}
+
.replication__row-btn:visited,
.replication__row-btn:hover {
text-decoration: none;
@@ -290,6 +298,10 @@ input.replication__filter-input[type="text"] {
margin-bottom: 0;
}
+button.replication__error-continue {
+ margin-left: 20px !important; //needed to override bootstrap
+}
+
.replication__error-cancel,
.replication__error-continue {
background-color: #0082BF;
@@ -311,3 +323,13 @@ td.replication__empty-row {
.replication__remote_icon_help:hover {
color: #af2d24;
}
+
+.replication__tooltip {
+ .tooltip-inner {
+ text-align: left
+ }
+}
+
+.replication__activity-caveat {
+ padding-left: 80px;
+}
diff --git a/app/addons/replication/base.js b/app/addons/replication/base.js
index d9ad42a6f..02efe80f5 100644
--- a/app/addons/replication/base.js
+++ b/app/addons/replication/base.js
@@ -13,9 +13,18 @@
import FauxtonAPI from '../../core/api';
import replication from './route';
import './assets/less/replication.less';
+import Actions from './actions';
replication.initialize = function () {
FauxtonAPI.addHeaderLink({ title: 'Replication', href: '#/replication', icon: 'fonticon-replicate' });
+ FauxtonAPI.session.on('authenticated', () => {
+ if (!FauxtonAPI.session.isLoggedIn()) {
+ //don't check until user is logged in
+ return;
+ }
+
+ Actions.checkForNewApi();
+ });
};
FauxtonAPI.registerUrls('replication', {
diff --git a/app/addons/replication/components/activity.js b/app/addons/replication/components/activity.js
index e50f1b847..fb9c75a11 100644
--- a/app/addons/replication/components/activity.js
+++ b/app/addons/replication/components/activity.js
@@ -10,351 +10,9 @@
// 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 {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 ?
- deleteDocs()}
- className="replication__bulk-select-trash fonticon fonticon-trash"
- title="Delete all selected">
- : null;
-
- return (
-
- );
-};
-
-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 () => {
- 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 (
-
-
-
-
-
-
-
- Source
-
-
-
- Target
-
-
-
- Type
-
-
-
- State
-
-
-
- State Time
-
-
-
- Actions
-
-
-
-
- {this.renderRows()}
-
-
- );
- }
-}
-
-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
-};
+import {DeleteModal} from './modals';
+import {ReplicationTable} from './common-table';
+import {ReplicationHeader} from './common-activity';
export default class Activity extends React.Component {
constructor (props) {
@@ -412,6 +70,9 @@ export default class Activity extends React.Component {
const {modalVisible} = this.state;
return (
+
+ Replications must have a replication document to display in the following table.
+
{
+ return (
+
+
+ {onChange(e.target.value);}}
+ />
+
+ );
+};
+
+ReplicationFilter.propTypes = {
+ value: React.PropTypes.string.isRequired,
+ onChange: React.PropTypes.func.isRequired
+};
+
+export const ReplicationHeader = ({filter, onFilterChange}) => {
+ return (
+
+ );
+};
+
+ReplicationHeader.propTypes = {
+ filter: React.PropTypes.string.isRequired,
+ onFilterChange: React.PropTypes.func.isRequired
+};
diff --git a/app/addons/replication/components/common-table.js b/app/addons/replication/components/common-table.js
new file mode 100644
index 000000000..4f287c545
--- /dev/null
+++ b/app/addons/replication/components/common-table.js
@@ -0,0 +1,405 @@
+// 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, Tooltip, OverlayTrigger} from "react-bootstrap";
+import moment from 'moment';
+import {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 RowStatus extends React.Component {
+ constructor (props) {
+ super(props);
+ this.state = {
+ modalVisible: false,
+ };
+ }
+
+ showModal () {
+ this.setState({modalVisible: true});
+ }
+
+ closeModal () {
+ this.setState({modalVisible: false});
+ }
+
+ getErrorIcon () {
+ const {status} = this.props;
+ if (status !== 'error' && status !== 'retrying') {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ );
+ }
+
+ render () {
+ const {statusTime, status} = this.props;
+ let momentTime = moment(statusTime);
+ let statusValue = {status} ;
+
+ if (momentTime.isValid()) {
+ const formattedStatusTime = momentTime.format("MMM Do, h:mm a");
+ const stateTimeTooltip = Last updated: {formattedStatusTime} ;
+ statusValue =
+
+ {status}
+ ;
+ }
+
+ return (
+
+ {statusValue}
+ {this.getErrorIcon()}
+
+ );
+ }
+};
+
+RowStatus.propTypes = {
+ statusTime: React.PropTypes.any,
+ status: React.PropTypes.string,
+ errorMsg: React.PropTypes.string.isRequired,
+};
+
+RowStatus.defaultProps = {
+ status: ''
+};
+
+const RowActions = ({onlyDeleteAction, _id, url, deleteDocs}) => {
+ const actions = [];
+ if (!onlyDeleteAction) {
+ actions.push(
+
+
+
+
+ );
+ actions.push(
+
+
+
+
+ );
+ }
+
+ actions.push(
+
+ deleteDocs(_id)}>
+
+
+ );
+
+ return (
+
+ );
+};
+
+RowActions.propTypes = {
+ _id: React.PropTypes.string.isRequired,
+ url: React.PropTypes.string,
+ error: React.PropTypes.bool.isRequired,
+ errorMsg: React.PropTypes.string.isRequired,
+ deleteDocs: React.PropTypes.func.isRequired
+};
+
+const Row = ({
+ _id,
+ source,
+ target,
+ type,
+ startTime,
+ status,
+ statusTime,
+ url,
+ selected,
+ selectDoc,
+ errorMsg,
+ deleteDocs,
+ onlyDeleteAction,
+ showStateRow
+}) => {
+ let momentTime = moment(startTime);
+ const formattedStartTime = momentTime.isValid() ? momentTime.format("MMM Do, h:mm a") : '';
+ let stateRow = null;
+
+ if (showStateRow) {
+ stateRow = ;
+ }
+
+ return (
+
+ selectDoc(_id)} />
+ {formatUrl(source)}
+ {formatUrl(target)}
+ {formattedStartTime}
+ {type}
+ {stateRow}
+
+
+
+
+
+ );
+};
+
+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,
+ statusTime: React.PropTypes.object.isRequired,
+ startTime: React.PropTypes.object,
+ selected: React.PropTypes.bool.isRequired,
+ selectDoc: React.PropTypes.func.isRequired,
+ errorMsg: React.PropTypes.string.isRequired,
+ deleteDocs: React.PropTypes.func.isRequired,
+ onlyDeleteAction: React.PropTypes.bool.isRequired,
+ showStateRow: React.PropTypes.bool.isRequired
+};
+
+const BulkSelectHeader = ({isSelected, deleteDocs, someDocsSelected, onCheck}) => {
+ const trash = someDocsSelected ?
+ deleteDocs()}
+ className="replication__bulk-select-trash fonticon fonticon-trash"
+ title="Delete all selected">
+ : null;
+
+ return (
+
+ );
+};
+
+BulkSelectHeader.propTypes = {
+ isSelected: React.PropTypes.bool.isRequired,
+ someDocsSelected: React.PropTypes.bool.isRequired,
+ onCheck: React.PropTypes.func.isRequired,
+ deleteDocs: React.PropTypes.func.isRequired
+};
+
+const EmptyRow = ({msg}) => {
+ return (
+
+
+ {msg}
+
+
+ );
+};
+
+EmptyRow.defaultProps = {
+ msg: "There is no replicator-db activity or history to display."
+};
+
+
+export 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 () => {
+ 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 '';
+ }
+
+ stateCol () {
+ if (this.props.showStateRow) {
+ return (
+
+ State
+
+
+ );
+ }
+
+ return null;
+ }
+
+ render () {
+
+ return (
+
+
+
+
+
+
+
+ Source
+
+
+
+ Target
+
+
+
+ Start Time
+
+
+
+ Type
+
+
+ {this.stateCol()}
+
+ Actions
+
+
+
+
+ {this.renderRows()}
+
+
+ );
+ }
+}
+
+ReplicationTable.defaultProps = {
+ onlyDeleteAction: false,
+ showStateRow: true
+};
diff --git a/app/addons/replication/components/modals.js b/app/addons/replication/components/modals.js
index b51616ed3..9ef7c96a9 100644
--- a/app/addons/replication/components/modals.js
+++ b/app/addons/replication/components/modals.js
@@ -20,17 +20,22 @@ export const DeleteModal = ({
visible,
onClose,
onClick,
- multipleDocs
+ multipleDocs,
+ isReplicationDB
}) => {
if (!visible) {
return null;
}
- let header = "You are deleting a replication document.";
+ let header = "";
+ let btnText = `Delete ${isReplicationDB ? 'Document' : 'Replication Job'}`;
+ let infoSection = `Deleting a replication ${isReplicationDB ? 'document' : 'job'} stops continuous replication
+ and incomplete one-time replication, but does not affect replicated documents.`;
if (multipleDocs > 1) {
- header = `You are deleting ${multipleDocs} replication documents.`;
+ header = `You are deleting ${multipleDocs} replication ${isReplicationDB ? 'documents' : 'jobs'}.`;
+ btnText = `Delete ${isReplicationDB ? 'Documents' : 'Replication Jobs'}`;
}
return (
@@ -39,19 +44,14 @@ export const DeleteModal = ({
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.
-
+
+ {infoSection}
Cancel
@@ -61,26 +61,40 @@ export const DeleteModal = ({
DeleteModal.propTypes = {
visible: React.PropTypes.bool.isRequired,
+ isReplicationDB: 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}) => {
+DeleteModal.defaultProps = {
+ isReplicationDB: true
+};
+
+export const ErrorModal = ({visible, onClose, errorMsg, status}) => {
if (!visible) {
return null;
}
+ let title = "Replication Error";
+ let warning = The replication job will be tried at increasing intervals
;
+
+ if (status.toLowerCase() === 'failed') {
+ title = "Replication Error - Failed";
+ warning = null;
+ }
+
return (
onClose()}>
- Replication Error
+ {title}
{errorMsg}
+ {warning}
@@ -104,7 +118,7 @@ export const ConflictModal = ({visible, docId, onClose, onClick}) => {
return (
onClose()}>
- Fix Document Conflict
+ Custom ID Conflict
@@ -123,7 +137,6 @@ export const ConflictModal = ({visible, docId, onClose, onClick}) => {
Change Document ID
-
Overwrite Existing Document
diff --git a/app/addons/replication/components/remoteexample.js b/app/addons/replication/components/remoteexample.js
index 1992b325a..ddde70a70 100644
--- a/app/addons/replication/components/remoteexample.js
+++ b/app/addons/replication/components/remoteexample.js
@@ -13,7 +13,7 @@ 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.
@@ -21,13 +21,13 @@ const tooltipExisting = (
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.
+ If the remote database granted permissions to unauthenticated connections, you do not need to enter a username or password.
);
const tooltipNew = (
-
+
Enter the username and password of the remote account.
);
diff --git a/app/addons/replication/components/replicate-activity.js b/app/addons/replication/components/replicate-activity.js
new file mode 100644
index 000000000..11967933c
--- /dev/null
+++ b/app/addons/replication/components/replicate-activity.js
@@ -0,0 +1,116 @@
+// 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 {DeleteModal} from './modals';
+import {ReplicationTable} from './common-table';
+import {ReplicationHeader} from './common-activity';
+
+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 (docId) {
+ this.setState({
+ modalVisible: true,
+ unconfirmedDeleteDocId: docId
+ });
+ }
+
+ 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 (
+
+
+ Active _replicate jobs are displayed. Completed and failed jobs are not.
+
+
+
+
+
+ );
+ }
+}
+
+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
+};
diff --git a/app/addons/replication/components/submit.js b/app/addons/replication/components/submit.js
index cc921b3be..f82fd9254 100644
--- a/app/addons/replication/components/submit.js
+++ b/app/addons/replication/components/submit.js
@@ -17,6 +17,7 @@ const {ConfirmButton} = Components;
export const ReplicationSubmit = ({onClear, disabled, onClick}) =>
;
}
@@ -141,11 +157,30 @@ export default class ReplicationController extends React.Component {
/>;
}
+ if (tabSection === '_replicate') {
+ if (replicateLoading) {
+ return
;
+ }
+
+ return
;
+ }
+
if (activityLoading) {
return
;
}
-
return
;
}
- getCrumbs () {
- if (this.props.section === 'new replication') {
- return [
- {name: 'Replication', link: 'replication'},
- {name: 'New Replication'}
- ];
- }
-
- return [
- {name: 'Replication'}
- ];
- }
-
getHeaderComponents () {
- if (this.props.section === 'new replication') {
+ if (this.state.tabSection === 'new replication') {
return null;
}
@@ -186,25 +208,79 @@ export default class ReplicationController extends React.Component {
max={600}
startValue={300}
stepSize={60}
- onPoll={Actions.getReplicationActivity}
+ onPoll={this.getAllActivity.bind(this)}
/>
);
}
+ getTabElements () {
+ const {tabSection} = this.state;
+ const elements = [
+
+ ];
+
+ if (this.state.supportNewApi) {
+ elements.push(
+
+ );
+ }
+
+ return elements;
+ }
+
+ onTabChange (section, url) {
+ Actions.changeTabSection(section, url);
+ }
+
+ getCrumbs () {
+ if (this.state.tabSection === 'new replication') {
+ return [{'name': 'Job Configuration'}];
+ }
+
+ return [];
+ }
+
+ getTabs () {
+ if (this.state.tabSection === 'new replication') {
+ return null;
+ }
+
+ return (
+
+ {this.getTabElements()}
+
+ );
+ }
+
render () {
+ const { checkingAPI } = this.state;
+
+ if (checkingAPI) {
+ return ;
+ }
+
return (
-
+
{this.getHeaderComponents()}
+ {this.getTabs()}
{this.showSection()}
diff --git a/app/addons/replication/route.js b/app/addons/replication/route.js
index 70b66a429..95719b482 100644
--- a/app/addons/replication/route.js
+++ b/app/addons/replication/route.js
@@ -13,13 +13,15 @@
import React from 'react';
import FauxtonAPI from '../../core/api';
import ReplicationController from './controller';
+import Actions from './actions';
const ReplicationRouteObject = FauxtonAPI.RouteObject.extend({
routes: {
'replication/_create': 'defaultView',
- 'replication/:dbname': 'defaultView',
+ 'replication/_create/:dbname': 'defaultView',
'replication/id/:id': 'fromId',
- 'replication': 'activityView'
+ 'replication': 'activityView',
+ 'replication/_replicate': 'replicateView'
},
selectedHeader: 'Replication',
@@ -40,24 +42,30 @@ const ReplicationRouteObject = FauxtonAPI.RouteObject.extend({
defaultView: function (databaseName) {
const localSource = databaseName || '';
+ Actions.changeTabSection('new replication');
+ Actions.clearReplicationForm();
return
;
},
fromId: function (replicationId) {
+ Actions.clearReplicationForm();
+ Actions.changeTabSection('new replication');
return
;
},
activityView: function () {
- return
;
+ Actions.changeTabSection('activity');
+ return
;
+ },
+
+ replicateView: function () {
+ Actions.changeTabSection('_replicate');
+ return
;
}
});
diff --git a/app/addons/replication/stores.js b/app/addons/replication/stores.js
index 715ea23ea..19badc7d4 100644
--- a/app/addons/replication/stores.js
+++ b/app/addons/replication/stores.js
@@ -16,6 +16,20 @@ import Constants from './constants';
import AccountActionTypes from '../auth/actiontypes';
import _ from 'lodash';
+// 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...
+const validFieldMap = {
+ remoteSource: '_remoteSource',
+ remoteTarget: '_remoteTarget',
+ localTarget: '_localTarget',
+ replicationType: '_replicationType',
+ replicationDocName: '_replicationDocName',
+ replicationSource: '_replicationSource',
+ replicationTarget: '_replicationTarget',
+ localSource: '_localSource',
+ replicationDocName: '_replicationDocName'
+};
+
const ReplicationStore = FauxtonAPI.Store.extend({
initialize () {
this.reset();
@@ -46,12 +60,30 @@ const ReplicationStore = FauxtonAPI.Store.extend({
this._statusDocs = [];
this._statusFilteredStatusDocs = [];
this._statusFilter = '';
+ this._replicateFilter = '';
this._allDocsSelected = false;
+ this._allReplicateSelected = false;
this._username = '';
this._password = '';
this._activityLoading = false;
+ this._tabSection = 'new replication';
+ this._supportNewApi = true;
this.loadActivitySort();
+
+ this._fetchingReplicateInfo = false;
+ this._replicateInfo = [];
+
+ this._checkingAPI = true;
+ this._supportNewApi = false;
+ },
+
+ supportNewApi () {
+ return this._supportNewApi;
+ },
+
+ checkingAPI () {
+ return this._checkingAPI;
},
getActivitySort () {
@@ -59,13 +91,14 @@ const ReplicationStore = FauxtonAPI.Store.extend({
},
loadActivitySort () {
- let sort = app.utils.localStorageGet('replication-activity-sort');
- if (!sort) {
- sort = {
+ const defaultSort = {
descending: false,
column: 'statusTime'
};
+ let sort = app.utils.localStorageGet('replication-activity-sort');
+ if (!sort) {
+ sort = defaultSort;
this.setActivitySort(sort);
}
@@ -77,6 +110,23 @@ const ReplicationStore = FauxtonAPI.Store.extend({
this._activitySort = sort;
},
+ isReplicateInfoLoading () {
+ return this._fetchingReplicateInfo;
+ },
+
+ getReplicateInfo () {
+ return this._replicateInfo.filter(doc => {
+ return _.values(doc).filter(item => {
+ if (!item) {return false;}
+ return item.toString().toLowerCase().match(this._replicateFilter);
+ }).length > 0;
+ });
+ },
+
+ setReplicateInfo (info) {
+ this._replicateInfo = info;
+ },
+
setCredentials (username, password) {
this._username = username;
this._password = password;
@@ -177,6 +227,29 @@ const ReplicationStore = FauxtonAPI.Store.extend({
this._allDocsSelected = false;
},
+ selectReplicate (id) {
+ const doc = this._replicateInfo.find(doc => doc._id === id);
+ if (!doc) {
+ return;
+ }
+
+ doc.selected = !doc.selected;
+ this._allReplicateSelected = false;
+ },
+
+ selectAllReplicate () {
+ this._allReplicateSelected = !this._allReplicateSelected;
+ this.getReplicateInfo().forEach(doc => doc.selected = this._allReplicateSelected);
+ },
+
+ someReplicateSelected () {
+ return this.getReplicateInfo().some(doc => doc.selected);
+ },
+
+ getAllReplicateSelected () {
+ return this._allReplicateSelected;
+ },
+
selectAllDocs () {
this._allDocsSelected = !this._allDocsSelected;
this.getFilteredReplicationStatus().forEach(doc => doc.selected = this._allDocsSelected);
@@ -197,25 +270,23 @@ const ReplicationStore = FauxtonAPI.Store.extend({
getStatusFilter () {
return this._statusFilter;
},
- // to cut down on boilerplate
- updateFormField (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',
- localTarget: '_localTarget',
- replicationType: '_replicationType',
- replicationDocName: '_replicationDocName',
- replicationSource: '_replicationSource',
- replicationTarget: '_replicationTarget',
- localSource: '_localSource'
- };
+ setReplicateFilter (filter) {
+ this._replicateFilter = filter;
+ },
+ getReplicateFilter () {
+ return this._replicateFilter;
+ },
+ // to cut down on boilerplate
+ updateFormField (fieldName, value) {
this[validFieldMap[fieldName]] = value;
},
+ clearReplicationForm () {
+ _.values(validFieldMap).forEach(fieldName => this[fieldName] = '');
+ },
+
getRemoteSource () {
return this._remoteSource;
},
@@ -242,6 +313,10 @@ const ReplicationStore = FauxtonAPI.Store.extend({
});
},
+ getTabSection () {
+ return this._tabSection;
+ },
+
dispatch ({type, options}) {
switch (type) {
@@ -263,13 +338,17 @@ const ReplicationStore = FauxtonAPI.Store.extend({
this._loading = false;
break;
+ case ActionTypes.REPLICATION_FETCHING_FORM_STATE:
+ this._loading = true;
+ break;
+
case ActionTypes.REPLICATION_UPDATE_FORM_FIELD:
this.changeAfterSubmit();
this.updateFormField(options.fieldName, options.value);
break;
case ActionTypes.REPLICATION_CLEAR_FORM:
- this.reset();
+ this.clearReplicationForm();
break;
case ActionTypes.REPLICATION_STARTING:
@@ -289,6 +368,10 @@ const ReplicationStore = FauxtonAPI.Store.extend({
this.setStatusFilter(options);
break;
+ case ActionTypes.REPLICATION_FILTER_REPLICATE:
+ this.setReplicateFilter(options);
+ break;
+
case ActionTypes.REPLICATION_TOGGLE_DOC:
this.selectDoc(options);
break;
@@ -297,7 +380,16 @@ const ReplicationStore = FauxtonAPI.Store.extend({
this.selectAllDocs();
break;
+ case ActionTypes.REPLICATION_TOGGLE_REPLICATE:
+ this.selectReplicate(options);
+ break;
+
+ case ActionTypes.REPLICATION_TOGGLE_ALL_REPLICATE:
+ this.selectAllReplicate();
+ break;
+
case ActionTypes.REPLICATION_SET_STATE_FROM_DOC:
+ this._loading = false;
this.setStateFromDoc(options);
break;
@@ -317,6 +409,32 @@ const ReplicationStore = FauxtonAPI.Store.extend({
this._allDocsSelected = false;
break;
+ case ActionTypes.REPLICATION_CHANGE_TAB_SECTION:
+ this._tabSection = options;
+ break;
+
+ case ActionTypes.REPLICATION_CLEAR_SELECTED_DOCS:
+ this._allDocsSelected = false;
+ break;
+
+ case ActionTypes.REPLICATION_SUPPORT_NEW_API:
+ this._checkingAPI = false;
+ this._supportNewApi = options;
+ break;
+
+ case ActionTypes.REPLICATION_FETCHING_REPLICATE_STATUS:
+ this._fetchingReplicateInfo = true;
+ break;
+
+ case ActionTypes.REPLICATION_REPLICATE_STATUS:
+ this._fetchingReplicateInfo = false;
+ this.setReplicateInfo(options);
+ break;
+
+ case ActionTypes.REPLICATION_CLEAR_SELECTED_REPLICATES:
+ this._allReplicateSelected = false;
+ break;
+
case AccountActionTypes.AUTH_SHOW_PASSWORD_MODAL:
this._isPasswordModalVisible = true;
break;
diff --git a/app/addons/replication/tests/nightwatch/replication.js b/app/addons/replication/tests/nightwatch/replication.js
index ba852d58b..c523c8fa3 100644
--- a/app/addons/replication/tests/nightwatch/replication.js
+++ b/app/addons/replication/tests/nightwatch/replication.js
@@ -46,13 +46,13 @@ module.exports = {
.checkForDatabaseCreated(newDatabaseName1, waitTime)
.createDocument(docName1, newDatabaseName1)
.loginToGUI()
- .url(baseUrl + '/#replication/_create')
+ .url(baseUrl + '/#/replication/_create')
.waitForElementVisible('button#replicate', waitTime, true)
.waitForElementVisible('#replication-source', waitTime, true)
// select LOCAL as the source
.clickWhenVisible('#replication-source')
- .keys(['\uE006'])
+ .keys(['\uE015', '\uE006'])
.waitForElementVisible('.replication__input-react-select', waitTime, true)
// enter our source DB
@@ -91,13 +91,13 @@ module.exports = {
// now login and fill in the replication form
.loginToGUI()
- .url(baseUrl + '/#replication/_create')
+ .url(baseUrl + '/#/replication/_create')
.waitForElementVisible('button#replicate', waitTime, true)
.waitForElementVisible('#replication-source', waitTime, true)
// select the LOCAL db as the source
.clickWhenVisible('#replication-source')
- .keys(['\uE006'])
+ .keys(['\uE015', '\uE006'])
.waitForElementVisible('.replication__input-react-select', waitTime, true)
.setValue('.replication__input-react-select .Select-input input', [newDatabaseName1, client.Keys.ENTER])
@@ -142,13 +142,13 @@ module.exports = {
// now login and fill in the replication form
.loginToGUI()
- .url(baseUrl + '/#replication/_create')
+ .url(baseUrl + '/#/replication/_create')
.waitForElementVisible('button#replicate', waitTime, true)
.waitForElementVisible('#replication-source', waitTime, true)
// select the LOCAL db as the source
.clickWhenVisible('#replication-source')
- .keys(['\uE006'])
+ .keys(['\uE015', '\uE006'])
.waitForElementVisible('.replication__input-react-select', waitTime, true)
.setValue('.replication__input-react-select .Select-input input', [newDatabaseName1, client.Keys.ENTER])
diff --git a/app/core/api.js b/app/core/api.js
index efcb7f9bc..20c9f0d89 100644
--- a/app/core/api.js
+++ b/app/core/api.js
@@ -20,6 +20,7 @@ import Flux from "flux";
import $ from "jquery";
import Backbone from "backbone";
import _ from "lodash";
+import Promise from "bluebird";
Backbone.$ = $;
Backbone.ajax = function () {
@@ -32,7 +33,8 @@ Object.assign(FauxtonAPI, {
utils: utils,
Store: Store,
Events: _.extend({}, Backbone.Events),
- dispatcher: new Flux.Dispatcher()
+ dispatcher: new Flux.Dispatcher(),
+ Promise: Promise
});
// Pass along all constants
diff --git a/assets/less/notification-center.less b/assets/less/notification-center.less
index 07935b3b9..ae606414f 100644
--- a/assets/less/notification-center.less
+++ b/assets/less/notification-center.less
@@ -145,6 +145,7 @@ body #dashboard #notification-center-btn {
}
p {
margin-bottom: 0;
+ overflow-wrap: break-word;
}
div.flex-body {
overflow: hidden;
diff --git a/docker/dc.selenium.yml b/docker/dc.selenium.yml
index 41ddd2bef..5d1c3855b 100644
--- a/docker/dc.selenium.yml
+++ b/docker/dc.selenium.yml
@@ -11,4 +11,4 @@ services:
image: klaemo/couchdb:2.0-dev@sha256:e9b71abaff6aeaa34ee28604c3aeb78f3a7c789ad74a7b88148e2ef78f1e3b21
command: '--with-haproxy -a tester:testerpass'
ports:
- - "5984:5984"
+ - "5984:5984"
\ No newline at end of file
diff --git a/jest-setup.js b/jest-setup.js
index 5b28e7780..ec630562d 100644
--- a/jest-setup.js
+++ b/jest-setup.js
@@ -17,4 +17,9 @@ window.$ = window.jQuery = require('jquery');
window._ = require('lodash');
window.Backbone = require('backbone');
+Object.defineProperty(window.location, 'origin', {
+ writable: true,
+ value: 'http://dev:8000'
+});
+