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
openstack_dashboard
dashboards/project/static/dashboard/project/workflow/launch-instance/flavor
test/integration_tests/regions

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

@ -22,11 +22,17 @@
LaunchInstanceFlavorController.$inject = [ LaunchInstanceFlavorController.$inject = [
'$scope', '$scope',
'horizon.dashboard.project.workflow.launch-instance.basePath',
'horizon.framework.widgets.charts.quotaChartDefaults', 'horizon.framework.widgets.charts.quotaChartDefaults',
'launchInstanceModel' 'launchInstanceModel'
]; ];
function LaunchInstanceFlavorController($scope, quotaChartDefaults, launchInstanceModel) { function LaunchInstanceFlavorController(
$scope,
basePath,
quotaChartDefaults,
launchInstanceModel
) {
var ctrl = this; var ctrl = this;
ctrl.defaultIfUndefined = defaultIfUndefined; ctrl.defaultIfUndefined = defaultIfUndefined;
@ -84,9 +90,7 @@
* exposing only the data needed by this specific view. * exposing only the data needed by this specific view.
*/ */
ctrl.availableFlavorFacades = []; ctrl.availableFlavorFacades = [];
ctrl.displayedAvailableFlavorFacades = [];
ctrl.allocatedFlavorFacades = []; ctrl.allocatedFlavorFacades = [];
ctrl.displayedAllocatedFlavorFacades = [];
// Convenience references to launch instance model elements // Convenience references to launch instance model elements
ctrl.flavors = []; ctrl.flavors = [];
@ -95,18 +99,55 @@
ctrl.instanceCount = 1; ctrl.instanceCount = 1;
// Data that drives the transfer table for flavors // Data that drives the transfer table for flavors
ctrl.transferTableModel = { ctrl.tableData = {
allocated: ctrl.allocatedFlavorFacades, allocated: ctrl.allocatedFlavorFacades,
displayedAllocated: ctrl.displayedAllocatedFlavorFacades, available: ctrl.availableFlavorFacades,
available: ctrl.availableFlavorFacades,
displayedAvailable: ctrl.displayedAvailableFlavorFacades
}; };
// 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 // Each flavor has an instances chart...but it is the same for all flavors
ctrl.instancesChartData = {}; ctrl.instancesChartData = {};
// We can pick at most, 1 flavor at a time // We can pick at most, 1 flavor at a time
ctrl.allocationLimits = { ctrl.tableLimits = {
maxAllocation: 1 maxAllocation: 1
}; };
@ -114,18 +155,22 @@
var novaLimitsWatcher = $scope.$watch(function () { var novaLimitsWatcher = $scope.$watch(function () {
return launchInstanceModel.novaLimits; return launchInstanceModel.novaLimits;
}, function (newValue, oldValue, scope) { }, function (newValue, oldValue, scope) {
var ctrl = scope.selectFlavorCtrl; var ctrl = scope.ctrl;
ctrl.novaLimits = newValue; ctrl.novaLimits = newValue;
ctrl.updateFlavorFacades(); if (!angular.equals(newValue, oldValue)) {
ctrl.updateFlavorFacades();
}
}, true); }, true);
// Flavor facades depend on flavors // Flavor facades depend on flavors
var flavorsWatcher = $scope.$watchCollection(function() { var flavorsWatcher = $scope.$watchCollection(function() {
return launchInstanceModel.flavors; return launchInstanceModel.flavors;
}, function (newValue, oldValue, scope) { }, function (newValue, oldValue, scope) {
var ctrl = scope.selectFlavorCtrl; var ctrl = scope.ctrl;
ctrl.flavors = newValue; ctrl.flavors = newValue;
ctrl.updateFlavorFacades(); if (!angular.equals(newValue, oldValue)) {
ctrl.updateFlavorFacades();
}
}); });
// Flavor quota charts depend on the current instance count // Flavor quota charts depend on the current instance count
@ -133,7 +178,7 @@
return launchInstanceModel.newInstanceSpec.instance_count; return launchInstanceModel.newInstanceSpec.instance_count;
}, function (newValue, oldValue, scope) { }, function (newValue, oldValue, scope) {
if (angular.isDefined(newValue)) { if (angular.isDefined(newValue)) {
var ctrl = scope.selectFlavorCtrl; var ctrl = scope.ctrl;
// Ignore any values <1 // Ignore any values <1
ctrl.instanceCount = Math.max(1, newValue); ctrl.instanceCount = Math.max(1, newValue);
ctrl.updateFlavorFacades(); ctrl.updateFlavorFacades();
@ -143,13 +188,15 @@
// Update the new instance model when the allocated flavor changes // Update the new instance model when the allocated flavor changes
var facadesWatcher = $scope.$watchCollection( var facadesWatcher = $scope.$watchCollection(
"selectFlavorCtrl.allocatedFlavorFacades", "ctrl.allocatedFlavorFacades",
function (newValue, oldValue, scope) { function (newValue, oldValue, scope) {
if (newValue && newValue.length > 0) { if (!angular.equals(newValue, oldValue)) {
launchInstanceModel.newInstanceSpec.flavor = newValue[0].flavor; if (newValue && newValue.length > 0) {
scope.selectFlavorCtrl.validateFlavor(); launchInstanceModel.newInstanceSpec.flavor = newValue[0].flavor;
} else { scope.ctrl.validateFlavor();
delete launchInstanceModel.newInstanceSpec.flavor; } else {
delete launchInstanceModel.newInstanceSpec.flavor;
}
} }
} }
); );
@ -157,18 +204,22 @@
var sourceWatcher = $scope.$watchCollection(function() { var sourceWatcher = $scope.$watchCollection(function() {
return launchInstanceModel.newInstanceSpec.source; return launchInstanceModel.newInstanceSpec.source;
}, function (newValue, oldValue, scope) { }, function (newValue, oldValue, scope) {
var ctrl = scope.selectFlavorCtrl; var ctrl = scope.ctrl;
ctrl.source = newValue && newValue.length ? newValue[0] : null; ctrl.source = newValue && newValue.length ? newValue[0] : null;
ctrl.updateFlavorFacades(); if (!angular.equals(newValue, oldValue)) {
ctrl.updateFlavorFacades();
}
ctrl.validateFlavor(); ctrl.validateFlavor();
}); });
var cinderLimitsWatcher = $scope.$watch(function () { var cinderLimitsWatcher = $scope.$watch(function () {
return launchInstanceModel.cinderLimits; return launchInstanceModel.cinderLimits;
}, function (newValue, oldValue, scope) { }, function (newValue, oldValue, scope) {
var ctrl = scope.selectFlavorCtrl; var ctrl = scope.ctrl;
ctrl.cinderLimits = newValue; ctrl.cinderLimits = newValue;
ctrl.updateFlavorFacades(); if (!angular.equals(newValue, oldValue)) {
ctrl.updateFlavorFacades();
}
}, true); }, true);
var volumeSizeWatcher = $scope.$watchCollection(function () { var volumeSizeWatcher = $scope.$watchCollection(function () {

@ -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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<div ng-controller="LaunchInstanceFlavorController as selectFlavorCtrl"> <div ng-controller="LaunchInstanceFlavorController as ctrl">
<p class="step-description" translate> <p class="step-description" translate>
Flavors manage the sizing for the compute, memory and storage capacity of the instance. Flavors manage the sizing for the compute, memory and storage capacity of the instance.
</p> </p>
<transfer-table
tr-model="selectFlavorCtrl.transferTableModel" <transfer-table tr-model="ctrl.tableData"
limits="selectFlavorCtrl.allocationLimits"> limits="ctrl.tableLimits" clone-content>
<allocated ng-model="selectFlavorCtrl.allocatedFlavorFacades.length" <hz-dynamic-table
validate-number-min="1" name="allocated-flavor"> config="$isAvailableTable ? ctrl.availableTableConfig : ctrl.allocatedTableConfig"
<table st-magic-search st-table="selectFlavorCtrl.displayedAllocatedFlavorFacades" items="$isAvailableTable ? ($sourceItems | filterAvailable:trCtrl.allocatedIds) : $sourceItems"
st-safe-src="selectFlavorCtrl.allocatedFlavorFacades" validate-number-min="1" ng-model="ctrl.allocatedFlavorFacades.length" name="allocated-flavor"
hz-table class="table table-striped table-rsp table-detail"> item-actions="trCtrl.itemActions"
<thead> filter-facets="$isAvailableTable && ctrl.filterFacets"
<tr> table="ctrl">
<th class="expander"></th> </hz-dynamic-table>
<th st-sort="name" class="rsp-p1" translate>Name</th> </transfer-table> <!-- End Flavors Transfer Table -->
<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>
</div> </div>

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

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