diff options
author | suelockwood <deathbear@apache.org> | 2014-01-15 19:31:23 -0500 |
---|---|---|
committer | suelockwood <deathbear@apache.org> | 2014-03-25 20:59:57 -0400 |
commit | 85c28725fbb9e0c2446984dc23eaeb4121d3b860 (patch) | |
tree | 5a6ca750cacbf8409f999c728d4751309619f66e | |
parent | 1fb7cea25b60585b16bdd786cd3e2b299fa384df (diff) | |
download | couchdb-Fauxton-replicator-addon.tar.gz |
Replicator & ReplicatorAuth extensionFauxton-replicator-addon
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.&%$-]+)*@)*((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.,?'\\+&%$#=~_-]+))*$"; + 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">×</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">×</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 & 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.&%$-]+)*@)*((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.,?'\\+&%$#=~_-]+))*$", + 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.&%$-]+)*@)*((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.,?'\\+&%$#=~_-]+))*$", + 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" }, |