modelName in xos models, log window, deferred display of detail when collection not ready, beef up detailShower/listViewShower
diff --git a/planetstack/core/xoslib/dashboards/xosAdminSite.html b/planetstack/core/xoslib/dashboards/xosAdminSite.html
index 95dd688..1096a2a 100644
--- a/planetstack/core/xoslib/dashboards/xosAdminSite.html
+++ b/planetstack/core/xoslib/dashboards/xosAdminSite.html
@@ -20,8 +20,6 @@
</div>
<div id="contentPanel">
-content
-
<div id="detailBox">
<div id="detail"></div>
<div id="linkedObjs1"></div>
@@ -33,12 +31,13 @@
</div>
<div id="logPanel">
-log
-
-<div id="successBox">
-</div>
-<div id="errorBox">
-</div>
+<table id="logTable">
+<thead>
+<tr><th>status</th><th>operation</th><th>code</th><th>message</th></tr>
+</thead>
+<tbody>
+</tbody>
+</table>
</div>
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;
},
diff --git a/planetstack/core/xoslib/templates/xosAdmin.html b/planetstack/core/xoslib/templates/xosAdmin.html
index 3093b73..8e4588c 100644
--- a/planetstack/core/xoslib/templates/xosAdmin.html
+++ b/planetstack/core/xoslib/templates/xosAdmin.html
@@ -22,13 +22,22 @@
<button class="btn btn-default btn-xosnav" onclick="<%= router %>.navigate('<%= routeUrl %>', {trigger: true})"><%= name %></button><br>
</script>
+<script type="text/template" id="xos-log-template">
+ <tr id="<%= logMessageId %>">
+ <td><%= success %></td>
+ <td><%= what %></td>
+ <td><%= status %></td>
+ <td><%= statusText %></td>
+ </tr>
+</script>
+
<script type="text/template" id="xos-navbutton">
<li>
<a href="<%= routeUrl %>">
<i class="<%= iconClass %>"></i>
<%= name %>
</a>
- </lid>
+ </li>
</script>
<!-- Deployment -->