Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge "Split instance details and source in Launch Instance wizard."
- Loading branch information
Showing
10 changed files
with
458 additions
and
337 deletions.
There are no files selected for viewing
180 changes: 180 additions & 0 deletions
180
...s/project/static/dashboard/project/workflow/launch-instance/details/details.controller.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
/* | ||
* Copyright 2015 Hewlett Packard Enterprise Development Company LP | ||
* (c) Copyright 2015 ThoughtWorks Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
(function () { | ||
'use strict'; | ||
|
||
/** | ||
* @ngdoc controller | ||
* @name LaunchInstanceDetailsController | ||
* @description | ||
* The `LaunchInstanceDetailsController` controller provides functions for | ||
* configuring the details step of the Launch Instance Wizard. | ||
* | ||
*/ | ||
angular | ||
.module('horizon.dashboard.project.workflow.launch-instance') | ||
.controller('LaunchInstanceDetailsController', LaunchInstanceDetailsController); | ||
|
||
LaunchInstanceDetailsController.$inject = [ | ||
'$scope', | ||
'horizon.framework.widgets.charts.donutChartSettings', | ||
'horizon.framework.widgets.charts.quotaChartDefaults' | ||
]; | ||
|
||
function LaunchInstanceDetailsController($scope, | ||
donutChartSettings, | ||
quotaChartDefaults | ||
) { | ||
|
||
var ctrl = this; | ||
|
||
// Error text for invalid fields | ||
ctrl.instanceNameError = gettext('A name is required for your instance.'); | ||
ctrl.instanceCountError = gettext( | ||
'Instance count is required and must be an integer of at least 1' | ||
); | ||
ctrl.maxInstanceCount = 1; | ||
|
||
/* | ||
* Donut chart | ||
*/ | ||
ctrl.chartSettings = donutChartSettings; | ||
ctrl.maxInstances = 1; // Must have default value > 0 | ||
ctrl.totalInstancesUsed = 0; | ||
|
||
if ($scope.model.novaLimits && $scope.model.novaLimits.maxTotalInstances) { | ||
ctrl.maxInstances = $scope.model.novaLimits.maxTotalInstances; | ||
} | ||
|
||
if ($scope.model.novaLimits && $scope.model.novaLimits.totalInstancesUsed) { | ||
ctrl.totalInstancesUsed = $scope.model.novaLimits.totalInstancesUsed; | ||
} | ||
|
||
ctrl.instanceStats = { | ||
title: gettext('Total Instances'), | ||
maxLimit: ctrl.maxInstances, | ||
label: '100%', | ||
data: [ | ||
{ | ||
label: quotaChartDefaults.usageLabel, | ||
value: 1, | ||
colorClass: quotaChartDefaults.usageColorClass | ||
}, | ||
{ | ||
label: quotaChartDefaults.addedLabel, | ||
value: 1, | ||
colorClass: quotaChartDefaults.addedColorClass | ||
}, | ||
{ | ||
label: quotaChartDefaults.remainingLabel, | ||
value: 1, | ||
colorClass: quotaChartDefaults.remainingColorClass | ||
} | ||
] | ||
}; | ||
|
||
syncInstanceChartAndLimits(); | ||
|
||
var specifiedInstancesWatcher = createWatcher(getInstanceCount, updateChart); | ||
var maxInstancesWatcher = createWatcher(getMaxInstances, resetMaxInstances); | ||
var instancesUsedWatcher = createWatcher(getTotalInstancesUsed, resetTotalInstancesUsed); | ||
|
||
// Explicitly remove watchers on desruction of this controller | ||
$scope.$on('$destroy', function() { | ||
specifiedInstancesWatcher(); | ||
maxInstancesWatcher(); | ||
instancesUsedWatcher(); | ||
}); | ||
|
||
//////////////////// | ||
|
||
function getMaxInstances() { | ||
return $scope.model.novaLimits.maxTotalInstances; | ||
} | ||
|
||
function resetMaxInstances(newMaxInstances) { | ||
ctrl.maxInstances = Math.max(1, newMaxInstances); | ||
syncInstanceChartAndLimits(); | ||
} | ||
|
||
function getTotalInstancesUsed() { | ||
return $scope.model.novaLimits.totalInstancesUsed; | ||
} | ||
|
||
function resetTotalInstancesUsed() { | ||
ctrl.totalInstancesUsed = $scope.model.novaLimits.totalInstancesUsed; | ||
syncInstanceChartAndLimits(); | ||
} | ||
|
||
function getInstanceCount() { | ||
return $scope.model.newInstanceSpec.instance_count; | ||
} | ||
|
||
function createWatcher(watchExpression, listener) { | ||
return $scope.$watch( | ||
watchExpression, | ||
function (newValue, oldValue) { | ||
if (newValue !== oldValue) { | ||
listener(newValue); | ||
} | ||
} | ||
); | ||
} | ||
|
||
function syncInstanceChartAndLimits() { | ||
updateChart(); | ||
updateMaxInstanceCount(); | ||
} | ||
|
||
function updateChart() { | ||
|
||
// Initialize instance_count to 1 | ||
if ($scope.model.newInstanceSpec.instance_count <= 0) { | ||
$scope.model.newInstanceSpec.instance_count = 1; | ||
} | ||
|
||
var data = ctrl.instanceStats.data; | ||
var added = $scope.model.newInstanceSpec.instance_count || 1; | ||
var remaining = Math.max(0, ctrl.maxInstances - ctrl.totalInstancesUsed - added); | ||
|
||
ctrl.instanceStats.maxLimit = ctrl.maxInstances; | ||
data[0].value = ctrl.totalInstancesUsed; | ||
data[1].value = added; | ||
data[2].value = remaining; | ||
var quotaCalc = Math.round((ctrl.totalInstancesUsed + added) / ctrl.maxInstances * 100); | ||
ctrl.instanceStats.overMax = quotaCalc > 100 ? true : false; | ||
ctrl.instanceStats.label = quotaCalc + '%'; | ||
ctrl.instanceStats = angular.extend({}, ctrl.instanceStats); | ||
} | ||
|
||
/* | ||
* Validation | ||
*/ | ||
|
||
// Update the maximum instance count based on nova limits | ||
function updateMaxInstanceCount() { | ||
ctrl.maxInstanceCount = ctrl.maxInstances - ctrl.totalInstancesUsed; | ||
|
||
var instanceCountText = gettext( | ||
'The instance count must not exceed your quota available of %(maxInstanceCount)s instances' | ||
); | ||
var instanceCountObj = { maxInstanceCount: ctrl.maxInstanceCount }; | ||
ctrl.instanceCountMaxError = interpolate(instanceCountText, instanceCountObj, true); | ||
} | ||
|
||
} | ||
})(); |
183 changes: 183 additions & 0 deletions
183
...ject/static/dashboard/project/workflow/launch-instance/details/details.controller.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
/* | ||
* Copyright 2015 Hewlett Packard Enterprise Development Company LP | ||
* (c) Copyright 2015 ThoughtWorks Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
(function() { | ||
'use strict'; | ||
|
||
describe('Launch Instance Details Step', function() { | ||
var noop = angular.noop; | ||
|
||
beforeEach(module('horizon.dashboard.project')); | ||
|
||
describe('LaunchInstanceDetailsController', function() { | ||
var scope, ctrl, deferred; | ||
|
||
beforeEach(module(function($provide) { | ||
$provide.value('horizon.framework.widgets.charts.donutChartSettings', noop); | ||
$provide.value('horizon.framework.widgets.charts.quotaChartDefaults', noop); | ||
})); | ||
|
||
beforeEach(inject(function($controller, $rootScope, $q) { | ||
scope = $rootScope.$new(); | ||
deferred = $q.defer(); | ||
scope.initPromise = deferred.promise; | ||
|
||
scope.model = { | ||
newInstanceSpec: { source: [], source_type: '' }, | ||
images: [ { id: 'image-1' }, { id: 'image-2' } ], | ||
imageSnapshots: [], | ||
volumes: [], | ||
volumeSnapshots: [], | ||
novaLimits: { | ||
maxTotalInstances: 10, | ||
totalInstancesUsed: 1 | ||
} | ||
}; | ||
|
||
ctrl = $controller('LaunchInstanceDetailsController', { $scope: scope }); | ||
|
||
scope.$apply(); | ||
})); | ||
|
||
it('should define error messages for invalid fields', function() { | ||
expect(ctrl.instanceNameError).toBeDefined(); | ||
expect(ctrl.instanceCountError).toBeDefined(); | ||
}); | ||
|
||
it('should update chart on creation', function() { | ||
var totalInstancesUsed = ctrl.instanceStats.data[0].value; | ||
var totalInstancesAdded = ctrl.instanceStats.data[1].value; | ||
var totalInstancesRemaining = ctrl.instanceStats.data[2].value; | ||
|
||
expect(totalInstancesUsed).toEqual(1); | ||
expect(totalInstancesAdded).toEqual(1); | ||
expect(totalInstancesRemaining).toEqual(8); | ||
}); | ||
|
||
it('should update maximum instance creation limit on initialization', function() { | ||
expect(ctrl.maxInstanceCount).toEqual(9); | ||
}); | ||
|
||
describe('novaLimits.maxTotalInstances watcher', function() { | ||
|
||
it('should update maxInstanceCount when maxTotalInstances changes', function() { | ||
scope.model.novaLimits.maxTotalInstances = 9; | ||
scope.$apply(); | ||
|
||
expect(ctrl.maxInstanceCount).toBe(8); | ||
|
||
// check chart data and labels | ||
var totalInstancesUsed = ctrl.instanceStats.data[0].value; | ||
var totalInstancesAdded = ctrl.instanceStats.data[1].value; | ||
var totalInstancesRemaining = ctrl.instanceStats.data[2].value; | ||
|
||
expect(ctrl.instanceStats.label).toBe('22%'); | ||
expect(totalInstancesUsed).toEqual(1); | ||
expect(totalInstancesAdded).toEqual(1); | ||
expect(totalInstancesRemaining).toEqual(7); | ||
}); | ||
|
||
it('should update maxInstances when maxTotaInstances changes', function() { | ||
scope.model.novaLimits.maxTotalInstances = 9; | ||
scope.$apply(); | ||
|
||
expect(ctrl.maxInstances).toBe(9); | ||
expect(ctrl.instanceStats.maxLimit).toEqual(9); | ||
}); | ||
}); | ||
|
||
describe('instanceCount watcher', function() { | ||
|
||
it('should reset instance count to 1 if instance count set to 0', function() { | ||
scope.model.newInstanceSpec.instance_count = 0; | ||
scope.$apply(); | ||
|
||
expect(scope.model.newInstanceSpec.instance_count).toBe(1); | ||
}); | ||
|
||
it('should reset instance count to 1 if instance count set to -1', function() { | ||
scope.model.newInstanceSpec.instance_count = -1; | ||
scope.$apply(); | ||
|
||
expect(scope.model.newInstanceSpec.instance_count).toBe(1); | ||
}); | ||
|
||
it('should update chart stats if instance count = 2', function() { | ||
scope.model.newInstanceSpec.instance_count = 2; | ||
scope.$apply(); | ||
|
||
// check chart data and labels | ||
var totalInstancesUsed = ctrl.instanceStats.data[0].value; | ||
var totalInstancesAdded = ctrl.instanceStats.data[1].value; | ||
var totalInstancesRemaining = ctrl.instanceStats.data[2].value; | ||
|
||
expect(ctrl.instanceStats.label).toBe('30%'); | ||
expect(totalInstancesUsed).toEqual(1); | ||
expect(totalInstancesAdded).toEqual(2); | ||
expect(totalInstancesRemaining).toEqual(7); | ||
}); | ||
}); | ||
|
||
describe('the instanceStats chart is set up correctly', function() { | ||
|
||
it('chart should have a title of "Total Instances"', function() { | ||
expect(ctrl.instanceStats.title).toBe('Total Instances'); | ||
}); | ||
|
||
it('chart should have a maxLimit value defined', function() { | ||
expect(ctrl.instanceStats.maxLimit).toBeDefined(); | ||
}); | ||
|
||
it('instanceStats.overMax should get set to true if instance_count exceeds maxLimit', | ||
function() { | ||
scope.model.newInstanceSpec.instance_count = 11; | ||
scope.$apply(); | ||
|
||
// check chart data and labels | ||
var totalInstancesUsed = ctrl.instanceStats.data[0].value; | ||
var totalInstancesAdded = ctrl.instanceStats.data[1].value; | ||
var totalInstancesRemaining = ctrl.instanceStats.data[2].value; | ||
|
||
expect(ctrl.instanceStats.label).toBe('120%'); | ||
expect(totalInstancesUsed).toEqual(1); | ||
expect(totalInstancesAdded).toEqual(11); | ||
expect(totalInstancesRemaining).toEqual(0); | ||
|
||
// check to ensure overMax | ||
expect(ctrl.instanceStats.overMax).toBe(true); | ||
} | ||
); | ||
}); | ||
|
||
describe('novaLimits.totalInstancesUsed watcher', function() { | ||
|
||
it('should update chart stats when totalInstancesUsed changes', function() { | ||
scope.model.novaLimits.totalInstancesUsed = 1; | ||
scope.$apply(); | ||
|
||
expect(ctrl.maxInstanceCount).toBe(9); | ||
|
||
// check chart data and labels | ||
expect(ctrl.instanceStats.label).toBe('20%'); | ||
expect(ctrl.instanceStats.data[0].value).toEqual(1); | ||
expect(ctrl.instanceStats.data[1].value).toEqual(1); | ||
expect(ctrl.instanceStats.data[2].value).toEqual(8); | ||
}); | ||
}); | ||
|
||
}); | ||
}); | ||
})(); |
5 changes: 5 additions & 0 deletions
5
...oards/project/static/dashboard/project/workflow/launch-instance/details/details.help.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<h1 translate>Instance Details Help</h1> | ||
|
||
<p translate>An instance name is required and used to help you uniquely identify your instance in the dashboard.</p> | ||
|
||
<p translate>If you select an availability zone and plan to use the 'boot from volume' option in the Source step, make sure that the availability zone you select for the instance is the same availability zone where your bootable volume resides.</p> |
Oops, something went wrong.