From 336d0a05256eca4c8075eb35cb0a601a2e65859e Mon Sep 17 00:00:00 2001 From: Ying Zuo Date: Sat, 17 Dec 2016 15:28:36 -0800 Subject: [PATCH] Add charts to show volume quotas on Angular launch instance modal Added two charts for Number of Volumes and Total Volume Storage quotas on Angular launch instance modal when cinder is enabled. The charts reflect the volume usage of the new instances to be created as the user changes the configuration on the modal. Updated the chart styling for the charts to align better. Change-Id: Ie744ada2317624153fcfdf9abdf4d7b26996a35e Partially-implements: blueprint launch-instance-volume-quotas --- .../widgets/charts/pie-chart.directive.js | 24 +++--- .../framework/widgets/charts/pie-chart.html | 2 +- .../framework/widgets/charts/pie-chart.scss | 1 + .../widgets/charts/pie-chart.spec.js | 35 +++++++- .../flavor/flavor.controller.js | 51 +++++++++++- .../launch-instance/flavor/flavor.html | 20 +++++ .../launch-instance/flavor/flavor.spec.js | 83 +++++++++++++++++-- .../launch-instance-model.service.js | 19 ++++- .../launch-instance-model.service.spec.js | 19 +++++ .../scss/components/_transfer_tables.scss | 4 + ...stance-volume-quotas-490070a36cadfe8d.yaml | 4 + 11 files changed, 241 insertions(+), 21 deletions(-) create mode 100644 releasenotes/notes/bp-launch-instance-volume-quotas-490070a36cadfe8d.yaml diff --git a/horizon/static/framework/widgets/charts/pie-chart.directive.js b/horizon/static/framework/widgets/charts/pie-chart.directive.js index 9b7f25ed5d..04e23fe025 100644 --- a/horizon/static/framework/widgets/charts/pie-chart.directive.js +++ b/horizon/static/framework/widgets/charts/pie-chart.directive.js @@ -170,26 +170,30 @@ } }); + function getChartLabel(type, total, unit) { + if (unit) { + var totalWithUnit = total + " " + unit; + } + return interpolate( + gettext('%(total)s %(type)s'), + { total: totalWithUnit || total, + type: type }, + true + ); + } + // set labels depending on whether this is a max or total chart if (!showChart) { scope.model.total = null; scope.model.totalLabel = gettext('No Limit'); } else if (angular.isDefined(scope.chartData.maxLimit)) { scope.model.total = scope.chartData.maxLimit; - scope.model.totalLabel = interpolate( - gettext('%(total)s Max'), - { total: scope.model.total }, - true - ); + scope.model.totalLabel = getChartLabel("Max", scope.model.total, scope.chartData.unit); } else { scope.model.total = d3.sum(scope.chartData.data, function (d) { return d.value; }); - scope.model.totalLabel = interpolate( - gettext('%(total)s Total'), - { total: scope.model.total }, - true - ); + scope.model.totalLabel = getChartLabel("Total", scope.model.total, scope.chartData.unit); } scope.model.tooltipData.enabled = false; diff --git a/horizon/static/framework/widgets/charts/pie-chart.html b/horizon/static/framework/widgets/charts/pie-chart.html index f99f3ae7d1..b6b47f5305 100644 --- a/horizon/static/framework/widgets/charts/pie-chart.html +++ b/horizon/static/framework/widgets/charts/pie-chart.html @@ -2,7 +2,7 @@
- {$ ::chartData.title $} ({$ model.totalLabel $}) + {$ ::chartData.title $}
({$ model.totalLabel $})
' + + ''; + $elementMaxWithUnit = angular.element(markupMaxWithUnit); + $compile($elementMaxWithUnit)($scope); $scope.$apply(); })); @@ -126,6 +150,10 @@ expect($elementTotal.html().trim()).not.toBe(''); }); + it('Max chart with unit should be compiled', function () { + expect($elementMaxWithUnit.html().trim()).not.toBe(''); + }); + it('Max chart should have svg element', function () { expect($elementMax.find('svg').length).toBe(1); }); @@ -255,6 +283,11 @@ expect(cleanSpaces(firstKeyLabel.textContent)).toEqual('1 Current Usage'); expect(cleanSpaces(secondKeyLabel.textContent)).toEqual('1 Added'); }); + + it('Max chart with unit should have the unit in its title', function () { + var title = $elementMaxWithUnit.find('.pie-chart-title').text().trim(); + expect(title).toBe('Total Volume Storage (1000 GiB Max)'); + }); }); })(); diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/flavor/flavor.controller.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/flavor/flavor.controller.js index 35b5373f71..c2a43299c8 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/flavor/flavor.controller.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/flavor/flavor.controller.js @@ -40,6 +40,8 @@ ctrl.chartTotalInstancesLabel = gettext('Total Instances'); ctrl.chartTotalVcpusLabel = gettext('Total VCPUs'); ctrl.chartTotalRamLabel = gettext('Total RAM'); + ctrl.chartTotalVolumeLabel = gettext('Total Volumes'); + ctrl.chartTotalVolumeStorageLabel = gettext('Total Volume Storage'); ctrl.filterFacets = [ { @@ -160,6 +162,25 @@ ctrl.validateFlavor(); }); + var cinderLimitsWatcher = $scope.$watch(function () { + return launchInstanceModel.cinderLimits; + }, function (newValue, oldValue, scope) { + var ctrl = scope.selectFlavorCtrl; + ctrl.cinderLimits = newValue; + ctrl.updateFlavorFacades(); + }, true); + + var volumeSizeWatcher = $scope.$watchCollection(function () { + return [launchInstanceModel.newInstanceSpec.source_type, + launchInstanceModel.newInstanceSpec.vol_size, + launchInstanceModel.newInstanceSpec.vol_create]; + }, function (newValue, oldValue) { + if (!angular.equals(newValue, oldValue)) { + ctrl.updateFlavorFacades(); + ctrl.validateFlavor(); + } + }); + // $scope.$on('$destroy', function() { novaLimitsWatcher(); @@ -167,6 +188,8 @@ instanceCountWatcher(); facadesWatcher(); sourceWatcher(); + cinderLimitsWatcher(); + volumeSizeWatcher(); }); ////////// @@ -240,6 +263,7 @@ */ for (var i = 0; i < ctrl.availableFlavorFacades.length; i++) { var facade = ctrl.availableFlavorFacades[i]; + var createVolume = launchInstanceModel.newInstanceSpec.vol_create; facade.instancesChartData = instancesChartData; @@ -253,7 +277,27 @@ ctrl.chartTotalRamLabel, ctrl.instanceCount * facade.ram, launchInstanceModel.novaLimits.totalRAMUsed, - launchInstanceModel.novaLimits.maxTotalRAMSize); + launchInstanceModel.novaLimits.maxTotalRAMSize, + "MB" + ); + + if (launchInstanceModel.cinderLimits) { + facade.volumeChartData = ctrl.getChartData( + ctrl.chartTotalVolumeLabel, + createVolume ? ctrl.instanceCount : 0, + launchInstanceModel.cinderLimits.totalVolumesUsed, + launchInstanceModel.cinderLimits.maxTotalVolumes + ); + + facade.volumeStorageChartData = ctrl.getChartData( + ctrl.chartTotalVolumeStorageLabel, + createVolume ? (ctrl.instanceCount * Math.max(facade.totalDisk, + launchInstanceModel.newInstanceSpec.vol_size)) : 0, + launchInstanceModel.cinderLimits.totalGigabytesUsed, + launchInstanceModel.cinderLimits.maxTotalVolumeGigabytes, + "GiB" + ); + } var errors = ctrl.getErrors(facade.flavor); facade.errors = errors; @@ -261,7 +305,7 @@ } } - function getChartData(title, added, totalUsed, maxAllowed) { + function getChartData(title, added, totalUsed, maxAllowed, unit) { var used = ctrl.defaultIfUndefined(totalUsed, 0); var allowed = ctrl.defaultIfUndefined(maxAllowed, 1); @@ -288,7 +332,8 @@ maxLimit: allowed, label: quotaCalc + '%', overMax: overMax, - data: [usageData, addedData, remainingData] + data: [usageData, addedData, remainingData], + unit: unit }; return chartData; diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/flavor/flavor.html b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/flavor/flavor.html index cfc4cbaa03..b18b545991 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/flavor/flavor.html +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/flavor/flavor.html @@ -113,6 +113,16 @@ limitations under the License. chart-settings="chartSettings"> +
+
+ +
+
+ +
+
@@ -224,6 +234,16 @@ limitations under the License. chart-settings="chartSettings">
+
+
+ +
+
+ +
+
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/flavor/flavor.spec.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/flavor/flavor.spec.js index c388fb032d..c8863a2ed3 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/flavor/flavor.spec.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/flavor/flavor.spec.js @@ -35,6 +35,7 @@ model = { newInstanceSpec: { }, novaLimits: { }, + cinderLimits: { }, flavors: [] }; defaults = { usageLabel: "label", @@ -54,7 +55,10 @@ it('defines expected labels', function () { var props = [ 'chartTotalInstancesLabel', - 'chartTotalVcpusLabel', 'chartTotalRamLabel' + 'chartTotalVcpusLabel', + 'chartTotalRamLabel', + 'chartTotalVolumeLabel', + 'chartTotalVolumeStorageLabel' ]; props.forEach(function (prop) { @@ -75,6 +79,11 @@ }); }); + it('includes the unit if it is provided', function () { + var data = ctrl.getChartData('fakeTitle', 1, 2, 3, "MB"); + expect(data.unit).toBe('MB'); + }); + describe("watches", function () { beforeEach(function() { @@ -93,14 +102,14 @@ ctrl.validateFlavor.calls.reset(); }); - it("establishes five watches", function () { + it("establishes seven watches", function () { // Count calls to $watch (note: $watchCollection // also calls $watch) - expect(scope.$watch.calls.count()).toBe(5); + expect(scope.$watch.calls.count()).toBe(7); }); - it("establishes three watch collections", function () { - expect(scope.$watchCollection.calls.count()).toBe(3); + it("establishes four watch collections", function () { + expect(scope.$watchCollection.calls.count()).toBe(4); }); describe("novaLimits watch", function () { @@ -245,6 +254,40 @@ }); }); + describe("cinderLimits watcher", function () { + + it("initializes cinderLimits", function () { + expect(ctrl.cinderLimits).toEqual({}); + }); + + it("should call updateFlavorFacades", function () { + model.cinderLimits = {test: "test"}; + scope.$apply(); + expect(ctrl.cinderLimits).toEqual({test: "test"}); + expect(ctrl.updateFlavorFacades.calls.count()).toBe(1); + }); + }); + + describe("volume size watcher", function () { + + it("should call updateFlavorFacades when source type is changed", function () { + model.newInstanceSpec.source_type = "image"; + scope.$apply(); + expect(ctrl.updateFlavorFacades.calls.count()).toBe(1); + }); + + it("should call updateFlavorFacades when volume size is changed", function () { + model.newInstanceSpec.vol_size = 10; + scope.$apply(); + expect(ctrl.updateFlavorFacades.calls.count()).toBe(1); + }); + + it("should call updateFlavorFacades when volume create is changed", function () { + model.newInstanceSpec.vol_create = true; + scope.$apply(); + expect(ctrl.updateFlavorFacades.calls.count()).toBe(1); + }); + }); }); describe("when having allocated flavors", function () { @@ -412,6 +455,36 @@ }); }); + describe("test updateFlavorFacades", function () { + + beforeEach(function () { + ctrl.flavors = [{name: "tiny"}]; + }); + + it("should set volumeChartData and volumeStorageChartData", function () { + ctrl.updateFlavorFacades(); + expect(ctrl.availableFlavorFacades.length).toBe(1); + expect(ctrl.availableFlavorFacades[0].volumeChartData).toBeDefined(); + expect(ctrl.availableFlavorFacades[0].volumeStorageChartData).toBeDefined(); + }); + + it("should call getChartData", function() { + spyOn(ctrl, 'getChartData'); + ctrl.updateFlavorFacades(); + expect(ctrl.getChartData.calls.count()).toBe(5); + }); + }); + + describe("test validateFlavor", function () { + + it("should call validateFlavor when source type is changed", function () { + spyOn(ctrl, 'validateFlavor'); + model.newInstanceSpec.source_type = "image"; + scope.$apply(); + expect(ctrl.validateFlavor.calls.count()).toBe(1); + }); + }); + }); }); diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.js index 2d2e3b86af..f9b2bab6b3 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.js @@ -561,6 +561,7 @@ function addVolumeSourcesIfEnabled(config) { var volumeDeferred = $q.defer(); var volumeSnapshotDeferred = $q.defer(); + var absoluteLimitsDeferred = $q.defer(); serviceCatalog .ifTypeEnabled('volumev2') .then(onVolumeServiceEnabled, onCheckVolumeV3); @@ -576,8 +577,10 @@ .then(onBootToVolumeSupported); if (!config || !config.disable_volume) { getVolumes().then(resolveVolumes, failVolumes); + getAbsoluteLimits().then(resolveAbsoluteLimitsDeferred, resolveAbsoluteLimitsDeferred); } else { resolveVolumes(); + resolveAbsoluteLimitsDeferred(); } if (!config || !config.disable_volume_snapshot) { getVolumeSnapshots().then(resolveVolumeSnapshots, failVolumeSnapshots); @@ -592,6 +595,9 @@ return cinderAPI.getVolumes({status: 'available', bootable: 1}) .then(onGetVolumes); } + function getAbsoluteLimits() { + return cinderAPI.getAbsoluteLimits().then(onGetCinderLimits); + } function getVolumeSnapshots() { return cinderAPI.getVolumeSnapshots({status: 'available'}) .then(onGetVolumeSnapshots); @@ -599,6 +605,7 @@ function resolvePromises() { volumeDeferred.resolve(); volumeSnapshotDeferred.resolve(); + absoluteLimitsDeferred.resolve(); } function resolveVolumes() { volumeDeferred.resolve(); @@ -612,10 +619,14 @@ function failVolumeSnapshots() { volumeSnapshotDeferred.resolve(); } + function resolveAbsoluteLimitsDeferred() { + absoluteLimitsDeferred.resolve(); + } return $q.all( [ volumeDeferred.promise, - volumeSnapshotDeferred.promise + volumeSnapshotDeferred.promise, + absoluteLimitsDeferred.promise ]); } @@ -743,6 +754,12 @@ finalSpec.source_id = ''; } + // Cinder Limits + + function onGetCinderLimits(response) { + model.cinderLimits = response.data; + } + // Nova Limits function onGetNovaLimits(data) { diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.spec.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.spec.js index 1a5db23afb..23d3f742ba 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.spec.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.spec.js @@ -232,6 +232,15 @@ var deferred = $q.defer(); deferred.resolve({ data: { items: snapshots } }); + return deferred.promise; + }, + getAbsoluteLimits: function() { + var limits = { maxTotalVolumes: 100, + totalVolumesUsed: 2, + maxTotalVolumeGigabytes: 1000, + totalGigabytesUsed: 10 }; + var deferred = $q.defer(); + deferred.resolve({ data: limits }); return deferred.promise; } }); @@ -732,6 +741,16 @@ expect(model.allowedBootSources).toContain(VOLUME_SNAPSHOT); }); + it('should have maxTotalVolumes and maxTotalVolumeGigabytes if cinder ' + + 'is enabled', function() { + cinderEnabled = true; + model.initialize(true); + scope.$apply(); + + expect(model.cinderLimits.maxTotalVolumes).toBe(100); + expect(model.cinderLimits.maxTotalVolumeGigabytes).toBe(1000); + }); + }); describe('Post Initialization Model - Initializing', function() { diff --git a/openstack_dashboard/static/dashboard/scss/components/_transfer_tables.scss b/openstack_dashboard/static/dashboard/scss/components/_transfer_tables.scss index a677cd91fc..8999605ec9 100644 --- a/openstack_dashboard/static/dashboard/scss/components/_transfer_tables.scss +++ b/openstack_dashboard/static/dashboard/scss/components/_transfer_tables.scss @@ -10,6 +10,10 @@ .transfer-section { margin-top: $padding-large-vertical; + + .row .pie-chart { + margin-top: 10px; + } } .magic-search-bar, .basic-search-bar { diff --git a/releasenotes/notes/bp-launch-instance-volume-quotas-490070a36cadfe8d.yaml b/releasenotes/notes/bp-launch-instance-volume-quotas-490070a36cadfe8d.yaml new file mode 100644 index 0000000000..d311a4de73 --- /dev/null +++ b/releasenotes/notes/bp-launch-instance-volume-quotas-490070a36cadfe8d.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added two charts to show the Number of Volumes and Total Volume Storage + quotas on launch instance modal when cinder is enabled.