Merge "Add pagination to Flavors table in Launch Instance wizard"

This commit is contained in:
Zuul 2022-05-18 12:08:20 +00:00 committed by Gerrit Code Review
commit 4ffef9ec2b
5 changed files with 134 additions and 283 deletions

View File

@ -0,0 +1,39 @@
<div ng-controller="LaunchInstanceFlavorController as ctrl">
<dl class="flavor-details">
<span class="h5" translate>Impact on your quota</span>
<div class="row">
<div class="col-xs-4">
<pie-chart chart-data="item.instancesChartData"
chart-settings="chartSettings"></pie-chart>
</div>
<div class="col-xs-4">
<pie-chart chart-data="item.vcpusChartData"
chart-settings="chartSettings"></pie-chart>
</div>
<div class="col-xs-4">
<pie-chart chart-data="item.ramChartData"
chart-settings="chartSettings"></pie-chart>
</div>
</div>
<div class="row" ng-if="ctrl.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="ctrl.metadataDefs.flavor">
<div class="row" ng-if="item.extras">
<div class="col-sm-12">
<metadata-display
available="::ctrl.metadataDefs.flavor"
existing="::item.extras">
</metadata-display>
</div>
</div>
</div>
</dl>
</div>

View File

@ -22,11 +22,17 @@
LaunchInstanceFlavorController.$inject = [
'$scope',
'horizon.dashboard.project.workflow.launch-instance.basePath',
'horizon.framework.widgets.charts.quotaChartDefaults',
'launchInstanceModel'
];
function LaunchInstanceFlavorController($scope, quotaChartDefaults, launchInstanceModel) {
function LaunchInstanceFlavorController(
$scope,
basePath,
quotaChartDefaults,
launchInstanceModel
) {
var ctrl = this;
ctrl.defaultIfUndefined = defaultIfUndefined;
@ -84,9 +90,7 @@
* exposing only the data needed by this specific view.
*/
ctrl.availableFlavorFacades = [];
ctrl.displayedAvailableFlavorFacades = [];
ctrl.allocatedFlavorFacades = [];
ctrl.displayedAllocatedFlavorFacades = [];
// Convenience references to launch instance model elements
ctrl.flavors = [];
@ -95,18 +99,55 @@
ctrl.instanceCount = 1;
// Data that drives the transfer table for flavors
ctrl.transferTableModel = {
allocated: ctrl.allocatedFlavorFacades,
displayedAllocated: ctrl.displayedAllocatedFlavorFacades,
available: ctrl.availableFlavorFacades,
displayedAvailable: ctrl.displayedAvailableFlavorFacades
ctrl.tableData = {
allocated: ctrl.allocatedFlavorFacades,
available: ctrl.availableFlavorFacades,
};
// We need backticks for cell templates, and backticks need ES6
/* eslint-env es6 */
ctrl.availableTableConfig = {
selectAll: false,
trackId: 'id',
detailsTemplateUrl: basePath + 'flavor/flavor-details.html',
columns: [
{id: 'name', title: gettext('Name'), priority: 1},
{id: 'vcpus', title: gettext('VCPUS'), priority: 1,
template: `<span class="invalid fa fa-exclamation-triangle"
ng-show="item.errors.vcpus"
uib-popover="{$ item.errors.vcpus $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="'mouseenter'"/>
<span>{$ item.vcpus $}</span>`},
{id: 'ram', title: gettext('RAM'), priority: 1,
template: `<span class="invalid fa fa-exclamation-triangle"
ng-show="item.errors.ram"
uib-popover="{$ item.errors.ram $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="'mouseenter'"/>
<span>{$ item.ram | mb $}</span>`},
{id: 'totalDisk', title: gettext('Total Disk'), filters: ['gb'], priority: 1},
{id: 'rootDisk', title: gettext('Root Disk'), priority: 2,
template: `<span class="invalid fa fa-exclamation-triangle"
ng-show="item.errors.disk"
uib-popover="{$ item.errors.disk $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="'mouseenter'"/>
<span>{$ item.rootDisk | gb $}</span>`},
{id: 'ephemeralDisk', title: gettext('Ephemeral Disk'), filters: ['gb'], priority: 2},
{id: 'isPublic', title: gettext('Public'), filters: ['yesno'], priority: 1}
]
};
ctrl.allocatedTableConfig = angular.copy(ctrl.availableTableConfig);
ctrl.allocatedTableConfig.noItemsMessage = gettext(
'Select a flavor from the available flavors below.');
// Each flavor has an instances chart...but it is the same for all flavors
ctrl.instancesChartData = {};
// We can pick at most, 1 flavor at a time
ctrl.allocationLimits = {
ctrl.tableLimits = {
maxAllocation: 1
};
@ -114,18 +155,22 @@
var novaLimitsWatcher = $scope.$watch(function () {
return launchInstanceModel.novaLimits;
}, function (newValue, oldValue, scope) {
var ctrl = scope.selectFlavorCtrl;
var ctrl = scope.ctrl;
ctrl.novaLimits = newValue;
ctrl.updateFlavorFacades();
if (!angular.equals(newValue, oldValue)) {
ctrl.updateFlavorFacades();
}
}, true);
// Flavor facades depend on flavors
var flavorsWatcher = $scope.$watchCollection(function() {
return launchInstanceModel.flavors;
}, function (newValue, oldValue, scope) {
var ctrl = scope.selectFlavorCtrl;
var ctrl = scope.ctrl;
ctrl.flavors = newValue;
ctrl.updateFlavorFacades();
if (!angular.equals(newValue, oldValue)) {
ctrl.updateFlavorFacades();
}
});
// Flavor quota charts depend on the current instance count
@ -133,7 +178,7 @@
return launchInstanceModel.newInstanceSpec.instance_count;
}, function (newValue, oldValue, scope) {
if (angular.isDefined(newValue)) {
var ctrl = scope.selectFlavorCtrl;
var ctrl = scope.ctrl;
// Ignore any values <1
ctrl.instanceCount = Math.max(1, newValue);
ctrl.updateFlavorFacades();
@ -143,13 +188,15 @@
// Update the new instance model when the allocated flavor changes
var facadesWatcher = $scope.$watchCollection(
"selectFlavorCtrl.allocatedFlavorFacades",
"ctrl.allocatedFlavorFacades",
function (newValue, oldValue, scope) {
if (newValue && newValue.length > 0) {
launchInstanceModel.newInstanceSpec.flavor = newValue[0].flavor;
scope.selectFlavorCtrl.validateFlavor();
} else {
delete launchInstanceModel.newInstanceSpec.flavor;
if (!angular.equals(newValue, oldValue)) {
if (newValue && newValue.length > 0) {
launchInstanceModel.newInstanceSpec.flavor = newValue[0].flavor;
scope.ctrl.validateFlavor();
} else {
delete launchInstanceModel.newInstanceSpec.flavor;
}
}
}
);
@ -157,18 +204,22 @@
var sourceWatcher = $scope.$watchCollection(function() {
return launchInstanceModel.newInstanceSpec.source;
}, function (newValue, oldValue, scope) {
var ctrl = scope.selectFlavorCtrl;
var ctrl = scope.ctrl;
ctrl.source = newValue && newValue.length ? newValue[0] : null;
ctrl.updateFlavorFacades();
if (!angular.equals(newValue, oldValue)) {
ctrl.updateFlavorFacades();
}
ctrl.validateFlavor();
});
var cinderLimitsWatcher = $scope.$watch(function () {
return launchInstanceModel.cinderLimits;
}, function (newValue, oldValue, scope) {
var ctrl = scope.selectFlavorCtrl;
var ctrl = scope.ctrl;
ctrl.cinderLimits = newValue;
ctrl.updateFlavorFacades();
if (!angular.equals(newValue, oldValue)) {
ctrl.updateFlavorFacades();
}
}, true);
var volumeSizeWatcher = $scope.$watchCollection(function () {

View File

@ -12,253 +12,20 @@ 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.
-->
<div ng-controller="LaunchInstanceFlavorController as selectFlavorCtrl">
<div ng-controller="LaunchInstanceFlavorController as ctrl">
<p class="step-description" translate>
Flavors manage the sizing for the compute, memory and storage capacity of the instance.
</p>
<transfer-table
tr-model="selectFlavorCtrl.transferTableModel"
limits="selectFlavorCtrl.allocationLimits">
<allocated ng-model="selectFlavorCtrl.allocatedFlavorFacades.length"
validate-number-min="1" name="allocated-flavor">
<table st-magic-search st-table="selectFlavorCtrl.displayedAllocatedFlavorFacades"
st-safe-src="selectFlavorCtrl.allocatedFlavorFacades"
hz-table class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th class="expander"></th>
<th st-sort="name" class="rsp-p1" translate>Name</th>
<th st-sort="vcpus" class="rsp-p1" translate>VCPUS</th>
<th st-sort="ram" class="rsp-p1" translate>RAM</th>
<th st-sort="totalDisk" class="rsp-p1" translate>Total Disk</th>
<th st-sort="rootDisk" class="rsp-p2" translate>Root Disk</th>
<th st-sort="ephemeralDisk" class="rsp-p2" translate>Ephemeral Disk</th>
<th st-sort="isPublic" class="rsp-p1" translate>Public</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-if="selectFlavorCtrl.displayedAllocatedFlavorFacades.length === 0">
<td colspan="10">
<div class="no-rows-help">
{$ ::trCtrl.helpText.noneAllocText $}
</div>
</td>
</tr>
<tr ng-repeat-start="item in selectFlavorCtrl.displayedAllocatedFlavorFacades track by item.id">
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ ::expandDetailsText $}"></span>
</td>
<td class="rsp-p1 word-break">{$ ::item.name $}</td>
<td class="rsp-p1">
<span class="invalid fa fa-exclamation-triangle"
ng-show="item.errors.vcpus"
uib-popover="{$ item.errors.vcpus $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="'mouseenter'"/>
{$ ::item.vcpus $}
</td>
<td class="rsp-p1">
<span class="invalid fa fa-exclamation-triangle"
ng-show="item.errors.ram"
uib-popover="{$ item.errors.ram $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="'mouseenter'"/>
{$ ::item.ram | mb $}
</td>
<td class="rsp-p1">{$ ::item.totalDisk | gb $}</td>
<td class="rsp-p2">
<span class="invalid fa fa-exclamation-triangle"
ng-show="item.errors.disk"
uib-popover="{$ item.errors.disk $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="'mouseenter'"/>
{$ ::item.rootDisk | gb $}
</td>
<td class="rsp-p2">{$ ::item.ephemeralDisk | gb $}</td>
<td class="rsp-p1">{$ ::item.isPublic | yesno $}</td>
<td class="action-col">
<action-list button-tooltip="item.disabledMessage"
bt-model="tooltipModel"
bt-disabled="!isAvailableTable || item.enabled"
warning-classes="'invalid'">
<notifications>
<span class="fa fa-exclamation-triangle invalid"
ng-show="isAvailableTable && !item.enabled"></span>
</notifications>
<action action-classes="'btn btn-sm btn-default'"
callback="trCtrl.deallocate"
item="item"
disabled="isAvailableTable && !item.enabled">
<span class="fa fa-arrow-down"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td colspan="9" class="detail">
<span class="h5" translate>Impact on your quota</span>
<div class="row">
<div class="col-xs-4">
<pie-chart chart-data="item.instancesChartData"
chart-settings="chartSettings"></pie-chart>
</div>
<div class="col-xs-4">
<pie-chart chart-data="item.vcpusChartData"
chart-settings="chartSettings"></pie-chart>
</div>
<div class="col-xs-4">
<pie-chart chart-data="item.ramChartData"
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">
<metadata-display
available="::selectFlavorCtrl.metadataDefs.flavor"
existing="::item.extras">
</metadata-display>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</allocated>
<available>
<hz-magic-search-context filter-facets="selectFlavorCtrl.filterFacets">
<hz-magic-search-bar>
</hz-magic-search-bar>
<table st-magic-search st-table="selectFlavorCtrl.displayedAvailableFlavorFacades"
st-safe-src="selectFlavorCtrl.availableFlavorFacades"
hz-table class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th class="expander"></th>
<th st-sort="name" class="rsp-p1" translate>Name</th>
<th st-sort="vcpus" class="rsp-p1" translate>VCPUS</th>
<th st-sort-default st-sort="ram" class="rsp-p1" translate>RAM</th>
<th st-sort="totalDisk" class="rsp-p1" translate>Total Disk</th>
<th st-sort="rootDisk" class="rsp-p2" translate>Root Disk</th>
<th st-sort="ephemeralDisk" class="rsp-p2" translate>Ephemeral Disk</th>
<th st-sort="isPublic" class="rsp-p1" translate>Public</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-if="selectFlavorCtrl.displayedAvailableFlavorFacades.length === 0">
<td colspan="10">
<div class="no-rows-help">
{$ ::trCtrl.helpText.noneAvailText $}
</div>
</td>
</tr>
<tr ng-repeat-start="item in selectFlavorCtrl.displayedAvailableFlavorFacades track by item.id" ng-if="!trCtrl.allocatedIds[item.id]">
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ ::expandDetailsText $}"></span>
</td>
<td class="rsp-p1 word-break">{$ ::item.name $}</td>
<td class="rsp-p1">
<span class="invalid fa fa-exclamation-triangle"
ng-show="item.errors.vcpus"
uib-popover="{$ item.errors.vcpus $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="'mouseenter'"/>
{$ ::item.vcpus $}
</td>
<td class="rsp-p1">
<span class="invalid fa fa-exclamation-triangle"
ng-show="item.errors.ram"
uib-popover="{$ item.errors.ram $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="'mouseenter'"/>
{$ ::item.ram | mb $}
</td>
<td class="rsp-p1">{$ ::item.totalDisk | gb $}</td>
<td class="rsp-p2">
<span class="invalid fa fa-exclamation-triangle"
ng-show="item.errors.disk"
uib-popover="{$ item.errors.disk $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="'mouseenter'"/>
{$ ::item.rootDisk | gb $}
</td>
<td class="rsp-p2">{$ ::item.ephemeralDisk | gb $}</td>
<td class="rsp-p1">{$ ::item.isPublic | yesno $}</td>
<td class="action-col">
<action-list button-tooltip="item.disabledMessage"
bt-model="tooltipModel"
bt-disabled="!isAvailableTable || item.enabled"
warning-classes="'invalid'">
<notifications>
<span class="fa fa-exclamation-triangle invalid"
ng-show="isAvailableTable && !item.enabled"></span>
</notifications>
<action action-classes="'btn btn-sm btn-default'"
callback="trCtrl.allocate"
item="item"
disabled="isAvailableTable && !item.enabled">
<span class="fa fa-arrow-up"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row" ng-if="!trCtrl.allocatedIds[item.id]">
<td colspan="9" class="detail">
<span class="h5" translate>Impact on your quota</span>
<div class="row">
<div class="col-xs-4">
<pie-chart chart-data="item.instancesChartData"
chart-settings="chartSettings"></pie-chart>
</div>
<div class="col-xs-4">
<pie-chart chart-data="item.vcpusChartData"
chart-settings="chartSettings"></pie-chart>
</div>
<div class="col-xs-4">
<pie-chart chart-data="item.ramChartData"
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">
<metadata-display
available="::selectFlavorCtrl.metadataDefs.flavor"
existing="::item.extras">
</metadata-display>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</hz-magic-search-context>
</available>
</transfer-table>
<transfer-table tr-model="ctrl.tableData"
limits="ctrl.tableLimits" clone-content>
<hz-dynamic-table
config="$isAvailableTable ? ctrl.availableTableConfig : ctrl.allocatedTableConfig"
items="$isAvailableTable ? ($sourceItems | filterAvailable:trCtrl.allocatedIds) : $sourceItems"
validate-number-min="1" ng-model="ctrl.allocatedFlavorFacades.length" name="allocated-flavor"
item-actions="trCtrl.itemActions"
filter-facets="$isAvailableTable && ctrl.filterFacets"
table="ctrl">
</hz-dynamic-table>
</transfer-table> <!-- End Flavors Transfer Table -->
</div>

View File

@ -46,7 +46,7 @@
remainingColorClass: "class3"
};
ctrl = $controller('LaunchInstanceFlavorController as selectFlavorCtrl',
ctrl = $controller('LaunchInstanceFlavorController as ctrl',
{ $scope:scope,
'horizon.framework.widgets.charts.quotaChartDefaults': defaults,
launchInstanceModel: model });
@ -177,7 +177,7 @@
});
});
describe("selectFlavorCtrl.allocatedFlavorFacades", function () {
describe("ctrl.allocatedFlavorFacades", function () {
it("deletes flavor if falsy facade", function () {
model.newInstanceSpec.flavor = "to be removed";
@ -420,9 +420,7 @@
it('initializes empty facades', function () {
expect(ctrl.availableFlavorFacades).toEqual([]);
expect(ctrl.displayedAvailableFlavorFacades).toEqual([]);
expect(ctrl.allocatedFlavorFacades).toEqual([]);
expect(ctrl.displayedAllocatedFlavorFacades).toEqual([]);
});
it('initializes empty flavors', function () {
@ -438,13 +436,11 @@
});
it('initializes transfer table model', function () {
expect(ctrl.transferTableModel).toBeDefined();
var mod = ctrl.transferTableModel;
expect(ctrl.tableData).toBeDefined();
var mod = ctrl.tableData;
expect(mod.allocated).toBe(ctrl.allocatedFlavorFacades);
expect(mod.displayedAllocated).toBe(ctrl.displayedAllocatedFlavorFacades);
expect(mod.available).toBe(ctrl.availableFlavorFacades);
expect(mod.displayedAvailable).toBe(ctrl.displayedAvailableFlavorFacades);
expect(Object.keys(mod).length).toBe(4);
expect(Object.keys(mod).length).toBe(2);
});
it('initializes chart data', function () {
@ -452,8 +448,8 @@
});
it('allows only one allocation', function () {
expect(ctrl.allocationLimits).toBeDefined();
expect(ctrl.allocationLimits.maxAllocation).toBe(1);
expect(ctrl.tableLimits).toBeDefined();
expect(ctrl.tableLimits.maxAllocation).toBe(1);
});
describe('Functions', function () {

View File

@ -464,7 +464,5 @@ class InstanceAvailableResourceMenuRegion(baseregion.BaseRegion):
class InstanceFlavorMenuRegion(InstanceAvailableResourceMenuRegion):
_action_column_btn_locator = (by.By.CSS_SELECTOR, "td.action-col button")
def _get_column_text(self, cols):
return cols[1].text.strip()