Skip to content

Commit

Permalink
Merge "Split instance details and source in Launch Instance wizard."
Browse files Browse the repository at this point in the history
  • Loading branch information
Jenkins authored and openstack-gerrit committed Nov 24, 2015
2 parents 474bdd7 + ba91041 commit 758adb1
Show file tree
Hide file tree
Showing 10 changed files with 458 additions and 337 deletions.
@@ -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);
}

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

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

0 comments on commit 758adb1

Please sign in to comment.