http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/view/change-name-invoke.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/view/change-name-invoke.js b/src/main/webapp/assets/js/view/change-name-invoke.js new file mode 100644 index 0000000..30c2277 --- /dev/null +++ b/src/main/webapp/assets/js/view/change-name-invoke.js @@ -0,0 +1,57 @@ +/* + * 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 entity expungement as a modal + */ +define([ + "underscore", "jquery", "backbone", "brooklyn-utils", + "text!tpl/apps/change-name-modal.html" +], function(_, $, Backbone, Util, ChangeNameModalHtml) { + return Backbone.View.extend({ + template: _.template(ChangeNameModalHtml), + initialize: function() { + this.title = "Change Name of "+this.options.entity.get('name'); + }, + render: function() { + this.$el.html(this.template({ name: this.options.entity.get('name') })); + return this; + }, + onSubmit: function() { + var self = this; + var newName = this.$("#new-name").val(); + var url = this.options.entity.get('links').rename + "?name=" + encodeURIComponent(newName); + var ajax = $.ajax({ + type: "POST", + url: url, + contentType: "application/json", + success: function() { + self.options.target.reload(); + }, + error: function(response) { + self.showError(Util.extractError(response, "Error contacting server", url)); + } + }); + return ajax; + }, + showError: function (message) { + this.$(".change-name-error-container").removeClass("hide"); + this.$(".change-name-error-message").html(message); + } + }); +});
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/view/effector-invoke.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/view/effector-invoke.js b/src/main/webapp/assets/js/view/effector-invoke.js new file mode 100644 index 0000000..7c9e0bd --- /dev/null +++ b/src/main/webapp/assets/js/view/effector-invoke.js @@ -0,0 +1,171 @@ +/* + * 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 an entity effector as a modal. + */ +define([ + "underscore", "jquery", "backbone", + "model/location", + "text!tpl/apps/effector-modal.html", + "text!tpl/app-add-wizard/deploy-location-row.html", + "text!tpl/app-add-wizard/deploy-location-option.html", + "text!tpl/apps/param.html", + "text!tpl/apps/param-list.html", + "bootstrap" +], function (_, $, Backbone, Location, EffectorModalHtml, + DeployLocationRowHtml, DeployLocationOptionHtml, ParamHtml, ParamListHtml) { + + var EffectorInvokeView = Backbone.View.extend({ + template:_.template(EffectorModalHtml), + locationRowTemplate:_.template(DeployLocationRowHtml), + locationOptionTemplate:_.template(DeployLocationOptionHtml), + effectorParam:_.template(ParamHtml), + effectorParamList:_.template(ParamListHtml), + + events:{ + "click .invoke-effector":"invokeEffector", + "shown": "onShow", + "hide": "onHide" + }, + + initialize:function () { + this.locations = this.options.locations /* for testing */ + || new Location.Collection(); + }, + + onShow: function() { + this.delegateEvents(); + this.$el.fadeTo(500,1); + }, + + onHide: function() { + this.undelegateEvents(); + }, + + render:function () { + var that = this, params = this.model.get("parameters") + this.$el.html(this.template({ + name:this.model.get("name"), + entityName:this.options.entity.get("name"), + description:this.model.get("description")?this.model.get("description"):"" + })) + // do we have parameters to render? + if (params.length !== 0) { + this.$(".modal-body").html(this.effectorParamList({})) + // select the body of the table we just rendered and append params + var $tbody = this.$("tbody") + _(params).each(function (param) { + // TODO: this should be another view whose implementation is specific to + // the type of the parameter (i.e. text, dates, checkboxes etc. can all + // be handled separately). + $tbody.append(that.effectorParam({ + name:param.name, + type:param.type, + description:param.description?param.description:"", + defaultValue:param.defaultValue + })) + }) + var container = this.$("#selector-container") + if (container.length) { + this.locations.fetch({async:false}) + container.empty() + var chosenLocation = this.locations[0]; + container.append(that.locationRowTemplate({ + initialValue : chosenLocation, + rowId : 0 + })) + var $selectLocations = container.find('.select-location') + .append(this.locationOptionTemplate({ + id: "", + name: "None" + })) + .append("<option disabled>------</option>"); + this.locations.each(function(aLocation) { + var $option = that.locationOptionTemplate({ + id:aLocation.id, + url:aLocation.getLinkByName("self"), + name:aLocation.getPrettyName() + }) + $selectLocations.append($option) + }) + $selectLocations.each(function(i) { + var url = $($selectLocations[i]).parent().attr('initialValue'); + $($selectLocations[i]).val(url) + }) + } + } + this.$(".modal-body").find('*[rel="tooltip"]').tooltip() + return this + }, + + extractParamsFromTable:function () { + var parameters = {}; + + // iterate over the rows + // TODO: this should be generic alongside the rendering of parameters. + this.$(".effector-param").each(function (index) { + var key = $(this).find(".param-name").text(); + var valElement = $(this).find(".param-value"); + var value; + if (valElement.attr('id') == 'selector-container') { + value = $(this).find(".param-value option:selected").attr("value") + } else if (valElement.is(":checkbox")) { + value = ("checked" == valElement.attr("checked")) ? "true" : "false"; + } else { + value = valElement.val(); + } + //treat empty field as null value + if (value !== '') { + parameters[key] = value; + } + }); + return parameters + }, + + invokeEffector:function () { + var that = this + var url = this.model.getLinkByName("self") + var parameters = this.extractParamsFromTable() + this.$el.fadeTo(500,0.5); + $.ajax({ + type:"POST", + url:url+"?timeout=0", + data:JSON.stringify(parameters), + contentType:"application/json", + success:function (data) { + that.$el.modal("hide") + that.$el.fadeTo(500,1); + if (that.options.openTask) + that.options.tabView.openTab('activities/subtask/'+data.id); + }, + error: function(data) { + that.$el.fadeTo(100,1).delay(200).fadeTo(200,0.2).delay(200).fadeTo(200,1); + // TODO render the error better than poor-man's flashing + // (would just be connection error -- with timeout=0 we get a task even for invalid input) + + console.error("ERROR invoking effector") + console.debug(data) + }}) + // un-delegate events + this.undelegateEvents() + } + + }) + return EffectorInvokeView +}) http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/view/entity-activities.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/view/entity-activities.js b/src/main/webapp/assets/js/view/entity-activities.js new file mode 100644 index 0000000..07dc948 --- /dev/null +++ b/src/main/webapp/assets/js/view/entity-activities.js @@ -0,0 +1,249 @@ +/* + * 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 the list of activities/tasks the entity performed. + */ +define([ + "underscore", "jquery", "backbone", "brooklyn-utils", "view/viewutils", + "view/activity-details", + "text!tpl/apps/activities.html", "text!tpl/apps/activity-table.html", + "text!tpl/apps/activity-row-details.html", "text!tpl/apps/activity-row-details-main.html", + "text!tpl/apps/activity-full-details.html", + "bootstrap", "jquery-datatables", "datatables-extensions", "moment" +], function (_, $, Backbone, Util, ViewUtils, ActivityDetailsView, + ActivitiesHtml, ActivityTableHtml, ActivityRowDetailsHtml, ActivityRowDetailsMainHtml, ActivityFullDetailsHtml) { + + var ActivitiesView = Backbone.View.extend({ + template:_.template(ActivitiesHtml), + table:null, + refreshActive:true, + selectedId:null, + selectedRow:null, + events:{ + "click #activities-root .activity-table tr":"rowClick", + 'click #activities-root .refresh':'refreshNow', + 'click #activities-root .toggleAutoRefresh':'toggleAutoRefresh', + 'click #activities-root .showDrillDown':'showDrillDown', + 'click #activities-root .toggleFullDetail':'toggleFullDetail' + }, + initialize:function () { + _.bindAll(this) + this.$el.html(this.template({ })); + this.$('#activities-root').html(_.template(ActivityTableHtml)) + var that = this, + $table = that.$('#activities-root .activity-table'); + that.collection.url = that.model.getLinkByName("activities"); + that.table = ViewUtils.myDataTable($table, { + "fnRowCallback": function( nRow, aData, iDisplayIndex, iDisplayIndexFull ) { + $(nRow).attr('id', aData[0]) + $(nRow).addClass('activity-row') + }, + "aaSorting": [[ 2, "desc" ]], + "aoColumnDefs": [ + { + "mRender": function ( data, type, row ) { + return Util.escape(data) + }, + "aTargets": [ 1, 3 ] + }, + { + "mRender": function ( data, type, row ) { + if ( type === 'display' ) { + data = moment(data).calendar(); + } + return Util.escape(data) + }, + "aTargets": [ 2 ] + }, + { "bVisible": false, "aTargets": [ 0 ] } + ] + }); + + // TODO domain-specific filters + ViewUtils.addAutoRefreshButton(that.table); + ViewUtils.addRefreshButton(that.table); + + ViewUtils.fadeToIndicateInitialLoad($table); + that.collection.on("reset", that.renderOnLoad, that); + ViewUtils.fetchRepeatedlyWithDelay(this, this.collection, + { fetchOptions: { reset: true }, doitnow: true, + enablement: function() { return that.refreshActive } }); + }, + refreshNow: function() { + this.collection.fetch({reset: true}); + }, + render:function () { + this.updateActivitiesNow(); + var details = this.options.tabView ? this.options.tabView.options.preselectTabDetails : null; + if (details && details!=this.lastPreselectTabDetails) { + this.lastPreselectTabDetails = details; + // should be a path + this.queuedTasksToOpen = details.split("/"); + } + this.tryOpenQueuedTasks(); + return this; + }, + tryOpenQueuedTasks: function() { + if (!this.queuedTasksToOpen || this.tryingOpenQueuedTasks) return; + this.openingQueuedTasks = true; + var $lastActivityPanel = null; + while (true) { + var task = this.queuedTasksToOpen.shift(); + if (task == undefined) { + this.openingQueuedTasks = false; + return; + } + if (task == 'subtask') { + var subtask = this.queuedTasksToOpen.shift(); + $lastActivityPanel = this.showDrillDownTask(subtask, $lastActivityPanel); + } else { + log("unknown queued task for activities panel: "+task) + // skip it, just continue + } + } + }, + beforeClose:function () { + this.collection.off("reset", this.renderOnLoad); + }, + renderOnLoad: function() { + this.loaded = true; + this.render(); + ViewUtils.cancelFadeOnceLoaded(this.table); + }, + toggleAutoRefresh:function () { + ViewUtils.toggleAutoRefresh(this); + }, + enableAutoRefresh: function(isEnabled) { + this.refreshActive = isEnabled + }, + refreshNow: function() { + this.collection.fetch(); + this.table.fnAdjustColumnSizing(); + }, + updateActivitiesNow: function() { + var that = this; + if (this.table == null || this.collection.length==0 || this.viewIsClosed) { + // nothing to do + } else { + var topLevelTasks = [] + for (taskI in this.collection.models) { + var task = this.collection.models[taskI] + var submitter = task.get("submittedByTask") + if ((submitter==null) || + (submitter!=null && this.collection.get(submitter.metadata.id)==null) + ) { + topLevelTasks.push(task) + } + } + ViewUtils.updateMyDataTable(that.table, topLevelTasks, function(task, index) { + return [ task.get("id"), + task.get("displayName"), + task.get("submitTimeUtc"), + task.get("currentStatus") + ]; + }); + this.showDetailRow(true); + } + return this; + }, + rowClick:function(evt) { + var row = $(evt.currentTarget).closest("tr"); + var id = row.attr("id"); + if (id==null) + // is the details row, ignore click here + return; + this.showDrillDownTask(id); + return; + }, + showDrillDown: function(event) { + this.showDrillDownTask($(event.currentTarget).closest("td.row-expansion").attr("id")); + }, + showDrillDownTask: function(taskId, optionalParent) { +// log("showing initial drill down "+taskId) + var that = this; + + var activityDetailsPanel = new ActivityDetailsView({ + taskId: taskId, + tabView: that, + collection: this.collection, + breadcrumbs: '' + }) + activityDetailsPanel.addToView(optionalParent || this.$(".activity-table")); + return activityDetailsPanel.$el; + }, + + showDetailRow: function(updateOnly) { + var id = this.selectedId, + that = this; + if (id==null) return; + var task = this.collection.get(id); + if (task==null) return; + if (!updateOnly) { + var html = _.template(ActivityRowDetailsHtml, { + task: task==null ? null : task.attributes, + link: that.model.getLinkByName("activities")+"/"+id, + updateOnly: updateOnly + }) + $('tr#'+id).next().find('td.row-expansion').html(html) + $('tr#'+id).next().find('td.row-expansion').attr('id', id) + } else { + // just update + $('tr#'+id).next().find('.task-description').html(Util.escape(task.attributes.description)) + } + + var html = _.template(ActivityRowDetailsMainHtml, { + task: task==null ? null : task.attributes, + link: that.model.getLinkByName("activities")+"/"+id, + updateOnly: updateOnly + }) + $('tr#'+id).next().find('.expansion-main').html(html) + + + if (!updateOnly) { + $('tr#'+id).next().find('.row-expansion .opened-row-details').hide() + $('tr#'+id).next().find('.row-expansion .opened-row-details').slideDown(300) + } + }, + toggleFullDetail: function(evt) { + var i = $('.toggleFullDetail'); + var id = i.closest("td.row-expansion").attr('id') + i.toggleClass('active') + if (i.hasClass('active')) + this.showFullActivity(id) + else + this.hideFullActivity(id) + }, + showFullActivity: function(id) { + id = this.selectedId + var $details = $("td.row-expansion#"+id+" .expansion-footer"); + var task = this.collection.get(id); + var html = _.template(ActivityFullDetailsHtml, { task: task }); + $details.html(html); + $details.slideDown(100); + _.defer(function() { ViewUtils.setHeightAutomatically($('textarea',$details), 30, 200) }) + }, + hideFullActivity: function(id) { + id = this.selectedId + var $details = $("td.row-expansion#"+id+" .expansion-footer"); + $details.slideUp(100); + } + }); + + return ActivitiesView; +}); http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/view/entity-advanced.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/view/entity-advanced.js b/src/main/webapp/assets/js/view/entity-advanced.js new file mode 100644 index 0000000..fe18430 --- /dev/null +++ b/src/main/webapp/assets/js/view/entity-advanced.js @@ -0,0 +1,177 @@ +/* + * 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 entity advanced tab. + * + * @type {*} + */ +define(["underscore", "jquery", "backbone", "brooklyn", "brooklyn-utils", "view/viewutils", + "text!tpl/apps/advanced.html", "view/change-name-invoke", "view/add-child-invoke", "view/policy-new" +], function(_, $, Backbone, Brooklyn, Util, ViewUtils, + AdvancedHtml, ChangeNameInvokeView, AddChildInvokeView, NewPolicyView) { + var EntityAdvancedView = Backbone.View.extend({ + events: { + "click button#change-name": "showChangeNameModal", + "click button#add-child": "showAddChildModal", + "click button#add-new-policy": "showNewPolicyModal", + "click button#reset-problems": "confirmResetProblems", + "click button#expunge": "confirmExpunge", + "click button#unmanage": "confirmUnmanage", + "click #advanced-tab-error-closer": "closeAdvancedTabError" + }, + template: _.template(AdvancedHtml), + initialize:function() { + _.bindAll(this); + this.$el.html(this.template()); + + this.model.on('change', this.modelChange, this); + this.modelChange(); + + ViewUtils.getRepeatedlyWithDelay(this, this.model.get('links').locations, this.renderLocationData); + ViewUtils.get(this, this.model.get('links').tags, this.renderTags); + + ViewUtils.attachToggler(this.$el); + }, + modelChange: function() { + this.$('#entity-name').html(Util.toDisplayString(this.model.get("name"))); + ViewUtils.updateTextareaWithData($("#advanced-entity-json", this.$el), Util.toTextAreaString(this.model), true, false, 250, 600); + }, + renderLocationData: function(data) { + ViewUtils.updateTextareaWithData($("#advanced-locations", this.$el), Util.toTextAreaString(data), true, false, 250, 600); + }, + renderTags: function(data) { + var list = ""; + for (tag in data) + list += "<div class='activity-tag-giftlabel'>"+Util.toDisplayString(data[tag])+"</div>"; + if (!list) list = "No tags"; + this.$('#advanced-entity-tags').html(list); + }, + reload: function() { + this.model.fetch(); + }, + + showModal: function(modal) { + if (this.activeModal) + this.activeModal.close(); + this.activeModal = modal; + Brooklyn.view.showModalWith(modal); + }, + showChangeNameModal: function() { + this.showModal(new ChangeNameInvokeView({ + entity: this.model, + target:this + })); + }, + showAddChildModal: function() { + this.showModal(new AddChildInvokeView({ + entity: this.model, + target:this + })); + }, + showNewPolicyModal: function () { + this.showModal(new NewPolicyView({ + entity: this.model, + })); + }, + + confirmResetProblems: function () { + var entity = this.model.get("name"); + var title = "Confirm the reset of problem indicators in " + entity; + var q = "<p>Are you sure you want to reset the problem indicators for this entity?</p>" + + "<p>If a problem has been fixed externally, but the fix is not being detected, this will clear problems. " + + "If the problem is not actually fixed, many feeds and enrichers will re-detect it, but note that some may not, " + + "and the entity may show as healthy when it is not." + + "</p>"; + Brooklyn.view.requestConfirmation(q, title).done(this.doResetProblems); + }, + doResetProblems: function() { + this.post(this.model.get('links').sensors+"/"+"service.notUp.indicators", {}); + this.post(this.model.get('links').sensors+"/"+"service.problems", {}); + }, + post: function(url, data) { + var self = this; + + $.ajax({ + type: "POST", + url: url, + data: JSON.stringify(data), + contentType: "application/json", + success: function() { + self.reload(); + }, + error: function(response) { + self.showAdvancedTabError(Util.extractError(response, "Error contacting server", url)); + } + }); + }, + + confirmExpunge: function () { + var entity = this.model.get("name"); + var title = "Confirm the expunging of " + entity; + var q = "<p>Are you certain you want to expunge this entity?</p>" + + "<p>When possible, Brooklyn will delete all of its resources.</p>" + + "<p><span class='label label-important'>Important</span> " + + "<b>This action is irreversible</b></p>"; + this.unmanageAndOrExpunge(q, title, true); + }, + confirmUnmanage: function () { + var entity = this.model.get("name"); + var title = "Confirm the unmanagement of " + entity; + var q = "<p>Are you certain you want to unmanage this entity?</p>" + + "<p>Its resources will be left running.</p>" + + "<p><span class='label label-important'>Important</span> " + + "<b>This action is irreversible</b></p>"; + this.unmanageAndOrExpunge(q, title, false); + }, + unmanageAndOrExpunge: function (question, title, releaseResources) { + var self = this; + Brooklyn.view.requestConfirmation(question, title).done(function() { + return $.ajax({ + type: "POST", + url: self.model.get("links").expunge + "?release=" + releaseResources + "&timeout=0", + contentType: "application/json" + }).done(function() { + self.trigger("entity.expunged"); + }).fail(function() { + // (would just be connection error -- with timeout=0 we get a task even for invalid input) + self.showAdvancedTabError("Error connecting to Brooklyn server"); + + log("ERROR unmanaging/expunging"); + log(data); + }); + }); + }, + + showAdvancedTabError: function(errorMessage) { + self.$("#advanced-tab-error-message").html(_.escape(errorMessage)); + self.$("#advanced-tab-error-section").removeClass("hide"); + }, + closeAdvancedTabError: function() { + self.$("#advanced-tab-error-section").addClass("hide"); + }, + + beforeClose:function() { + if (this.activeModal) + this.activeModal.close(); + this.options.tabView.configView.close(); + this.model.off(); + } + }); + return EntityAdvancedView; +}); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/view/entity-config.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/view/entity-config.js b/src/main/webapp/assets/js/view/entity-config.js new file mode 100644 index 0000000..f517bcb --- /dev/null +++ b/src/main/webapp/assets/js/view/entity-config.js @@ -0,0 +1,516 @@ +/* + * 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 entity config tab. + * + * @type {*} + */ +define([ + "underscore", "jquery", "backbone", "brooklyn-utils", "zeroclipboard", "view/viewutils", + "model/config-summary", "text!tpl/apps/config.html", "text!tpl/apps/config-name.html", + "jquery-datatables", "datatables-extensions" +], function (_, $, Backbone, Util, ZeroClipboard, ViewUtils, ConfigSummary, ConfigHtml, ConfigNameHtml) { + + // TODO consider extracting all such usages to a shared ZeroClipboard wrapper? + ZeroClipboard.config({ moviePath: '//cdnjs.cloudflare.com/ajax/libs/zeroclipboard/1.3.1/ZeroClipboard.swf' }); + + var configHtml = _.template(ConfigHtml), + configNameHtml = _.template(ConfigNameHtml); + + // TODO refactor to share code w entity-sensors.js + // in meantime, see notes there! + var EntityConfigView = Backbone.View.extend({ + template: configHtml, + configMetadata:{}, + refreshActive:true, + zeroClipboard: null, + + events:{ + 'click .refresh':'updateConfigNow', + 'click .filterEmpty':'toggleFilterEmpty', + 'click .toggleAutoRefresh':'toggleAutoRefresh', + 'click #config-table div.secret-info':'toggleSecrecyVisibility', + + 'mouseup .valueOpen':'valueOpen', + 'mouseover #config-table tbody tr':'noteFloatMenuActive', + 'mouseout #config-table tbody tr':'noteFloatMenuSeemsInactive', + 'mouseover .floatGroup':'noteFloatMenuActive', + 'mouseout .floatGroup':'noteFloatMenuSeemsInactive', + 'mouseover .clipboard-item':'noteFloatMenuActiveCI', + 'mouseout .clipboard-item':'noteFloatMenuSeemsInactiveCI', + 'mouseover .hasFloatLeft':'showFloatLeft', + 'mouseover .hasFloatDown':'enterFloatDown', + 'mouseout .hasFloatDown':'exitFloatDown', + 'mouseup .light-popup-menu-item':'closeFloatMenuNow', + }, + + initialize:function () { + _.bindAll(this); + this.$el.html(this.template()); + + var that = this, + $table = this.$('#config-table'); + that.table = ViewUtils.myDataTable($table, { + "fnRowCallback": function( nRow, aData, iDisplayIndex, iDisplayIndexFull ) { + $(nRow).attr('id', aData[0]); + $('td',nRow).each(function(i,v){ + if (i==1) $(v).attr('class','config-value'); + }); + return nRow; + }, + "aoColumnDefs": [ + { // name (with tooltip) + "mRender": function ( data, type, row ) { + // name (column 1) should have tooltip title + var actions = that.getConfigActions(data.name); + // if data.description or .type is absent we get an error in html rendering (js) + // unless we set it explicitly (there is probably a nicer way to do this however?) + var context = _.extend(data, { + description: data['description'], type: data['type']}); + return configNameHtml(context); + }, + "aTargets": [ 1 ] + }, + { // value + "mRender": function ( data, type, row ) { + var escapedValue = Util.toDisplayString(data); + if (type!='display') + return escapedValue; + + var hasEscapedValue = (escapedValue!=null && (""+escapedValue).length > 0); + configName = row[0], + actions = that.getConfigActions(configName); + + // NB: the row might not yet exist + var $row = $('tr[id="'+configName+'"]'); + + // datatables doesn't seem to expose any way to modify the html in place for a cell, + // so we rebuild + + var result = "<span class='value'>"+(hasEscapedValue ? escapedValue : '')+"</span>"; + + var isSecret = Util.isSecret(configName); + if (isSecret) { + result += "<span class='secret-indicator'>(hidden)</span>"; + } + + if (actions.open) + result = "<a href='"+actions.open+"'>" + result + "</a>"; + if (escapedValue==null || escapedValue.length < 3) + // include whitespace so we can click on it, if it's really small + result += " "; + + var existing = $row.find('.dynamic-contents'); + // for the json url, use the full url (relative to window.location.href) + var jsonUrl = actions.json ? new URI(actions.json).resolve(new URI(window.location.href)).toString() : null; + // prefer to update in place, so menus don't disappear, also more efficient + // (but if menu is changed, we do recreate it) + if (existing.length>0) { + if (that.checkFloatMenuUpToDate($row, actions.open, '.actions-open', 'open-target') && + that.checkFloatMenuUpToDate($row, escapedValue, '.actions-copy') && + that.checkFloatMenuUpToDate($row, actions.json, '.actions-json-open', 'open-target') && + that.checkFloatMenuUpToDate($row, jsonUrl, '.actions-json-copy', 'copy-value')) { +// log("updating in place "+configName) + existing.html(result); + return $row.find('td.config-value').html(); + } + } + + // build the menu - either because it is the first time, or the actions are stale +// log("creating "+configName); + + var downMenu = ""; + if (actions.open) + downMenu += "<div class='light-popup-menu-item valueOpen actions-open' open-target='"+actions.open+"'>" + + "Open</div>"; + if (hasEscapedValue) downMenu += + "<div class='light-popup-menu-item handy valueCopy actions-copy clipboard-item'>Copy Value</div>"; + if (actions.json) downMenu += + "<div class='light-popup-menu-item handy valueOpen actions-json-open' open-target='"+actions.json+"'>" + + "Open REST Link</div>"; + if (actions.json && hasEscapedValue) downMenu += + "<div class='light-popup-menu-item handy valueCopy actions-json-copy clipboard-item' copy-value='"+ + jsonUrl+"'>Copy REST Link</div>"; + if (downMenu=="") { +// log("no actions for "+configName); + downMenu += + "<div class='light-popup-menu-item'>(no actions)</div>"; + } + downMenu = "<div class='floatDown'><div class='light-popup'><div class='light-popup-body'>" + + downMenu + + "</div></div></div>"; + result = "<span class='hasFloatLeft dynamic-contents'>" + result + + "</span>" + + "<div class='floatLeft'><span class='icon-chevron-down hasFloatDown'></span>" + + downMenu + + "</div>"; + result = "<div class='floatGroup"+ + (isSecret ? " secret-info" : "")+ + "'>" + result + "</div>"; + // also see updateFloatMenus which wires up the JS for these classes + + return result; + }, + "aTargets": [ 2 ] + }, + // ID in column 0 is standard (assumed in ViewUtils) + { "bVisible": false, "aTargets": [ 0 ] } + ] + }); + + this.zeroClipboard = new ZeroClipboard(); + this.zeroClipboard.on( "dataRequested" , function(client) { + try { + // the zeroClipboard instance is a singleton so check our scope first + if (!$(this).closest("#config-table").length) return; + var text = $(this).attr('copy-value'); + if (!text) text = $(this).closest('.floatGroup').find('.value').text(); + +// log("Copying config text '"+text+"' to clipboard"); + client.setText(text); + + // show the word "copied" for feedback; + // NB this occurs on mousedown, due to how flash plugin works + // (same style of feedback and interaction as github) + // the other "clicks" are now triggered by *mouseup* + var $widget = $(this); + var oldHtml = $widget.html(); + $widget.html('<b>Copied!</b>'); + // use a timeout to restore because mouseouts can leave corner cases (see history) + setTimeout(function() { $widget.html(oldHtml); }, 600); + } catch (e) { + log("Zeroclipboard failure; falling back to prompt mechanism"); + log(e); + Util.promptCopyToClipboard(text); + } + }); + // these seem to arrive delayed sometimes, so we also work with the clipboard-item class events + this.zeroClipboard.on( "mouseover", function() { that.noteFloatMenuZeroClipboardItem(true, this); } ); + this.zeroClipboard.on( "mouseout", function() { that.noteFloatMenuZeroClipboardItem(false, this); } ); + this.zeroClipboard.on( "mouseup", function() { that.closeFloatMenuNow(); } ); + + ViewUtils.addFilterEmptyButton(this.table); + ViewUtils.addAutoRefreshButton(this.table); + ViewUtils.addRefreshButton(this.table); + this.loadConfigMetadata(); + this.updateConfigPeriodically(); + this.toggleFilterEmpty(); + return this; + }, + + beforeClose: function () { + if (this.zeroClipboard) { + this.zeroClipboard.destroy(); + } + }, + + floatMenuActive: false, + lastFloatMenuRowId: null, + lastFloatFocusInTextForEventUnmangling: null, + updateFloatMenus: function() { + $('#config-table *[rel="tooltip"]').tooltip(); + this.zeroClipboard.clip( $('.valueCopy') ); + }, + showFloatLeft: function(event) { + this.noteFloatMenuFocusChange(true, event, "show-left"); + this.showFloatLeftOf($(event.currentTarget)); + }, + showFloatLeftOf: function($hasFloatLeft) { + $hasFloatLeft.next('.floatLeft').show(); + }, + enterFloatDown: function(event) { + this.noteFloatMenuFocusChange(true, event, "show-down"); +// log("entering float down"); + var fdTarget = $(event.currentTarget); +// log( fdTarget ); + this.floatDownFocus = fdTarget; + var that = this; + setTimeout(function() { + that.showFloatDownOf( fdTarget ); + }, 200); + }, + exitFloatDown: function(event) { +// log("exiting float down"); + this.floatDownFocus = null; + }, + showFloatDownOf: function($hasFloatDown) { + if ($hasFloatDown != this.floatDownFocus) { +// log("float down did not hover long enough"); + return; + } + var down = $hasFloatDown.next('.floatDown'); + down.show(); + $('.light-popup', down).show(2000); + }, + noteFloatMenuActive: function(focus) { + this.noteFloatMenuFocusChange(true, focus, "menu"); + + // remove dangling zc events (these don't always get removed, apparent bug in zc event framework) + // this causes it to flash sometimes but that's better than leaving the old item highlighted + if (focus.toElement && $(focus.toElement).hasClass('clipboard-item')) { + // don't remove it + } else { + var zc = $(focus.target).closest('.floatGroup').find('div.zeroclipboard-is-hover'); + zc.removeClass('zeroclipboard-is-hover'); + } + }, + noteFloatMenuSeemsInactive: function(focus) { this.noteFloatMenuFocusChange(false, focus, "menu"); }, + noteFloatMenuActiveCI: function(focus) { this.noteFloatMenuFocusChange(true, focus, "menu-clip-item"); }, + noteFloatMenuSeemsInactiveCI: function(focus) { this.noteFloatMenuFocusChange(false, focus, "menu-clip-item"); }, + noteFloatMenuZeroClipboardItem: function(seemsActive,focus) { + this.noteFloatMenuFocusChange(seemsActive, focus, "clipboard"); + if (seemsActive) { + // make the table row highlighted (as the default hover event is lost) + // we remove it when the float group goes away + $(focus).closest('tr').addClass('zeroclipboard-is-hover'); + } else { + // sometimes does not get removed by framework - though this doesn't seem to help + // as you can see by logging this before and after: +// log(""+$(focus).attr('class')) + // the problem is that the framework seems sometime to trigger this event before adding the class + // see in noteFloatMenuActive where we do a different check + $(focus).removeClass('zeroclipboard-is-hover'); + } + }, + noteFloatMenuFocusChange: function(seemsActive, focus, caller) { +// log(""+new Date().getTime()+" note active "+caller+" "+seemsActive); + var delayCheckFloat = true; + var focusRowId = null; + var focusElement = null; + if (focus) { + focusElement = focus.target ? focus.target : focus; + if (seemsActive) { + this.lastFloatFocusInTextForEventUnmangling = $(focusElement).text(); + focusRowId = focus.target ? $(focus.target).closest('tr').attr('id') : $(focus).closest('tr').attr('id'); + if (this.floatMenuActive && focusRowId==this.lastFloatMenuRowId) { + // lastFloatMenuRowId has not changed, when moving within a floatgroup + // (but we still get mouseout events when the submenu changes) +// log("redundant mousein from "+ focusRowId ); + return; + } + } else { + // on mouseout, skip events which are bogus + // first, if the toElement is in the same floatGroup + focusRowId = focus.toElement ? $(focus.toElement).closest('tr').attr('id') : null; + if (focusRowId==this.lastFloatMenuRowId) { + // lastFloatMenuRowId has not changed, when moving within a floatgroup + // (but we still get mouseout events when the submenu changes) +// log("skipping, internal mouseout from "+ focusRowId ); + return; + } + // check (a) it is the 'out' event corresponding to the most recent 'in' + // (because there is a race where it can say in1, in2, out1 rather than in1, out2, in2 + if ($(focusElement).text() != this.lastFloatFocusInTextForEventUnmangling) { +// log("skipping, not most recent mouseout from "+ focusRowId ); + return; + } + if (focus.toElement) { + if ($(focus.toElement).hasClass('global-zeroclipboard-container')) { +// log("skipping out, as we are moving to clipboard container"); + return; + } + if (focus.toElement.name && focus.toElement.name=="global-zeroclipboard-flash-bridge") { +// log("skipping out, as we are moving to clipboard movie"); + return; + } + } + } + } +// log( "moving to "+focusRowId ); + if (seemsActive && focusRowId) { +// log("setting lastFloat when "+this.floatMenuActive + ", from "+this.lastFloatMenuRowId ); + if (this.lastFloatMenuRowId != focusRowId) { + if (this.lastFloatMenuRowId) { + // the floating menu has changed, hide the old +// log("hiding old menu on float-focus change"); + this.closeFloatMenuNow(); + } + } + // now show the new, if possible (might happen multiple times, but no matter + if (focusElement) { +// log("ensuring row "+focusRowId+" is showing on change"); + this.showFloatLeftOf($(focusElement).closest('tr').find('.hasFloatLeft')); + this.lastFloatMenuRowId = focusRowId; + } else { + this.lastFloatMenuRowId = null; + } + } + this.floatMenuActive = seemsActive; + if (!seemsActive) { + this.scheduleCheckFloatMenuNeedsHiding(delayCheckFloat); + } + }, + scheduleCheckFloatMenuNeedsHiding: function(delayCheckFloat) { + if (delayCheckFloat) { + this.checkTime = new Date().getTime()+299; + setTimeout(this.checkFloatMenuNeedsHiding, 300); + } else { + this.checkTime = new Date().getTime()-1; + this.checkFloatMenuNeedsHiding(); + } + }, + closeFloatMenuNow: function() { +// log("closing float menu due do direct call (eg click)"); + this.checkTime = new Date().getTime()-1; + this.floatMenuActive = false; + this.checkFloatMenuNeedsHiding(); + }, + checkFloatMenuNeedsHiding: function() { +// log(""+new Date().getTime()+" checking float menu - "+this.floatMenuActive); + if (new Date().getTime() <= this.checkTime) { +// log("aborting check as another one scheduled"); + return; + } + + // we use a flag to determine whether to hide the float menu + // because the embedded zero-clipboard flash objects cause floatGroup + // to get a mouseout event when the "Copy" menu item is hovered + if (!this.floatMenuActive) { +// log("HIDING FLOAT MENU") + $('.floatLeft').hide(); + $('.floatDown').hide(); + $('.zeroclipboard-is-hover').removeClass('zeroclipboard-is-hover'); + lastFloatMenuRowId = null; + } else { +// log("we're still in") + } + }, + valueOpen: function(event) { + window.open($(event.target).attr('open-target'),'_blank'); + }, + + render: function() { + return this; + }, + checkFloatMenuUpToDate: function($row, actionValue, actionSelector, actionAttribute) { + if (typeof actionValue === 'undefined' || actionValue==null || actionValue=="") { + if ($row.find(actionSelector).length==0) return true; + } else { + if (actionAttribute) { + if ($row.find(actionSelector).attr(actionAttribute)==actionValue) return true; + } else { + if ($row.find(actionSelector).length>0) return true; + } + } + return false; + }, + + /** + * Returns the actions loaded to view.configMetadata[name].actions + * for the given name, or an empty object. + */ + getConfigActions: function(configName) { + var allMetadata = this.configMetadata || {}; + var metadata = allMetadata[configName] || {}; + return metadata.actions || {}; + }, + + toggleFilterEmpty: function() { + ViewUtils.toggleFilterEmpty(this.$('#config-table'), 2); + return this; + }, + + toggleAutoRefresh: function() { + ViewUtils.toggleAutoRefresh(this); + return this; + }, + + enableAutoRefresh: function(isEnabled) { + this.refreshActive = isEnabled; + return this; + }, + + toggleSecrecyVisibility: function(event) { + $(event.target).closest('.secret-info').toggleClass('secret-revealed'); + }, + + /** + * Loads current values for all config on an entity and updates config table. + */ + isRefreshActive: function() { return this.refreshActive; }, + updateConfigNow:function () { + var that = this; + ViewUtils.get(that, that.model.getConfigUpdateUrl(), that.updateWithData, + { enablement: that.isRefreshActive }); + }, + updateConfigPeriodically:function () { + var that = this; + ViewUtils.getRepeatedlyWithDelay(that, that.model.getConfigUpdateUrl(), function(data) { that.updateWithData(data); }, + { enablement: that.isRefreshActive }); + }, + updateWithData: function (data) { + var that = this; + $table = that.$('#config-table'); + var options = {}; + + if (that.fullRedraw) { + options.refreshAllRows = true; + that.fullRedraw = false; + } + ViewUtils.updateMyDataTable($table, data, function(value, name) { + var metadata = that.configMetadata[name]; + if (metadata==null) { + // kick off reload metadata when this happens (new config for which no metadata known) + // but only if we haven't loaded metadata for a while + metadata = { 'name':name }; + that.configMetadata[name] = metadata; + that.loadConfigMetadataIfStale(name, 10000); + } + return [name, metadata, value]; + }, options); + + that.updateFloatMenus(); + }, + + loadConfigMetadata: function() { + var url = this.model.getLinkByName('config'), + that = this; + that.lastConfigMetadataLoadTime = new Date().getTime(); + $.get(url, function (data) { + _.each(data, function(config) { + var actions = {}; + _.each(config.links, function(v, k) { + if (k.slice(0, 7) == "action:") { + actions[k.slice(7)] = v; + } + }); + that.configMetadata[config.name] = { + name: config.name, + description: config.description, + actions: actions, + type: config.type + }; + }); + that.fullRedraw = true; + that.updateConfigNow(); + that.table.find('*[rel="tooltip"]').tooltip(); + }); + return this; + }, + + loadConfigMetadataIfStale: function(configName, recency) { + var that = this; + if (!that.lastConfigMetadataLoadTime || that.lastConfigMetadataLoadTime + recency < new Date().getTime()) { +// log("reloading metadata because new config "+configName+" identified") + that.loadConfigMetadata(); + } + } + }); + return EntityConfigView; +}); http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/view/entity-details.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/view/entity-details.js b/src/main/webapp/assets/js/view/entity-details.js new file mode 100644 index 0000000..f54c572 --- /dev/null +++ b/src/main/webapp/assets/js/view/entity-details.js @@ -0,0 +1,180 @@ +/* + * 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. +*/ +/** + * Renders details information about an application (sensors, summary, effectors, etc.). + * + * Options preselectTab (e.g. 'activities') and preselectTabDetails ('subtasks/1234') can be set + * before a render to cause the given tab / details to be opened. + * + * @type {*} + */ +define([ + "underscore", "jquery", "backbone", "./entity-summary", + "./entity-sensors", "./entity-effectors", "./entity-policies", + "./entity-activities", "./entity-advanced", "model/task-summary", "text!tpl/apps/details.html" +], function (_, $, Backbone, SummaryView, SensorsView, EffectorsView, PoliciesView, ActivitiesView, AdvancedView, TaskSummary, DetailsHtml) { + + var EntityDetailsView = Backbone.View.extend({ + template:_.template(DetailsHtml), + events:{ + 'click .entity-tabs a':'tabSelected' + }, + initialize:function () { + var self = this; + var tasks = new TaskSummary.Collection; + + this.$el.html(this.template({})) + this.sensorsView = new SensorsView({ + model:this.model, + tabView:this, + }) + this.effectorsView = new EffectorsView({ + model:this.model, + tabView:this, + }) + this.policiesView = new PoliciesView({ + model:this.model, + tabView:this, + }) + this.activitiesView = new ActivitiesView({ + model:this.model, + tabView:this, + collection:tasks + }) + // summary comes after others because it uses the tasks + this.summaryView = new SummaryView({ + model:this.model, + tabView:this, + application:this.options.application, + tasks:tasks, + }) + this.advancedView = new AdvancedView({ + model: this.model, + tabView:this, + application:this.options.application + }); + // propagate to app tree view + this.advancedView.on("entity.expunged", function() { self.trigger("entity.expunged"); }) + + this.$("#summary").html(this.summaryView.render().el); + this.$("#sensors").html(this.sensorsView.render().el); + this.$("#effectors").html(this.effectorsView.render().el); + this.$("#policies").html(this.policiesView.render().el); + this.$("#activities").html(this.activitiesView.render().el); + this.$("#advanced").html(this.advancedView.render().el); + }, + beforeClose:function () { + this.summaryView.close(); + this.sensorsView.close(); + this.effectorsView.close(); + this.policiesView.close(); + this.activitiesView.close(); + this.advancedView.close(); + }, + getEntityHref: function() { + return $("#app-tree .entity_tree_node_wrapper.active a").attr("href"); + }, + render: function(optionalParent) { + this.summaryView.render() + this.sensorsView.render() + this.effectorsView.render() + this.policiesView.render() + this.activitiesView.render() + this.advancedView.render() + + if (optionalParent) { + optionalParent.html(this.el) + } + var entityHref = this.getEntityHref(); + if (entityHref) { + $("a[data-toggle='tab']").each(function(i,a) { + $(a).attr('href',entityHref+"/"+$(a).attr("data-target").slice(1)); + }); + } else { + log("could not find entity href for tab"); + } + if (this.options.preselectTab) { + var tabLink = this.$('a[data-target="#'+this.options.preselectTab+'"]'); + var showFn = function() { tabLink.tab('show'); }; + if (optionalParent) showFn(); + else _.defer(showFn); + } + return this; + }, + tabSelected: function(event) { + // TODO: the bootstrap JS code still prevents shift-click from working + // have to add the following logic to bootstrap tab click handler also +// if (event.metaKey || event.shiftKey) +// // trying to open in a new tab, do not act on it here! +// return; + event.preventDefault(); + + var tabName = $(event.currentTarget).attr("data-target").slice(1); + var route = this.getTab(tabName); + if (route) { + if (route[0]=='#') route = route.substring(1); + Backbone.history.navigate(route); + } + // caller will ensure tab is shown + }, + getTab: function(tabName, entityId, entityHref) { + if (!entityHref) { + if (entityId) { + entityHref = this.getEntityHref(); + if (!entityHref.endsWith(entityId)) { + lastSlash = entityHref.lastIndexOf('/'); + if (lastSlash>=0) { + entityHref = entityHref.substring(0, lastSlash+1) + '/' + entityId; + } else { + log("malformed entityHref when opening tab: "+entityHref) + entityHref = this.getEntityHref(); + } + } + } else { + entityHref = this.getEntityHref(); + } + } + if (entityHref && tabName) + return entityHref+"/"+tabName; + return null; + }, + /** for tabs to redirect to other tabs; entityId and entityHref are optional (can supply either, or null to use current entity); + * tabPath is e.g. 'sensors' or 'activities/subtask/1234' */ + openTab: function(tabPath, entityId, entityHref) { + var route = this.getTab(tabPath, entityId, entityHref); + if (!route) return; + if (route[0]=='#') route = route.substring(1); + Backbone.history.navigate(route); + + tabPaths = tabPath.split('/'); + if (!tabPaths) return; + var tabName = tabPaths.shift(); + if (!tabName) + // ignore leading / + tabName = tabPaths.shift(); + if (!tabName) return; + + this.options.preselectTab = tabName; + if (tabPaths) + this.options.preselectTabDetails = tabPaths.join('/'); + this.render(); + } + }); + return EntityDetailsView; +}); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/view/entity-effectors.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/view/entity-effectors.js b/src/main/webapp/assets/js/view/entity-effectors.js new file mode 100644 index 0000000..974fbec --- /dev/null +++ b/src/main/webapp/assets/js/view/entity-effectors.js @@ -0,0 +1,92 @@ +/* + * 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 the effectors tab.You must supply the model and optionally the element + * on which the view binds itself. + * + * @type {*} + */ +define([ + "underscore", "jquery", "backbone", "view/viewutils", "model/effector-summary", + "view/effector-invoke", "text!tpl/apps/effector.html", "text!tpl/apps/effector-row.html", "bootstrap" +], function (_, $, Backbone, ViewUtils, EffectorSummary, EffectorInvokeView, EffectorHtml, EffectorRowHtml) { + + var EntityEffectorsView = Backbone.View.extend({ + template:_.template(EffectorHtml), + effectorRow:_.template(EffectorRowHtml), + events:{ + "click .show-effector-modal":"showEffectorModal" + }, + initialize:function () { + this.$el.html(this.template({})) + var that = this + this._effectors = new EffectorSummary.Collection() + // fetch the list of effectors and create a view for each one + this._effectors.url = this.model.getLinkByName("effectors") + that.loadedData = false; + ViewUtils.fadeToIndicateInitialLoad(this.$('#effectors-table')); + this.$(".has-no-effectors").hide(); + + this._effectors.fetch({success:function () { + that.loadedData = true; + that.render() + ViewUtils.cancelFadeOnceLoaded(that.$('#effectors-table')); + }}) + // attach a fetch simply to fade this tab when not available + // (the table is statically rendered) + ViewUtils.fetchRepeatedlyWithDelay(this, this._effectors, { period: 10*1000 }) + }, + render:function () { + if (this.viewIsClosed) + return; + var that = this + var $tableBody = this.$('#effectors-table tbody').empty() + if (this._effectors.length==0) { + if (that.loadedData) + this.$(".has-no-effectors").show(); + } else { + this.$(".has-no-effectors").hide(); + this._effectors.each(function (effector) { + $tableBody.append(that.effectorRow({ + name:effector.get("name"), + description:effector.get("description"), + // cid is mapped to id (here) which is mapped to name (in Effector.Summary), + // so it is consistent across resets + cid:effector.id + })) + }) + } + return this + }, + showEffectorModal:function (eventName) { + // get the model that we need to show, create its view and show it + var cid = $(eventName.currentTarget).attr("id") + var effectorModel = this._effectors.get(cid); + var modal = new EffectorInvokeView({ + el:"#effector-modal", + model:effectorModel, + entity:this.model, + tabView:this.options.tabView, + openTask:true + }) + modal.render().$el.modal('show') + } + }) + return EntityEffectorsView +}) \ No newline at end of file http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/view/entity-policies.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/view/entity-policies.js b/src/main/webapp/assets/js/view/entity-policies.js new file mode 100644 index 0000000..74ba885 --- /dev/null +++ b/src/main/webapp/assets/js/view/entity-policies.js @@ -0,0 +1,244 @@ +/* + * 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 the policies tab. You must supply the model and optionally the element + * on which the view binds itself. + */ +define([ + "underscore", "jquery", "backbone", "brooklyn", + "model/policy-summary", "model/policy-config-summary", + "view/viewutils", "view/policy-config-invoke", "view/policy-new", + "text!tpl/apps/policy.html", "text!tpl/apps/policy-row.html", "text!tpl/apps/policy-config-row.html", + "jquery-datatables", "datatables-extensions" +], function (_, $, Backbone, Brooklyn, + PolicySummary, PolicyConfigSummary, + ViewUtils, PolicyConfigInvokeView, NewPolicyView, + PolicyHtml, PolicyRowHtml, PolicyConfigRowHtml) { + + var EntityPoliciesView = Backbone.View.extend({ + + template: _.template(PolicyHtml), + policyRow: _.template(PolicyRowHtml), + + events:{ + 'click .refresh':'refreshPolicyConfigNow', + 'click .filterEmpty':'toggleFilterEmpty', + "click #policies-table tr":"rowClick", + "click .policy-start":"callStart", + "click .policy-stop":"callStop", + "click .policy-destroy":"callDestroy", + "click .show-policy-config-modal":"showPolicyConfigModal", + "click .add-new-policy": "showNewPolicyModal" + }, + + initialize:function () { + _.bindAll(this) + this.$el.html(this.template({ })); + var that = this; + // fetch the list of policies and create a row for each one + that._policies = new PolicySummary.Collection(); + that._policies.url = that.model.getLinkByName("policies"); + + this.loadedData = false; + ViewUtils.fadeToIndicateInitialLoad(this.$('#policies-table')); + that.render(); + this._policies.on("all", this.render, this) + ViewUtils.fetchRepeatedlyWithDelay(this, this._policies, { + doitnow: true, + success: function () { + that.loadedData = true; + ViewUtils.cancelFadeOnceLoaded(that.$('#policies-table')); + }}); + }, + + render:function () { + if (this.viewIsClosed) + return; + var that = this, + $tbody = this.$('#policies-table tbody').empty(); + if (that._policies.length==0) { + if (this.loadedData) + this.$(".has-no-policies").show(); + this.$("#policy-config").hide(); + this.$("#policy-config-none-selected").hide(); + } else { + this.$(".has-no-policies").hide(); + that._policies.each(function (policy) { + // TODO better to use datatables, and a json array, as we do elsewhere + $tbody.append(that.policyRow({ + cid:policy.get("id"), + name:policy.get("name"), + state:policy.get("state"), + summary:policy + })); + if (that.activePolicy) { + that.$("#policies-table tr[id='"+that.activePolicy+"']").addClass("selected"); + that.showPolicyConfig(that.activePolicy); + that.refreshPolicyConfig(); + } else { + that.$("#policy-config").hide(); + that.$("#policy-config-none-selected").show(); + } + }); + } + return that; + }, + + toggleFilterEmpty:function() { + ViewUtils.toggleFilterEmpty($('#policy-config-table'), 2); + }, + + refreshPolicyConfigNow:function () { + this.refreshPolicyConfig(); + }, + + rowClick:function(evt) { + evt.stopPropagation(); + var row = $(evt.currentTarget).closest("tr"), + id = row.attr("id"), + policy = this._policies.get(id); + $("#policies-table tr").removeClass("selected"); + if (this.activePolicy == id) { + // deselected + this.activePolicy = null; + this._config = null; + $("#policy-config-table").dataTable().fnDestroy(); + $("#policy-config").slideUp(100); + $("#policy-config-none-selected").slideDown(100); + } else { + row.addClass("selected"); + // fetch the list of policy config entries + this._config = new PolicyConfigSummary.Collection(); + this._config.url = policy.getLinkByName("config"); + ViewUtils.fadeToIndicateInitialLoad($('#policy-config-table')); + this.showPolicyConfig(id); + var that = this; + this._config.fetch().done(function () { + that.showPolicyConfig(id); + ViewUtils.cancelFadeOnceLoaded($('#policy-config-table')) + }); + } + }, + + showPolicyConfig:function (activePolicyId) { + var that = this; + if (activePolicyId != null && that.activePolicy != activePolicyId) { + // TODO better to use a json array, as we do elsewhere + var $table = $('#policy-config-table'), + $tbody = $table.find('tbody'); + $table.dataTable().fnClearTable(); + $("#policy-config-none-selected").slideUp(100); + if (that._config.length==0) { + $(".has-no-policy-config").show(); + } else { + $(".has-no-policy-config").hide(); + that.activePolicy = activePolicyId; + var policyConfigRow = _.template(PolicyConfigRowHtml); + that._config.each(function (config) { + $tbody.append(policyConfigRow({ + cid:config.cid, + name:config.get("name"), + description:config.get("description"), + type:config.get("type"), + reconfigurable:config.get("reconfigurable"), + link:config.getLinkByName('self'), + value: config.get("defaultValue") + })); + $tbody.find('*[rel="tooltip"]').tooltip(); + }); + that.currentStateUrl = that._policies.get(that.activePolicy).getLinkByName("config") + "/current-state"; + $("#policy-config").slideDown(100); + $table.slideDown(100); + ViewUtils.myDataTable($table, { + "bAutoWidth": false, + "aoColumns" : [ + { sWidth: '220px' }, + { sWidth: '240px' }, + { sWidth: '25px' } + ] + }); + $table.dataTable().fnAdjustColumnSizing(); + } + } + that.refreshPolicyConfig(); + }, + + refreshPolicyConfig:function() { + var that = this; + if (that.viewIsClosed || !that.currentStateUrl) return; + var $table = that.$('#policy-config-table').dataTable(), + $rows = that.$("tr.policy-config-row"); + $.get(that.currentStateUrl, function (data) { + if (that.viewIsClosed) return; + // iterate over the sensors table and update each sensor + $rows.each(function (index, row) { + var key = $(this).find(".policy-config-name").text(); + var v = data[key]; + if (v !== undefined) { + $table.fnUpdate(_.escape(v), row, 1, false); + } + }); + }); + $table.dataTable().fnStandingRedraw(); + }, + + showPolicyConfigModal: function (evt) { + var cid = $(evt.currentTarget).attr("id"); + var currentValue = $(evt.currentTarget) + .parent().parent() + .find(".policy-config-value") + .text(); + Brooklyn.view.showModalWith(new PolicyConfigInvokeView({ + model: this._config.get(cid), + policy: this.model, + currentValue: currentValue + })); + }, + + showNewPolicyModal: function () { + var self = this; + Brooklyn.view.showModalWith(new NewPolicyView({ + entity: this.model, + onSave: function (policy) { + console.log("New policy", policy); + self._policies.add(policy); + } + })); + }, + + callStart:function(event) { this.doPost(event, "start"); }, + callStop:function(event) { this.doPost(event, "stop"); }, + callDestroy:function(event) { this.doPost(event, "destroy"); }, + doPost:function(event, linkname) { + event.stopPropagation(); + var that = this, + url = $(event.currentTarget).attr("link"); + $.ajax({ + type:"POST", + url:url, + success:function() { + that._policies.fetch(); + } + }); + } + + }); + + return EntityPoliciesView; +});
