http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/router.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/router.js b/src/main/webapp/assets/js/router.js new file mode 100644 index 0000000..d80d35c --- /dev/null +++ b/src/main/webapp/assets/js/router.js @@ -0,0 +1,240 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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([ + "brooklyn", "underscore", "jquery", "backbone", + "model/application", "model/app-tree", "model/location", + "model/server-extended-status", + "view/home", "view/application-explorer", "view/catalog", "view/script-groovy", + "text!tpl/help/page.html","text!tpl/labs/page.html", "text!tpl/home/server-caution.html" +], function (Brooklyn, _, $, Backbone, + Application, AppTree, Location, + serverStatus, + HomeView, ExplorerView, CatalogView, ScriptGroovyView, + HelpHtml, LabsHtml, ServerCautionHtml) { + + var ServerCautionOverlay = Backbone.View.extend({ + template: _.template(ServerCautionHtml), + scheduledRedirect: false, + initialize: function() { + var that = this; + this.carryOnRegardless = false; + _.bindAll(this); + serverStatus.addCallback(this.renderAndAddCallback); + }, + renderAndAddCallback: function() { + this.renderOnUpdate(); + serverStatus.addCallback(this.renderAndAddCallback); + }, + renderOnUpdate: function() { + var that = this; + if (this.carryOnRegardless) return this.renderEmpty(); + + var state = { + loaded: serverStatus.loaded, + up: serverStatus.isUp(), + shuttingDown: serverStatus.isShuttingDown(), + healthy: serverStatus.isHealthy(), + master: serverStatus.isMaster(), + masterUri: serverStatus.getMasterUri(), + }; + + if (state.loaded && state.up && state.healthy && state.master) { + // this div shows nothing in normal operation + return this.renderEmpty(); + } + + this.warningActive = true; + this.$el.html(this.template(state)); + $('#application-content').fadeTo(500,0.1); + this.$el.fadeTo(200,1); + + $("#dismiss-standby-warning", this.$el).click(function() { + that.carryOnRegardless = true; + if (that.redirectPending) { + log("Cancelling redirect, using this non-master instance"); + clearTimeout(that.redirectPending); + that.redirectPending = null; + } + that.renderOnUpdate(); + }); + + if (!state.master && state.masterUri) { + if (!this.scheduledRedirect && !this.redirectPending) { + log("Not master; will redirect shortly to: "+state.masterUri); + var destination = state.masterUri + "#" + Backbone.history.fragment; + var time = 10; + this.scheduledRedirect = true; + log("Redirecting to " + destination + " in " + time + " seconds"); + this.redirectPending = setTimeout(function () { + // re-check, in case the server's status changed in the wait + if (!serverStatus.isMaster()) { + if (that.redirectPending) { + window.location.href = destination; + } else { + log("Cancelled redirect, using this non-master instance"); + } + } else { + log("Cancelled redirect, this instance is now master"); + } + }, time * 1000); + } + } + return this; + }, + renderEmpty: function() { + var that = this; + this.warningActive = false; + this.$el.fadeTo(200,0.2, function() { + if (!that.warningActive) + that.$el.empty(); + }); + $('#application-content').fadeTo(200,1); + return this; + }, + beforeClose: function() { + this.stopListening(); + }, + warnIfNotLoaded: function() { + if (!this.loaded) + this.renderOnUpdate(); + } + }); + // look for ha-standby-overlay for compatibility with older index.html copies + var serverCautionOverlay = new ServerCautionOverlay({ el: $("#server-caution-overlay").length ? $("#server-caution-overlay") : $("#ha-standby-overlay")}); + serverCautionOverlay.render(); + + var Router = Backbone.Router.extend({ + routes:{ + 'v1/home/*trail':'homePage', + 'v1/applications/:app/entities/*trail':'applicationsPage', + 'v1/applications/*trail':'applicationsPage', + 'v1/applications':'applicationsPage', + 'v1/locations':'catalogPage', + 'v1/catalog/:kind(/:id)':'catalogPage', + 'v1/catalog':'catalogPage', + 'v1/script/groovy':'scriptGroovyPage', + 'v1/help':'helpPage', + 'labs':'labsPage', + '*path':'defaultRoute' + }, + + showView: function(selector, view) { + // close the previous view - does binding clean-up and avoids memory leaks + if (this.currentView) { + this.currentView.close(); + } + // render the view inside the selector element + $(selector).html(view.render().el); + this.currentView = view; + return view; + }, + + defaultRoute: function() { + this.homePage('auto') + }, + + applications: new Application.Collection, + appTree: new AppTree.Collection, + locations: new Location.Collection, + + homePage:function (trail) { + var that = this; + var veryFirstViewLoad, homeView; + // render the page after we fetch the collection -- no rendering on error + function render() { + homeView = new HomeView({ + collection:that.applications, + locations:that.locations, + cautionOverlay:serverCautionOverlay, + appRouter:that + }); + veryFirstViewLoad = !that.currentView; + that.showView("#application-content", homeView); + } + this.applications.fetch({success:function () { + render(); + // show add application wizard if none already exist and this is the first page load + if ((veryFirstViewLoad && trail=='auto' && that.applications.isEmpty()) || (trail=='add_application') ) { + if (serverStatus.isMaster()) { + homeView.createApplication(); + } + } + }, error: render}); + }, + applicationsPage:function (app, trail, tab) { + if (trail === undefined) trail = app + var that = this + this.appTree.fetch({success:function () { + var appExplorer = new ExplorerView({ + collection:that.appTree, + appRouter:that + }) + that.showView("#application-content", appExplorer) + if (trail !== undefined) appExplorer.show(trail) + }}) + }, + catalogPage: function (catalogItemKind, id) { + var catalogResource = new CatalogView({ + locations: this.locations, + appRouter: this, + kind: catalogItemKind, + id: id + }); + this.showView("#application-content", catalogResource); + }, + scriptGroovyPage:function () { + if (this.scriptGroovyResource === undefined) + this.scriptGroovyResource = new ScriptGroovyView({}) + this.showView("#application-content", this.scriptGroovyResource) + $(".nav1").removeClass("active") + $(".nav1_script").addClass("active") + $(".nav1_script_groovy").addClass("active") + }, + helpPage:function () { + $("#application-content").html(_.template(HelpHtml, {})) + $(".nav1").removeClass("active") + $(".nav1_help").addClass("active") + }, + labsPage:function () { + $("#application-content").html(_.template(LabsHtml, {})) + $(".nav1").removeClass("active") + }, + + /** Triggers the Backbone.Router process which drives this GUI through Backbone.history, + * after starting background server health checks and waiting for confirmation of health + * (or user click-through). */ + startBrooklynGui: function() { + serverStatus.whenUp(function() { Backbone.history.start(); }); + serverStatus.autoUpdate(); + _.delay(serverCautionOverlay.warnIfNotLoaded, 2*1000) + } + }); + + $.ajax({ + type: "GET", + url: "/v1/server/user", + dataType: "text" + }).done(function (data) { + if (data != null) { + $("#user").html(_.escape(data)); + } + }); + + return Router +})
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/util/brooklyn-utils.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/util/brooklyn-utils.js b/src/main/webapp/assets/js/util/brooklyn-utils.js new file mode 100644 index 0000000..5f3915c --- /dev/null +++ b/src/main/webapp/assets/js/util/brooklyn-utils.js @@ -0,0 +1,226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/* brooklyn utility methods */ + +define([ + 'jquery', 'underscore' +], function ($, _) { + + var Util = {}; + + /** + * @return {string} empty string if s is null or undefined, otherwise result of _.escape(s) + */ + Util.escape = function (s) { + if (s == undefined || s == null) return ""; + return _.escape(s); + }; + + function isWholeNumber(v) { + return (Math.abs(Math.round(v) - v) < 0.000000000001); + } + + Util.roundIfNumberToNumDecimalPlaces = function (v, mantissa) { + if (!_.isNumber(v) || mantissa < 0) + return v; + + if (isWholeNumber(v)) + return Math.round(v); + + var vk = v, xp = 1; + for (var i=0; i < mantissa; i++) { + vk *= 10; + xp *= 10; + if (isWholeNumber(vk)) { + return Math.round(vk)/xp; + } + } + return Number(v.toFixed(mantissa)); + }; + + Util.toDisplayString = function(data) { + if (data==null) return null; + data = Util.roundIfNumberToNumDecimalPlaces(data, 4); + if (typeof data !== 'string') + data = JSON.stringify(data); + return Util.escape(data); + }; + + Util.toTextAreaString = function(data) { + if (data==null) return null; + data = Util.roundIfNumberToNumDecimalPlaces(data, 8); + if (typeof data !== 'string') + data = JSON.stringify(data, null, 2); + return data; + }; + + if (!String.prototype.trim) { + // some older javascripts do not support 'trim' (including jasmine spec runner) so let's define it + String.prototype.trim = function(){ + return this.replace(/^\s+|\s+$/g, ''); + }; + } + + // from http://stackoverflow.com/questions/646628/how-to-check-if-a-string-startswith-another-string + if (typeof String.prototype.startsWith != 'function') { + String.prototype.startsWith = function (str){ + return this.slice(0, str.length) == str; + }; + } + if (typeof String.prototype.endsWith != 'function') { + String.prototype.endsWith = function (str){ + return this.slice(-str.length) == str; + }; + } + + // poor-man's copy + Util.promptCopyToClipboard = function(text) { + window.prompt("To copy to the clipboard, press Ctrl+C then Enter.", text); + }; + + /** + * Returns the path component of a string URL. e.g. http://example.com/bob/bob --> /bob/bob + */ + Util.pathOf = function(string) { + if (!string) return ""; + var a = document.createElement("a"); + a.href = string; + return a.pathname; + }; + + /** + * Extracts the value of the given input. Returns true/false for for checkboxes + * rather than "on" or "off". + */ + Util.inputValue = function($input) { + if ($input.attr("type") === "checkbox") { + return $input.is(":checked"); + } else { + return $input.val(); + } + }; + + /** + * Updates or initialises the given model with the values of named elements under + * the given element. Force-updates the model by setting silent: true. + */ + Util.bindModelFromForm = function(modelOrConstructor, $el) { + var model = _.isFunction(modelOrConstructor) ? new modelOrConstructor() : modelOrConstructor; + var inputs = {}; + + // Look up all named elements + $("[name]", $el).each(function(idx, inp) { + var input = $(inp); + var name = input.attr("name"); + inputs[name] = Util.inputValue(input); + }); + model.set(inputs, { silent: true }); + return model; + }; + + /** + * Parses xhrResponse.responseText as JSON and returns its message. Returns + * alternate message if parsing fails or the parsed object has no message. + * @param {jqXHR} xhrResponse + * @param {string} alternateMessage + * @param {string=} logMessage if false or null, does not log; + * otherwise it logs a message and the xhrResponse, with logMessage + * (or with alternateMessage if logMessage is true) + * @returns {*} + */ + Util.extractError = function (xhrResponse, alternateMessage, logMessage) { + if (logMessage) { + if (logMessage === true) { + console.error(alternateMessage); + } else { + console.error(logMessage); + } + console.log(xhrResponse); + } + + try { + var response = JSON.parse(xhrResponse.responseText); + return response.message ? response.message : alternateMessage; + } catch (e) { + return alternateMessage; + } + }; + + secretWords = [ "password", "passwd", "credential", "secret", "private", "access.cert", "access.key" ]; + + Util.isSecret = function (key) { + if (!key) return false; + key = key.toString().toLowerCase(); + for (secretWord in secretWords) + if (key.indexOf(secretWords[secretWord]) >= 0) + return true; + return false; + }; + + Util.logout = function logout() { + $.ajax({ + type: "POST", + dataType: "text", + url: "/logout", + success: function() { + window.location.replace("/"); + }, + failure: function() { + window.location.replace("/"); + } + }); + } + + Util.setSelectionRange = function (input, selectionStart, selectionEnd) { + if (input.setSelectionRange) { + input.focus(); + input.setSelectionRange(selectionStart, selectionEnd); + } + else if (input.createTextRange) { + var range = input.createTextRange(); + range.collapse(true); + range.moveEnd('character', selectionEnd); + range.moveStart('character', selectionStart); + range.select(); + } + }; + + Util.setCaretToPos = function (input, pos) { + Util.setSelectionRange(input, pos, pos); + }; + + $.fn.setCaretToStart = function() { + this.each(function(index, elem) { + Util.setCaretToPos(elem, 0); + $(elem).scrollTop(0); + }); + return this; + }; + + $("#logout-link").on("click", function (e) { + e.preventDefault(); + Util.logout() + return false; + }); + + return Util; + +}); + http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/util/brooklyn-view.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/util/brooklyn-view.js b/src/main/webapp/assets/js/util/brooklyn-view.js new file mode 100644 index 0000000..7151ae1 --- /dev/null +++ b/src/main/webapp/assets/js/util/brooklyn-view.js @@ -0,0 +1,352 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. +*/ + +/* brooklyn extensions for supporting views */ +define([ + "jquery", "underscore", "backbone", "brooklyn-utils", + "text!tpl/lib/basic-modal.html", + "text!tpl/lib/config-key-type-value-input-pair.html" + ], function ( + $, _, Backbone, Util, + ModalHtml, ConfigKeyInputHtml +) { + + var module = {}; + + module.refresh = true; + + /** Toggles automatic refreshes of instances of View. */ + module.toggleRefresh = function () { + this.refresh = !this.refresh; + return this.refresh; + }; + + // TODO this customising of the View prototype could be expanded to include + // other methods from viewutils. see discussion at + // https://github.com/brooklyncentral/brooklyn/pull/939 + + // add close method to all views for clean-up + // (NB we have to update the prototype _here_ before any views are instantiated; + // see "close" called below in "showView") + Backbone.View.prototype.close = function () { + // call user defined close method if exists + this.viewIsClosed = true; + if (_.isFunction(this.beforeClose)) { + this.beforeClose(); + } + _.each(this._periodicFunctions, function (i) { + clearInterval(i); + }); + this.remove(); + this.unbind(); + }; + + Backbone.View.prototype.viewIsClosed = false; + + /** + * Registers a callback (cf setInterval) that is unregistered cleanly when the view + * closes. The callback is run in the context of the owning view, so callbacks can + * refer to 'this' safely. + */ + Backbone.View.prototype.callPeriodically = function (uid, callback, interval) { + if (!this._periodicFunctions) { + this._periodicFunctions = {}; + } + var old = this._periodicFunctions[uid]; + if (old) clearInterval(old); + + // Wrap callback in function that checks whether updates are enabled + var periodic = function () { + if (module.refresh) { + callback.apply(this); + } + }; + // Bind this to the view + periodic = _.bind(periodic, this); + this._periodicFunctions[uid] = setInterval(periodic, interval); + }; + + /** + * A form that listens to modifications to its inputs, maintaining a model that is + * submitted when a button with class 'submit' is clicked. + * + * Expects a body view or a template function to render. + */ + module.Form = Backbone.View.extend({ + events: { + "change": "onChange", + "submit": "onSubmit" + }, + + initialize: function() { + if (!this.options.body && !this.options.template) { + throw new Error("body view or template function required by GenericForm"); + } else if (!this.options.onSubmit) { + throw new Error("onSubmit function required by GenericForm"); + } + this.onSubmitCallback = this.options.onSubmit; + this.model = new (this.options.model || Backbone.Model); + _.bindAll(this, "onSubmit", "onChange"); + this.render(); + }, + + beforeClose: function() { + if (this.options.body) { + this.options.body.close(); + } + }, + + render: function() { + if (this.options.body) { + this.options.body.render(); + this.$el.html(this.options.body.$el); + } else { + this.$el.html(this.options.template()); + } + // Initialise the model with existing values + Util.bindModelFromForm(this.model, this.$el); + return this; + }, + + onChange: function(e) { + var target = $(e.target); + var name = target.attr("name"); + this.model.set(name, Util.inputValue(target), { silent: true }); + }, + + onSubmit: function(e) { + e.preventDefault(); + // Could validate model + this.onSubmitCallback(this.model.clone()); + return false; + } + + }); + + /** + * A view to render another view in a modal. Give another view to render as + * the `body' parameter that has an onSubmit function that will be called + * when the modal's `Save' button is clicked, and/or an onCancel callback + * that will be called when the modal is closed without saving. + * + * The onSubmit callback should return either: + * <ul> + * <li><b>nothing</b>: the callback is treated as successful + * <li><b>true</b> or <b>false</b>: the callback is treated as appropriate + * <li>a <b>promise</b> with `done' and `fail' callbacks (for example a jqXHR object): + * The callback is treated as successful when the promise is done without error. + * <li><b>anything else</b>: the callback is treated as successful + * </ul> + * When the onSubmit callback is successful the modal is closed. + * + * The return value of the onCancel callback is ignored. + * + * The modal will still be open and visible when the onSubmit callback is called. + * The modal will have been closed when the onCancel callback is called. + */ + module.Modal = Backbone.View.extend({ + + id: _.uniqueId("modal"), + className: "modal", + template: _.template(ModalHtml), + + events: { + "hide": "onClose", + "click .modal-submit": "onSubmit" + }, + + initialize: function() { + if (!this.options.body) { + throw new Error("Modal view requires body to render"); + } + _.bindAll(this, "onSubmit", "onCancel", "show"); + if (this.options.autoOpen) { + this.show(); + } + }, + + beforeClose: function() { + if (this.options.body) { + this.options.body.close(); + } + }, + + render: function() { + var optionalTitle = this.options.body.title; + var title = _.isFunction(optionalTitle) + ? optionalTitle() + : _.isString(optionalTitle) + ? optionalTitle : this.options.title; + this.$el.html(this.template({ + title: title, + submitButtonText: this.options.submitButtonText || "Apply", + cancelButtonText: this.options.cancelButtonText || "Cancel" + })); + this.options.body.render(); + this.$(".modal-body").html(this.options.body.$el); + return this; + }, + + show: function() { + this.render().$el.modal(); + return this; + }, + + onSubmit: function(event) { + if (_.isFunction(this.options.body.onSubmit)) { + var submission = this.options.body.onSubmit.apply(this.options.body, [event]); + var self = this; + var submissionSuccess = function() { + // Closes view via event. + self.closingSuccessfully = true; + self.$el.modal("hide"); + }; + var submissionFailure = function () { + // Better response. + console.log("modal submission failed!", arguments); + }; + // Assuming no value is fine + if (!submission) { + submission = true; + } + if (_.isBoolean(submission) && submission) { + submissionSuccess(); + } else if (_.isBoolean(submission)) { + submissionFailure(); + } else if (_.isFunction(submission.done) && _.isFunction(submission.fail)) { + submission.done(submissionSuccess).fail(submissionFailure); + } else { + // assuming success and closing modal + submissionSuccess() + } + } + return false; + }, + + onCancel: function () { + if (_.isFunction(this.options.body.onCancel)) { + this.options.body.onCancel.apply(this.options.body); + } + }, + + onClose: function () { + if (!this.closingSuccessfully) { + this.onCancel(); + } + this.close(); + } + }); + + /** + * Shows a modal with yes/no buttons as a user confirmation prompt. + * @param {string} question The message to show in the body of the modal + * @param {string} [title] An optional title to show. Uses generic default if not given. + * @returns {jquery.Deferred} The promise from a jquery.Deferred object. The + * promise is resolved if the modal was submitted normally and rejected + * otherwise. + */ + module.requestConfirmation = function (question, title) { + var deferred = $.Deferred(); + var Confirmation = Backbone.View.extend({ + title: title || "Confirm action", + render: function () { + this.$el.html(question || ""); + }, + onSubmit: function () { + deferred.resolve(); + }, + onCancel: function () { + deferred.reject(); + } + }); + new module.Modal({ + body: new Confirmation(), + autoOpen: true, + submitButtonText: "Yes", + cancelButtonText: "No" + }); + return deferred.promise(); + }; + + /** Creates, displays and returns a modal with the given view used as its body */ + module.showModalWith = function (bodyView) { + return new module.Modal({body: bodyView}).show(); + }; + + /** + * Presents inputs for config key names/values with buttons to add/remove entries + * and a function to extract a map of name->value. + */ + module.ConfigKeyInputPairList = Backbone.View.extend({ + template: _.template(ConfigKeyInputHtml), + // Could listen to input change events and add 'error' class to any type inputs + // that duplicate values. + events: { + "click .config-key-row-remove": "rowRemove", + "keypress .last": "rowAdd" + }, + render: function () { + if (this.options.configKeys) { + var templated = _.map(this.options.configKeys, function (value, key) { + return this.templateRow(key, value); + }, this); + this.$el.html(templated.join("")); + } + this.$el.append(this.templateRow()); + this.markLast(); + return this; + }, + rowAdd: function (event) { + this.$el.append(this.templateRow()); + this.markLast(); + }, + rowRemove: function (event) { + $(event.currentTarget).parent().remove(); + if (this.$el.children().length == 0) { + this.rowAdd(); + } + this.markLast(); + }, + markLast: function () { + this.$(".last").removeClass("last"); + // Marking inputs rather than parent div to avoid weird behaviour when + // remove row button is triggered with the keyboard. + this.$(".config-key-type").last().addClass("last"); + this.$(".config-key-value").last().addClass("last"); + }, + templateRow: function (type, value) { + return this.template({type: type || "", value: value || ""}); + }, + getConfigKeys: function () { + var cks = {}; + this.$(".config-key-type").each(function (index, input) { + input = $(input); + var type = input.val() && input.val().trim(); + var value = input.next().val() && input.next().val().trim(); + if (type && value) { + cks[type] = value; + } + }); + return cks; + } + }); + + return module; + +}); http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/util/brooklyn.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/util/brooklyn.js b/src/main/webapp/assets/js/util/brooklyn.js new file mode 100644 index 0000000..702b59b --- /dev/null +++ b/src/main/webapp/assets/js/util/brooklyn.js @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + + +/** brooklyn extension to make console methods available and simplify access to other utils */ + +define([ + "underscore", "brooklyn-view", "brooklyn-utils" +], function (_, BrooklynViews, BrooklynUtils) { + + /** + * Makes the console API safe to use: + * - Stubs missing methods to prevent errors when no console is present. + * - Exposes a global `log` function that preserves line numbering and formatting. + * + * Idea from https://gist.github.com/bgrins/5108712 + */ + (function () { + var noop = function () {}, + consoleMethods = [ + 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', + 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', + 'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', + 'timeStamp', 'trace', 'warn' + ], + length = consoleMethods.length, + console = (window.console = window.console || {}); + + while (length--) { + var method = consoleMethods[length]; + + // Only stub undefined methods. + if (!console[method]) { + console[method] = noop; + } + } + + if (Function.prototype.bind) { + window.log = Function.prototype.bind.call(console.log, console); + } else { + window.log = function () { + Function.prototype.apply.call(console.log, console, arguments); + }; + } + })(); + + var template = _.template; + _.mixin({ + /** + * @param {string} text + * @return string The text with HTML comments removed. + */ + stripComments: function (text) { + return text.replace(/<!--(.|[\n\r\t])*?-->\r?\n?/g, ""); + }, + /** + * As the real _.template, calling stripComments on text. + */ + template: function (text, data, settings) { + return template(_.stripComments(text), data, settings); + } + }); + + var Brooklyn = { + view: BrooklynViews, + util: BrooklynUtils + }; + + return Brooklyn; +}); http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/util/dataTables.extensions.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/util/dataTables.extensions.js b/src/main/webapp/assets/js/util/dataTables.extensions.js new file mode 100644 index 0000000..74a548e --- /dev/null +++ b/src/main/webapp/assets/js/util/dataTables.extensions.js @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * + * --- + * + * This code has been created by the Apache Brooklyn contributors. + * It is heavily based on earlier software but rewritten for clarity + * and to preserve license integrity. + * + * This work is based on the existing jQuery DataTables plug-ins for: + * + * * fnStandingRedraw by Jonathan Hoguet, + * http://www.datatables.net/plug-ins/api/fnStandingRedraw + * + * * fnProcessingIndicator by Allan Chappell + * https://www.datatables.net/plug-ins/api/fnProcessingIndicator + * + */ +define([ + "jquery", "jquery-datatables" +], function($, dataTables) { + +$.fn.dataTableExt.oApi.fnStandingRedraw = function(oSettings) { + if (oSettings.oFeatures.bServerSide === false) { + // remember and restore cursor position + var oldDisplayStart = oSettings._iDisplayStart; + oSettings.oApi._fnReDraw(oSettings); + oSettings._iDisplayStart = oldDisplayStart; + oSettings.oApi._fnCalculateEnd(oSettings); + } + // and force draw + oSettings.oApi._fnDraw(oSettings); +}; + + +jQuery.fn.dataTableExt.oApi.fnProcessingIndicator = function(oSettings, bShow) { + if (typeof bShow === "undefined") bShow=true; + this.oApi._fnProcessingDisplay(oSettings, bShow); +}; + +}); http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/util/jquery.slideto.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/util/jquery.slideto.js b/src/main/webapp/assets/js/util/jquery.slideto.js new file mode 100644 index 0000000..17afeed --- /dev/null +++ b/src/main/webapp/assets/js/util/jquery.slideto.js @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * + * --- + * + * This code has been created by the Apache Brooklyn contributors. + * It is heavily based on earlier software but rewritten for readability + * and to preserve license integrity. + * + * Our influences are: + * + * * jquery.slideto.min.js in Swagger UI, provenance unknown, added in: + * https://github.com/wordnik/swagger-ui/commit/d2eb882e5262e135dfa3f5919796bbc3785880b8#diff-bd86720650a2ebd1ab11e870dc475564 + * + * Swagger UI is distributed under ASL but it is not clear that this code originated in that project. + * No other original author could be identified. + * + * * Nearly identical code referenced here: + * http://stackoverflow.com/questions/12375440/scrolling-works-in-chrome-but-not-in-firefox-or-ie + * + * Note that the project https://github.com/Sleavely/jQuery-slideto is NOT this. + * + */ +(function(jquery){ +jquery.fn.slideto=function(opts) { + opts = _.extend( { + highlight: true, + slide_duration: "slow", + highlight_duration: 3000, + highlight_color: "#FFFF99" }, + opts); + return this.each(function() { + $target=jquery(this); + jquery("body").animate( + { scrollTop: $target.offset().top }, + opts.slide_duration, + function() { + opts.highlight && + jquery.ui.version && + $target.effect( + "highlight", + { color: opts.highlight_color }, + opts.highlight_duration) + }) + }); +}}) (jQuery); http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/view/activity-details.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/view/activity-details.js b/src/main/webapp/assets/js/view/activity-details.js new file mode 100644 index 0000000..fa8b552 --- /dev/null +++ b/src/main/webapp/assets/js/view/activity-details.js @@ -0,0 +1,426 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. +*/ +/** + * Displays details on an activity/task + */ +define([ + "underscore", "jquery", "backbone", "brooklyn-utils", "view/viewutils", "moment", + "model/task-summary", + "text!tpl/apps/activity-details.html", "text!tpl/apps/activity-table.html", + + "bootstrap", "jquery-datatables", "datatables-extensions" +], function (_, $, Backbone, Util, ViewUtils, moment, + TaskSummary, + ActivityDetailsHtml, ActivityTableHtml) { + + var activityTableTemplate = _.template(ActivityTableHtml), + activityDetailsTemplate = _.template(ActivityDetailsHtml); + + function makeActivityTable($el) { + $el.html(_.template(ActivityTableHtml)); + var $subTable = $('.activity-table', $el); + $subTable.attr('width', 569-6-6 /* subtract padding */) + + return ViewUtils.myDataTable($subTable, { + "fnRowCallback": function( nRow, aData, iDisplayIndex, iDisplayIndexFull ) { + $(nRow).attr('id', aData[0]) + $(nRow).addClass('activity-row') + }, + "aoColumnDefs": [ { + "mRender": function ( data, type, row ) { return Util.escape(data) }, + "aTargets": [ 1, 2, 3 ] + }, { + "bVisible": false, + "aTargets": [ 0 ] + } ], + "aaSorting":[] // default not sorted (server-side order) + }); + } + + var ActivityDetailsView = Backbone.View.extend({ + template: activityDetailsTemplate, + taskLink: '', + task: null, + /* children of this task; see HasTaskChildren for difference between this and sub(mitted)Tasks */ + childrenTable: null, + /* tasks in the current execution context (this.collections) whose submittedByTask + * is the task we are drilled down on. this defaults to the passed in collection, + * which will be the last-viewed entity's exec-context; when children cross exec-context + * boundaries we have to rewire to point to the current entity's exec-context / tasks */ + subtasksTable: null, + children: null, + breadcrumbs: [], + firstLoad: true, + events:{ + "click #activities-children-table .activity-table tr":"childrenRowClick", + "click #activities-submitted-table .activity-table tr":"submittedRowClick", + 'click .showDrillDownSubmittedByAnchor':'showDrillDownSubmittedByAnchor', + 'click .showDrillDownBlockerOfAnchor':'showDrillDownBlockerOfAnchor', + 'click .backDrillDown':'backDrillDown' + }, + // requires taskLink or task; breadcrumbs is optional + initialize:function () { + var that = this; + this.taskLink = this.options.taskLink; + this.taskId = this.options.taskId; + if (this.options.task) + this.task = this.options.task; + else if (this.options.tabView) + this.task = this.options.tabView.collection.get(this.taskId); + if (!this.taskLink && this.task) this.taskLink = this.task.get('links').self; + if (!this.taskLink && this.taskId) this.taskLink = "v1/activities/"+this.taskId;; + + this.tabView = this.options.tabView || null; + + if (this.options.breadcrumbs) this.breadcrumbs = this.options.breadcrumbs; + + this.$el.html(this.template({ taskLink: this.taskLink, taskId: this.taskId, task: this.task, breadcrumbs: this.breadcrumbs })); + this.$el.addClass('activity-detail-panel'); + + this.childrenTable = makeActivityTable(this.$('#activities-children-table')); + this.subtasksTable = makeActivityTable(this.$('#activities-submitted-table')); + + ViewUtils.attachToggler(this.$el) + + if (this.task) { + this.renderTask() + this.setUpPolling() + } else { + ViewUtils.fadeToIndicateInitialLoad(this.$el); + this.$el.css('cursor', 'wait') + $.get(this.taskLink, function(data) { + ViewUtils.cancelFadeOnceLoaded(that.$el); + that.task = new TaskSummary.Model(data) + that.renderTask() + that.setUpPolling(); + }).fail(function() { log("unable to load "+that.taskLink) }) + } + + // initial subtasks may be available from parent, so try to render those + // (reliable polling for subtasks, and for children, is set up in setUpPolling ) + this.renderSubtasks() + }, + + refreshNow: function(initial) { + var that = this + $.get(this.taskLink, function(data) { + that.task = new TaskSummary.Model(data) + that.renderTask() + if (initial) that.setUpPolling(); + }) + }, + renderTask: function() { + // update task fields + var that = this, firstLoad = this.firstLoad; + this.firstLoad = false; + + if (firstLoad && this.task) { +// log("rendering "+firstLoad+" "+this.task.get('isError')+" "+this.task.id); + if (this.task.get('isError')) { + // on first load, expand the details if there is a problem + var $details = this.$(".toggler-region.task-detail .toggler-header"); + ViewUtils.showTogglerClickElement($details); + } + } + + this.updateFields('displayName', 'entityDisplayName', 'id', 'description', 'currentStatus', 'blockingDetails'); + this.updateFieldWith('blockingTask', + function(v) { + return "<a class='showDrillDownBlockerOfAnchor handy' link='"+_.escape(v.link)+"' id='"+v.metadata.id+"'>"+ + that.displayTextForLinkedTask(v)+"</a>" }) + this.updateFieldWith('result', + function(v) { + // use display string (JSON.stringify(_.escape(v)) because otherwise list of [null,null] is just "," + var vs = Util.toDisplayString(v); + if (vs.trim().length==0) { + return " (empty result)"; + } else if (vs.length<20 && !/\r|\n/.exec(v)) { + return " with result: <span class='result-literal'>"+vs+"</span>"; + } else { + return "<div class='result-literal'>"+vs.replace(/\n+/g,"<br>")+"</div>" + } + }) + this.updateFieldWith('tags', function(tags) { + var tagBody = ""; + for (var tag in tags) { + tagBody += "<div class='activity-tag-giftlabel'>"+Util.toDisplayString(tags[tag])+"</div>"; + } + return tagBody; + }) + + var submitTimeUtc = this.updateFieldWith('submitTimeUtc', + function(v) { return v <= 0 ? "-" : moment(v).format('D MMM YYYY H:mm:ss.SSS')+" <i>"+moment(v).fromNow()+"</i>" }) + var startTimeUtc = this.updateFieldWith('startTimeUtc', + function(v) { return v <= 0 ? "-" : moment(v).format('D MMM YYYY H:mm:ss.SSS')+" <i>"+moment(v).fromNow()+"</i>" }) + this.updateFieldWith('endTimeUtc', + function(v) { return v <= 0 ? "-" : moment(v).format('D MMM YYYY H:mm:ss.SSS')+" <i>"+moment(v).from(startTimeUtc, true)+" later</i>" }) + + ViewUtils.updateTextareaWithData(this.$(".task-json .for-textarea"), + Util.toTextAreaString(this.task), false, false, 150, 400) + + ViewUtils.updateTextareaWithData(this.$(".task-detail .for-textarea"), + this.task.get('detailedStatus'), false, false, 30, 250) + + this.updateFieldWith('streams', + function(streams) { + // Stream names presented alphabetically + var keys = _.keys(streams); + keys.sort(); + var result = ""; + for (var i = 0; i < keys.length; i++) { + var name = keys[i]; + var stream = streams[name]; + result += "<div class='activity-stream-div'>" + + "<span class='activity-label'>" + + _.escape(name) + + "</span><span>" + + "<a href='" + stream.link + "'>download</a>" + + (stream.metadata["sizeText"] ? " (" + _.escape(stream.metadata["sizeText"]) + ")" : "") + + "</span></div>"; + } + return result; + }); + + this.updateFieldWith('submittedByTask', + function(v) { return "<a class='showDrillDownSubmittedByAnchor handy' link='"+_.escape(v.link)+"' id='"+v.metadata.id+"'>"+ + that.displayTextForLinkedTask(v)+"</a>" }) + + if (this.task.get("children").length==0) + this.$('.toggler-region.tasks-children').hide(); + }, + setUpPolling: function() { + var that = this + + // on first load, clear any funny cursor + this.$el.css('cursor', 'auto') + + this.task.url = this.taskLink; + this.task.on("all", this.renderTask, this) + + ViewUtils.get(this, this.taskLink, function(data) { + // if we can get the data, then start fetching certain things repeatedly + // (would be good to skip the immediate "doitnow" below but not a big deal) + ViewUtils.fetchRepeatedlyWithDelay(that, that.task, { doitnow: true }); + + // and set up to load children (now that the task is guaranteed to be loaded) + that.children = new TaskSummary.Collection() + that.children.url = that.task.get("links").children + that.children.on("reset", that.renderChildren, that) + ViewUtils.fetchRepeatedlyWithDelay(that, that.children, { + fetchOptions: { reset: true }, doitnow: true, fadeTarget: that.$('.tasks-children') }); + }).fail( function() { that.$('.toggler-region.tasks-children').hide() } ); + + + $.get(this.task.get("links").entity, function(entity) { + if (that.collection==null || entity.links.activities != that.collection.url) { + // need to rewire collection to point to the right ExecutionContext + that.collection = new TaskSummary.Collection() + that.collection.url = entity.links.activities + that.collection.on("reset", that.renderSubtasks, that) + ViewUtils.fetchRepeatedlyWithDelay(that, that.collection, { + fetchOptions: { reset: true }, doitnow: true, fadeTarget: that.$('.tasks-submitted') }); + } else { + that.collection.on("reset", that.renderSubtasks, that) + that.collection.fetch({reset: true}); + } + }); + }, + + renderChildren: function() { + var that = this + var children = this.children + ViewUtils.updateMyDataTable(this.childrenTable, children, function(task, index) { + return [ task.get("id"), + (task.get("entityId") && task.get("entityId")!=that.task.get("entityId") ? task.get("entityDisplayName") + ": " : "") + + task.get("displayName"), + task.get("submitTimeUtc") <= 0 ? "-" : moment(task.get("submitTimeUtc")).calendar(), + task.get("currentStatus") + ]; + }); + if (children && children.length>0) { + this.$('.toggler-region.tasks-children').show(); + } else { + this.$('.toggler-region.tasks-children').hide(); + } + }, + renderSubtasks: function() { + var that = this + var taskId = this.taskId || (this.task ? this.task.id : null); + if (!this.collection) { + this.$('.toggler-region.tasks-submitted').hide(); + return; + } + if (!taskId) { + // task not available yet; just wait for it to be loaded + // (and in worst case, if it can't be loaded, this panel stays faded) + return; + } + + // find tasks submitted by this one which aren't included as children + // this uses collections -- which is everything in the current execution context + var subtasks = [] + for (var taskI in this.collection.models) { + var task = this.collection.models[taskI] + var submittedBy = task.get("submittedByTask") + if (submittedBy!=null && submittedBy.metadata!=null && submittedBy.metadata["id"] == taskId && + (!this.children || this.children.get(task.id)==null)) { + subtasks.push(task) + } + } + ViewUtils.updateMyDataTable(this.subtasksTable, subtasks, function(task, index) { + return [ task.get("id"), + (task.get("entityId") && (!that.task || task.get("entityId")!=that.task.get("entityId")) ? task.get("entityDisplayName") + ": " : "") + + task.get("displayName"), + task.get("submitTimeUtc") <= 0 ? "-" : moment(task.get("submitTimeUtc")).calendar(), + task.get("currentStatus") + ]; + }); + if (subtasks && subtasks.length>0) { + this.$('.toggler-region.tasks-submitted').show(); + } else { + this.$('.toggler-region.tasks-submitted').hide(); + } + }, + + displayTextForLinkedTask: function(v) { + return v.metadata.taskName ? + (v.metadata.entityDisplayName ? _.escape(v.metadata.entityDisplayName)+" <b>"+_.escape(v.metadata.taskName)+"</b>" : + _.escape(v.metadata.taskName)) : + v.metadata.taskId ? _.escape(v.metadata.taskId) : + _.escape(v.link) + }, + updateField: function(field) { + return this.updateFieldWith(field, _.escape) + }, + updateFields: function() { + _.map(arguments, this.updateField, this); + }, + updateFieldWith: function(field, f) { + var v = this.task.get(field) + if (v !== undefined && v != null && + (typeof v !== "object" || _.size(v) > 0)) { + this.$('.updateField-'+field, this.$el).html( f(v) ); + this.$('.ifField-'+field, this.$el).show(); + } else { + // blank if there is no value + this.$('.updateField-'+field).empty(); + this.$('.ifField-'+field).hide(); + } + return v + }, + childrenRowClick:function(evt) { + var row = $(evt.currentTarget).closest("tr"); + var id = row.attr("id"); + this.showDrillDownTask("subtask of", this.children.get(id).get("links").self, id, this.children.get(id)) + }, + submittedRowClick:function(evt) { + var row = $(evt.currentTarget).closest("tr"); + var id = row.attr("id"); + // submitted tasks are guaranteed to be in the collection, so this is safe + this.showDrillDownTask("subtask of", this.collection.get(id).get('links').self, id) + }, + + showDrillDownSubmittedByAnchor: function(from) { + var $a = $(from.target).closest('a'); + this.showDrillDownTask("submitter of", $a.attr("link"), $a.attr("id")) + }, + showDrillDownBlockerOfAnchor: function(from) { + var $a = $(from.target).closest('a'); + this.showDrillDownTask("blocker of", $a.attr("link"), $a.attr("id")) + }, + showDrillDownTask: function(relation, newTaskLink, newTaskId, newTask) { +// log("activities deeper drill down - "+newTaskId +" / "+newTaskLink) + var that = this; + + var newBreadcrumbs = [ relation + ' ' + + this.task.get('entityDisplayName') + ' ' + + this.task.get('displayName') ].concat(this.breadcrumbs) + + var activityDetailsPanel = new ActivityDetailsView({ + taskLink: newTaskLink, + taskId: newTaskId, + task: newTask, + tabView: that.tabView, + collection: this.collection, + breadcrumbs: newBreadcrumbs + }); + activityDetailsPanel.addToView(this.$el); + }, + addToView: function(parent) { + if (this.parent) { + log("WARN: adding details to view when already added") + this.parent = parent; + } + + if (Backbone.history && (!this.tabView || !this.tabView.openingQueuedTasks)) { + Backbone.history.navigate(Backbone.history.fragment+"/"+"subtask"+"/"+this.taskId); + } + + var $t = parent.closest('.slide-panel'); + var $t2 = $t.after('<div>').next(); + $t2.addClass('slide-panel'); + + // load the drill-down page + $t2.html(this.render().el) + + var speed = (!this.tabView || !this.tabView.openingQueuedTasks) ? 300 : 0; + $t.animate({ + left: -600 + }, speed, function() { + $t.hide() + }); + + $t2.show().css({ + left: 600 + , top: 0 + }).animate({ + left: 0 + }, speed); + }, + backDrillDown: function(event) { +// log("activities drill back from "+this.taskLink) + var that = this + var $t2 = this.$el.closest('.slide-panel') + var $t = $t2.prev() + + if (Backbone.history) { + var fragment = Backbone.history.fragment + var thisLoc = fragment.indexOf("/subtask/"+this.taskId); + if (thisLoc>=0) + Backbone.history.navigate( fragment.substring(0, thisLoc) ); + } + + $t2.animate({ + left: 569 //prevTable.width() + }, 300, function() { + that.$el.empty() + $t2.remove() + that.remove() + }); + + $t.show().css({ + left: -600 //-($t2.width()) + }).animate({ + left: 0 + }, 300); + } + }); + + return ActivityDetailsView; +}); http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/view/add-child-invoke.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/view/add-child-invoke.js b/src/main/webapp/assets/js/view/add-child-invoke.js new file mode 100644 index 0000000..1105afe --- /dev/null +++ b/src/main/webapp/assets/js/view/add-child-invoke.js @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. +*/ +/** + * Render as a modal + */ +define([ + "underscore", "jquery", "backbone", "brooklyn", "brooklyn-utils", "view/viewutils", + "text!tpl/apps/add-child-modal.html" +], function(_, $, Backbone, Brooklyn, Util, ViewUtils, + AddChildModalHtml) { + return Backbone.View.extend({ + template: _.template(AddChildModalHtml), + initialize: function() { + this.title = "Add Child to "+this.options.entity.get('name'); + }, + render: function() { + this.$el.html(this.template(this.options.entity.attributes)); + return this; + }, + onSubmit: function (event) { + var self = this; + var childSpec = this.$("#child-spec").val(); + var start = this.$("#child-autostart").is(":checked"); + var url = this.options.entity.get('links').children + (!start ? "?start=false" : ""); + var ajax = $.ajax({ + type: "POST", + url: url, + data: childSpec, + contentType: "application/yaml", + success: function() { + self.options.target.reload(); + }, + error: function(response) { + self.showError(Util.extractError(response, "Error contacting server", url)); + } + }); + return ajax; + }, + showError: function (message) { + this.$(".child-add-error-container").removeClass("hide"); + this.$(".child-add-error-message").html(message); + } + + }); +});
