summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsuelockwood <deathbear@apache.org>2014-01-15 19:31:23 -0500
committersuelockwood <deathbear@apache.org>2014-03-25 20:59:57 -0400
commit85c28725fbb9e0c2446984dc23eaeb4121d3b860 (patch)
tree5a6ca750cacbf8409f999c728d4751309619f66e
parent1fb7cea25b60585b16bdd786cd3e2b299fa384df (diff)
downloadcouchdb-Fauxton-replicator-addon.tar.gz
Replicator & ReplicatorAuth extensionFauxton-replicator-addon
-rw-r--r--.gitignore1
-rw-r--r--src/fauxton/app/addons/replicator/assets/less/replicator.less255
-rw-r--r--src/fauxton/app/addons/replicator/base.js26
-rw-r--r--src/fauxton/app/addons/replicator/happy.js177
-rw-r--r--src/fauxton/app/addons/replicator/resources.js230
-rw-r--r--src/fauxton/app/addons/replicator/route.js101
-rw-r--r--src/fauxton/app/addons/replicator/templates/active.html41
-rw-r--r--src/fauxton/app/addons/replicator/templates/complete.html43
-rw-r--r--src/fauxton/app/addons/replicator/templates/error.html41
-rw-r--r--src/fauxton/app/addons/replicator/templates/form.html71
-rw-r--r--src/fauxton/app/addons/replicator/templates/localremotetabs.html39
-rw-r--r--src/fauxton/app/addons/replicator/templates/nope.html17
-rw-r--r--src/fauxton/app/addons/replicator/templates/options.html35
-rw-r--r--src/fauxton/app/addons/replicator/templates/password_modal.html27
-rw-r--r--src/fauxton/app/addons/replicator/templates/sidebar_no_header.html20
-rw-r--r--src/fauxton/app/addons/replicator/templates/sidebartabs.html30
-rw-r--r--src/fauxton/app/addons/replicator/templates/statustable.html23
-rw-r--r--src/fauxton/app/addons/replicator/templates/validator.html125
-rw-r--r--src/fauxton/app/addons/replicator/tests/replicatorSpec.js28
-rw-r--r--src/fauxton/app/addons/replicator/views.js776
-rw-r--r--src/fauxton/settings.json.default1
21 files changed, 2107 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 5e4d22006..b050f070c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -96,6 +96,7 @@ src/fauxton/app/addons/*
!src/fauxton/app/addons/plugins
!src/fauxton/app/addons/logs
!src/fauxton/app/addons/stats
+!src/fauxton/app/addons/replicator
!src/fauxton/app/addons/replication
!src/fauxton/app/addons/contribute
!src/fauxton/app/addons/auth
diff --git a/src/fauxton/app/addons/replicator/assets/less/replicator.less b/src/fauxton/app/addons/replicator/assets/less/replicator.less
new file mode 100644
index 000000000..38da2a256
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/assets/less/replicator.less
@@ -0,0 +1,255 @@
+// 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.
+
+@brown: #3A2C2B;
+@red: #9d261d;
+@darkRed: #AF2D24;
+@linkRed: #DA4F49;
+@greyBrown: #A59D9D;
+@fontGrey: #808080;
+@secondarySidebar: #E4DFDC;
+@green: #46a546;
+
+#replication-status{
+ button.close{
+ font-size: 30px;
+ }
+}
+
+
+.replication-sidebar{
+ top: 0;
+}
+
+dl.replication-validator{
+ dt{
+ width: 300px;
+ text-align: left;
+ padding: 10px;
+ }
+ dd{
+ margin-left: 300px;
+ text-align: left;
+ padding: 10px;
+ pre {
+ max-width: 450px;
+ }
+ span:before {
+ padding-right: 10px;
+ }
+ .red {
+ color: @red;
+ }
+ .green{
+ color: @green;
+ }
+ }
+}
+ table.replication-table{
+ margin-top: 20px;
+ }
+
+form#replication {
+ padding-top: 40px;
+ position: relative;
+ max-width: none;
+ width: auto;
+
+ .actions{
+ padding: 15px 0;
+ }
+ #from_name {
+ margin-right: 20px;
+ }
+ [type="text"]{
+ width: 100%;
+ }
+ .autharea {
+ display: inline-block;
+ vertical-align: top;
+ padding-top: 10px;
+ }
+ button.fonticon-replicate{
+ padding: 15px 20px;
+ }
+ .span12{
+ margin-left: 0;
+ padding-bottom: 20px;
+ }
+ .nav-tabs{
+ max-height: 37px;
+ margin-bottom: 0;
+ border: 0;
+ }
+ .dropdown-menu {
+ width: 85%;
+ }
+ .tab-content.small-tabs{
+ margin-top: 0;
+ border: 1px solid #e3e3e3;
+ margin-bottom: 10px;
+ .tab-pane {
+ padding: 20px 20px;
+ }
+ > .active {
+ display: block;
+ background-color: #fff;
+ }
+ }
+ h3 {
+ margin-top: 0;
+ line-height: 27px
+ }
+ .form_set{
+ width: 350px;
+ display: inline-block;
+ border: 1px solid @greyBrown;
+ padding: 15px 10px 0;
+ margin-bottom: 20px;
+ &.middle{
+ width: 100px;
+ border: none;
+ position: relative;
+ height: 100px;
+ margin: 0;
+ }
+ input, select {
+ margin: 0 0 16px 5px;
+ height: 40px;
+ width: 318px;
+ }
+ .btn-group{
+ margin: 0 0 16px 5px;
+ .btn {
+ padding: 10px 57px;
+ }
+ }
+ &.local{
+ .local_option{
+ display: block;
+ }
+ .remote_option{
+ display: none;
+ }
+ .local-btn{
+ background-color: @red;
+ color: #fff;
+ }
+ .remote-btn{
+ background-color: #f5f5f5;
+ color: @fontGrey;
+ }
+ }
+ .local_option{
+ display: none;
+ }
+ .remote-btn{
+ background-color: @red;
+ color: #fff;
+ }
+ }
+
+
+ .options {
+ position: relative;
+ &:after{
+ content: '';
+ display: block;
+ position: absolute;
+ right: -16px;
+ top: 9px;
+ width: 0;
+ height: 0;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-bottom: 5px solid black;
+ border-top: none;
+ }
+ &.off {
+ &:after{
+ content: '';
+ display: block;
+ position: absolute;
+ right: -16px;
+ top: 9px;
+ width: 0;
+ height: 0;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-bottom: none;
+ border-top: 5px solid black;
+ }
+ }
+ }
+ .control-group{
+ label{
+ float: left;
+ min-height: 30px;
+ vertical-align: top;
+ padding-right: 5px;
+ min-width: 140px;
+ padding-left: 0px;
+ &.control-label{
+ width: 100px;
+ min-width: 100px;
+ font-size: 16px;
+ }
+ }
+ input[type=radio],
+ input[type=checkbox]{
+ margin: 0 0 2px 0;
+ }
+ .controls {
+ margin-left: 125px;
+ padding-right: 20px;
+ }
+ }
+
+ .circle{
+ z-index: 0;
+ position: absolute;
+ top: 20px;
+ left: 15px;
+
+ &:after {
+ width: 70px;
+ height: 70px;
+ content: '';
+ display: block;
+ position: relative;
+ margin: 0 auto;
+ border: 1px solid @greyBrown;
+ -webkit-border-radius: 40px;
+ -moz-border-radius: 40px;
+ border-radius:40px;
+ }
+ }
+ .swap {
+ text-decoration: none;
+ z-index: 30;
+ cursor: pointer;
+ position: absolute;
+ font-size: 40px;
+ width: 27px;
+ height: 12px;
+ top: 31px;
+ left: 30px;
+ &:hover {
+ color: @greyBrown;
+ }
+ }
+
+}
+
+.btn-group.toggle-btns input[type=radio] {
+display: none;
+}
diff --git a/src/fauxton/app/addons/replicator/base.js b/src/fauxton/app/addons/replicator/base.js
new file mode 100644
index 000000000..1465f6697
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/base.js
@@ -0,0 +1,26 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+define([
+ "app",
+ "api",
+ "addons/replicator/route",
+ "addons/replicator/views",
+],
+
+function(app, FauxtonAPI, replication, Views) {
+ replication.Views = Views;
+ replication.initialize = function() {
+ FauxtonAPI.addHeaderLink({title: "Replication", href: "#/replication", icon: "fonticon-replicate",});
+ };
+ return replication;
+});
diff --git a/src/fauxton/app/addons/replicator/happy.js b/src/fauxton/app/addons/replicator/happy.js
new file mode 100644
index 000000000..9516a66d3
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/happy.js
@@ -0,0 +1,177 @@
+// HAPPY JS.
+// WARNING: this has been editted to support custom error message handling.
+// $('#awesomeForm').isHappy({
+// fields: {
+// // reference the field you're talking about, probably by `id`
+// // but you could certainly do $('[name=name]') as well.
+// '#yourName': {
+// required: true,
+// message: 'Might we inquire your name'
+// },
+// '#email': {
+// required: true,
+// defaultMessage: 'How are we to reach you sans email??',
+// test:[
+ // {
+ // test: happy.email, // this can be *any* function that returns true or false
+ // message: "default message here"
+ // },
+ // {
+ // test: happy.equals,
+ // message: "this needs to equal that"
+ // }
+// ]
+// },
+// errorHandling: function(error){console.log(error.message);}
+// });
+//
+// if you don't pass in a function for errorHandling, it behaves as designed. - Sue
+
+(function($){
+ function trim(el) {
+ return (''.trim) ? el.val().trim() : $.trim(el.val());
+ }
+ $.fn.isHappy = function (config) {
+ var happy = {
+ USPhone: function (val) {
+ return (/^\(?(\d{3})\)?[\- ]?\d{3}[\- ]?\d{4}$/).test(val);
+ },
+
+ // matches mm/dd/yyyy (requires leading 0's (which may be a bit silly, what do you think?)
+ date: function (val) {
+ return (/^(?:0[1-9]|1[0-2])\/(?:0[1-9]|[12][0-9]|3[01])\/(?:\d{4})/).test(val);
+ },
+
+ email: function (val) {
+ return (/^(?:\w+\.?)*\w+@(?:\w+\.)+\w+$/).test(val);
+ },
+
+ minLength: function (val, length) {
+ return val.length >= length;
+ },
+
+ maxLength: function (val, length) {
+ return val.length <= length;
+ },
+
+ equal: function (val1, val2) {
+ return (val1 == val2);
+ }
+ };
+
+ function errorMessaging(error,selector){
+ if(config.errorHandling){
+ config.errorHandling(error);
+ }else{
+ var message = error.useRequiredMessage?error.defaultMessage:error.message;
+
+ $(selector).parent().find("#"+error.id).remove();
+ var errorEl = $('<span id="'+error.id+'" class="unhappyMessage">'+message+'</span>');
+ $(selector).after(errorEl);
+ }
+ }
+
+ function handleSubmit(e) {
+ var errors = false, i, l,
+ fields = getFields();
+ for (i = 0, l = fields.length; i < l; i += 1) {
+ if (!fields[i].testValid(true)) {
+ errors = true;
+ }
+ }
+ if (errors) {
+ if (isFunction(config.unHappy)) config.unHappy();
+ return false;
+ } else if (config.testMode) {
+ if (window.console) console.warn('would have submitted');
+ return false;
+ }
+ }
+
+ function isFunction (obj) {
+ return !!(obj && obj.constructor && obj.call && obj.apply);
+ }
+
+ function getFields(){
+ var fields = [];
+ for ( var formField in config.fields) {
+ fields.push(processField(config.fields[formField], formField));
+ }
+ return fields;
+ }
+
+ function processField(opts, selector) {
+ var field = $(selector),
+ errorM = {
+ defaultMessage: opts.defaultMessage || "This field is required",
+ message: " ",
+ id: field.attr('name') + '_unhappy',
+ useRequiredMessage: false
+ };
+
+ field.testValid = function (submit) {
+ var val,
+ el = $(this),
+ gotFunc,
+ error = false,
+ temp,
+ required = opts.required,
+ password = (field.attr('type') === 'password'),
+ arg = isFunction(opts.arg) ? opts.arg() : opts.arg,
+ tests = opts.tests || [];
+
+ if ($(this).length <= 0){return true;}
+ // clean it or trim it
+ if (isFunction(opts.clean)) {
+ val = opts.clean(el.val());
+ } else if (!opts.trim && !password) {
+ val = trim(el);
+ } else {
+ val = el.val();
+ }
+
+ // write it back to the field
+ el.val(val);
+
+ // get the value
+ gotTests = ((val.length > 0 || required === 'sometimes') && tests.length !== 0);
+
+ // check if we've got an error on our hands
+ if (submit === true && required === true && val.length === 0) {
+ error = true;
+ errorM.useRequiredMessage = true;
+ } else if (gotTests) {
+ for (var i = 0; i < opts.tests.length; i++){
+ if (isFunction(opts.tests[i].test) && !opts.tests[i].test(val, arg)){
+ errorM.message = opts.tests[i].message;
+ error = true;
+ break;
+ }
+ }
+ }
+
+ if (error) {
+ el.addClass('unhappy');
+ errorMessaging(errorM, el);
+ return false;
+ } else {
+ el.removeClass('unhappy');
+ $("#"+field.attr('name') + '_unhappy').remove();
+ return true;
+ }
+ };
+
+ field.on(config.when || 'blur', field.testValid);
+ return field;
+ }
+
+ if (config.submitButton) {
+ $(config.submitButton).on("click", handleSubmit);
+ } else {
+
+ this.bind('submit', handleSubmit);
+ }
+ return this;
+ };
+})(this.jQuery || this.Zepto);
+
diff --git a/src/fauxton/app/addons/replicator/resources.js b/src/fauxton/app/addons/replicator/resources.js
new file mode 100644
index 000000000..1f93bc0da
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/resources.js
@@ -0,0 +1,230 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+define([
+ "app",
+ "api",
+ 'addons/activetasks/resources',
+ "addons/documents/resources"
+],
+
+function (app, FauxtonAPI, ActiveTasks, Documents) {
+ var Replication = {};
+
+ //these are probably dupes from the database addon. I'm going to keep them seperate for now.
+
+ //Extend from top level document instead
+ Replication.DBModel = Backbone.Model.extend({
+ url: function(){
+ return app.host + "/" + this.id;
+ },
+ label: function () {
+ //for autocomplete
+ return this.get("name");
+ },
+ authValidation: function(password){
+ var username = app.session.get('userCtx').name,
+ that = this;
+ return $.ajax({
+ cache: false,
+ type: "POST",
+ url: "/_session",
+ dataType: "json",
+ data: {name: username, password: password},
+ success: function(){
+ that.passworderror = false;
+ },
+ error: function(resp){
+ that.passworderror = true;
+ }
+ });
+ }
+ });
+
+
+//extend alldocs funky, but theoretically
+ Replication.DBList = Backbone.Collection.extend({
+ model: Replication.DBModel,
+ url: function() {
+ return app.host + "/_all_dbs";
+ },
+ getFiltered: function(dbname){
+ var filtered = this.filter(function(rep) {
+ return rep.get("name") === dbname;
+ });
+ return new Replication.DBList(filtered);
+ },
+ getReplicatorDB: function(){
+ return this.getFiltered("_replicator");
+ },
+ createReplicatorDB: function(){
+ var db = new this.model();
+ return db.save({
+ id: "_replicator",
+ name: "_replicator"
+ });
+ },
+ parse: function(resp) {
+ // TODO: pagination!
+ return _.map(resp, function(database) {
+ return {
+ id: database,
+ name: database
+ };
+ });
+ }
+ });
+
+
+
+//extend from active tasks
+ Replication.Task = Backbone.Model.extend({});
+
+ Replication.Tasks = Backbone.Collection.extend({
+ model: Replication.Task,
+ url: function () {
+ return app.host + '/_active_tasks';
+ },
+ fetch: function (options) {
+ var fetchoptions = options || {};
+ fetchoptions.cache = false;
+ return Backbone.Model.prototype.fetch.call(this, fetchoptions);
+ },
+ parse: function(resp){
+ //only want replication tasks to return
+ return _.filter(resp, function(task){
+ return task.type === "replication";
+ });
+ }
+ });
+
+ Replication.Replicate = Backbone.Model.extend({
+ idAttribute: 'id',
+ documentation: "replication_doc",
+ url: function(){
+ var docid = this.get('id') ? "/"+this.get('id'):"";
+ return app.host + "/_replicator"+docid;
+ },
+ destroy: function() {
+ var url = this.url() + "?rev=" + this.get('_rev');
+ return $.ajax({
+ url: url,
+ dataType: 'json',
+ type: 'DELETE'
+ });
+ }
+ });
+
+ Replication.item = Backbone.Model.extend({
+ defaults: {
+ source: "Source is missing",
+ target: "Target is missing",
+ create_target: false,
+ continuous: false,
+ doc: {}
+ },
+ initialize: function(){
+
+ var regex = "^(http|https|ftp)://([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&amp;%$-]+)*@)*((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]).(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0).(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0).(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])|([a-zA-Z0-9-]+.)*[a-zA-Z0-9-]+.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(/($|[a-zA-Z0-9.,?'\\+&amp;%$#=~_-]+))*$";
+ this.urlregex = new RegExp(regex);
+ },
+ hasSource: function(){
+ //has a source
+ return this.get("source") !== this.defaults.source;
+ },
+ hasTarget: function(){
+ //has a target
+ return this.get("target") !== this.defaults.target;
+ },
+ hasUserCtx: function(){
+ //has username & has roles
+ return this.get("user_ctx") === undefined;
+ },
+ isBoolean: function(test){
+ return typeof test === "boolean";
+ },
+ isURL: function(type){
+ var checkType = type || "target",
+ url = this.get(checkType);
+ return this.urlregex.test(url);
+ },
+ isLocal: function(type){
+ var checkType = type || "target",
+ thisURL = this.get(checkType);
+
+ return (thisURL.indexOf(app.host) !== -1);
+ },
+
+ getDataforReplication: function(){
+ return {
+ source: this.get('doc').source,
+ target: this.get('doc').target,
+ create_target: this.get('create_target'),
+ user_ctx: this.get('user_ctx'),
+ continuous: this.get('continuous')
+ };
+ }
+ });
+
+ Replication.Replicator = Backbone.Collection.extend({
+ model: Replication.item,
+ idAttribute: '_id',
+ documentation: "replication_doc",
+ url: function(){
+ return app.host + "/_replicator/_all_docs?limit=100&include_docs=true";
+ },
+ getfiltered: function(type){
+ var filtered = this.filter(function(rep) {
+ return rep.get("_replication_state") === type;
+ });
+ return new Replication.Replicator(filtered);
+ },
+ getCompleted: function(){
+ return this.getfiltered("completed");
+ },
+ getErrored: function(){
+ return this.getfiltered("error");
+ },
+ getURL: function(source){
+ if (typeof source == "object") {
+ return source.url;
+ } else {
+ return source;
+ }
+ },
+ parse: function(resp){
+ var rows = resp.rows,
+ getSource = this.getURL;
+
+ return _.map(rows, function(row) {
+ var source = getSource(row.doc.source),
+ target = getSource(row.doc.target);
+
+ return {
+ _id: row.id,
+ _rev: row.value.rev,
+ value: row.value,
+ key: row.key,
+ create_target: row.doc.create_target || undefined,
+ continuous: row.doc.continuous || undefined,
+ _replication_state: row.doc._replication_state,
+ source: source || undefined,
+ target: target || undefined,
+ doc: row.doc,
+ user_ctx: row.value.user_ctx
+ };
+ });
+ }
+ });
+
+
+ return Replication;
+});
diff --git a/src/fauxton/app/addons/replicator/route.js b/src/fauxton/app/addons/replicator/route.js
new file mode 100644
index 000000000..85d601e99
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/route.js
@@ -0,0 +1,101 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+define([
+ "app",
+ "api",
+ "addons/replicator/resources",
+ "addons/replicator/views"
+],
+function(app, FauxtonAPI, Replication, Views) {
+ var RepRouteObject = FauxtonAPI.RouteObject.extend({
+ layout: "with_sidebar",
+ roles: ["_admin"],
+ routes: {
+ "replication": "showSection",
+ "replication/:section": "showSection",
+ "replication/:section/:source": "showSection"
+ },
+ selectedHeader: "Replication",
+ apiUrl: function() {
+ return [this.replicate.url(), this.replicate.documentation];
+ },
+ crumbs: [
+ {"name": "Replication", "link": "replication"}
+ ],
+ initialize: function(){
+ this.databases = new Replication.DBList({});
+ this.tasks = new Replication.Tasks({id: "ReplicationTasks"});
+ this.replicate = new Replication.Replicate({});
+ this.replicatorDB = new Replication.Replicator({});
+ this.setView("#sidebar-content", new Views.Sidebar({}));
+ },
+ showSection: function(section, source){
+ var maincontent;
+ $('.replication-nav-stacked').find('li').removeClass("active");
+ switch (section) {
+ case "new":
+ maincontent = new Views.ReplicationForm({
+ source: source || "",
+ collection: this.databases
+ });
+ this.crumbs = [
+ {"name": "Replication", "link": "replication"},
+ {"name": "Start a new replication", "link": "#"}
+ ];
+ break;
+ case "active":
+ maincontent = new Views.ActiveReplications({
+ collection: this.tasks
+ });
+ this.crumbs = [
+ {"name": "Replication", "link": "replication"},
+ {"name": "Active replications", "link": "#"}
+ ];
+ break;
+ case "errors":
+ maincontent = new Views.ReplicationErrors({
+ databases: this.databases,
+ collection: this.replicatorDB
+ });
+ this.crumbs = [
+ {"name": "Replication", "link": "replication"},
+ {"name": "Errors", "link": "#"}
+ ];
+ break;
+ case "complete":
+ maincontent = new Views.CompletedReplications({
+ databases: this.databases,
+ collection: this.replicatorDB
+ });
+ this.crumbs = [
+ {"name": "Replication", "link": "replication"},
+ {"name": "Completed", "link": "#"}
+ ];
+ break;
+ default:
+ maincontent = new Views.ReplicationForm({
+ source: source || "",
+ collection: this.databases
+ });
+ break;
+ }
+ $('a[data-type-select="'+section+'"]').parents('li').addClass('active');
+ this.setView("#dashboard-content", maincontent);
+ }
+ });
+
+
+ Replication.RouteObjects = [RepRouteObject];
+
+ return Replication;
+});
diff --git a/src/fauxton/app/addons/replicator/templates/active.html b/src/fauxton/app/addons/replicator/templates/active.html
new file mode 100644
index 000000000..cb5bfa9d9
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/templates/active.html
@@ -0,0 +1,41 @@
+<!--
+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.
+-->
+<td>
+ <a href="#database/_replicator/<%=docid%>">
+ <%=docid%>
+ </a>
+</td>
+
+<td>
+ <%=source%>
+</td>
+
+<td>
+ <%=target%>
+</td>
+
+<td>
+ <span class='<% if (continuous){%>icon-refresh <% } %>'>
+ <%=continuous%>
+ </span>
+</td>
+
+
+<td>
+ <%=docs_written|| "0"%> docs written
+</td>
+
+<td>
+ <button class="cancel button red fonticon-circle-x delete" data-source="<%=source%>" data-doc-id="<%=docid%>" data-rep-id="<%=repid%>" data-continuous="<%=continuous%>" data-target="<%=target%>">Cancel</a>
+</td>
diff --git a/src/fauxton/app/addons/replicator/templates/complete.html b/src/fauxton/app/addons/replicator/templates/complete.html
new file mode 100644
index 000000000..b946082b7
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/templates/complete.html
@@ -0,0 +1,43 @@
+<!--
+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.
+-->
+<td>
+ <a href="#database/_replicator/<%=docid%>">
+ <%=docid%>
+ </a>
+</td>
+
+<td>
+ <%=source%>
+</td>
+
+<td>
+ <%=target%>
+</td>
+
+<td>
+ <span class='<% if (continuous){ %>icon-refresh <% } %>'>
+ <%=continuous%>
+ </span>
+</td>
+
+
+<td>
+ <span class="green fonticon-check">
+ <%=status%>
+ </span>
+</td>
+
+<td>
+ <button class="retry button green fonticon-replicate " data-source="<%=source%>" data-doc-id="<%=docid%>" data-rep-id="<%=repid%>" data-continuous="<%=continuous%>" data-target="<%=target%>">Again?</a>
+</td>
diff --git a/src/fauxton/app/addons/replicator/templates/error.html b/src/fauxton/app/addons/replicator/templates/error.html
new file mode 100644
index 000000000..550007974
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/templates/error.html
@@ -0,0 +1,41 @@
+<!--
+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.
+-->
+<td>
+ <a href="#database/_replicator/<%=docid%>">
+ <%=docid%>
+ </a>
+</td>
+
+<td>
+ <%=source%>
+</td>
+
+<td>
+ <%=target%>
+</td>
+
+<td>
+ <span class='<% if (continuous){%>icon-refresh <% } %>'>
+ <%=continuous%>
+ </span>
+</td>
+
+
+<td>
+ <button class="validate button red fonticon-x " data-source="<%=source%>" data-doc-id="<%=docid%>" data-rep-id="<%=repid%>" data-continuous="<%=continuous%>" data-target="<%=target%>">Find Errors</a>
+</td>
+
+<td>
+ <button class=" button green fonticon-replicate retry" data-source="<%=source%>" data-doc-id="<%=docid%>" data-rep-id="<%=repid%>" data-continuous="<%=continuous%>" data-target="<%=target%>">Retry</a>
+</td>
diff --git a/src/fauxton/app/addons/replicator/templates/form.html b/src/fauxton/app/addons/replicator/templates/form.html
new file mode 100644
index 000000000..754e8aeae
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/templates/form.html
@@ -0,0 +1,71 @@
+<!--
+Licensed under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy of
+the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+-->
+<form id="replication" class="form-horizontal">
+ <div class="control-group" id="step4">
+ <label class="control-label">_id <br>
+ <small>(optional)</small>
+ </label>
+ <div class="source controls">
+ <input type="text" id="repID" name="_id" size="30" value="" data-validation="optional" placeholder="e.g. my_rep">
+ </div>
+ </div>
+ <!-- SOURCE -->
+ <div class="control-group" >
+ <label class="control-label">SOURCE DATABASE:</label>
+ <div class="source controls">
+ <div id="source_form"></div>
+ </div>
+
+ </div>
+
+ <div class="control-group">
+ <label class="control-label">TARGET DATABASE:</label>
+ <div class="target controls">
+ <div class="btn-group toggle-btns" id="create_target">
+
+ <label for="existing-target" class="btn active">
+ Existing Database
+ </label>
+
+ <label for="new-target" class="btn">
+ New Database
+ </label>
+
+ <input type="radio" id="existing-target" name="create_target" class="next" checked="checked" value="false">
+ <input type="radio" id="new-target" name="create_target" class="next" value="true">
+
+ </div>
+ </div>
+ </div>
+
+ <!--TARGET-->
+ <div class="control-group">
+ <div class="target controls">
+ <div id="target_form"></div>
+ <label for="continuous">
+ <input type="checkbox" name="continuous" value="true" id="continuous">
+ Make this replication continuous.
+ </label>
+ </div>
+ </div>
+
+ <div class="control-group">
+ <div class="actions target controls">
+ <button class="button green save btn-large fonticon-replicate" type="submit" >Replicate</button>
+ </div>
+ </div>
+
+</form>
+
+<div class="password_modal"></div>
diff --git a/src/fauxton/app/addons/replicator/templates/localremotetabs.html b/src/fauxton/app/addons/replicator/templates/localremotetabs.html
new file mode 100644
index 000000000..a87241dbc
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/templates/localremotetabs.html
@@ -0,0 +1,39 @@
+<!--
+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.
+-->
+
+<ul class="nav nav-tabs" id="<%=type%>Tabs">
+ <li class="active">
+ <a href="#" class="btn local-btn" data-bypass="true" data-tab="<%=type%>_local">
+ <% if (newDB){%>Create a new database locally <%}else{%> My Databases <%}%>
+ </a>
+ </li>
+ <li>
+ <a href="#" class="btn remote-btn" data-bypass="true" data-tab="<%=type%>_remote">
+ <% if (newDB){%>Create a new remote database <%}else{%> Remote Database <%}%>
+ </a>
+ </li>
+</ul>
+
+<div class="tab-content small-tabs">
+ <div class="tab-pane active" id="<%=type%>_local">
+ <input type="text" id="to_name" name="<%=type%>" autofocus size="30" placeholder="<%if(!newDB){%>Start typing to select a <%=type%> database<%}else{%>Name your database<%}%>" class="<%if(!newDB){%>auto<%}%>">
+ </div>
+
+ <div class="tab-pane" id="<%=type%>_remote">
+ <input type="text" id="<%=type%>_url" name="<%=type%>" size="30" class="next" placeholder="http://" >
+ <small>e.g. http://username:password@cloudant.com/database</small>
+ </div>
+</div>
+
+<div id="options-here"></div>
diff --git a/src/fauxton/app/addons/replicator/templates/nope.html b/src/fauxton/app/addons/replicator/templates/nope.html
new file mode 100644
index 000000000..50cd6c653
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/templates/nope.html
@@ -0,0 +1,17 @@
+<!--
+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.
+-->
+<td colspan="5">
+You have no <%=type%> replications
+</td>
+
diff --git a/src/fauxton/app/addons/replicator/templates/options.html b/src/fauxton/app/addons/replicator/templates/options.html
new file mode 100644
index 000000000..6bbf5d5f1
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/templates/options.html
@@ -0,0 +1,35 @@
+<!--
+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.
+-->
+
+<span class="options off">Advanced Options</span>
+<div class="advancedOptions hide">
+ <h4>Apply filters</h4>
+ <p>Sometimes you don't want to transfer all documents from source to target. You can include one or more filter functions in a design document on the source and then tell the replicator to use them.</p>
+ <label for="filter">Enter the design doc and filter name</label>
+ <input type="text" placeholder="myddoc/myfilter" name="filter" id="filter"/>
+ <label for="query">Add query parameters (optional)</label>
+ <input type="text" placeholder='{"key":"value"}' name="query_params" id="query"/>
+
+ <hr>
+ <h4>Named Document Replication</h4>
+ <p>Sometimes you only want to replicate some documents. For this simple case you do not need to write a filter function. Simply add the list of keys, separated by commas.</p>
+ <input type="text" placeholder="foo, bar, baz" name="doc_ids" id="doc_ids"/>
+
+ <hr>
+ <h4>Replicate through a proxy</h4>
+ <p>Pass a "proxy" argument in the replication data to have replication go through an HTTP proxy</p>
+ <input type="text" placeholder="http://localhost:8888" name="proxy" id="proxy"/>
+
+</div>
+
diff --git a/src/fauxton/app/addons/replicator/templates/password_modal.html b/src/fauxton/app/addons/replicator/templates/password_modal.html
new file mode 100644
index 000000000..bf02fd305
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/templates/password_modal.html
@@ -0,0 +1,27 @@
+<!--
+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.
+-->
+<div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+ <h3>Password: </h3>
+</div>
+<div class="modal-body">
+ <p>Replication requires authentication.</p>
+
+ <input name="password" type="password" placeholder="Enter your password"/>
+
+</div>
+<div class="modal-footer">
+ <a class="button green continue-button" data-bypass="true">Continue Replication</a>
+ <a data-dismiss="modal" class="button cancel-button outlineGray" data-bypass="true"> Cancel</a>
+</div>
diff --git a/src/fauxton/app/addons/replicator/templates/sidebar_no_header.html b/src/fauxton/app/addons/replicator/templates/sidebar_no_header.html
new file mode 100644
index 000000000..83a01f948
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/templates/sidebar_no_header.html
@@ -0,0 +1,20 @@
+<!--
+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.
+-->
+<div id="primary-navbar"></div>
+<div id="dashboard" class="container-fluid supportDash">
+ <div class="with-sidebar content-area">
+ <div id="sidebar-content" class="sidebar"></div>
+ <div id="dashboard-content" class="list window-resizeable"></div>
+ </div>
+</div>
diff --git a/src/fauxton/app/addons/replicator/templates/sidebartabs.html b/src/fauxton/app/addons/replicator/templates/sidebartabs.html
new file mode 100644
index 000000000..d668c6a74
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/templates/sidebartabs.html
@@ -0,0 +1,30 @@
+<!--
+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.
+-->
+
+ <ul class="nav nav-tabs nav-stacked replication-nav-stacked">
+ <li>
+ <a data-type-select="new" href="#replication/new" id="case-button" class="button blue round-btn fonticon-circle-plus">
+ New Replication
+ </a>
+ </li>
+ <li>
+ <a data-type-select="active" href="#replication/active">Active Replications</a>
+ </li>
+ <li>
+ <a data-type-select="errors" href="#replication/errors">Errors</a>
+ </li>
+ <li>
+ <a data-type-select="complete" href="#replication/complete">Completed Replications</a>
+ </li>
+</ul>
diff --git a/src/fauxton/app/addons/replicator/templates/statustable.html b/src/fauxton/app/addons/replicator/templates/statustable.html
new file mode 100644
index 000000000..70ab5ac4f
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/templates/statustable.html
@@ -0,0 +1,23 @@
+<!--
+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.
+-->
+ <thead>
+ <th>Doc ID</th>
+ <th>Source</th>
+ <th>Target</th>
+ <th>Continuous?</th>
+ <th>Status</th>
+ <th>Actions</th>
+ </thead>
+ <tbody id="replication-status">
+ </tbody>
diff --git a/src/fauxton/app/addons/replicator/templates/validator.html b/src/fauxton/app/addons/replicator/templates/validator.html
new file mode 100644
index 000000000..8980bc009
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/templates/validator.html
@@ -0,0 +1,125 @@
+<!--
+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.
+-->
+<td colspan="6">
+ <button class="close">&times;</button>
+ <p>Here are the basic issues that might have occurred. If your replication passes this test, look at it in the docs for other issues or contact us.</p>
+ <dl class="dl-horizontal replication-validator">
+
+ <dt>Does your replication have a Source?</dt>
+ <dd class="validate-source">
+ <% if (source){ %>
+ <span class="fonticon-circle-check green">YES</span>
+ <% } else {%>
+ <span class="fonticon-x red">NO</span>
+ <% } %>
+ </dd>
+
+ <% if (!model.isURL("source")){ %>
+ <dt>Is your source a full url?</dt>
+ <dd class="validate-target">
+ <span class="fonticon-x red"></span>
+ When passing in source and target the reference must be a full URL. e.g: <code>http://username.cloudant.com/dbname</code>
+ </dd>
+ <% } %>
+
+ <dt>Does your replication have a Target?</dt>
+ <dd class="validate-target">
+ <% if (target){ %>
+ <span class="fonticon-circle-check green">YES</span>
+ <% } else {%>
+ <span class="fonticon-x red">NO</span>
+ <% } %>
+ </dd>
+
+
+
+ <% if (!model.isURL("target")){ %>
+ <dt>Is your target a full url?</dt>
+ <dd class="validate-target">
+ <span class="fonticon-x red"></span>
+ When passing in source and target the reference must be a full URL. e.g: <code>http://username.cloudant.com/dbname</code>
+ </dd>
+ <% } %>
+
+ <% if (!typeof model.get("create_target") === "boolean"){ %>
+ <dt>Create_target primitive type...</dt>
+ <dd class="validate-target">
+ <span class="fonticon-x red">Not boolean</span>
+ Suggestion: <code>"create_target": true</code> must be a boolean (true || false)
+ </dd>
+ <% } %>
+
+ <% if (!typeof model.get("continuous") === "boolean"){ %>
+ <dt>Continuous primitive type...</dt>
+ <dd class="validate-target">
+ <span class="fonticon-x red">Not boolean</span>
+ Suggestion: <code>"continuous": true</code> must be a boolean (true || false)
+ </dd>
+ <% } %>
+
+ <dt>Does your replication have a user_ctx?</dt>
+ <dd class="validate-user">
+ <% if (user_ctx){ %>
+ <span class="fonticon-circle-check green">YES</span>
+ <% } else {%>
+ <span class="fonticon-x red">NO</span>
+ The user_ctx property is mandatory for cloudant. It is provided by default when submitting a replication in the client.
+ Example:
+ <pre>
+ {
+ "_id": "my_rep",
+ "source": "http://bserver.com:5984/foo",
+ "target": "bar",
+ "continuous": true,
+ "user_ctx": {
+ "name": "joe",
+ "roles": ["erlanger", "researcher"]
+ }
+ }
+ </pre>
+ <% } %>
+ </dd>
+
+ </dl>
+
+ <p>If everything passed, here are additional things to check: </p>
+ <dl class="dl-horizontal replication-validator">
+ <dt>Have you authenticated?</dt>
+ <dd><p>In order to access target and source databases, you must authenticate with the proper username &amp; password. You can authenticate in the database url, e.g. <code>http://username:password@username.cloudant.com/foo</code></p>
+
+
+ <p>Or when posting to _replicator, include headers if you wish to encrypt your passwords. </p>
+<pre>
+{
+ //...
+ source: {
+ url: "http://username.cloudant.com/foo",
+ headers: {
+ "Authorization": "BASIC dXNlcm5hbWU6cGFzc3dvcmQ="
+ }
+ }
+}
+</pre>
+ </dd>
+
+ <dt>Make sure remote databases exist: </dt>
+ <dd>Replicating from a database that isn't hosted under this account? Make sure that the database exists.</dd>
+
+
+
+ </dl>
+
+
+
+</td>
diff --git a/src/fauxton/app/addons/replicator/tests/replicatorSpec.js b/src/fauxton/app/addons/replicator/tests/replicatorSpec.js
new file mode 100644
index 000000000..702d7ec7a
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/tests/replicatorSpec.js
@@ -0,0 +1,28 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+define([
+ 'addons/cloudantreplication/base',
+ 'chai'
+], function (Replication, chai) {
+ var expect = chai.expect;
+
+ describe('Replication Addon', function(){
+
+ describe('Replication DBList Collection', function () {
+ var rep;
+
+ beforeEach(function () {
+ rep = new rep.DBList(["foo","bar","baz","bo"]);
+ });
+ });
+ });
+});
diff --git a/src/fauxton/app/addons/replicator/views.js b/src/fauxton/app/addons/replicator/views.js
new file mode 100644
index 000000000..2cb834205
--- /dev/null
+++ b/src/fauxton/app/addons/replicator/views.js
@@ -0,0 +1,776 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+define([
+ "app",
+ "api",
+ "addons/fauxton/components",
+ "addons/replicator/resources",
+ "addons/replicator/happy"
+],
+function(app, FauxtonAPI, Components, replication) {
+ var View = {},
+ Events ={},
+ pollingInfo ={
+ rate: 5,
+ intervalId: null
+ },
+ retryReplication = {};
+
+ _.extend(Events, Backbone.Events);
+
+ View.Sidebar = FauxtonAPI.View.extend({
+ template: 'addons/replicator/templates/sidebartabs',
+ afterRender: function(){
+ var historyFrag = Backbone.history.fragment.replace("replication/",""),
+ thisSection = (historyFrag=="replication")?'new':historyFrag;
+ $(".replication-nav-stacked").find('.active').removeClass('active');
+ $("#sidebar-content").find("[data-type-select='"+thisSection+"']").parents('li').addClass("active");
+ }
+ });
+
+
+ View.ReplicationItem = FauxtonAPI.View.extend({
+ tagName: "tr",
+ template: "addons/replicator/templates/active",
+ events: {
+ "click .cancel": "cancelReplication"
+ },
+ establish: function(){
+ return [this.model.fetch()];
+ },
+ cancelReplication: function(e){
+ //need to pass "cancel": true with source & target
+ var $currentTarget = this.$(e.currentTarget),
+ repID = $currentTarget.attr('data-rep-id'),
+ docID = $currentTarget.attr('data-doc-id'),
+ that = this;
+
+ this.newRepModel = new replication.Replicate({
+ "id": docID,
+ "replication_id": repID,
+ "cancel": true
+ });
+ this.newRepModel.fetch().then(function(){
+ that.deleteit();
+ });
+ },
+ deleteit: function(){
+ this.newRepModel.destroy().then(function(model, xhr, options){
+ var notification = FauxtonAPI.addNotification({
+ msg: "Replication stopped.",
+ type: "success",
+ clear: true
+ });
+ },
+ function(model, xhr, options){
+ var errorMessage = JSON.parse(xhr.responseText);
+ var notification = FauxtonAPI.addNotification({
+ msg: errorMessage.reason,
+ type: "error",
+ clear: true
+ });
+ });
+ },
+ serialize: function(){
+ return {
+ docs_written: this.model.get('docs_written'),
+ target: this.model.get('target').replace(/\:(\w*)\b/,":*****"),
+ source: this.model.get('source').replace(/\:(\w*)\b/,":*****"),
+ continuous: this.model.get('continuous'),
+ repid: this.model.get('replication_id'),
+ docid: this.model.get('doc_id')
+ };
+ }
+ });
+
+ View.ActiveReplications = FauxtonAPI.View.extend({
+ tagName: "table",
+ className: "replication-active replication-table table table-striped",
+ template: "addons/replicator/templates/statustable",
+ initialize: function(){
+ Events.bind('update:tasks', this.establish, this);
+ this.listenTo(this.collection, "reset", this.render);
+ },
+ establish: function(){
+ return [this.collection.fetch({reset: true})];
+ },
+ setPolling: function(){
+ var that = this;
+ this.cleanup();
+
+ pollingInfo.intervalId = setInterval(function() {
+ that.establish();
+ }, pollingInfo.rate*1000);
+
+ },
+ cleanup: function(){
+ clearInterval(pollingInfo.intervalId);
+ },
+ beforeRender: function(){
+ if (this.collection.length > 0 ) {
+ this.collection.forEach(function(item) {
+ this.insertView("#replication-status", new View.ReplicationItem({
+ model: item
+ }));
+ }, this);
+ } else {
+ this.insertView("#replication-status", new View.ReplicatorNone({
+ type: "active"
+ }));
+ }
+ },
+ afterRender: function(){
+ this.setPolling();
+ }
+ });
+
+
+ View.ReplicationErrors = FauxtonAPI.View.extend({
+ tagName: "table",
+ className: "replication-errors replication-table table table-striped",
+ template: "addons/replicator/templates/statustable",
+ initialize: function(options){
+ this.databases = options.databases;
+ this.replicatorDB = false;
+ },
+ establish: function(){
+ var deferred = FauxtonAPI.Deferred(),
+ that = this;
+
+ FauxtonAPI.when(this.databases.fetch()).always(function(resp) {
+ if (that.databases.getReplicatorDB().length > 0){
+
+ that.collection.fetch().done(function(){
+ that.replicatorDB = true;
+ deferred.resolve();
+ });
+ } else {
+ that.replicatorDB = false;
+ deferred.resolve();
+ }
+ });
+ return [deferred];
+ },
+ beforeRender: function(){
+ //put this into the collection
+ if (this.replicatorDB){
+ var errors = this.collection.getErrored();
+ if (errors.length > 0){
+ errors.forEach(function(item) {
+ //CAP R
+ this.insertView("#replication-status", new View.ReplicationError({
+ model: item
+ }));
+ }, this);
+ } else {
+ this.insertView("#replication-status", new View.ReplicatorNone({
+ type: "errored"
+ }));
+ }
+ } else {
+ this.insertView("#replication-status", new View.ReplicatorNone({
+ type: "errored"
+ }));
+ }
+ }
+ });
+
+
+ View.ReplicatorNone = FauxtonAPI.View.extend({
+ tagName: "tr",
+ template: "addons/replicator/templates/nope",
+ initialize: function(options){
+ this.type = options.type;
+ },
+ serialize: function(){
+ return {
+ type: this.type
+ };
+ }
+ });
+
+ View.ReplicationValidator = FauxtonAPI.View.extend({
+ tagName: "tr",
+ template: "addons/replicator/templates/validator",
+ events: {
+ "click .close": "remove"
+ },
+ serialize: function(){
+ return {
+ source: this.model.hasSource(),
+ target: this.model.hasTarget(),
+ user_ctx: this.model.hasUserCtx(),
+ auth: false,
+ model: this.model
+ };
+ }
+ });
+
+
+ View.ReplicationError = FauxtonAPI.View.extend({
+ template: "addons/replicator/templates/error",
+ tagName: "tr",
+ events: {
+ "click button.validate": "validate",
+ "click button.retry": "retryRep"
+ },
+ retryRep: function(){
+ retryReplication.source = {
+ url: this.model.get("source"),
+ local: this.model.isLocal("source")
+ };
+ retryReplication.target = {
+ url: this.model.get("target"),
+ local: this.model.isLocal("target")
+ };
+ FauxtonAPI.navigate('/replication/new');
+ },
+ validate: function(){
+ if (this.validateview){this.validateview.remove();}
+ this.validateview = new View.ReplicationValidator({
+ model: this.model
+ });
+ this.$el.after(this.validateview.render().el);
+ },
+ serialize: function(){
+ return {
+ status: this.model.get("_replication_state"),
+ target: this.model.get("target").replace(/\:(\w*)\b/,":*****"),
+ source: this.model.get("source").replace(/\:(\w*)\b/,":*****"),
+ continuous: this.model.get("continuous")||false,
+ repid: this.model.get("_id"),
+ docid: this.model.get("_id")
+ };
+ }
+ });
+
+ View.ReplicationComplete = FauxtonAPI.View.extend({
+ template: "addons/replicator/templates/complete",
+ tagName: "tr",
+ events: {
+ "click button.retry": "postReplication"
+ },
+ postReplication: function(){
+ var data = this.model.getDataforReplication(),
+ that = this;
+
+ this.newRepModel = new replication.Replicate({});
+ this.newRepModel.save(data,{
+ success: function(model, xhr, options){
+ var notification;
+ notification = FauxtonAPI.addNotification({
+ msg: "Replication from "+model.get('source').url+" to "+ model.get('target').url+" has been posted to the _replicator database.",
+ type: "success",
+ clear: true
+ });
+ model.fetch().done(
+ function(resp){
+ if (resp._replication_state === "error") {
+ notification = FauxtonAPI.addNotification({
+ msg: "Replication error. Check the _replicator database for errors.",
+ type: "error",
+ clear: true
+ });
+ FauxtonAPI.navigate('/replication/active');
+ } else if (resp._replication_state === "triggered"){
+ notification = FauxtonAPI.addNotification({
+ msg: "Replication has been triggered.",
+ type: "success",
+ clear: true
+ });
+ } else {
+ notification = FauxtonAPI.addNotification({
+ msg: "This replication has been posted to the _replicator database but hasn't been fired yet. Check back in a few mins to see it's state.",
+ clear: true
+ });
+ }
+ }
+ );
+ },
+ error: function(model, xhr, options){
+ var errorMessage = JSON.parse(xhr.responseText);
+ var notification = FauxtonAPI.addNotification({
+ msg: errorMessage.reason,
+ type: "error",
+ clear: true
+ });
+ }
+ });
+ },
+ serialize: function(){
+ return {
+ status: this.model.get("_replication_state"),
+ target: this.model.get("target").replace(/\:(\w*)\b/,":*****"),
+ source: this.model.get("source").replace(/\:(\w*)\b/,":*****"),
+ continuous: this.model.get("continuous")||false,
+ repid: this.model.get("_id"),
+ docid: this.model.get("_id")
+ };
+ }
+ });
+
+ View.CompletedReplications = FauxtonAPI.View.extend({
+ tagName: "table",
+ className: "replication-complete replication-table table table-striped",
+ template: "addons/replicator/templates/statustable",
+ initialize: function(options){
+ this.databases = options.databases;
+ this.replicatorDB = false;
+ },
+ establish: function(){
+ var deferred = FauxtonAPI.Deferred(),
+ that = this;
+
+ FauxtonAPI.when(this.databases.fetch()).always(function(resp) {
+ if (that.databases.getReplicatorDB().length > 0){
+
+ that.collection.fetch().done(function(){
+ that.replicatorDB = true;
+ deferred.resolve();
+ });
+ } else {
+ that.replicatorDB = false;
+ deferred.resolve();
+ }
+ });
+ return [deferred];
+ },
+ beforeRender: function(){
+ if (this.replicatorDB){
+ var completed = this.collection.getCompleted();
+ if (completed.length > 0){
+ completed.forEach(function(item) {
+ this.insertView("#replication-status", new View.ReplicationComplete({
+ model: item
+ }));
+ }, this);
+ } else {
+ this.insertView("#replication-status", new View.ReplicatorNone({
+ type: "completed"
+ }));
+ }
+ } else {
+ this.insertView("#replication-status", new View.ReplicatorNone({
+ type: "completed"
+ }));
+ }
+ }
+ });
+
+
+
+ View.ReplicationForm = FauxtonAPI.View.extend({
+ template: "addons/replicator/templates/form",
+ events: {
+ "submit #replication": "getPassword",
+ "change #create_target input[type='radio']": "showTargetTabs",
+ "click #create_target label": "buttonActiveState"
+ },
+ initialize: function(options){
+ this.status = options.status;
+ this.selectedSource = options.source;
+ Events.bind('replicate:start', this.replicateStart, this);
+ },
+ afterRender: function(){
+ // client side form validation
+ this.formvalidation();
+ //focus on first input
+ $(this.el).find("input:visible").eq(0).focus();
+ },
+ beforeRender: function(){
+ //insert the source and target tabs
+ var sourceDB = this.selectedSource;
+ this.insertView("#source_form", new View.LocalRemoteTabs({
+ selectedDB: sourceDB || "",
+ type: "source"
+ }));
+ this.targetForm = this.insertView("#target_form", new View.LocalRemoteTabs({
+ type: "target"
+ }));
+
+ },
+ buttonActiveState: function(e){
+ //Set the button state on LOCAL or NEW
+ var $currentTarget = this.$(e.currentTarget);
+ $currentTarget.parents("#create_target").find('.active').removeClass('active');
+ $currentTarget.addClass('active');
+ },
+ enableFields: function(){
+ this.$el.find('input','select').attr('disabled',false);
+ },
+ disableFields: function(){
+ this.$el.find('input[type="text"]:hidden','select:hidden').not("[type='radio']").attr('disabled',true);
+ },
+ establish: function(){
+ return [this.collection.fetch()];
+ },
+ formvalidation: function(e){
+ //client side form validation
+ var that = this;
+ this.$('form#replication').isHappy({
+ fields: {
+ 'input[name="source"]:visible': {
+ required: true,
+ tests: [
+ {
+ test: function(val){
+ return $('input[name="target"]:visible').val() !== val;
+ },
+ message: 'Your source cannot be the same DB as your target',
+ },
+ {
+ test: function(val){
+ if (that.$('input#source_url').is(':visible')){
+ return true;
+ } else {
+ var alreadyExists = that.collection.where({"name":val});
+ return alreadyExists.length !== 0;
+ }
+ },
+ message: "This database doesn't exist."
+ },
+ {
+ test: function(val){
+ if (!$('input[name="source"]:visible').is("#source_url")){
+ return true;
+ } else {
+ var regex = "^(http|https|ftp)://([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&amp;%$-]+)*@)*((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]).(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0).(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0).(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])|([a-zA-Z0-9-]+.)*[a-zA-Z0-9-]+.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(/($|[a-zA-Z0-9.,?'\\+&amp;%$#=~_-]+))*$",
+ urlregex = new RegExp(regex);
+ return urlregex.test(val);
+ }
+ },
+ message: 'Remote databases must be written as urls.',
+ }
+ ]
+ },
+ 'input[name="target"]:visible': {
+ required: true,
+ tests: [
+ {
+ test: function(val){
+ return $('input[name="source"]:visible').val() !== val;
+ },
+ message: 'Your target cannot be the same DB as your source.'
+ },
+
+ {
+ test: function(val){
+ var alreadyExists = that.collection.where({"name":val});
+ if (that.$('input#target_url').is(':visible') ){
+ return true;
+ } else if (that.$('[name="create_target"]:checked').val()==="false") {
+ return alreadyExists.length !== 0;
+ } else {
+ return alreadyExists.length === 0;
+ }
+ },
+ message: "This database doesn't exist. Select New Database if you want to create it."
+ },
+ {
+ test: function(val){
+ if (!$('input[name="target"]:visible').is("#target_url")){
+ return true;
+ } else {
+ var regex = "^(http|https|ftp)://([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&amp;%$-]+)*@)*((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]).(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0).(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0).(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])|([a-zA-Z0-9-]+.)*[a-zA-Z0-9-]+.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(/($|[a-zA-Z0-9.,?'\\+&amp;%$#=~_-]+))*$",
+ urlregex = new RegExp(regex);
+ return urlregex.test(val);
+ }
+ },
+ message: 'Remote databases must be written as urls.',
+ }
+ ]
+ }
+ }
+ });
+ },
+ showTargetTabs: function(e){
+ //Switch from local to new
+ if (this.targetForm){ this.targetForm.remove();}
+ this.targetForm = this.insertView("#target_form", new View.LocalRemoteTabs({
+ newDB: this.$('[name="create_target"]:checked').val()==="true",
+ type: "target"
+ }));
+ this.targetForm.render();
+ },
+ postToReplicator: function(json){
+ // Post to _replicator DB
+ var that = this;
+ this.newRepModel = new replication.Replicate({});
+ this.newRepModel.save(json,{
+ success: function(model, xhr, options){
+ var notification = FauxtonAPI.addNotification({
+ msg: "Replication from "+model.get('source').url+" to "+ model.get('target').url+" has been posted to the _replicator database.",
+ type: "success",
+ clear: true
+ });
+ that.updateButtonText(false);
+ that.checkReplicationState(model);
+ },
+ error: function(model, xhr, options){
+ var errorMessage = JSON.parse(xhr.responseText);
+ var notification = FauxtonAPI.addNotification({
+ msg: errorMessage.reason,
+ type: "error",
+ clear: true
+ });
+ that.updateButtonText(false);
+ }
+ });
+ this.enableFields();
+ },
+ checkReplicationState: function(model){
+ // check if it's been triggered
+ // redirect to /replication/active if the replication has been triggered
+ var replicator = model,
+ notification;
+ replicator.fetch().done(
+ function(resp){
+ if (resp._replication_state === "error") {
+ notification = FauxtonAPI.addNotification({
+ msg: "Replication error. Check the _replicator database for errors.",
+ type: "error",
+ clear: true
+ });
+ FauxtonAPI.navigate('/replication/errors');
+ } else if (resp._replication_state === "triggered"){
+ notification = FauxtonAPI.addNotification({
+ msg: "Replication has been triggered.",
+ type: "success",
+ clear: true
+ });
+ FauxtonAPI.navigate('/replication/active');
+ } else {
+ notification = FauxtonAPI.addNotification({
+ msg: "This replication has been posted to the _replicator database but hasn't been fired yet. Check the _replicator DB to see it's state.",
+ clear: true
+ });
+ }
+ }
+ );
+ },
+ updateButtonText: function(showWaitingText){
+ var $button = this.$('#replication button[type=submit]');
+ if(showWaitingText){
+ $button.text('Starting replication...').attr('disabled', true);
+ } else {
+ $button.text('Replication').attr('disabled', false);
+ }
+ },
+
+ replicateStart: function(password){
+ var formData = this.scrubFormData(),
+ username = app.session.get('userCtx').name;
+ if ($('#source_local').is(':visible')){
+ formData.source = this.setAuthHeaders(formData.source,username,password);
+ }
+ if ($('#target_local').is(':visible')){
+ formData.target = this.setAuthHeaders(formData.target,username,password);
+ }
+
+ this.submit(formData);
+ },
+ getPassword: function(e){
+ e.preventDefault();
+ this.updateButtonText(true);
+ var formData = this.scrubFormData();
+
+ if ($('#source_local').is(':visible') || $('#target_local').is(':visible')){
+ this.passwordPopup();
+ } else {
+ this.submit(formData);
+ }
+ },
+
+ submit: function(formData){
+ var that = this;
+ if (this.collection.getReplicatorDB().length === 0){
+ FauxtonAPI.when(this.collection.createReplicatorDB()).always(function(resp){
+ that.postToReplicator(formData);
+ that.enableFields();
+ });
+ }else{
+ this.postToReplicator(formData);
+ this.enableFields();
+ }
+
+
+
+ },
+ setAuthHeaders: function(source,user,pass){
+ var basicHeader = new FauxtonAPI.session.createBasicAuthHeader(user,pass),
+ json = {};
+ json.url = window.location.origin +"/"+ app.utils.safeURLName(source);
+ json.headers = {
+ "Authorization": basicHeader.basicAuthHeader
+ };
+ return json;
+ },
+ passwordPopup: function(){
+ //insert Password Modal
+ var AuthenticationView = FauxtonAPI.getExtensions('replicator:Authentication');
+
+ if (AuthenticationView){
+ var model = new this.collection.model(),
+ password = this.insertView("#password_modal", new AuthenticationView({
+ model: model
+ }));
+ password.render();
+ } else {
+ this.submit(formData);
+ }
+ },
+ scrubFormData: function(){
+ //DISABLE HIDDEN FIELDS
+ this.disableFields();
+ var data = {};
+ _.map(this.$('#replication').serializeArray(), function(formData){
+ if(formData.value !== ''){
+ //clean booleans & whitespaces
+ if (formData.name == "_id"){
+ data[formData.name]=formData.value.replace(/\s/g, '').toLowerCase();
+ } else if (formData.name == "create_target" || formData.name == "continuous"){
+ data[formData.name] = (formData.value ==="true")?true:false;
+ } else {
+ //Lotta stuff needs to be scrubbed before it's in proper json to submit
+ data[formData.name] = formData.value.trim().replace(/\s/g, '-');
+ }
+ }
+ });
+ data.user_ctx = FauxtonAPI.session.get('userCtx');
+ return data;
+ }
+ });
+
+ View.PasswordModal = FauxtonAPI.View.extend({
+ tagName: "div",
+ className: "modal",
+ template: 'addons/replicator/templates/password_modal',
+ events: {
+ "click a.cancel-button" : "hideModal",
+ "click a.continue-button": "checkValidation"
+ },
+ initialize: function(){
+ this.showModal();
+ },
+ triggerReplication: function(){
+ Events.trigger('replicate:start', this.$('[name="password"]').val());
+ },
+ checkValidation: function(e){
+ e.preventDefault();
+ var password = this.$('[name="password"]').val(),
+ that = this;
+ FauxtonAPI.when(this.model.authValidation(password)).always(function(){
+ if (that.model.passworderror){
+ var notification = FauxtonAPI.addNotification({
+ msg: "Your password is incorrect.",
+ type: "error",
+ clear: true
+ });
+ } else {
+ that.triggerReplication();
+ that.hideModal();
+ }
+ });
+ },
+ hideModal: function(e){
+ if(e){e.preventDefault();}
+ $(this.el).modal('hide');
+ },
+ showModal: function(){
+ $(this.el).modal({show:true});
+ }
+ });
+
+ View.AdvancedOptions = FauxtonAPI.View.extend({
+ className: "authenticate",
+ template: "addons/replicator/templates/options",
+ events: {
+ "click .options": "toggleAdvancedOptions",
+ },
+ toggleAdvancedOptions: function(e){
+ this.$(e.currentTarget).toggleClass("off");
+ this.$('.advancedOptions').toggle("hidden").find('input').removeAttr('disabled');
+ }
+ });
+
+ View.LocalRemoteTabs = FauxtonAPI.View.extend({
+ template: "addons/replicator/templates/localremotetabs",
+ events: {
+ "click .nav-tabs a": "tabs"
+ },
+ afterRender: function(){
+ this.dbSearchTypeahead = new Components.DbSearchTypeahead({
+ dbLimit: 30,
+ el: "input.auto",
+ updater: function(item){
+ return item;
+ }
+ });
+ this.dbSearchTypeahead.render();
+ this.preselectedDatabase();
+ },
+ initialize: function(options){
+ this.type = options.type;
+ this.newDB = options.newDB || false;
+ this.selected = options.selectedDB || "";
+ },
+
+ preselectedDatabase: function(){
+ //if selected database is passed through from the _all_dbs page
+ if (this.selected){
+ this.$('input[type="text"]:visible').val(this.selected);
+ } else if (retryReplication[this.type]) {
+ this.setLocation();
+ }
+ },
+ setLocation: function(){
+ if (retryReplication[this.type].local){
+ var urlArray = retryReplication[this.type].url.split('/'),
+ location = urlArray[urlArray.length-1];
+ this.$('input[type="text"]:visible').val(location);
+ } else {
+ this.$('.remote-btn').trigger("click");
+ this.$('input[type="text"]:visible').val(retryReplication[this.type].url);
+ }
+ delete retryReplication[this.type];
+ },
+ showAdvancedOptions: function(e){
+ //not called for now
+ if (this.$(e.currentTarget).attr('name') === "source"){
+ if (this.advancedOptions){ this.advancedOptions.remove();}
+ this.advancedOptions = this.insertView("#options-here", new View.AdvancedOptions({}));
+ this.advancedOptions.render();
+ }
+ },
+ tabs: function(e){
+ e.preventDefault();
+ var $currentTarget = this.$(e.currentTarget),
+ getTabID = "#"+$currentTarget.attr('data-tab');
+
+ $currentTarget.parents('ul').find('.active').removeClass('active');
+ $currentTarget.parents('li').addClass('active');
+
+ $(getTabID).parents('.tab-content').find('.active').removeClass('active');
+ $(getTabID).addClass('active');
+ },
+ serialize: function(){
+ return {
+ newDB: this.newDB,
+ username: app.session.get('userCtx').name,
+ type: this.type
+ };
+ }
+ });
+
+
+ return View;
+});
diff --git a/src/fauxton/settings.json.default b/src/fauxton/settings.json.default
index 1bc88f649..bbf80160d 100644
--- a/src/fauxton/settings.json.default
+++ b/src/fauxton/settings.json.default
@@ -8,6 +8,7 @@
{ "name": "config" },
{ "name": "logs" },
{ "name": "stats" },
+ { "name": "replicator" },
{ "name": "replication" },
{ "name": "plugins" },
{ "name": "contribute" },