Merge "Add charts to show volume quotas on Angular launch instance modal"

This commit is contained in:
Jenkins 2017-06-20 12:04:53 +00:00 committed by Gerrit Code Review
commit cbb85753fb
11 changed files with 241 additions and 21 deletions

View File

@ -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;

View File

@ -2,7 +2,7 @@
<chart-tooltip tooltip-data="model.tooltipData"></chart-tooltip>
<div class="pie-chart-title" ng-if="::model.settings.showTitle && chartData.title">
{$ ::chartData.title $} ({$ model.totalLabel $})
{$ ::chartData.title $} <br/>({$ model.totalLabel $})
</div>
<svg class="svg-pie-chart"

View File

@ -24,6 +24,7 @@
.pie-chart-label {
font-size: $font-size-large;
text-anchor: middle;
width: 100%;
text {
font-size: $font-size-large;

View File

@ -25,7 +25,7 @@
describe('pie chart directive', function () {
var $scope, $elementMax, $elementTotal, $elementOverMax,
var $scope, $elementMax, $elementTotal, $elementOverMax, $elementMaxWithUnit,
$elementNoQuota, quotaChartDefaults;
beforeEach(module('templates'));
@ -57,6 +57,24 @@
]
};
$scope.testDataMaxWithUnit = {
title: 'Total Volume Storage',
maxLimit: 1000,
unit: "GiB",
data: [
{ label: quotaChartDefaults.usageLabel,
value: 50,
colorClass: quotaChartDefaults.usageColorClass },
{ label: quotaChartDefaults.addedLabel,
value: 10,
colorClass: quotaChartDefaults.addedColorClass },
{ label: quotaChartDefaults.remainingLabel,
value: 940,
colorClass: quotaChartDefaults.remainingColorClass,
hideKey: true }
]
};
$scope.testDataMax = {};
$scope.testDataOverMax = {};
$scope.testDataNoQuota = {};
@ -111,6 +129,12 @@
$elementNoQuota = angular.element(markupNoQuota);
$compile($elementNoQuota)($scope);
// Max chart with unit markup
var markupMaxWithUnit = '<pie-chart chart-data="testDataMaxWithUnit" ' +
' chart-settings="chartSettings">' +
'</pie-chart>';
$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)');
});
});
})();

View File

@ -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;

View File

@ -113,6 +113,16 @@ limitations under the License.
chart-settings="chartSettings"></pie-chart>
</div>
</div>
<div class="row" ng-if="selectFlavorCtrl.cinderLimits">
<div class="col-xs-4">
<pie-chart chart-data="item.volumeChartData"
chart-settings="chartSettings"></pie-chart>
</div>
<div class="col-xs-4">
<pie-chart chart-data="item.volumeStorageChartData"
chart-settings="chartSettings"></pie-chart>
</div>
</div>
<div ng-if="selectFlavorCtrl.metadataDefs.flavor">
<div class="row" ng-if="item.extras">
<div class="col-sm-12">
@ -224,6 +234,16 @@ limitations under the License.
chart-settings="chartSettings"></pie-chart>
</div>
</div>
<div class="row" ng-if="selectFlavorCtrl.cinderLimits">
<div class="col-xs-4">
<pie-chart chart-data="item.volumeChartData"
chart-settings="chartSettings"></pie-chart>
</div>
<div class="col-xs-4">
<pie-chart chart-data="item.volumeStorageChartData"
chart-settings="chartSettings"></pie-chart>
</div>
</div>
<div ng-if="selectFlavorCtrl.metadataDefs.flavor">
<div class="row" ng-if="item.extras">
<div class="col-sm-12">

View File

@ -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);
});
});
});
});

View File

@ -569,6 +569,7 @@
function addVolumeSourcesIfEnabled(config) {
var volumeDeferred = $q.defer();
var volumeSnapshotDeferred = $q.defer();
var absoluteLimitsDeferred = $q.defer();
serviceCatalog
.ifTypeEnabled('volumev2')
.then(onVolumeServiceEnabled, onCheckVolumeV3);
@ -584,8 +585,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);
@ -600,6 +603,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);
@ -607,6 +613,7 @@
function resolvePromises() {
volumeDeferred.resolve();
volumeSnapshotDeferred.resolve();
absoluteLimitsDeferred.resolve();
}
function resolveVolumes() {
volumeDeferred.resolve();
@ -620,10 +627,14 @@
function failVolumeSnapshots() {
volumeSnapshotDeferred.resolve();
}
function resolveAbsoluteLimitsDeferred() {
absoluteLimitsDeferred.resolve();
}
return $q.all(
[
volumeDeferred.promise,
volumeSnapshotDeferred.promise
volumeSnapshotDeferred.promise,
absoluteLimitsDeferred.promise
]);
}
@ -751,6 +762,12 @@
finalSpec.source_id = '';
}
// Cinder Limits
function onGetCinderLimits(response) {
model.cinderLimits = response.data;
}
// Nova Limits
function onGetNovaLimits(data) {

View File

@ -233,6 +233,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;
}
});
@ -751,6 +760,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() {

View File

@ -10,6 +10,10 @@
.transfer-section {
margin-top: $padding-large-vertical;
.row .pie-chart {
margin-top: 10px;
}
}
.magic-search-bar, .basic-search-bar {

View File

@ -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.