blob: ef6025088dadf329379ec34cd5f0132a875d9365 [file] [log] [blame]
/*
* 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([
"underscore", "jquery", "backbone", "model/catalog-application",
"model/location", "view/location-wizard", "view/viewutils",
"js-yaml", "brooklyn-yaml-completion-proposals", "codemirror",
"text!tpl/editor/page.html",
"text!tpl/editor/location-alert.html",
// no constructor
"jquery-slideto",
"jquery-wiggle",
"jquery-ba-bbq",
"handlebars",
"bootstrap"
], function (_, $, Backbone, CatalogApplication, Location, LocationWizard, ViewUtils, jsYaml, BrooklynCompletion, CodeMirror, EditorHtml, LocationAlertHtml) {
var _DEFAULT_BLUEPRINT =
'name: Sample Blueprint\n'+
'description: runs `sleep` for sixty seconds then stops triggering ON_FIRE in Brooklyn\n'+
'location: localhost\n'+
'services:\n'+
'- type: org.apache.brooklyn.entity.software.base.VanillaSoftwareProcess\n'+
' launch.command: |\n'+
' echo hello world\n'+
' nohup sleep 60 &\n'+
' echo $! > ${PID_FILE:-pid.txt}\n';
var _DEFAULT_CATALOG =
'brooklyn.catalog:\n'+
' version: 0.0.1\n'+
' items:\n'+
' - id: example\n'+
' description: This is an example catalog application\n'+
' itemType: template\n'+
' item:\n'+
' name: Sample Blueprint Template\n'+
' services:\n'+
' - type: <your service here>\n'+
' location: <your cloud here>\n';
// is the user working on an app blueprint or a catalog item
var MODE_APP = "app";
var MODE_CATALOG = "catalog";
var EditorView = Backbone.View.extend({
tagName:"div",
className:"container container-fluid",
events: {
'click #button-run':'runBlueprint',
'click #button-delete':'removeBlueprint',
'click #button-switch-app':'switchModeApp',
'click #button-switch-catalog':'switchModeCatalog',
'click #button-example':'populateExampleBlueprint',
'click #button-docs':'openDocumentation',
'click .add-location':'createLocation'
},
editorTemplate:_.template(EditorHtml),
editor: null,
initialize:function () {
if (!this.options.type || this.options.type === MODE_APP) {
this.setMode(MODE_APP);
} else if (this.options.type === MODE_CATALOG) {
this.setMode(MODE_CATALOG);
} else {
console.log("unknown mode '"+this.options.type+"'; using '"+MODE_APP+"'");
this.setMode(MODE_APP);
}
this.options.catalog = new CatalogApplication.Collection();
this.options.locations.on('reset', this.renderLocationAlert, this);
ViewUtils.fetchRepeatedlyWithDelay(this, this.options.locations, { fetchOptions: { reset: true }, doitnow: true });
},
setMode: function(mode) {
if (this.mode === mode) return;
this.mode = mode;
this.refresh();
},
beforeClose:function () {
this.options.locations.off('reset', this.renderLocationAlert);
},
render:function (eventName) {
this.$el
.append(_.template(LocationAlertHtml, {
locations: this.options.locations
}))
.append(_.template(EditorHtml))
.find('#location-alert').hide();
this.loadEditor();
this.refresh();
// Codemirror need to have the DOM loaded to be able to display the editor correctly (line numbers for example)
// The timeout is there for this particular purpose.
var vm = this;
setTimeout(function() {
vm.initializeEditor();
}, 10);
return this;
},
renderLocationAlert: function(event) {
if (event.size() === 0) {
this.$('#location-alert').show();
} else {
this.$('#location-alert').hide();
}
},
refresh: function() {
$("#button-run", this.$el).html(this.mode==MODE_CATALOG ? "Add to Catalog" : "Deploy");
if (this.mode==MODE_CATALOG) {
$("#button-switch-catalog", this.$el).addClass('active')
$("#button-switch-app", this.$el).removeClass('active')
} else {
$("#button-switch-app", this.$el).addClass('active')
$("#button-switch-catalog", this.$el).removeClass('active')
}
this.refreshOnMinorChange();
},
refreshOnMinorChange: function() {
var yaml = this.editor && this.editor.getValue();
var parse = this.parse();
// fine to switch to catalog
/* always enable the switch to app button -- worst case it's invalid
if (this.mode==MODE_CATALOG && yaml && (parse.problem || parse.result['brooklyn.catalog']) {
$("#button-switch-app", this.$el).attr('disabled', 'disabled');
} else {
$("#button-switch-app", this.$el).attr('disabled', false);
}
*/
if (!yaml) {
// no yaml
$("#button-run", this.$el).attr('disabled','disabled');
$("#button-delete", this.$el).attr('disabled','disabled');
// example only shown when empty
$("#button-example", this.$el).show();
} else {
$("#button-run", this.$el).attr('disabled',false);
// we have yaml
$("#button-delete", this.$el).attr('disabled',false);
$("#button-example", this.$el).hide();
}
},
initializeEditor: function() {
var vm = this;
var cm = this.editor;
var loading = this.$('.composer-editor-loading');
if (typeof(cm) !== "undefined") {
var itemText = "";
if (this.options.typeId === '_') {
// _ indicates a literal is being supplied
itemText = this.options.content;
} else {
if (this.options.content) {
console.log('ignoring content when typeId is not _; given:', this.options.type, this.options.typeId, this.options.content);
}
if (this.options.typeId) {
cm.setOption('readOnly', 'nocursor');
loading.show();
this.options.catalog.fetch({
data: $.param({allVersions: true}),
success: function () {
var item = vm.options.catalog.getId(vm.options.typeId);
var itemText = '# unknown type - this is an example blueprint that would reference it\n'+
'services:\n- type: ' + vm.options.typeId + '\n';
if (item) {
itemText = item['attributes']['planYaml'];
}
cm.getDoc().setValue(itemText);
},
error: function() {
vm.showFailure('Could not load the item: ' + vm.options.typeId);
}
}).done(function() {
cm.setOption('readOnly', false);
cm.refresh();
loading.fadeOut();
});
}
}
cm.getDoc().setValue(itemText);
if (!itemText) {
// could populateExampleBlueprint -- but now that is opt-in
}
//better not to focus as focussing puts the cursor at the beginning which is odd
//and cmd-shift-[ and tab are intercepted so user can't navigate
// cm.focus();
cm.refresh();
}
},
loadEditor: function() {
if (this.editor == null) {
this.editor = CodeMirror.fromTextArea(this.$("#yaml_code")[0], {
lineNumbers: true,
viewportMargin: Infinity, /* recommended if height auto */
extraKeys: {"Ctrl-Space": "autocomplete"},
mode: {
name: "yaml",
globalVars: true
},
hintOptions: {
completeSingle: false,
// closeOnUnfocus: false, // handy for debugging
},
placeholder: "How to use it?\n- Enter blueprint yaml here\n- Drag & drop text files here\n\nPress Ctrl+Space for completion."
});
var oldYamlHint = CodeMirror.hint.yaml;
var that = this;
CodeMirror.hint.yaml = function(cm) {
var result = {from: cm.getCursor(), to: cm.getCursor(), list: [] };
result.list = BrooklynCompletion.getCompletionProposals(that.mode, cm);
//result.list = result.list.concat(otherwords.list);
// could return other proposals *additionally* but better only if we found nothing else
if (!result.list) {
result = CodeMirror.hint.anyword(cm);
}
return result;
};
var that = this;
this.editor.on("changes", function(editor, changes) {
that.refreshOnMinorChange();
});
}
},
parse: function(forceRefresh) {
if (!forceRefresh && this.lastParse && this.lastParse.input === this.editor.getValue()) {
// up to date
return this.lastParse;
}
if (!this.editor) {
this.lastParse = { problem: "no editor yet" };
} else {
this.lastParse = { input: this.editor.getValue() };
try {
// TODO use new listener for the parser to get tree w parsing info
this.lastParse.result = jsYaml.safeLoad(this.editor.getValue());
} catch (e) {
this.lastParse.problem = e;
}
}
return this.lastParse;
},
validate: function() {
var yaml = this.editor.getValue();
try{
var parsed = this.parse(true);
if (parsed.problem) throw parsed.problem;
if (this.mode!=MODE_CATALOG && parsed.result['brooklyn.catalog'] &&
!parsed.result['services']) {
console.log("This document is using brooklyn.catalog syntax but you are attempting to deploy it.");
throw "This document is using brooklyn.catalog syntax but you are attempting to deploy it.";
}
return true;
}catch (e){
this.showFailure(e.message || e);
return false;
}
},
convertBlueprintToCatalog: function() {
if (this.mode === MODE_APP && this.lastParse && !this.lastParse.problems && this.lastParse.result && this.lastParse.result['services']) {
// convert to catalog syntax if it has services at the root
var newBlueprint = "brooklyn.catalog:\n"+
" version: 0.0.1\n"+
" items:\n"+
" - id: TODO_identifier_for_this_item \n"+
" description: Some text to display about this\n"+
" iconUrl: http://www.apache.org/foundation/press/kit/poweredBy/Apache_PoweredBy.png\n"+
" itemType: template\n"+
" item:\n"+
// indent 6 spaces:
this.editor.getValue().replace(/^(.*$)/gm,' $1');
this.mode = MODE_CATALOG;
this.editor.setValue(newBlueprint);
this.refresh();
}
},
runBlueprint: function() {
if (this.validate()){
if (this.mode === MODE_CATALOG) {
this.submitCatalog();
}else{
this.submitApplication();
}
}
},
removeBlueprint: function() {
this.initializeEditor();
},
switchMode: function() {
this.setMode(this.mode == MODE_CATALOG ? MODE_APP : MODE_CATALOG);
},
switchModeApp: function() { this.setMode(MODE_APP); },
switchModeCatalog: function() {
var plan = this.parse().result;
if (plan && plan.services) {
this.convertBlueprintToCatalog();
} else {
this.setMode(MODE_CATALOG);
}
},
populateExampleBlueprint: function() {
this.editor.setValue(this.mode==MODE_CATALOG ? _DEFAULT_CATALOG : _DEFAULT_BLUEPRINT);
},
openDocumentation: function() {
window.open(this.mode==MODE_CATALOG ? "https://brooklyn.apache.org/v/latest/ops/catalog/" : "https://brooklyn.apache.org/v/latest/yaml/yaml-reference.html", "_blank");
},
onSubmissionComplete: function(succeeded, data, type) {
var that = this;
if(succeeded){
log("Submit [succeeded] ... redirecting back to " + type);
if(type && type === 'catalog'){
Backbone.history.navigate('v1/catalog', {trigger: true});
}else{
// no need to refresh apps (this.collection) because homePage route does that
Backbone.history.navigate('v1/home', {trigger: true});
}
}else{
log("Submit [failed] ... " + data.responseText);
var response, summary="Server responded with an error";
try {
if (data.responseText) {
response = JSON.parse(data.responseText)
if (response) {
summary = response.message;
}
}
} catch (e) {
summary = data.responseText;
}
that.showFailure(summary);
}
},
submitApplication: function () {
var that = this
$.ajax({
url:'/v1/applications',
type:'post',
contentType:'application/yaml',
processData:false,
data: that.editor.getValue(),
success:function (data) {
that.onSubmissionComplete(true, data);
},
error:function (data) {
that.onSubmissionComplete(false, data);
}
});
return false
},
submitCatalog: function () {
var that = this;
$.ajax({
url:'/v1/catalog',
type:'post',
contentType:'application/yaml',
processData:false,
data: that.editor.getValue(),
success:function (data) {
that.onSubmissionComplete(true, data, 'catalog');
},
error:function (data) {
that.onSubmissionComplete(false, data, 'catalog');
}
});
return false;
},
showFailure: function(text) {
var _text = text || "Failure performing the specified action";
log("showing error: "+_text);
this.$('div.error-message .error-message-text').html(_.escape(_text));
// flash the error, but make sure it goes away (we do not currently have any other logic for hiding this error message)
this.$('div.error-message').slideDown(250).delay(10000).slideUp(500);
},
createLocation: function () {
if (this._modal) {
this._modal.close()
}
var that = this;
this._modal = new LocationWizard({
onLocationCreated: function(wizard, data) {
that.options.locations.fetch({reset:true});
},
onFinish: function(wizard) {
that.$(".add-app #modal-container .modal").modal('hide');
},
isModal: true
});
this.$(".modal-location #modal-container").html(this._modal.render().el);
this.$(".modal-location #modal-container .modal")
.on("hidden",function () {
that._modal.close();
that.options.locations.fetch({reset:true});
}).modal('show');
}
});
return EditorView;
});