modelName in xos models, log window, deferred display of detail when collection not ready, beef up detailShower/listViewShower
diff --git a/planetstack/core/xoslib/static/css/xosAdminSite.css b/planetstack/core/xoslib/static/css/xosAdminSite.css
index e73a51f..5148def 100644
--- a/planetstack/core/xoslib/static/css/xosAdminSite.css
+++ b/planetstack/core/xoslib/static/css/xosAdminSite.css
@@ -6,12 +6,15 @@
     color:blue;
     text-decoration:underline;
 }
+#logTable td, th {
+    border: 1px solid black;
+}
 
 #navigationPanel {
   position: absolute;
   top: 100px;
   left: 0;
-  width: 130px;
+  width: 220px;
   bottom: 0;
   overflow: hidden;
   background-color: #F0F0F0;
@@ -31,7 +34,7 @@
 #logPanel {
   position: absolute;
   top: auto;
-  left: 130px;
+  left: 220px;
   right: 0;
   bottom: 0;
   width: auto;
@@ -44,7 +47,7 @@
     position: fixed;
     top: 100px;
     bottom: 100px;
-    left: 130px;
+    left: 220px;
     right: 0;
     overflow: auto;
     background: #f0F0E0;
diff --git a/planetstack/core/xoslib/static/js/xosAdminSite.js b/planetstack/core/xoslib/static/js/xosAdminSite.js
index 3d87ca6..660556e 100644
--- a/planetstack/core/xoslib/static/js/xosAdminSite.js
+++ b/planetstack/core/xoslib/static/js/xosAdminSite.js
@@ -1,13 +1,7 @@
 OBJS = ['deployment', 'image', 'networkTemplate', 'network', 'networkSliver', 'networkDeployment', 'node', 'service', 'site', 'slice', 'sliceDeployment', 'slicePrivilege', 'sliver', 'user', 'sliceRole', 'userDeployment'];
 NAV_OBJS = ['deployment', 'site', 'slice', 'user'];
 
-function assert(outcome, description) {
-    if (!outcome) {
-        console.log(description);
-    }
-}
-
-XOSAdminApp = new XOSApplication();
+XOSAdminApp = new XOSApplication({logTableId: "#logTable"});
 
 XOSAdminApp.addRegions({
     navigation: "#navigationPanel",
@@ -32,7 +26,6 @@
     for (var index in NAV_OBJS) {

         name = NAV_OBJS[index];

         collection_name = name+"s";

-        //nav_url = "/" + collection_name;

         nav_url = "#" + collection_name;

         id = "nav-"+name;

         icon_class = ICON_CLASSES[collection_name] || "icon-cog";

@@ -89,27 +82,6 @@
     var api = {};

     var routes = {};

 

-    function listViewShower(listViewName) {

-        return function() {

-            XOSAdminApp.detail.show(new XOSAdminApp[listViewName]);

-        }

-    };

-

-    function detailShower(detailName, collection_name) {

-        shower = function(model_id) {

-            model = xos[collection_name].get(model_id);

-            if (model == undefined) {

-                $("#detail").html("not ready yet");

-                return;

-            }

-            detailViewClass = XOSAdminApp[detailName];

-            detailView = new detailViewClass({model: model});

-            XOSAdminApp.detail.show(detailView);

-            detailView.showLinkedItems();

-        }

-        return shower;

-    };

-

     for (var index in OBJS) {

         name = OBJS[index];

         collection_name = name + "s";

@@ -118,13 +90,13 @@
         listViewName = collection_name + "ListView";

         detailViewName = collection_name + "DetailView";

 

-        api[api_command] = listViewShower(listViewName);

+        api[api_command] = XOSAdminApp.listViewShower(listViewName, "detail");

         routes[nav_url] = api_command;

 

         nav_url = collection_name + "/:id";

         api_command = "detail" + collection_name.charAt(0).toUpperCase() + collection_name.slice(1);

 

-        api[api_command] = detailShower(detailViewName, collection_name);

+        api[api_command] = XOSAdminApp.detailShower(detailViewName, collection_name, "detail");

         routes[nav_url] = api_command;

     };

 

diff --git a/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js b/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js
index 6216396..b2395d7 100644
--- a/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js
+++ b/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js
@@ -65,13 +65,19 @@
                  },
 
         initialize: function(){
+          this.isLoaded = false;
           this.sortVar = 'name';

           this.sortOrder = 'asc';

+          this.on( "sort", this.sorted );

         },

 

         relatedCollections: [],

         foreignCollections: [],

 

+        sorted: function() {

+            this.isLoaded = true;

+        },

+

         simpleComparator: function( model ){

           parts=this.sortVar.split(".");

           result = model.get(parts[0]);

@@ -182,102 +188,103 @@
 
     function xoslib() {
         // basic REST
-        this.sliver = XOSModel.extend({ urlRoot: SLIVER_API });
+        this.sliver = XOSModel.extend({ urlRoot: SLIVER_API, modelName: "sliver" });
         this.sliverCollection = XOSCollection.extend({ urlRoot: SLIVER_API,
                                                        relatedCollections: {"networkSlivers": "sliver"},
                                                        foreignCollections: ["slices", "deployments", "images", "nodes", "users"],
                                                        model: this.sliver});
         this.slivers = new this.sliverCollection();
 
-        this.slice = XOSModel.extend({ urlRoot: SLICE_API });
+        this.slice = XOSModel.extend({ urlRoot: SLICE_API, modelName: "slice" });
         this.sliceCollection = XOSCollection.extend({ urlRoot: SLICE_API,
                                                        relatedCollections: {"slivers": "slice", "sliceDeployments": "slice", "slicePrivileges": "slice"},
                                                        foreignCollections: ["services", "sites"],
                                                        model: this.slice});
         this.slices = new this.sliceCollection();
 
-        this.sliceDeployment = XOSModel.extend({ urlRoot: SLICEDEPLOYMENT_API });
+        this.sliceDeployment = XOSModel.extend({ urlRoot: SLICEDEPLOYMENT_API, modelName: "sliceDeployment" });
         this.sliceDeploymentCollection = XOSCollection.extend({ urlRoot: SLICEDEPLOYMENT_API,
                                                        foreignCollections: ["slices", "deployments"],
                                                        model: this.slice});
         this.sliceDeployments = new this.sliceDeploymentCollection();
 
-        this.slicePrivilege = XOSModel.extend({ urlRoot: SLICEPRIVILEGE_API });
+        this.slicePrivilege = XOSModel.extend({ urlRoot: SLICEPRIVILEGE_API, modelName: "slicePrivilege" });
         this.slicePrivilegeCollection = XOSCollection.extend({ urlRoot: SLICEPRIVILEGE_API,
                                                        foreignCollections: ["slices", "users", "sliceRoles"],
                                                        model: this.slice});
         this.slicePrivileges = new this.slicePrivilegeCollection();
 
-        this.sliceRole = XOSModel.extend({ urlRoot: SLICEROLE_API });
+        this.sliceRole = XOSModel.extend({ urlRoot: SLICEROLE_API, modelName: "sliceRole" });
         this.sliceRoleCollection = XOSCollection.extend({ urlRoot: SLICEROLE_API,
                                                        model: this.slice});
         this.sliceRoles = new this.sliceRoleCollection();
 
-        this.node = XOSModel.extend({ urlRoot: NODE_API });
+        this.node = XOSModel.extend({ urlRoot: NODE_API, modelName: "node" });
         this.nodeCollection = XOSCollection.extend({ urlRoot: NODE_API,
                                                        foreignCollections: ["sites", "deployments"],
                                                        model: this.node});
         this.nodes = new this.nodeCollection();
 
-        this.site = XOSModel.extend({ urlRoot: SITE_API });
+        this.site = XOSModel.extend({ urlRoot: SITE_API, modelName: "site" });
         this.siteCollection = XOSCollection.extend({ urlRoot: SITE_API,
                                                        model: this.site});
         this.sites = new this.siteCollection();
 
-        this.user = XOSModel.extend({ urlRoot: USER_API });
+        this.user = XOSModel.extend({ urlRoot: USER_API, modelName: "user" });
         this.userCollection = XOSCollection.extend({ urlRoot: USER_API,
                                                        relatedCollections: {"slicePrivileges": "user", "slices": "owner", "userDeployments": "user"},
                                                        foreignCollections: ["sites"],
                                                        model: this.user});
         this.users = new this.userCollection();
 
-        this.userDeployment = XOSModel.extend({ urlRoot: USER_API });
+        this.userDeployment = XOSModel.extend({ urlRoot: USERDEPLOYMENT_API, modelName: "userDeployment" });
         this.userDeploymentCollection = XOSCollection.extend({ urlRoot: USERDEPLOYMENT_API,
                                                        foreignCollections: ["users","deployments"],
                                                        model: this.user});
         this.userDeployments = new this.userDeploymentCollection();
 
-        this.deployment = XOSModel.extend({ urlRoot: DEPLOYMENT_API });
+        this.deployment = XOSModel.extend({ urlRoot: DEPLOYMENT_API, modelName: "deployment" });
         this.deploymentCollection = XOSCollection.extend({ urlRoot: DEPLOYMENT_API,
                                                            relatedCollections: {"slivers": "deployment", "networkDeployments": "deployment", "userDeployments": "deployment"},
                                                            model: this.deployment});
         this.deployments = new this.deploymentCollection();
 
-        this.image = XOSModel.extend({ urlRoot: IMAGE_API });
+        this.image = XOSModel.extend({ urlRoot: IMAGE_API, modelName: "image" });
         this.imageCollection = XOSCollection.extend({ urlRoot: IMAGE_API,
                                                            model: this.image});
         this.images = new this.imageCollection();
 
-        this.networkTemplate = XOSModel.extend({ urlRoot: NETWORKTEMPLATE_API });
+        this.networkTemplate = XOSModel.extend({ urlRoot: NETWORKTEMPLATE_API, modelName: "networkTemplate" });
         this.networkTemplateCollection = XOSCollection.extend({ urlRoot: NETWORKTEMPLATE_API,
                                                            model: this.networkTemplate});
         this.networkTemplates = new this.networkTemplateCollection();
 
-        this.network = XOSModel.extend({ urlRoot: NETWORK_API });
+        this.network = XOSModel.extend({ urlRoot: NETWORK_API, modelName: "network" });
         this.networkCollection = XOSCollection.extend({ urlRoot: NETWORK_API,
                                                            relatedCollections: {"networkDeployments": "network", "networkSlivers": "network"},
                                                            foreignCollections: ["slices", "networkTemplates"],
                                                            model: this.network});
         this.networks = new this.networkCollection();
 
-        this.networkSliver = XOSModel.extend({ urlRoot: NETWORKSLIVER_API });
+        this.networkSliver = XOSModel.extend({ urlRoot: NETWORKSLIVER_API, modelName: "networkSliver" });
         this.networkSliverCollection = XOSCollection.extend({ urlRoot: NETWORKSLIVER_API,
                                                            model: this.networkSliver});
         this.networkSlivers = new this.networkSliverCollection();
 
-        this.networkDeployment = XOSModel.extend({ urlRoot: NETWORKDEPLOYMENT_API });
+        this.networkDeployment = XOSModel.extend({ urlRoot: NETWORKDEPLOYMENT_API, modelName: "networkDeployment" });
         this.networkDeploymentCollection = XOSCollection.extend({ urlRoot: NETWORKDEPLOYMENT_API,
                                                            model: this.networkDeployment});
         this.networkDeployments = new this.networkDeploymentCollection();
 
-        this.service = XOSModel.extend({ urlRoot: SERVICE_API });
+        this.service = XOSModel.extend({ urlRoot: SERVICE_API, modelName: "sliver" });
         this.serviceCollection = XOSCollection.extend({ urlRoot: SERVICE_API,
                                                        model: this.service});
         this.services = new this.serviceCollection();
 
         // enhanced REST
-        this.slicePlus = XOSModel.extend({ urlRoot: SLICEPLUS_API, relatedCollections: {'slivers': "slice"} });
+        this.slicePlus = XOSModel.extend({ urlRoot: SLICEPLUS_API, modelName: "slicePlus" });
         this.slicePlusCollection = XOSCollection.extend({ urlRoot: SLICEPLUS_API,
+                                                          relatedCollections: {'slivers': "slice"},
                                                           model: this.slicePlus});
         this.slicesPlus = new this.slicePlusCollection();
 
diff --git a/planetstack/core/xoslib/static/js/xoslib/xosHelper.js b/planetstack/core/xoslib/static/js/xoslib/xosHelper.js
index 0a1be26..0ba1c3d 100644
--- a/planetstack/core/xoslib/static/js/xoslib/xosHelper.js
+++ b/planetstack/core/xoslib/static/js/xoslib/xosHelper.js
@@ -1,3 +1,15 @@
+function assert(outcome, description) {
+    if (!outcome) {
+        console.log(description);
+    }
+}
+
+HTMLView = Marionette.ItemView.extend({
+  render: function() {
+      this.$el.append(this.options.html);
+  },
+});
+
 XOSApplication = Marionette.Application.extend({
     detailBoxId: "#detailBox",
     errorBoxId: "#errorBox",
@@ -6,27 +18,131 @@
     successCloseButtonId: "#close-success-box",
     errorTemplate: "#xos-error-template",
     successTemplate: "#xos-success-template",
+    logMessageCount: 0,
 
-    hideError: function(result) {
-        $(this.errorBoxId).hide();
-        $(this.successBoxId).hide();
+    hideError: function() {
+        if (this.logWindowId) {
+        } else {
+            $(this.errorBoxId).hide();
+            $(this.successBoxId).hide();
+        }
     },
 
     showSuccess: function(result) {
-         $(this.successBoxId).show();
-         $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
-         $(this.successCloseButtonId).unbind().bind('click', function() {
-             $(this.successBoxId).hide();
-         });
+         if (this.logTableId) {
+             result["success"] = "success";
+             this.appendLogWindow(result);
+         } else {
+             $(this.successBoxId).show();
+             $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
+             $(this.successCloseButtonId).unbind().bind('click', function() {
+                 $(this.successBoxId).hide();
+             });
+         }
     },
 
     showError: function(result) {
-         $(this.errorBoxId).show();
-         $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
-         $(this.errorCloseButtonId).unbind().bind('click', function() {
-             $(this.errorBoxId).hide();
-         });
+         if (this.logTableId) {
+             result["success"] = "failure";
+             this.appendLogWindow(result);
+         } else {
+             $(this.errorBoxId).show();
+             $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
+             $(this.errorCloseButtonId).unbind().bind('click', function() {
+                 $(this.errorBoxId).hide();
+             });
+         }
     },
+
+    showInformational: function(result) {
+         if (this.logTableId) {
+             result["success"] = "information";
+             return this.appendLogWindow(result);
+         } else {
+             return undefined;
+         }
+    },
+
+    appendLogWindow: function(result) {
+        // compute a new logMessageId for this log message
+        logMessageId = "logMessage" + this.logMessageCount;
+        this.logMessageCount = this.logMessageCount + 1;
+        result["logMessageId"] = logMessageId;
+
+        logMessageTemplate=$("#xos-log-template").html();
+        assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
+        newRow = _.template(logMessageTemplate, result);
+        assert(newRow != undefined, "newRow is undefined");
+
+        if (result["infoMsgId"] != undefined) {
+            // We were passed the logMessageId of an informational message,
+            // and the caller wants us to replace that message with our own.
+            // i.e. replace an informational message with a success or an error.
+            console.log(result["infoMsgId"]);
+            console.log($("."+result["infoMsgId"]));
+            $("#"+result["infoMsgId"]).replaceWith(newRow);
+        } else {
+            // Create a brand new log message rather than replacing one.
+            logTableBody = $(this.logTableId + " tbody");
+            logTableBody.prepend(newRow);
+        }
+        return logMessageId;
+    },
+
+    hideLinkedItems: function(result) {
+        index=0;
+        while (index<4) {

+            this["linkedObjs" + (index+1)].empty();

+            index = index + 1;

+        }

+    },

+

+    listViewShower: function(listViewName, regionName) {

+        var app=this;

+        return function() {

+            app[regionName].show(new app[listViewName]);

+            app.hideLinkedItems();

+        }

+    },

+

+    detailShower: function(detailName, collection_name, regionName) {

+        var app=this;

+        showModelId = function(model_id) {

+            showModel = function(model) {

+                                console.log(app);

+                detailViewClass = app[detailName];

+                detailView = new detailViewClass({model: model});

+                app[regionName].show(detailView);

+                detailView.showLinkedItems();

+            }

+

+            collection = xos[collection_name];

+            model = collection.get(model_id);

+            if (model == undefined) {

+                if (!collection.isLoaded) {

+                    // If the model cannot be found, then maybe it's because

+                    // we haven't finished loading the collection yet. So wait for

+                    // the sort event to complete, then try again.

+                    collection.once("sort", function() {

+                        collection = xos[collection_name];

+                        model = collection.get(model_id);

+                        if (model == undefined) {

+                            // We tried. It's not here. Complain to the user.

+                            app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));

+                        } else {

+                            showModel(model);

+                        }

+                    });

+                } else {

+                    // The collection was loaded, the user must just be asking for something we don't have.

+                    app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));

+                }

+            } else {

+                showModel(model);

+            }

+        }

+        return showModelId;

+    },

 });
 
 /* XOSDetailView
@@ -53,21 +169,27 @@
                 this.dirty = true;

             },

 

-            saveError: function(model, result, xhr) {

+            saveError: function(model, result, xhr, infoMsgId) {

+                result["what"] = "save " + model.__proto__.modelName;

+                result["infoMsgId"] = infoMsgId;

                 this.app.showError(result);

             },

 

-            saveSuccess: function(model, result, xhr) {

-                this.app.showSuccess({status: xhr.xhr.status, statusText: xhr.xhr.statusText});

+            saveSuccess: function(model, result, xhr, infoMsgId) {

+                result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};

+                result["what"] = "save " + model.__proto__.modelName;

+                result["infoMsgId"] = infoMsgId;

+                this.app.showSuccess(result);

             },
 
             submitClicked: function(e) {
                 this.app.hideError();

                 e.preventDefault();

+                var infoMsgId = this.app.showInformational( {what: "save " + this.model.__proto__.modelName, status: "", statusText: "in progress..."} );

                 var data = Backbone.Syphon.serialize(this);

-                var thisView = this;

-                this.model.save(data, {error: function(model, result, xhr) { thisView.saveError(model, result, xhr); },

-                                       success: function(model, result, xhr) { thisView.saveSuccess(model, result, xhr); }});

+                var that = this;

+                this.model.save(data, {error: function(model, result, xhr) { that.saveError(model,result,xhr,infoMsgId);},

+                                       success: function(model, result, xhr) { that.saveSuccess(model,result,xhr,infoMsgId);}});

                 this.dirty = false;

             },