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.