diff --git a/brooklyn-dist/dist/licensing/overrides.yaml b/brooklyn-dist/dist/licensing/overrides.yaml index a2f6107f7c..c6e6440dab 100644 --- a/brooklyn-dist/dist/licensing/overrides.yaml +++ b/brooklyn-dist/dist/licensing/overrides.yaml @@ -346,6 +346,16 @@ notice: Copyright (c) Twitter, Inc. and other contributors (2013-2014) license: MIT +# CodeMirror: used by jsGUI to provide edit in place for any textarea +- id: codemirror.js + name: CodeMirror + version: "5.7.0" + files: codemirror*.{js,css} + url: https://codemirror.net/ + organization: { name: "CodeMirror", url:"https://codemirror.net" } + notice: Copyright (c) Marijn Haverbeke (2015) + license: MIT + # used for CLI to build catalog - id: marked.js version: 0.3.1 @@ -379,5 +389,3 @@ url: https://github.com/carhartl/jquery-cookie notice: Copyright (c) 2013 Klaus Hartl license: MIT - - diff --git a/brooklyn-ui/src/build/optimize-js.json b/brooklyn-ui/src/build/optimize-js.json index 60855d907c..9cca7767af 100644 --- a/brooklyn-ui/src/build/optimize-js.json +++ b/brooklyn-ui/src/build/optimize-js.json @@ -16,3 +16,4 @@ // Set to "none" to skip minification optimize: "uglify" }) + diff --git a/brooklyn-ui/src/main/license/files/LICENSE b/brooklyn-ui/src/main/license/files/LICENSE index 4a1f38b6d0..2a6ca8dbb3 100644 --- a/brooklyn-ui/src/main/license/files/LICENSE +++ b/brooklyn-ui/src/main/license/files/LICENSE @@ -396,6 +396,14 @@ This project includes the software: marked.js Used under the following license: The MIT License (http://opensource.org/licenses/MIT) Copyright (c) Christopher Jeffrey (2011-2014) +This project includes the software: CodeMirror + Available at: https://codemirror.net/ + Developed by: CodeMirror (https://codemirror.net) + Inclusive of: codemirror*.{js,css} + Version used: 5.7.0 + Used under the following license: The MIT License (http://opensource.org/licenses/MIT) + Copyright (c) Marijn Haverbeke (2015) + --------------------------------------------------- (3) Licenses for bundled software diff --git a/brooklyn-ui/src/main/license/source-inclusions.yaml b/brooklyn-ui/src/main/license/source-inclusions.yaml index 2adf94915b..32476f4c89 100644 --- a/brooklyn-ui/src/main/license/source-inclusions.yaml +++ b/brooklyn-ui/src/main/license/source-inclusions.yaml @@ -39,3 +39,4 @@ - id: js-yaml.js - id: jquery.form.js - id: marked.js +- id: codemirror.js diff --git a/brooklyn-ui/src/main/webapp/assets/css/brooklyn.css b/brooklyn-ui/src/main/webapp/assets/css/brooklyn.css index fde2a3c76c..c2ad67352c 100644 --- a/brooklyn-ui/src/main/webapp/assets/css/brooklyn.css +++ b/brooklyn-ui/src/main/webapp/assets/css/brooklyn.css @@ -25,7 +25,6 @@ BODY { textarea { white-space: pre; word-wrap: normal; - overflow-x: scroll; } /* HEADER */ @@ -269,3 +268,15 @@ textarea { content: 'Not Available'; */ } + +button.catalog-submit-button.btn { + margin-top: 10px; +} + +.CodeMirror .cm-s-default { + height: 560px; +} + +.CodeMirror .CodeMirror-placeholder { + color: #999; +} diff --git a/brooklyn-ui/src/main/webapp/assets/js/config.js b/brooklyn-ui/src/main/webapp/assets/js/config.js index 6710ff1ef9..24965a86c0 100644 --- a/brooklyn-ui/src/main/webapp/assets/js/config.js +++ b/brooklyn-ui/src/main/webapp/assets/js/config.js @@ -30,11 +30,13 @@ require.config({ "underscore":"libs/underscore", "backbone":"libs/backbone", "bootstrap":"libs/bootstrap", + "jquery-form":"libs/jquery.form", "jquery-datatables":"libs/jquery.dataTables", "jquery-slideto":"util/jquery.slideto", "jquery-wiggle":"libs/jquery.wiggle.min", "jquery-ba-bbq":"libs/jquery.ba-bbq.min", + "moment":"libs/moment", "handlebars":"libs/handlebars-1.0.rc.1", "brooklyn":"util/brooklyn", @@ -47,7 +49,17 @@ require.config({ "uri":"libs/URI", "zeroclipboard":"libs/ZeroClipboard", "js-yaml":"libs/js-yaml", - + + "codemirror":"lib/codemirror", + + "mode":"mode", + "addon":"addon", + + "codemirror-mode-yaml":"mode/yaml/yaml", + "codemirror-addon-show-hint":"addon/hint/show-hint", + "codemirror-addon-anyword-hint":"addon/hint/anyword-hint", + "codemirror-addon-display-placeholder":"addon/display/placeholder", + "tpl":"../tpl" }, @@ -70,6 +82,14 @@ require.config({ "jquery-wiggle": { deps: [ "jquery" ] }, "jquery-ba-bbq": { deps: [ "jquery" ] }, "handlebars": { deps: [ "jquery" ] }, + "codemirror":{ + exports:"CodeMirror" + }, + "codemirror-mode-yaml":{ deps: ["codemirror"] }, + "codemirror-addon-show-hint":{ deps: ["codemirror"] }, + "codemirror-addon-anyword-hint":{ deps: ["codemirror"] }, + "codemirror-addon-display-placeholder":{ deps: ["codemirror"] }, + "bootstrap": { deps: [ "jquery" ] /* http://stackoverflow.com/questions/9227406/bootstrap-typeerror-undefined-is-not-a-function-has-no-method-tab-when-us */ } } }); diff --git a/brooklyn-ui/src/main/webapp/assets/js/libs/bootstrap.js b/brooklyn-ui/src/main/webapp/assets/js/libs/bootstrap.js index 6bb52bc753..5d9330c15c 100644 --- a/brooklyn-ui/src/main/webapp/assets/js/libs/bootstrap.js +++ b/brooklyn-ui/src/main/webapp/assets/js/libs/bootstrap.js @@ -196,6 +196,7 @@ } Button.prototype.toggle = function () { + // TODO: buttons-radio :div is replaced by :ul var $parent = this.$element.parent('[data-toggle="buttons-radio"]') $parent && $parent diff --git a/brooklyn-ui/src/main/webapp/assets/js/router.js b/brooklyn-ui/src/main/webapp/assets/js/router.js index d80d35c612..c9af7a286f 100644 --- a/brooklyn-ui/src/main/webapp/assets/js/router.js +++ b/brooklyn-ui/src/main/webapp/assets/js/router.js @@ -20,12 +20,12 @@ 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", + "view/home", "view/editor", "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, + HomeView, EditorView, ExplorerView, CatalogView, ScriptGroovyView, HelpHtml, LabsHtml, ServerCautionHtml) { var ServerCautionOverlay = Backbone.View.extend({ @@ -122,6 +122,8 @@ define([ var Router = Backbone.Router.extend({ routes:{ 'v1/home/*trail':'homePage', + 'v1/editor/*trail':'editorPage', + 'v1/editor':'editorPage', 'v1/applications/:app/entities/*trail':'applicationsPage', 'v1/applications/*trail':'applicationsPage', 'v1/applications':'applicationsPage', @@ -135,6 +137,7 @@ define([ }, showView: function(selector, view) { + console.log("showView"); // close the previous view - does binding clean-up and avoids memory leaks if (this.currentView) { this.currentView.close(); @@ -177,6 +180,16 @@ define([ } }, error: render}); }, + editorPage: function (trail) { + console.log("editorPage"); + var editorView = new EditorView({ + collection: this.applications, + appRouter: this + }); + this.showView("#application-content", editorView); + $(".nav1").removeClass("active"); + $(".nav1_editor").addClass("active"); + }, applicationsPage:function (app, trail, tab) { if (trail === undefined) trail = app var that = this diff --git a/brooklyn-ui/src/main/webapp/assets/js/util/brooklyn-utils.js b/brooklyn-ui/src/main/webapp/assets/js/util/brooklyn-utils.js index 5f3915c00b..d88d93736e 100644 --- a/brooklyn-ui/src/main/webapp/assets/js/util/brooklyn-utils.js +++ b/brooklyn-ui/src/main/webapp/assets/js/util/brooklyn-utils.js @@ -113,6 +113,7 @@ define([ if ($input.attr("type") === "checkbox") { return $input.is(":checked"); } else { + // TODO: get codemirror.getValue() in case this is a textarea ... return $input.val(); } }; diff --git a/brooklyn-ui/src/main/webapp/assets/js/view/application-add-wizard.js b/brooklyn-ui/src/main/webapp/assets/js/view/application-add-wizard.js index 2c4f01216c..d19c9f6b45 100644 --- a/brooklyn-ui/src/main/webapp/assets/js/view/application-add-wizard.js +++ b/brooklyn-ui/src/main/webapp/assets/js/view/application-add-wizard.js @@ -21,7 +21,7 @@ * Also creates an empty Application model. */ define([ - "underscore", "jquery", "backbone", "brooklyn-utils", "js-yaml", + "underscore", "jquery", "backbone", "brooklyn-utils", "js-yaml", "codemirror", "model/entity", "model/application", "model/location", "model/catalog-application", "text!tpl/app-add-wizard/modal-wizard.html", "text!tpl/app-add-wizard/create.html", @@ -33,14 +33,17 @@ define([ "text!tpl/app-add-wizard/deploy-version-option.html", "text!tpl/app-add-wizard/deploy-location-row.html", "text!tpl/app-add-wizard/deploy-location-option.html", + // ↓ not part of the constructor + "codemirror-mode-yaml", + "codemirror-addon-show-hint", + "codemirror-addon-anyword-hint", + "codemirror-addon-display-placeholder", "bootstrap" - -], function (_, $, Backbone, Util, JsYaml, Entity, Application, Location, CatalogApplication, +], function (_, $, Backbone, Util, JsYaml, CodeMirror, Entity, Application, Location, CatalogApplication, ModalHtml, CreateHtml, CreateStepTemplateEntryHtml, CreateEntityEntryHtml, RequiredConfigEntryHtml, EditConfigEntryHtml, DeployHtml, DeployVersionOptionHtml, DeployLocationRowHtml, DeployLocationOptionHtml ) { - /** Special ID to indicate that no locations will be provided when starting the server. */ var NO_LOCATION_INDICATOR = "__NONE__"; @@ -111,6 +114,7 @@ define([ this.model.yaml = ""; this.model.mode = "template"; // or "yaml" or "other" this.currentStep = 0; + log("ModalWizard::initialize()"); this.steps = [ { step_id:'what-app', @@ -159,6 +163,7 @@ define([ this.updateButtonVisibility(); }, updateButtonVisibility:function () { + log("::updateButtonVisibility()") var currentStepObj = this.steps[this.currentStep] setVisibility(this.$("#prev_step"), (this.currentStep > 0)) @@ -187,14 +192,13 @@ define([ } // finish from config step, preview step, and from first step if yaml tab selected (and valid) - var finishVisible = (this.currentStep >= 1) - var finishEnabled = finishVisible + var finishVisible = (this.currentStep >= 1); + var finishEnabled = finishVisible; if (!finishEnabled && this.currentStep==0) { if (this.model.mode == "yaml") { // should do better validation than non-empty finishVisible = true; - var yaml_code = this.$("#yaml_code").val() - if (yaml_code) { + if (self.editor && self.editor && self.editor.getValue()) { finishEnabled = true; } } @@ -262,12 +266,16 @@ define([ this.renderCurrentStep(); }, nextStep:function () { + log("ModalWizard::nextStep()"); if (this.currentStep == 0) { if (this.currentView.validate()) { + log("nextStep ... validate"); var yaml = (this.currentView && this.currentView.selectedTemplate && this.currentView.selectedTemplate.yaml); + log("nextStep ... yaml: " + yaml); if (yaml) { try { yaml = JsYaml.safeLoad(yaml); + log("nextStep ... safeYaml: " + yaml); hasLocation = yaml.location || yaml.locations; if (!hasLocation) { // look for locations defined in locations @@ -288,9 +296,10 @@ define([ yaml = false; } } + log("nextStep ... boolYaml: " + yaml); if (yaml) { // it's a yaml catalog template which includes a location, show the yaml tab - $("ul#app-add-wizard-create-tab").find("a[href='#yamlTab']").tab('show'); + $("ul#app-add-wizard-create-tab").find("a[href='#yamlTab']").tab('show'); $("#yaml_code").setCaretToStart(); } else { // it's a java catalog template or yaml template without a location, go to wizard @@ -305,15 +314,13 @@ define([ } }, previewStep:function () { + log("ModalWizard::previewStep()"); if (this.currentView.validate()) { this.currentStep = 0; var that = this; this.renderCurrentStep(function callback(view) { // Drop any "None" locations. that.model.spec.pruneLocations(); - $("textarea#yaml_code").val(JsYaml.safeDump(oldSpecToCamp(that.model.spec.toJSON()))); - $("ul#app-add-wizard-create-tab").find("a[href='#yamlTab']").tab('show'); - $("#yaml_code").setCaretToStart(); }); } else { // call to validate should showFailure @@ -353,6 +360,7 @@ define([ template:_.template(CreateHtml), wizard: null, initialize:function () { + log("ModalWizard.StepCreate::initialize()"); var self = this self.catalogEntityIds = [] @@ -367,7 +375,8 @@ define([ self.catalogEntityItems = result self.catalogEntityIds = _.map(result, function(item) { return item.id }) self.$(".entity-type-input").typeahead().data('typeahead').source = self.catalogEntityIds - }) + }); + this.options.catalog.applications = new CatalogApplication.Collection(); this.options.catalog.applications.fetch({ data: $.param({ @@ -378,8 +387,9 @@ define([ $('#catalog-applications-throbber').hide(); $('#catalog-applications-empty').hide(); if (collection.size() > 0) { - self.addTemplateLozenges() + self.addTemplateLozenges(); } else { + log("collection is empty"); $('#catalog-applications-empty').show(); self.showYamlTab(); } @@ -397,11 +407,12 @@ define([ }, updateForState: function () {}, render:function () { - this.renderConfiguredEntities() - this.delegateEvents() - return this + this.renderConfiguredEntities(); + this.delegateEvents(); + return this; }, onTabChange: function(e) { + log("onTabChange()"); var tabText = $(e.target).text(); if (tabText=="Catalog") { $("li.text-filter").show() @@ -421,6 +432,7 @@ define([ this.options.wizard.updateButtonVisibility(); }, onYamlCodeChange: function() { + log("ModalWizard.StepCreate::onYamlCodeChange() ... event"); if (this.options.wizard) this.options.wizard.updateButtonVisibility(); }, @@ -430,7 +442,7 @@ define([ window.location.href="#v1/catalog/new"; }, showYamlTab: function() { - $("ul#app-add-wizard-create-tab").find("a[href='#yamlTab']").tab('show') + $("ul#app-add-wizard-create-tab").find("a[href='#yamlTab']").tab('show'); $("#yaml_code").focus(); }, applyFilter: function(e) { @@ -454,6 +466,7 @@ define([ }) }, addTemplateLozenge: function(that, item) { + log("ModalWizard.StepCreate::addTemplateLozenge() ... planYaml: " + item.get('planYaml')); var $tempel = _.template(CreateStepTemplateEntryHtml, { id: item.get('id'), type: item.get('type'), @@ -465,6 +478,7 @@ define([ $("#create-step-template-entries", that.$el).append($tempel) }, templateClick: function(event) { + log("ModalWizard.StepCreate::templateClick()"); var $tl = $(event.target).closest(".template-lozenge"); var wasSelected = $tl.hasClass("selected") $(".template-lozenge").removeClass("selected") @@ -476,6 +490,7 @@ define([ name: $tl.data("name"), yaml: $tl.data("yaml"), }; + log("::templateClick() ... selectedYaml: " + this.selectedTemplate.yaml); if (this.selectedTemplate.yaml) { $("textarea#yaml_code").val(this.selectedTemplate.yaml); } else { @@ -574,8 +589,10 @@ define([ }, validate:function () { - var that = this - var tabName = $('#app-add-wizard-create-tab li[class="active"] a').attr('href') + var that = this; + // TODO: enforce ';' as javascript conventions + var tabName = $('#app-add-wizard-create-tab li[class="active"] a').attr('href'); + log("::validate() ... tabName: "+tabName); if (tabName=='#entitiesTab') { delete this.model.spec.attributes["id"] var allokay = true @@ -601,9 +618,13 @@ define([ return true } } else if (tabName=='#yamlTab') { - this.model.yaml = this.$("#yaml_code").val(); - if (this.model.yaml) { - return true; + if (self.editor !== null) { + this.model.yaml = self.editor.getValue(); + if (this.model.yaml) { + return true; + } + } else { + console.info("No text in the editor!"); } } else { console.info("NOT IMPLEMENTED YET") @@ -801,7 +822,6 @@ define([ return candidate.get("id")==loc_id; }); if (!locationValid) { - log("invalid location "+loc_id); this.showFailure("Invalid location "+loc_id); this.model.spec.set("locations",[]); } else { @@ -817,9 +837,11 @@ define([ this.model.spec.set("name", ""); }, validate:function () { + log("::validate()"); + log(this.model.spec); this.model.spec.set("config", this.getConfigMap()) if (this.model.spec.get("locations").length !== 0) { - return true + return true; } else { this.showFailure("A location is required"); return false; diff --git a/brooklyn-ui/src/main/webapp/assets/js/view/catalog.js b/brooklyn-ui/src/main/webapp/assets/js/view/catalog.js index 7d4ab2a72d..b94bd69a87 100644 --- a/brooklyn-ui/src/main/webapp/assets/js/view/catalog.js +++ b/brooklyn-ui/src/main/webapp/assets/js/view/catalog.js @@ -17,22 +17,23 @@ * under the License. */ define([ - "underscore", "jquery", "backbone", "brooklyn", + "underscore", "jquery", "backbone", "brooklyn", "codemirror", "model/location", "model/entity", "text!tpl/catalog/page.html", "text!tpl/catalog/details-entity.html", "text!tpl/catalog/details-generic.html", "text!tpl/catalog/details-location.html", "text!tpl/catalog/add-catalog-entry.html", - "text!tpl/catalog/add-yaml.html", - "text!tpl/catalog/add-location.html", "text!tpl/catalog/nav-entry.html", - + // ↓ not part of the constructor + "codemirror-mode-yaml", + "codemirror-addon-show-hint", + "codemirror-addon-anyword-hint", "bootstrap", "jquery-form" -], function(_, $, Backbone, Brooklyn, +], function(_, $, Backbone, Brooklyn, CodeMirror, Location, Entity, CatalogPageHtml, DetailsEntityHtml, DetailsGenericHtml, LocationDetailsHtml, - AddCatalogEntryHtml, AddYamlHtml, AddLocationHtml, EntryHtml) { + AddCatalogEntryHtml, EntryHtml) { // Holds the currently active details type, e.g. applications, policies. Bit of a workaround // to share the active view with all instances of AccordionItemView, so clicking the 'reload @@ -134,54 +135,58 @@ define([ initialize: function() { _.bindAll(this); }, + editor: null, render: function (initialView) { this.$el.html(this.template()); - if (initialView) { - if (initialView == "entity") initialView = "yaml"; - - this.$("[data-context='"+initialView+"']").addClass("active"); - this.showFormForType(initialView) - } return this; }, clearWithHtml: function(template) { if (this.contextView) this.contextView.close(); this.context = undefined; - this.$(".btn").removeClass("active"); this.$("#catalog-add-form").html(template); }, beforeClose: function () { if (this.contextView) this.contextView.close(); }, - showContext: function(event) { - var $event = $(event.currentTarget); - var context = $event.data("context"); - if (this.context !== context) { - if (this.contextView) { - this.contextView.close(); - } - this.showFormForType(context) + setupCodeEditor: function() { + if (this.editor === null) { + this.editor = CodeMirror.fromTextArea(document.getElementById("new-blueprint"), { + height: "150px", + lineNumbers: true, + extraKeys: {"Ctrl-Space": "autocomplete"}, + textWrapping: true, + mode: {name: "yaml", globalVars: true} + }); + + this.editor.setValue("# Please add your blueprint here\n"); } }, - showFormForType: function (type) { - this.context = type; - if (type == "yaml" || type == "entity") { - this.contextView = newYamlForm(this, this.options.parent); - } else if (type == "location") { - this.contextView = newLocationForm(this, this.options.parent); - } else if (type !== undefined) { - console.log("unknown catalog type " + type); - this.showFormForType("yaml"); - return; - } - Backbone.history.navigate("/v1/catalog/new/" + type); - this.$("#catalog-add-form").html(this.contextView.$el); + showContext: function(event) { + var $event = $(event.currentTarget); + + // de-select other tab menu entries + $event.parent().find("li.btn.btn-large.show-context").removeClass("active"); + + // setup code editor + this.setupCodeEditor(); + + // display appropriate tab + $(event.currentTarget).parent().parent().parent().find("div.context").css({ + display: "none" + }).filter("#context-" + $event.data("context")).css({ + display: "block" + }); + + // restore cursor position + this.editor.refresh(); + this.editor.focus(); } }); function newYamlForm(addView, addViewParent) { + log("newYamlForm(" + addView + ", " + addViewParent + ")"); return new Brooklyn.view.Form({ - template: _.template(AddYamlHtml), + template: null, onSubmit: function (model) { var submitButton = this.$(".catalog-submit-button"); // "loading" is an indicator to Bootstrap, not a string to display @@ -218,6 +223,7 @@ define([ // Could adapt to edit existing locations too. function newLocationForm(addView, addViewParent) { + log("newLocationForm(" + addView + ", " + addViewParent + ")"); // Renders with config key list var body = new (Backbone.View.extend({ beforeClose: function() { @@ -227,7 +233,7 @@ define([ }, render: function() { this.configKeyList = new Brooklyn.view.ConfigKeyInputPairList().render(); - var template = _.template(AddLocationHtml); + var template = null; this.$el.html(template); this.$("#new-location-config").html(this.configKeyList.$el); }, diff --git a/brooklyn-ui/src/main/webapp/assets/tpl/app-add-wizard/create.html b/brooklyn-ui/src/main/webapp/assets/tpl/app-add-wizard/create.html index 5ec0150963..48a9235714 100644 --- a/brooklyn-ui/src/main/webapp/assets/tpl/app-add-wizard/create.html +++ b/brooklyn-ui/src/main/webapp/assets/tpl/app-add-wizard/create.html @@ -30,7 +30,7 @@