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
This commit is contained in:
parent
c01c4d9873
commit
336d0a0525
@ -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;
|
||||
|
||||
|
@ -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"
|
||||
|
@ -24,6 +24,7 @@
|
||||
.pie-chart-label {
|
||||
font-size: $font-size-large;
|
||||
text-anchor: middle;
|
||||
width: 100%;
|
||||
|
||||
text {
|
||||
font-size: $font-size-large;
|
||||
|
@ -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)');
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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() {
|
||||
|
@ -10,6 +10,10 @@
|
||||
|
||||
.transfer-section {
|
||||
margin-top: $padding-large-vertical;
|
||||
|
||||
.row .pie-chart {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.magic-search-bar, .basic-search-bar {
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user