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

This commit is contained in:
Zuul 2020-03-08 21:24:39 +00:00 committed by Gerrit Code Review
commit c0a7a43d3f
4 changed files with 119 additions and 296 deletions

View File

@ -1,34 +1,30 @@
<td colspan="{$ ::ctrl.tableHeadCells.length +2 $}" class="detail">
<div class="item">
<dl class="col-xs-4">
<dt translate>Min Disk (GB)</dt>
<dd>
{$ (item.properties ? item.min_disk : item.volume_image_metadata.min_disk) || '--' $}
</dd>
</dl>
<dl class="col-xs-4">
<dt translate>Min RAM (MB)</dt>
<dd>
{$ (item.properties ? item.min_ram : item.volume_image_metadata.min_ram) || '--' $}
</dd>
</dl>
</div>
<div class="row">
<dl class="col-xs-4">
<dt translate>Min Disk (GB)</dt>
<dd>
{$ (row.properties ? row.min_disk : row.volume_image_metadata.min_disk) || '--' $}
</dd>
</dl>
<dl class="col-xs-4">
<dt translate>Min RAM (MB)</dt>
<dd>
{$ (row.properties ? row.min_ram : row.volume_image_metadata.min_ram) || '--' $}
</dd>
</dl>
<div ng-if="model.metadataDefs.image || model.metadataDefs.volume">
<div ng-if="item.properties && model.metadataDefs.image">
<metadata-display
available="::model.metadataDefs.image"
existing="::item.properties">
</metadata-display>
</div>
<div ng-if="model.metadataDefs.image || model.metadataDefs.volume">
<div ng-if="row.properties && model.metadataDefs.image">
<metadata-display
available="::model.metadataDefs.image"
existing="::row.properties">
</metadata-display>
</div>
<div ng-if="row.volume_image_metadata && model.metadataDefs.volume">
<metadata-display
available="::model.metadataDefs.volume"
existing="::row.volume_image_metadata">
</metadata-display>
</div>
<div ng-if="item.volume_image_metadata && model.metadataDefs.volume">
<metadata-display
available="::model.metadataDefs.volume"
existing="::item.volume_image_metadata">
</metadata-display>
</div>
</td>
</div>

View File

@ -33,11 +33,6 @@
LaunchInstanceSourceController.$inject = [
'$scope',
'horizon.dashboard.project.workflow.launch-instance.boot-source-types',
'bytesFilter',
'dateFilter',
'decodeFilter',
'diskFormatFilter',
'gbFilter',
'horizon.dashboard.project.workflow.launch-instance.basePath',
'horizon.framework.widgets.transfer-table.events',
'horizon.framework.widgets.magic-search.events'
@ -45,11 +40,6 @@
function LaunchInstanceSourceController($scope,
bootSourceTypes,
bytesFilter,
dateFilter,
decodeFilter,
diskFormatFilter,
gbFilter,
basePath,
events,
magicSearchEvents
@ -78,8 +68,6 @@
/*
* Transfer table
*/
ctrl.tableHeadCells = [];
ctrl.tableBodyCells = [];
ctrl.tableData = {
available: [],
allocated: selection,
@ -87,7 +75,21 @@
displayedAllocated: []
};
ctrl.helpText = {};
ctrl.sourceDetails = basePath + 'source/source-details.html';
ctrl.availableTableConfig = {
selectAll: false,
trackId: 'id',
detailsTemplateUrl: basePath + 'source/source-details.html',
columns: []
};
ctrl.allocatedTableConfig = angular.copy(ctrl.availableTableConfig);
ctrl.allocatedTableConfig.noItemsMessage = gettext(
'Select an item from Available items below');
ctrl.tableLimits = {
maxAllocation: 1
};
var bootSources = {
image: {
@ -130,35 +132,66 @@
{ label: gettext('VMDK'), key: 'vmdk' }
];
// Mapping for dynamic table headers
var tableHeadCellsMap = {
var diskFormatsObj = diskFormats.reduce(function (acc, cur) {
acc[cur.key] = cur.label;
return acc;
}, {});
function getImageDiskFormat(key) {
return diskFormatsObj[key];
}
function getVolumeDiskFormat(data) {
return diskFormatsObj[data.disk_format];
}
var statuses = [
{ label: gettext('Available'), key: 'available' },
{ label: gettext('Creating'), key: 'creating' },
{ label: gettext('Deleting'), key: 'deleting' },
{ label: gettext('Error'), key: 'error' },
{ label: gettext('Error Deleting'), key: 'error_deleting' }
];
var statusesObj = statuses.reduce(function (acc, cur) {
acc[cur.key] = cur.label;
return acc;
}, {});
function getStatus(status) {
return statusesObj[status];
}
// Mapping for dynamic table columns
var tableColumnsMap = {
image: [
{ text: gettext('Name') },
{ text: gettext('Updated') },
{ text: gettext('Size') },
{ text: gettext('Type') },
{ text: gettext('Visibility') }
{ id: 'name', title: gettext('Name'), priority: 1 },
{ id: 'updated_at', title: gettext('Updated'), filters: ['simpleDate'], priority: 2 },
{ id: 'size', title: gettext('Size'), filters: ['bytes'], priority: 2 },
{ id: 'disk_format', title: gettext('Type'), filters: [getImageDiskFormat], priority: 2 },
{ id: 'visibility', title: gettext('Visibility'), filters: [getVisibility], priority: 2 }
],
snapshot: [
{ text: gettext('Name') },
{ text: gettext('Updated') },
{ text: gettext('Size') },
{ text: gettext('Type') },
{ text: gettext('Visibility') }
{ id: 'name', title: gettext('Name'), priority: 1 },
{ id: 'updated_at', title: gettext('Updated'), filters: ['simpleDate'], priority: 2 },
{ id: 'size', title: gettext('Size'), filters: ['bytes'], priority: 2 },
{ id: 'disk_format', title: gettext('Type'), filters: [getImageDiskFormat], priority: 2 },
{ id: 'visibility', title: gettext('Visibility'), filters: [getVisibility], priority: 2 }
],
volume: [
{ text: gettext('Name') },
{ text: gettext('Description') },
{ text: gettext('Size') },
{ text: gettext('Type') },
{ text: gettext('Availability Zone') }
{ id: 'name', title: gettext('Name'), priority: 1 },
{ id: 'description', title: gettext('Description'), filters: ['noValue'], priority: 2 },
{ id: 'size', title: gettext('Size'), filters: ['gb'], priority: 2 },
{ id: 'volume_image_metadata', title: gettext('Type'),
filters: [getVolumeDiskFormat], priority: 2 },
{ id: 'availability_zone', title: gettext('Availability Zone'), priority: 2 }
],
volume_snapshot: [
{ text: gettext('Name') },
{ text: gettext('Description') },
{ text: gettext('Size') },
{ text: gettext('Created') },
{ text: gettext('Status') }
{ id: 'name', title: gettext('Name'), priority: 1 },
{ id: 'description', title: gettext('Description'), filters: ['noValue'], priority: 2 },
{ id: 'size', title: gettext('Size'), filters: ['gb'], priority: 2 },
{ id: 'created_at', title: gettext('Created'), filters: ['simpleDate'], priority: 2 },
{ id: 'status', title: gettext('Status'), filters: [getStatus], priority: 2 }
]
};
@ -169,37 +202,9 @@
'community': gettext('Community')
};
// Mapping for dynamic table data
var tableBodyCellsMap = {
image: [
{ key: 'name', classList: ['hi-light', 'word-break'] },
{ key: 'updated_at', filter: dateFilter, filterArg: 'short' },
{ key: 'size', filter: bytesFilter, classList: ['number'] },
{ key: 'disk_format', filter: diskFormatFilter, filterRawData: true },
{ key: 'visibility', filter: decodeFilter, filterArg: _visibilitymap }
],
snapshot: [
{ key: 'name', classList: ['hi-light', 'word-break'] },
{ key: 'updated_at', filter: dateFilter, filterArg: 'short' },
{ key: 'size', filter: bytesFilter, classList: ['number'] },
{ key: 'disk_format', filter: diskFormatFilter, filterRawData: true },
{ key: 'visibility', filter: decodeFilter, filterArg: _visibilitymap }
],
volume: [
{ key: 'name', classList: ['hi-light', 'word-break'] },
{ key: 'description' },
{ key: 'size', filter: gbFilter, classList: ['number'] },
{ key: 'volume_image_metadata', filter: diskFormatFilter },
{ key: 'availability_zone' }
],
volume_snapshot: [
{ key: 'name', classList: ['hi-light', 'word-break'] },
{ key: 'description' },
{ key: 'size', filter: gbFilter, classList: ['number'] },
{ key: 'created_at', filter: dateFilter, filterArg: 'short' },
{ key: 'status' }
]
};
function getVisibility(visibility) {
return _visibilitymap[visibility];
}
/**
* Creates a map of functions that sort by the key at a given index for
@ -208,8 +213,8 @@
ctrl.sortByField = [];
var sortFunction = function(columnIndex, comparedObject) {
var cell = tableBodyCellsMap[ctrl.currentBootSource];
var key = cell[columnIndex].key;
var cell = tableColumnsMap[ctrl.currentBootSource];
var key = cell[columnIndex].id;
return comparedObject[key];
};
@ -257,13 +262,7 @@
label: gettext('Status'),
name: 'status',
singleton: true,
options: [
{ label: gettext('Available'), key: 'available' },
{ label: gettext('Creating'), key: 'creating' },
{ label: gettext('Deleting'), key: 'deleting' },
{ label: gettext('Error'), key: 'error' },
{ label: gettext('Error Deleting'), key: 'error_deleting' }
]
options: statuses
},
type: {
label: gettext('Type'),
@ -479,8 +478,7 @@
function changeBootSource(key, preSelection) {
updateDataSource(key, preSelection);
updateHelpText(key);
updateTableHeadCells(key);
updateTableBodyCells(key);
updateTableColumns(key);
updateFacets(key);
}
@ -502,12 +500,9 @@
});
}
function updateTableHeadCells(key) {
refillArray(ctrl.tableHeadCells, tableHeadCellsMap[key]);
}
function updateTableBodyCells(key) {
refillArray(ctrl.tableBodyCells, tableBodyCellsMap[key]);
function updateTableColumns(key) {
refillArray(ctrl.availableTableConfig.columns, tableColumnsMap[key]);
refillArray(ctrl.allocatedTableConfig.columns, tableColumnsMap[key]);
}
function updateFacets(key) {

View File

@ -82,10 +82,10 @@
it('initializes transfer table variables', function() {
// NOTE: these are set by the default, not the initial values.
// Arguably we shouldn't even set the original values.
expect(ctrl.tableHeadCells).toBeDefined();
expect(ctrl.tableHeadCells.length).toEqual(5);
expect(ctrl.tableBodyCells).toBeDefined();
expect(ctrl.tableBodyCells.length).toEqual(5);
expect(ctrl.availableTableConfig.columns).toBeDefined();
expect(ctrl.availableTableConfig.columns.length).toEqual(5);
expect(ctrl.allocatedTableConfig.columns).toBeDefined();
expect(ctrl.allocatedTableConfig.columns.length).toEqual(5);
expect(ctrl.tableData).toBeDefined();
expect(Object.keys(ctrl.tableData).length).toEqual(4);
expect(ctrl.helpText).toBeDefined();
@ -291,8 +291,8 @@
// check table data
expect(ctrl.tableData).toBeDefined();
expect(Object.keys(ctrl.tableData)).toEqual(tableKeys);
expect(ctrl.tableHeadCells.length).toBeGreaterThan(0);
expect(ctrl.tableBodyCells.length).toBeGreaterThan(0);
expect(ctrl.availableTableConfig.columns.length).toBeGreaterThan(0);
expect(ctrl.allocatedTableConfig.columns.length).toBeGreaterThan(0);
});
it('updates the scope appropriately, with Cinder available', function() {

View File

@ -101,184 +101,16 @@
type="text">
</div>
<transfer-table help-text="ctrl.helpText"
tr-model="ctrl.tableData">
<allocated validate-number-min="1"
ng-model="ctrl.tableData.allocated.length">
<table class="table table-striped table-rsp table-detail modern"
hz-table
st-safe-src="ctrl.tableData.allocated"
st-table="ctrl.tableData.displayAllocated">
<!-- transfer table, allocated table head -->
<thead>
<tr>
<th class="expander"></th>
<th ng-class="ctrl.tableHeadCells[0].classList">
{$ ctrl.tableHeadCells[0].text $}
</th>
<th ng-class="ctrl.tableHeadCells[1].classList">
{$ ctrl.tableHeadCells[1].text $}
</th>
<th ng-class="ctrl.tableHeadCells[2].classList">
{$ ctrl.tableHeadCells[2].text $}
</th>
<th ng-class="ctrl.tableHeadCells[3].classList">
{$ ctrl.tableHeadCells[3].text $}
</th>
<th ng-class="ctrl.tableHeadCells[4].classList">
{$ ctrl.tableHeadCells[4].text $}
</th>
<th class="action"></th>
</tr>
</thead><!-- /transfer table, allocated table head -->
<!-- transfer table, allocated table body -->
<tbody>
<tr ng-if="ctrl.tableData.allocated.length === 0">
<td colspan="{$ ctrl.tableHeadCells.length + 2 $}">
<div class="no-rows-help">
{$ ::trCtrl.helpText.noneAllocText $}
</div>
</td>
</tr>
<tr ng-repeat-start="row in ctrl.selection">
<td class="expander">
<span class="fa fa-chevron-right"
hz-expand-detail
title="{$ ::trCtrl.helpText.expandDetailsText $}"></span>
</td>
<td ng-class="ctrl.tableBodyCells[0].classList">
{$ ctrl.tableBodyCells[0].filter ? ctrl.tableBodyCells[0].filter(row[ctrl.tableBodyCells[0].key], ctrl.tableBodyCells[0].filterArg) : row[ctrl.tableBodyCells[0].key] $}
</td>
<td ng-class="ctrl.tableBodyCells[1].classList">
{$ ctrl.tableBodyCells[1].filter ? ctrl.tableBodyCells[1].filter(row[ctrl.tableBodyCells[1].key], ctrl.tableBodyCells[1].filterArg) : row[ctrl.tableBodyCells[1].key] $}
</td>
<td ng-class="ctrl.tableBodyCells[2].classList">
{$ ctrl.tableBodyCells[2].filter ? ctrl.tableBodyCells[2].filter(row[ctrl.tableBodyCells[2].key], ctrl.tableBodyCells[2].filterArg) : row[ctrl.tableBodyCells[2].key] $}
</td>
<td ng-class="ctrl.tableBodyCells[3].classList">
{$ ctrl.tableBodyCells[3].filter ? ctrl.tableBodyCells[3].filter(ctrl.tableBodyCells[3].filterRawData ? row : row[ctrl.tableBodyCells[3].key], ctrl.tableBodyCells[3].filterArg) : row[ctrl.tableBodyCells[3].key] $}
</td>
<td ng-class="ctrl.tableBodyCells[4].classList">
<span ng-if="model.newInstanceSpec.source_type.type === 'volume' && row.availability_zone !== model.newInstanceSpec.availability_zone"
class="invalid fa fa-exclamation-triangle"
uib-popover="{$ ::trCtrl.helpText.volumeAZHelpText $}"
popover-trigger="'mouseenter'"
popover-append-to-body="true"
popover-placement="top"></span>
{$ ctrl.tableBodyCells[4].filter ? ctrl.tableBodyCells[4].filter(row[ctrl.tableBodyCells[4].key], ctrl.tableBodyCells[4].filterArg) : row[ctrl.tableBodyCells[4].key] $}
</td>
<td class="actions_column">
<action-list>
<action action-classes="'btn btn-default'"
callback="trCtrl.deallocate"
item="row">
<span class="fa fa-arrow-down"></span>
</action>
</action-list>
</td>
</tr>
<tr class="detail-row"
ng-repeat-end
ng-include="ctrl.sourceDetails">
</tr>
</tbody>
</table>
</allocated>
<available>
<hz-magic-search-context filter-facets="ctrl.sourceFacets">
<hz-magic-search-bar>
</hz-magic-search-bar>
<table st-table="ctrl.tableData.displayedAvailable"
st-safe-src="ctrl.tableData.available"
hz-table
st-magic-search
class="table table-striped table-rsp table-detail modern">
<!-- transfer table, available table head -->
<thead>
<tr>
<th class="expander"></th>
<th st-sort="ctrl.sortByField[0]">
{$ ctrl.tableHeadCells[0].text $}
</th>
<th st-sort="ctrl.sortByField[1]">
{$ ctrl.tableHeadCells[1].text $}
</th>
<th st-sort="ctrl.sortByField[2]">
{$ ctrl.tableHeadCells[2].text $}
</th>
<th st-sort="ctrl.sortByField[3]">
{$ ctrl.tableHeadCells[3].text $}
</th>
<th st-sort="ctrl.sortByField[4]">
{$ ctrl.tableHeadCells[4].text $}
</th>
<th class="action"></th>
</tr>
</thead>
<tbody>
<tr ng-if="trCtrl.numAvailable() === 0">
<td colspan="{$ ctrl.tableHeadCells.length + 2 $}">
<div class="no-rows-help">
{$ ::trCtrl.helpText.noneAvailText $}
</div>
</td>
</tr>
<tr ng-repeat-start="row in ctrl.tableData.displayedAvailable track by row.id"
ng-if="!trCtrl.allocatedIds[row.id]">
<td class="expander">
<span class="fa fa-chevron-right"
hz-expand-detail
title="{$ ::trCtrl.helpText.expandDetailsText $}">
</span>
</td>
<td ng-class="ctrl.tableBodyCells[0].classList">
{$ ctrl.tableBodyCells[0].filter ? ctrl.tableBodyCells[0].filter(row[ctrl.tableBodyCells[0].key], ctrl.tableBodyCells[0].filterArg) : row[ctrl.tableBodyCells[0].key] $}
</td>
<td ng-class="ctrl.tableBodyCells[1].classList">
{$ ctrl.tableBodyCells[1].filter ? ctrl.tableBodyCells[1].filter(row[ctrl.tableBodyCells[1].key], ctrl.tableBodyCells[1].filterArg) : row[ctrl.tableBodyCells[1].key] $}
</td>
<td ng-class="ctrl.tableBodyCells[2].classList">
{$ ctrl.tableBodyCells[2].filter ? ctrl.tableBodyCells[2].filter(row[ctrl.tableBodyCells[2].key], ctrl.tableBodyCells[2].filterArg) : row[ctrl.tableBodyCells[2].key] $}
</td>
<td ng-class="ctrl.tableBodyCells[3].classList">
{$ ctrl.tableBodyCells[3].filter ? ctrl.tableBodyCells[3].filter(ctrl.tableBodyCells[3].filterRawData ? row : row[ctrl.tableBodyCells[3].key], ctrl.tableBodyCells[3].filterArg) : row[ctrl.tableBodyCells[3].key] $}
</td>
<td ng-class="ctrl.tableBodyCells[4].classList">
<span ng-if="model.newInstanceSpec.source_type.type === 'volume' && row.availability_zone !== model.newInstanceSpec.availability_zone"
class="invalid fa fa-exclamation-triangle"
uib-popover="{$ ::trCtrl.helpText.volumeAZHelpText $}"
popover-trigger="'mouseenter'"
popover-append-to-body="true"
popover-placement="top"></span>
{$ ctrl.tableBodyCells[4].filter ? ctrl.tableBodyCells[4].filter(row[ctrl.tableBodyCells[4].key], ctrl.tableBodyCells[4].filterArg) : row[ctrl.tableBodyCells[4].key] $}
</td>
<td class="actions_column">
<action-list>
<action action-classes="'btn btn-default'"
callback="trCtrl.allocate"
item="row">
<span class="fa fa-arrow-up"></span>
</action>
</action-list>
</td>
</tr>
<tr class="detail-row"
ng-repeat-end
ng-include="ctrl.sourceDetails"
ng-if="!trCtrl.allocatedIds[row.id]">
</tr>
</tbody>
</table>
</hz-magic-search-context>
</available>
<transfer-table help-text="ctrl.helpText" tr-model="ctrl.tableData" limits="ctrl.tableLimits" clone-content>
<hz-dynamic-table
config="$isAvailableTable ? ctrl.availableTableConfig : ctrl.allocatedTableConfig"
items="$isAvailableTable ? (ctrl.tableData.available | filterAvailable:trCtrl.allocatedIds) : $sourceItems"
item-actions="trCtrl.itemActions"
filter-facets="$isAvailableTable && ctrl.sourceFacets"
table="ctrl">
</hz-dynamic-table>
</transfer-table>
</div>
<div ng-if="model.allowedBootSources.length === 0">
<div translate class="subtitle text-danger">There are no allowed boot