Further improvements to jsgui catalog page * Displays as much as possible of catalogue item when selected, then refreshes model and only updates display if model contents change. * Can delete models if button with class delete is clicked. * Improved presentation of errors loading models * Removes add-new-thing catalog button, to return later
Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/6dc52f55 Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/6dc52f55 Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/6dc52f55 Branch: refs/heads/master Commit: 6dc52f55d4e0d1295c80faf817ed7c54cdb98782 Parents: 02a60db Author: Sam Corbett <sam.corb...@cloudsoftcorp.com> Authored: Tue Jul 8 13:16:46 2014 +0100 Committer: Sam Corbett <sam.corb...@cloudsoftcorp.com> Committed: Wed Jul 9 14:30:30 2014 +0100 ---------------------------------------------------------------------- .../src/main/webapp/assets/js/view/catalog.js | 194 ++++++++++++------- .../assets/tpl/catalog/details-entity.html | 30 +-- .../assets/tpl/catalog/details-location.html | 15 +- .../webapp/assets/tpl/catalog/nav-entry.html | 2 +- .../main/webapp/assets/tpl/catalog/page.html | 2 - 5 files changed, 153 insertions(+), 90 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6dc52f55/usage/jsgui/src/main/webapp/assets/js/view/catalog.js ---------------------------------------------------------------------- diff --git a/usage/jsgui/src/main/webapp/assets/js/view/catalog.js b/usage/jsgui/src/main/webapp/assets/js/view/catalog.js index 3048a2f..85fe1a5 100644 --- a/usage/jsgui/src/main/webapp/assets/js/view/catalog.js +++ b/usage/jsgui/src/main/webapp/assets/js/view/catalog.js @@ -12,66 +12,71 @@ define([ ], function(_, $, Backbone, FormatJSON, Location, Entity, AddLocationModalView, CatalogPageHtml, DetailsEntityHtml, DetailsGenericHtml, EntryHtml, LocationDetailsHtml) { + // Holds the currently active details type, e.g. applications, policies + var activeDetails; + // TODO: Loading item's details should perform page navigation var DetailsView = Backbone.View.extend({ - genericDetails: _.template(DetailsGenericHtml), - entityDetails: _.template(DetailsEntityHtml), - locationDetails: _.template(LocationDetailsHtml), + events: { + "click .delete": "deleteItem" + }, - render: function() { - this.$el.html("<div class='catalog-details'><h3>Select an entry on the left</h3></div>"); + initialize: function() { + _.bindAll(this); }, - showDetailsFor: function(event, type) { - var $event = $(event.currentTarget); - if ($event.hasClass("active")) return; + render: function(extraMessage) { + this.$el.html("<div class='catalog-details'>" + + "<h3>Select an entry on the left</h3>" + + (extraMessage ? extraMessage : "") + + "</div>"); + }, - $(".accordion-nav-row").removeClass("active"); - $event.addClass('active'); - var chosenId = $event.attr('id'); - var url, template, Model; - if (type == 'applications' || type == 'entities') { - // app templates are just normal entities, in the API - url = '/v1/catalog/entities/' + chosenId; - template = this.entityDetails; - Model = Entity.Model; - } else if (type == 'locations') { - url = chosenId; - template = this.locationDetails; - Model = Location.Model; - } else { - url = '/v1/catalog/' + type + '/' + chosenId; - template = this.genericDetails; - } + show: function(model, template) { + // Keep the previously open section open between items + var open = this.$(".in").attr("id"); + var newHtml = $(template({model: model})); + $(newHtml).find("#"+open).addClass("in"); + this.$el.html(newHtml); - // TODO: Set 'Loading' template - //this.$el.html(this.genericDetails({title: chosenId})); + // rewire events. previous callbacks are removed automatically. + this.delegateEvents() + }, + + showDetailsFor: function(model, template) { + this.activeModel = model; var that = this; - $.ajax({ url: url, - success: function (data) { - var defaults = { - "description": undefined, - "planYaml": undefined, - "sensors": [], - "effectors": [], - "id": undefined, - "name": undefined, - "spec": undefined, - "config": undefined - }; - if (Model) { - defaults['model'] = new Model(data); - } - that.$el.html(template(_.extend(defaults, data))) - }, - error: function (xhr, textStatus, error) { - that.$el.html(that.genericDetails({ - title: chosenId, - json: FormatJSON({ "status": textStatus, "error": error }) - })); + // Load the view with currently available data and refresh once the load is complete. + // Only refreshes the view if the model changes and the user hasn't selected another + // item while the load was executing. + this.show(model, template); + model.on("change", function() { + if (that.activeModel.cid === model.cid) { + that.show(model, template); } }); + model.fetch() + .fail(function(xhr, textStatus, errorThrown) { + console.log("error loading", model.id, ":", errorThrown); + if (that.activeModel.cid === model.cid) { + model.error = true; + that.show(model, template); + } + }) + // Runs after the change event fires, or after the xhr completes + .always(function () { + model.off("change"); + }); + }, + + deleteItem: function(event) { + // Could use wait flag to block removal of model from collection + // until server confirms deletion and success handler to perform + // removal. Useful if delete fails for e.g. lack of entitlement. + this.activeModel.destroy(); + var displayName = $(event.currentTarget).data("name"); + this.render(displayName ? "Deleted " + displayName : ""); } }); @@ -81,6 +86,7 @@ define([ if (!this.name) { throw new Error("Catalog collection must know its name"); } + _.bindAll(this); }, url: function() { return "/v1/catalog/" + this.name; @@ -109,20 +115,32 @@ define([ // Generic templates this.template = _.template(this.options.template || EntryHtml); - // Returns template applied to function arguments. Alter if collection altered. Will be run - // in the context of the AccordionItemView. - this.templateFn = this.options.templateFn || function(model, index) { - return this.template({type: model.get("type"), id: model.get("id")}); + this.detailsTemplate = _.template(this.options.detailsTemplate || DetailsGenericHtml); + + // Returns template applied to function arguments. Alter if collection altered. + // Will be run in the context of the AccordionItemView. + this.templateArgs = this.options.templateArgs || function(model, index) { + return {type: model.get("type"), id: model.get("id")}; }; - // undefined argument is for existing models - this.collection = this.options.collection || new Catalog(undefined, {"name": this.name}); - this.collection.on("sync", this.renderEntries); + // undefined argument is used for existing model items + var collectionModel = this.options.model || Backbone.Model; + this.collection = this.options.collection || new Catalog(undefined, { + name: this.name, + model: collectionModel + }); + // Refreshes entries list when the collection is synced with the server or + // any of its members are destroyed. + this.collection + .on("sync", this.renderEntries) + .on("destroy", this.renderEntries); this.refresh(); }, + beforeClose: function() { this.collection.off(); }, + render: function() { this.$el.html(accordionBodyTemplate({ name: this.name, @@ -131,19 +149,43 @@ define([ this.renderEntries(); return this; }, + renderEntries: function() { - var elements = this.collection.map(this.templateFn, this); - this.$el.find(".accordion-body") + var templater = function(model, index) { + var args = _.extend({cid: model.cid}, this.templateArgs(model)); + return this.template(args); + }; + var elements = this.collection.map(templater, this); + this.$(".accordion-body") .empty() .append(elements.join('')); + // Rehighlight active model + if (this.activeCid && activeDetails === this.name) { + $(".accordion-nav-row").removeClass("active"); + this.setActiveItem(this.$("[data-cid='"+this.activeCid+"'")); + } }, + refresh: function() { this.collection.fetch(); }, + + setActiveItem: function($element) { + $(".accordion-nav-row").removeClass("active"); + $element.addClass("active"); + activeDetails = this.name; + }, + showDetails: function(event) { - // TODO: Incorporate model from view collection. - this.options.details.showDetailsFor(event, this.name); + var $event = $(event.currentTarget); + if (!$event.hasClass("active")) { + this.setActiveItem($event); + var cid = this.activeCid = $(event.currentTarget).data("cid"); + var model = this.collection.get(cid); + this.options.details.showDetailsFor(model, this.detailsTemplate); + } }, + toggle: function() { var body = this.$(".accordion-body"); var hidden = this.hidden = body.css("display") == "none"; @@ -162,7 +204,7 @@ define([ className:"container container-fluid", entryTemplate:_.template(EntryHtml), - events:{ + events: { 'click .refresh':'refresh', 'click #add-new-thing':'createNewThing', 'click #add-new-entity':'addNewCatalogResource', @@ -176,19 +218,34 @@ define([ $(".nav1_catalog").addClass("active"); this.detailsView = new DetailsView(); this.accordion = this.options.accordion || [ - new AccordionItemView({name: "applications", details: this.detailsView, autoOpen: true}), - new AccordionItemView({name: "entities", details: this.detailsView}), - new AccordionItemView({name: "policies", details: this.detailsView}), + new AccordionItemView({ + name: "applications", + details: this.detailsView, + detailsTemplate: DetailsEntityHtml, + model: Entity.Model, + autoOpen: true + }), + new AccordionItemView({ + name: "entities", + details: this.detailsView, + detailsTemplate: DetailsEntityHtml, + model: Entity.Model + }), + new AccordionItemView({ + name: "policies", + detailsTemplate: DetailsGenericHtml, + details: this.detailsView + }), new AccordionItemView({ name: "locations", details: this.detailsView, + detailsTemplate: LocationDetailsHtml, collection: this.options.locations, - templateFn: function(location, index) { - // this reference is AccordionItemView intentionally - return this.template({ + templateArgs: function(location, index) { + return { type: location.getPrettyName(), id: location.getLinkByName("self") - }); + }; } }) ]; @@ -207,7 +264,6 @@ define([ _.each(this.accordion, function(child) { parent.append(child.render().$el); }); -// this.accordion[0].toggle(); return this }, http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6dc52f55/usage/jsgui/src/main/webapp/assets/tpl/catalog/details-entity.html ---------------------------------------------------------------------- diff --git a/usage/jsgui/src/main/webapp/assets/tpl/catalog/details-entity.html b/usage/jsgui/src/main/webapp/assets/tpl/catalog/details-entity.html index ab02b6e..8aeaf49 100644 --- a/usage/jsgui/src/main/webapp/assets/tpl/catalog/details-entity.html +++ b/usage/jsgui/src/main/webapp/assets/tpl/catalog/details-entity.html @@ -20,9 +20,9 @@ under the License. <div class="catalog-details"> - <h2><%= name %></h2> - <p><%= id %></p> - <p><%= description %></p> + <h2><%= model.get("name") %></h2> + <p><%= model.get("id") %></p> + <p><%= model.get("description") %></p> <div id="catalog-details-accordion" class="accordion"> <div class="accordion-group"> @@ -33,8 +33,8 @@ under the License. </div> <div id="collapseYaml" class="accordion-body collapse"> <div class="accordion-inner"> - <% if (planYaml) { %> - <textarea rows="15"><%= planYaml %></textarea> + <% if (model.get("planYaml")) { %> + <textarea rows="15" readonly><%= model.get("planYaml") %></textarea> <% } else { %> <p>No plan</p> <% } %> @@ -49,7 +49,11 @@ under the License. </div> <div id="collapseConfiguration" class="accordion-body collapse"> <div class="accordion-inner"> - <% if (!config || _.isEmpty(config)) { %> + <% if (model.error) { %> + <p><i class="icon-exclamation-sign"></i> Could not load configuration</p> + <% } else if (!model.get("config")) { %> + <p>Loading...</p> + <% } else if (_.isEmpty(model.get("config"))) { %> <p>None available</p> <% } else { %> <% var skip = [ @@ -59,7 +63,7 @@ under the License. 'priority', 'reconfigurable' ]; %> - <% _.each(config, function(object, index) { %> + <% _.each(model.get("config"), function(object, index) { %> <p><strong><%= object.name %></strong>: <%= object.description %></p> <table class="table table-striped table-condensed nonDatatables"> <tbody> @@ -86,7 +90,9 @@ under the License. </div> <div id="collapseSensors" class="accordion-body collapse"> <div class="accordion-inner"> - <% if (!sensors || _.isEmpty(sensors)) { %> + <% if (model.error) { %> + <p><i class="icon-exclamation-sign"></i> Could not load sensors</p> + <% } else if (!model.get("sensors") || _.isEmpty(model.get("sensors"))) { %> <p>No sensors</p> <% } else { %> <table class="table table-striped table-condensed nonDatatables"> @@ -98,7 +104,7 @@ under the License. </tr> </thead> <tbody> - <% _.each(sensors, function(object, index) { %> + <% _.each(model.get("sensors"), function(object, index) { %> <tr> <td><%= object.name %></td> <td><%= object.type %></td> @@ -119,10 +125,12 @@ under the License. </div> <div id="collapseEffectors" class="accordion-body collapse"> <div class="accordion-inner"> - <% if (!effectors || _.isEmpty(effectors)) { %> + <% if (model.error) { %> + <p><i class="icon-exclamation-sign"></i> Could not load effectors</p> + <% } else if (!model.get("effectors") || _.isEmpty(model.get("effectors"))) { %> <p>No effectors</p> <% } else { %> - <% _.each(effectors, function(object, index) { %> + <% _.each(model.get("effectors"), function(object, index) { %> <p><strong><%= object.name %></strong>: <%= object.description %></p> <% if (!object.parameters || _.isEmpty(object.parameters)) { %> <p>No parameters</p> http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6dc52f55/usage/jsgui/src/main/webapp/assets/tpl/catalog/details-location.html ---------------------------------------------------------------------- diff --git a/usage/jsgui/src/main/webapp/assets/tpl/catalog/details-location.html b/usage/jsgui/src/main/webapp/assets/tpl/catalog/details-location.html index 3a5cf66..a9474fc 100644 --- a/usage/jsgui/src/main/webapp/assets/tpl/catalog/details-location.html +++ b/usage/jsgui/src/main/webapp/assets/tpl/catalog/details-location.html @@ -2,18 +2,19 @@ <h3><%= model.getPrettyName() %></h3> - <!-- TODO --> - <!--<div class="float-right"><button id="<%= id %>" class="btn btn-danger delete-location">Delete</button></div>--> + <div class="float-right"> + <button data-name="<%= model.getPrettyName() %>" class="btn btn-danger delete">Delete</button> + </div> -<% if (typeof config === 'undefined' || _.isEmpty(config)) { %> +<% if (!model.get("config") || _.isEmpty(model.get("config"))) { %> <em>No special configuration</em> <% } else { %> <br/> <table> - <tr><td><b>ID:</b> </td><td><%= id %></td></tr> - <tr><td><b>Name:</b> </td><td><%= name !== undefined ? name : "" %></td></tr> - <tr><td><b>Spec:</b> </td><td><%= spec !== undefined ? spec : "" %></td></tr> + <tr><td><strong>ID:</strong> </td><td><%= model.get("id") || "" %></td></tr> + <tr><td><strong>Name:</strong> </td><td><%= model.get("name") || "" %></td></tr> + <tr><td><strong>Spec:</strong> </td><td><%= model.get("spec") || "" %></td></tr> </table> <br/> @@ -23,7 +24,7 @@ <th>Value</th> </tr></thead> <tbody> - <% _.each(config, function(value, key){ %> + <% _.each(model.get("config"), function(value, key) { %> <tr> <td style="border-left:none; width:50%;"><%- key%></td> <td><%- value%></td> http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6dc52f55/usage/jsgui/src/main/webapp/assets/tpl/catalog/nav-entry.html ---------------------------------------------------------------------- diff --git a/usage/jsgui/src/main/webapp/assets/tpl/catalog/nav-entry.html b/usage/jsgui/src/main/webapp/assets/tpl/catalog/nav-entry.html index 16cb043..02f0242 100644 --- a/usage/jsgui/src/main/webapp/assets/tpl/catalog/nav-entry.html +++ b/usage/jsgui/src/main/webapp/assets/tpl/catalog/nav-entry.html @@ -1 +1 @@ -<div id="<%= id %>" class="accordion-nav-row"><%= type %></div> +<div data-cid="<%= cid %>" class="accordion-nav-row"><%= type %></div> http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6dc52f55/usage/jsgui/src/main/webapp/assets/tpl/catalog/page.html ---------------------------------------------------------------------- diff --git a/usage/jsgui/src/main/webapp/assets/tpl/catalog/page.html b/usage/jsgui/src/main/webapp/assets/tpl/catalog/page.html index 0fa8af4..0a375a9 100644 --- a/usage/jsgui/src/main/webapp/assets/tpl/catalog/page.html +++ b/usage/jsgui/src/main/webapp/assets/tpl/catalog/page.html @@ -6,8 +6,6 @@ <h3>Catalog</h3> <div class="apps-tree-toolbar"> - <i id="add-new-thing" class="icon-br-plus-sign handy"/> - <i class="icon-br-refresh refresh handy"/> </div> </div>