From ceb262824337cffd6cf83f035849e32c8aa414e5 Mon Sep 17 00:00:00 2001 From: Scott Baker Date: Sun, 11 Jan 2015 13:44:30 -0800 Subject: [PATCH] tenant view, WIP --- .../core/xoslib/dashboards/xosTenant.html | 72 ++++++ planetstack/core/xoslib/static/js/picker.js | 4 + .../core/xoslib/static/js/xosTenant.js | 218 ++++++++++++++++++ .../xoslib/static/js/xoslib/xos-backbone.js | 73 +++++- .../core/xoslib/static/js/xoslib/xosHelper.js | 86 ++++++- .../core/xoslib/templates/xosAdmin.html | 32 ++- 6 files changed, 466 insertions(+), 19 deletions(-) create mode 100644 planetstack/core/xoslib/dashboards/xosTenant.html create mode 100644 planetstack/core/xoslib/static/js/xosTenant.js diff --git a/planetstack/core/xoslib/dashboards/xosTenant.html b/planetstack/core/xoslib/dashboards/xosTenant.html new file mode 100644 index 0000000..49236d4 --- /dev/null +++ b/planetstack/core/xoslib/dashboards/xosTenant.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ Are you sure about this? +
+ +
+
+ +
+
+
+
+ +
+ +
+ + + +
+
+
+ +
+ +
+
+
+
+
+
+
+
+ +
+
+ +{% include 'xosAdmin.html' %} diff --git a/planetstack/core/xoslib/static/js/picker.js b/planetstack/core/xoslib/static/js/picker.js index 0302cf4..075bdc5 100644 --- a/planetstack/core/xoslib/static/js/picker.js +++ b/planetstack/core/xoslib/static/js/picker.js @@ -48,3 +48,7 @@ function init_picker(selector, ordered) { }); }); }; + +function init_spinner(selector, value) { + var spinner = $(selector).spinner( "value", value); +}; diff --git a/planetstack/core/xoslib/static/js/xosTenant.js b/planetstack/core/xoslib/static/js/xosTenant.js new file mode 100644 index 0000000..5095505 --- /dev/null +++ b/planetstack/core/xoslib/static/js/xosTenant.js @@ -0,0 +1,218 @@ +XOSTenantSite = XOSModel.extend( { + listFields: ["name", "allocated"], + modelName: "tenantSite", + collectionName: "tenantSites" +}); + +XOSTenantSiteCollection = XOSCollection.extend( { + listFields: ["name", "allocated"], + modelName: "tenantSite", + collectionName: "tenantSites", + + updateFromSlice: function(slice) { + var tenantSites = []; + var id = 0; + for (siteName in slice.attributes.site_allocation) { + allocated = slice.attributes.site_allocation[siteName]; + tenantSites.push(new XOSTenantSite( { name: siteName, allocated: allocated, id: id} )); + id = id + 1; + } + for (index in xos.tenantview.models[0].attributes.blessed_site_names) { + siteName = xos.tenantview.models[0].attributes.blessed_site_names[index]; + if (! (siteName in slice.attributes.site_allocation)) { + tenantSites.push(new XOSTenantSite( { name: siteName, allocated: 0, id: id} )); + id = id + 1; + } + } + this.set(tenantSites); + }, +}); + +XOSTenantButtonView = Marionette.ItemView.extend({ + template: "#xos-tenant-buttons-template", + + events: {"click button.btn-tenant-create": "createClicked", + "click button.btn-tenant-delete": "deleteClicked", + "click button.btn-tenant-add-user": "addUserClicked", + "click button.btn-tenant-save": "saveClicked", + }, + + createClicked: function(e) { + }, + + deleteClicked: function(e) { + this.options.linkedView.deleteClicked.call(this.options.linkedView, e); + }, + + addUserClicked: function(e) { + }, + + saveClicked: function(e) { + this.options.linkedView.submitContinueClicked.call(this.options.linkedView, e); + }, + }); + +XOSTenantApp = new XOSApplication({ + logTableId: "#logTable", + statusMsgId: "#statusMsg", + hideTabsByDefault: true +}); + +XOSTenantApp.addRegions({ + tenantSliceSelector: "#tenantSliceSelector", + tenantSummary: "#tenantSummary", + tenantSiteList: "#tenantSiteList", + tenantButtons: "#tenantButtons", +}); + +XOSTenantApp.buildViews = function() { + XOSTenantApp.tenantSites = new XOSTenantSiteCollection(); + + tenantSummaryClass = XOSDetailView.extend({template: "#xos-detail-template", + app: XOSTenantApp, + detailFields: ["serviceClass", "image_preference", "network_ports", "mount_data_sets"]}); + + XOSTenantApp.tenantSummaryView = tenantSummaryClass; + + tenantSiteItemClass = XOSItemView.extend({template: "#xos-listitem-template", + app: XOSTenantApp}); + + XOSTenantApp.tenantSiteItemView = tenantSiteItemClass; + + tenantSiteListClass = XOSDataTableView.extend({template: "#xos-list-template", + app: XOSTenantApp, + childView: tenantSiteItemClass, + collection: XOSTenantApp.tenantSites, + title: "sites", + inputType: {allocated: "spinner"}, + noDeleteColumn: true, + }); + + XOSTenantApp.tenantSiteListView = tenantSiteListClass; + + XOSTenantApp.tenantSliceSelectorView = SliceSelectorView.extend( { + sliceChanged: function(id) { + //console.log("navigate to " + id); + XOSTenantApp.Router.navigate("slice/" + id, {trigger: true}); + }, + }); + + xos.sites.fetch(); + xos.slicesPlus.fetch(); + xos.tenantview.fetch(); +}; + +make_choices = function(list_of_names, list_of_values) { + result = []; + if (!list_of_values) { + for (index in list_of_names) { + displayName = list_of_names[index]; + result.push( [displayName, displayName] ); + } + } + return result; +}; + +XOSTenantApp.viewSlice = function(model) { + if (!model && xos.slicesPlus.models.length > 0) { + model = xos.slicesPlus.models[0]; + } + + sliceSelector = new XOSTenantApp.tenantSliceSelectorView({collection: xos.slicesPlus, + selectedID: model.id, + } ); + XOSTenantApp.sliceSelector = sliceSelector; + XOSTenantApp.tenantSliceSelector.show(sliceSelector); + + tenantSummary = new XOSTenantApp.tenantSummaryView({model: model, + choices: {mount_data_sets: make_choices(xos.tenantview.models[0].attributes.public_volume_names, null), + image_preference: make_choices(xos.tenantview.models[0].attributes.blessed_image_names, null)}, + }); + XOSTenantApp.tenantSummary.show(tenantSummary); + + tenantSites = new XOSTenantSiteCollection(); + tenantSites.updateFromSlice(model); + XOSTenantApp.tenantSites = tenantSites; + + tenantSiteList = new XOSTenantApp.tenantSiteListView({collection: tenantSites }); + XOSTenantApp.tenantSiteList.show(tenantSiteList); + // on xos.slicePlus.sort, need to update xostenantapp.tenantSites + + XOSTenantApp.tenantButtons.show( new XOSTenantButtonView( { app: XOSTenantApp, + linkedView: tenantSummary } ) ); +}; + +XOSTenantApp.initRouter = function() { + router = XOSRouter; + var api = {}; + var routes = {}; + + nav_url = "slice/:id"; + api_command = "viewSlice"; + api[api_command] = function(id) { XOSTenantApp.viewSlice(xos.slicesPlus.get(id)); }; + routes[nav_url] = api_command; + + nav_url = "increase/:collectionName/:id/:fieldName"; + api_command = "increase"; + api[api_command] = function(collectionName, id, fieldName) { + XOSTenantApp.Router.showPreviousURL(); + model = XOSTenantApp[collectionName].get(id); + model.set(fieldName, model.get(fieldName) + 1); + }; + routes[nav_url] = api_command; + + nav_url = "decrease/:collectionName/:id/:fieldName"; + api_command = "decrease"; + api[api_command] = function(collectionName, id, fieldName) { + XOSTenantApp.Router.showPreviousURL(); + model = XOSTenantApp[collectionName].get(id); + model.set(fieldName, Math.max(0, model.get(fieldName) - 1)); + }; + routes[nav_url] = api_command; + + nav_url = "*path"; + api_command = "defaultRoute"; + api[api_command] = function() { XOSTenantApp.viewSlice(undefined); }; + routes[nav_url] = api_command; + + XOSTenantApp.Router = new router({ appRoutes: routes, controller: api }); +}; + +XOSTenantApp.startNavigation = function() { + Backbone.history.start(); + XOSTenantApp.navigationStarted = true; +} + +XOSTenantApp.collectionLoadChange = function() { + stats = xos.getCollectionStatus(); + + if (!XOSTenantApp.navigationStarted) { + if (stats["isLoaded"] + stats["failedLoad"] >= stats["startedLoad"]) { + XOSTenantApp.startNavigation(); + + //if (xos.slicesPlus.models.length > 0) { + // XOSTenantApp.Router.navigate("slice/" + xos.slicesPlus.models[0].id, {trigger:true}); + //} + } else { + $("#tenantSummary").html("

Loading...

"); + $("#xos-startup-progress").progressbar({value: stats["completedLoad"], max: stats["startedLoad"]}); + } + } +}; + +XOSTenantApp.on("start", function() { + XOSTenantApp.buildViews(); + + XOSTenantApp.initRouter(); + + // fire it once to initially show the progress bar + XOSTenantApp.collectionLoadChange(); + + // fire it each time the collection load status is updated + Backbone.on("xoslib:collectionLoadChange", XOSTenantApp.collectionLoadChange); +}); + +$(document).ready(function(){ + XOSTenantApp.start(); +}); + diff --git a/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js b/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js index 47ea66a..762a2b5 100644 --- a/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js +++ b/planetstack/core/xoslib/static/js/xoslib/xos-backbone.js @@ -34,8 +34,16 @@ if (! window.XOSLIB_LOADED ) { USERDEPLOYMENT_API = "/plstackapi/userdeployments/"; SLICEPLUS_API = "/xoslib/slicesplus/"; + TENANTVIEW_API = "/xoslib/tenantview/" XOSModel = Backbone.Model.extend({ + relatedCollections: [], + foreignCollections: [], + foreignFields: {}, + m2mFields: {}, + readonlyFields: [], + detailLinkFields: [], + /* from backbone-tastypie.js */ //idAttribute: 'resource_uri', @@ -152,6 +160,10 @@ if (! window.XOSLIB_LOADED ) { relatedCollections: [], foreignCollections: [], + foreignFields: {}, + m2mFields: {}, + readonlyFields: [], + detailLinkFields: [], sorted: function() { //console.log("sorted " + this.modelName); @@ -333,6 +345,22 @@ if (! window.XOSLIB_LOADED ) { }, }); + function get_defaults(modelName) { + if ((typeof xosdefaults !== "undefined") && xosdefaults[modelName]) { + return xosdefaults[modelName]; + } + return undefined; + } + + function extend_defaults(modelName, stuff) { + defaults = get_defaults(modelName); + if (defaults) { + return $.extend({}, defaults, stuff); + } else { + return stuff; + } + } + function define_model(lib, attrs) { modelName = attrs.modelName; modelClassName = modelName; @@ -358,7 +386,7 @@ if (! window.XOSLIB_LOADED ) { for (key in attrs) { value = attrs[key]; - if ($.inArray(key, ["urlRoot", "modelName", "collectionName", "listFields", "addFields", "detailFields", "detailLinkFields", "foreignFields", "inputType", "relatedCollections", "foreignCollections"])>=0) { + if ($.inArray(key, ["urlRoot", "modelName", "collectionName", "listFields", "addFields", "detailFields", "detailLinkFields", "foreignFields", "inputType", "relatedCollections", "foreignCollections", "defaults"])>=0) { modelAttrs[key] = value; collectionAttrs[key] = value; } @@ -367,9 +395,15 @@ if (! window.XOSLIB_LOADED ) { } } - if ((typeof xosdefaults !== "undefined") && xosdefaults[modelName]) { - modelAttrs["defaults"] = xosdefaults[modelName]; + if (!modelAttrs.defaults) { + modelAttrs.defaults = get_defaults(modelName); } + console.log(modelName); + console.log(modelAttrs); + +// if ((typeof xosdefaults !== "undefined") && xosdefaults[modelName]) { +// modelAttrs["defaults"] = xosdefaults[modelName]; +// } if ((typeof xosvalidators !== "undefined") && xosvalidators[modelName]) { modelAttrs["validators"] = xosvalidators[modelName]; @@ -629,9 +663,36 @@ if (! window.XOSLIB_LOADED ) { // enhanced REST // XXX this really needs to somehow be combined with Slice, to avoid duplication define_model(this, {urlRoot: SLICEPLUS_API, - relatedCollections: {'slivers': "slice"}, - modelName: "slicePlus", - collectionName: "slicesPlus"}); + relatedCollections: {"slivers": "slice", "slicePrivileges": "slice", "networks": "owner"}, + foreignCollections: ["services", "sites"], + foreignFields: {"service": "services", "site": "sites"}, + listFields: ["backend_status", "id", "name", "enabled", "description", "slice_url", "site", "max_slivers", "service"], + detailFields: ["backend_status", "name", "site", "enabled", "description", "slice_url", "max_slivers"], + inputType: {"enabled": "checkbox"}, + modelName: "slicePlus", + collectionName: "slicesPlus", + defaults: extend_defaults("slice", {"network_ports": "", "site_allocation": []}), + xosValidate: function(attrs, options) { + errors = XOSModel.prototype.xosValidate(this, attrs, options); + // validate that slice.name starts with site.login_base + site = attrs.site || this.site; + if ((site!=undefined) && (attrs.name!=undefined)) { + site = xos.sites.get(site); + if (attrs.name.indexOf(site.attributes.login_base+"_") != 0) { + errors = errors || {}; + errors["name"] = "must start with " + site.attributes.login_base + "_"; + } + } + return errors; + }, + }); + + define_model(this, {urlRoot: TENANTVIEW_API, + modelName: "tenantview", + collectionName: "tenantview", + listFields: [], + detailFields: [], + }); this.listObjects = function() { return this.allCollectionNames; }; diff --git a/planetstack/core/xoslib/static/js/xoslib/xosHelper.js b/planetstack/core/xoslib/static/js/xoslib/xosHelper.js index 6c0c2f3..0cadf79 100644 --- a/planetstack/core/xoslib/static/js/xoslib/xosHelper.js +++ b/planetstack/core/xoslib/static/js/xoslib/xosHelper.js @@ -4,6 +4,41 @@ HTMLView = Marionette.ItemView.extend({ }, }); +SliceSelectorOption = Marionette.ItemView.extend({ + template: "#xos-sliceselector-option", + tagName: "option", + attributes: function() { + console.log("XXX"); + console.log(this.options.selectedID); + console.log(this.model.get("id")); + if (this.options.selectedID == this.model.get("id")) { + return { value: this.model.get("id"), selected: 1 }; + } else { + return { value: this.model.get("id") }; + } + }, +}); + +SliceSelectorView = Marionette.CompositeView.extend({ + template: "#xos-sliceselector-select", + childViewContainer: "select", + childView: SliceSelectorOption, + + events: {"change select": "onSliceChanged"}, + + childViewOptions: function() { + return { selectedID: this.options.selectedID || this.selectedID || null }; + }, + + onSliceChanged: function() { + this.sliceChanged(this.$el.find("select").val()); + }, + + sliceChanged: function(id) { + console.log("sliceChanged " + id); + }, +}); + FilteredCompositeView = Marionette.CompositeView.extend( { showCollection: function() { var ChildView; @@ -25,6 +60,7 @@ XOSRouter = Marionette.AppRouter.extend({ onRoute: function(x,y,z) { this.routeStack.push(Backbone.history.fragment); + this.routeStack = this.routeStack.slice(-32); // limit the size of routeStack to something reasonable }, prevPage: function() { @@ -33,8 +69,8 @@ XOSRouter = Marionette.AppRouter.extend({ showPreviousURL: function() { prevPage = this.prevPage(); - console.log("showPreviousURL"); - console.log(this.routeStack); + //console.log("showPreviousURL"); + //console.log(this.routeStack); if (prevPage) { this.navigate("#"+prevPage, {trigger: false, replace: true} ); } @@ -103,6 +139,8 @@ XOSApplication = Marionette.Application.extend({ parsed_error=undefined; width=640; // django stacktraces like wide width } + console.log(responseText); + console.log(parsed_error); if (parsed_error) { $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error)); } else { @@ -392,7 +430,7 @@ XOSButtonView = Marionette.ItemView.extend({ }, submitDeleteClicked: function(e) { - this.options.linkedView.submitDeleteClicked.call(this.options.linkedView, e); + this.options.linkedView.deleteClicked.call(this.options.linkedView, e); }, addClicked: function(e) { @@ -501,13 +539,13 @@ XOSDetailView = Marionette.ItemView.extend({ } if (isNew) { - this.model.attributes.humanReadableName = "new " + model.modelName; + this.model.attributes.humanReadableName = "new " + this.model.modelName; this.model.addToCollection = this.collection; } else { this.model.addToCollection = undefined; } - var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} ); + var infoMsgId = this.app.showInformational( {what: "save " + this.model.modelName + " " + this.model.attributes.humanReadableName, status: "", statusText: "in progress..."} ); this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);}, success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId); @@ -609,12 +647,13 @@ XOSDetailView = Marionette.ItemView.extend({ collectionName: this.model.collectionName, addFields: this.model.addFields, listFields: this.model.listFields, - detailFields: this.model.detailFields, + detailFields: this.options.detailFields || this.detailFields || this.model.detailFields, foreignFields: this.model.foreignFields, detailLinkFields: this.model.detailLinkFields, inputType: this.model.inputType, model: this.model, detailView: this, + choices: this.options.choices || this.choices || this.model.choices || {}, }}, }); @@ -829,6 +868,7 @@ XOSDataTableView = Marionette.View.extend( { view.columnsByIndex = []; view.columnsByFieldName = {}; _.each(this.collection.listFields, function(fieldName) { + inputType = view.options.inputType || view.inputType || {}; mRender = undefined; mSearchText = undefined; sTitle = fieldNameToHumanReadable(fieldName); @@ -840,6 +880,8 @@ XOSDataTableView = Marionette.View.extend( { } else if (fieldName in view.collection.foreignFields) { var foreignCollection = view.collection.foreignFields[fieldName]; mSearchText = function(x) { return idToName(x, foreignCollection, "humanReadableName"); }; + } else if (inputType[fieldName] == "spinner") { + mRender = function(x,y,z) { return xosDataTableSpinnerTemplate( {value: x, collectionName: view.collection.collectionName, fieldName: fieldName, id: z.id} ); }; } if ($.inArray(fieldName, view.collection.detailLinkFields)>=0) { var collectionName = view.collection.collectionName; @@ -850,9 +892,11 @@ XOSDataTableView = Marionette.View.extend( { view.columnsByFieldName[fieldName] = thisColumn; }); - deleteColumn = {sTitle: "", bSortable: false, mRender: function(x,y,z) { return xosDeleteButtonTemplate({modelName: view.collection.modelName, id: z.id}); }, mData: function() { return "delete"; }}; - view.columnsByIndex.push(deleteColumn); - view.columnsByFieldName["delete"] = deleteColumn; + if (!view.noDeleteColumn) { + deleteColumn = {sTitle: "", bSortable: false, mRender: function(x,y,z) { return xosDeleteButtonTemplate({modelName: view.collection.modelName, id: z.id}); }, mData: function() { return "delete"; }}; + view.columnsByIndex.push(deleteColumn); + view.columnsByFieldName["delete"] = deleteColumn; + }; oTable = $(this.el).find("table").dataTable( { "bJQueryUI": true, @@ -893,7 +937,7 @@ XOSDataTableView = Marionette.View.extend( { // content of the collection var populateTable = function() { - console.log("populatetable!"); + //console.log("populatetable!"); // clear out old row views rows = []; @@ -1044,3 +1088,25 @@ idToSelect = function(variable, selectedId, collectionName, fieldName, readOnly, return result; } +choicesToOptions = function(selectedValue, choices) { + result=""; + for (index in choices) { + choice = choices[index]; + displayName = choice[0]; + value = choice[1]; + if (value == selectedValue) { + selected = " selected"; + } else { + selected = ""; + } + result = result + ''; + } + return result; +} + +choicesToSelect = function(variable, selectedValue, choices) { + result = ''; + return result; +} diff --git a/planetstack/core/xoslib/templates/xosAdmin.html b/planetstack/core/xoslib/templates/xosAdmin.html index 4862cea..ac90e94 100644 --- a/planetstack/core/xoslib/templates/xosAdmin.html +++ b/planetstack/core/xoslib/templates/xosAdmin.html @@ -158,9 +158,14 @@ <% args = arguments; %> <% _.each(detailFields, function(fieldName) { %> <%= fieldNameToHumanReadable(fieldName) %>: - <% readOnly = $.inArray(fieldName, model.readOnlyFields)>=0 ? " readonly" : ""; console.log(fieldName + " " + readOnly); console.log(model.readOnlyFields); %> - <% if (fieldName in foreignFields) { %> + <% readOnly = $.inArray(fieldName, model.readOnlyFields)>=0 ? " readonly" : ""; %> + <% if (fieldName in choices) { %> + <%= choicesToSelect(fieldName, model.attributes[fieldName], choices[fieldName]) %> + <% } else if (fieldName in foreignFields) { %> <%= idToSelect(fieldName, model.attributes[fieldName], foreignFields[fieldName], "humanReadableName", readOnly) %> + <% } else if (inputType[fieldName] == "spinner") { %> + + <%= xosSpinnerTemplate({id: "picker_" + fieldName, fieldName: fieldName, value: model.attributes[fieldName]}) %> <% } else if (inputType[fieldName] == "checkbox") { %> <%= readOnly %>> <% } else if (inputType[fieldName] == "picker") { %> @@ -223,9 +228,19 @@ + + + + + + + -- 2.43.0