You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2016/02/01 18:52:20 UTC

[02/51] [abbrv] [partial] brooklyn-ui git commit: move subdir from incubator up a level as it is promoted to its own repo (first non-incubator commit!)

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 += "&nbsp;&nbsp;&nbsp;&nbsp;";
+
+                                         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;
+});