From 314d31378ca2116a9c7f90e5fc2a35443adf6160 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Fri, 20 Sep 2019 16:27:39 +0200 Subject: [PATCH] mgr/dashboard: Use checkbox for multi selection. Note, the 'multiClick' selection mode does not render a checkbox in the header column, this is only available in 'checkbox' mode, but this mode only allows the selection of a row by clicking the checkbox, it's not possible to select a row by clicking somewhere within it. Because of that the 'multiClick' mode is preferred. Signed-off-by: Volker Theile --- .../dashboard/frontend/e2e/page-helper.po.ts | 9 +++ .../frontend/e2e/rgw/buckets.e2e-spec.ts | 55 +++++++++++-------- .../dashboard/frontend/e2e/rgw/buckets.po.ts | 10 ++-- .../frontend/e2e/rgw/users.e2e-spec.ts | 17 +++--- .../dashboard/frontend/e2e/rgw/users.po.ts | 18 +++--- .../osd/osd-list/osd-list.component.html | 2 +- .../osd/osd-list/osd-list.component.spec.ts | 6 +- .../rgw-bucket-list.component.html | 2 +- .../rgw-bucket-list.component.spec.ts | 6 +- .../rgw-bucket-list.component.ts | 7 ++- .../rgw-user-list.component.html | 2 +- .../rgw-user-list.component.spec.ts | 6 +- .../rgw-user-list/rgw-user-list.component.ts | 7 ++- .../datatable/table/table.component.html | 3 - .../datatable/table/table.component.scss | 4 ++ .../shared/datatable/table/table.component.ts | 20 +++++++ 16 files changed, 110 insertions(+), 64 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts index c19aefaaa8b10..e142d869e122e 100644 --- a/src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts +++ b/src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts @@ -289,4 +289,13 @@ export abstract class PageHelper { getTableRows() { return $$('datatable-row-wrapper'); } + + /** + * Uncheck all checked table rows. + */ + async uncheckAllTableRows() { + await $$('.datatable-body-cell-label .datatable-checkbox input[type=checkbox]:checked').each( + (e: ElementFinder) => e.click() + ); + } } diff --git a/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.e2e-spec.ts index a7e127f1e2e96..8dbb80ead3e14 100644 --- a/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.e2e-spec.ts @@ -3,6 +3,7 @@ import { BucketsPageHelper } from './buckets.po'; describe('RGW buckets page', () => { let buckets: BucketsPageHelper; + const bucket_name = '000test'; beforeAll(async () => { buckets = new BucketsPageHelper(); @@ -12,35 +13,44 @@ describe('RGW buckets page', () => { await BucketsPageHelper.checkConsole(); }); - it('should open and show breadcrumb', async () => { - await buckets.navigateTo(); - await expect($('.breadcrumb-item.active').getText()).toBe('Buckets'); - }); + describe('breadcrumb tests', () => { + beforeEach(async () => { + await buckets.navigateTo(); + }); - it('should create bucket', async () => { - await buckets.navigateTo('create'); - await buckets.create( - '000test', - '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', - 'default-placement' - ); - await expect(buckets.getFirstTableCellWithText('000test').isPresent()).toBe(true); + it('should open and show breadcrumb', async () => { + await expect($('.breadcrumb-item.active').getText()).toBe('Buckets'); + }); }); - it('should edit bucket', async () => { - await buckets.navigateTo(); - await buckets.edit('000test', 'dev'); - await expect(buckets.getTable().getText()).toMatch('dev'); - }); + describe('create, edit & delete bucket tests', () => { + beforeEach(async () => { + await buckets.navigateTo(); + await buckets.uncheckAllTableRows(); + }); + + it('should create bucket', async () => { + await buckets.navigateTo('create'); + await buckets.create( + bucket_name, + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + 'default-placement' + ); + await expect(buckets.getFirstTableCellWithText(bucket_name).isPresent()).toBe(true); + }); + + it('should edit bucket', async () => { + await buckets.edit(bucket_name, 'dev'); + await expect(buckets.getTable().getText()).toMatch('dev'); + }); - it('should delete bucket', async () => { - await buckets.navigateTo(); - await buckets.delete('000test'); + it('should delete bucket', async () => { + await buckets.delete(bucket_name); + }); }); describe('Invalid Input in Create and Edit tests', () => { it('should test invalid inputs in create fields', async () => { - await buckets.navigateTo('create'); await buckets.testInvalidCreate(); }); @@ -51,10 +61,9 @@ describe('RGW buckets page', () => { '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 'default-placement' ); - await buckets.testInvalidEdit('000rq'); - await buckets.navigateTo(); + await buckets.uncheckAllTableRows(); await buckets.delete('000rq'); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.po.ts index b2cd3c45ad62f..cf3f604560363 100644 --- a/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.po.ts +++ b/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.po.ts @@ -44,7 +44,7 @@ export class BucketsPageHelper extends PageHelper { @PageHelper.restrictTo(pages.index) async edit(name: string, new_owner: string) { - await this.getFirstTableCellWithText(name).click(); // click on the bucket you want to edit in the table + await this.waitClickableAndClick(this.getFirstTableCellWithText(name)); // wait for table to load and click await element(by.cssContainingText('button', 'Edit')).click(); // click button to move to edit page await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit'); await expect(element(by.css('input[name=placement-target]')).getAttribute('value')).toBe( @@ -89,7 +89,8 @@ export class BucketsPageHelper extends PageHelper { await expect(versioningValueCell.getText()).toEqual(this.versioningStateEnabled); // Disable versioning: - await this.getFirstTableCellWithText(name).click(); // click on the bucket you want to edit in the table + await this.uncheckAllTableRows(); + await this.waitClickableAndClick(this.getFirstTableCellWithText(name)); // wait for table to load and click await element(by.cssContainingText('button', 'Edit')).click(); // click button to move to edit page await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit'); await element(by.css('input[id=suspended]')).click(); @@ -184,10 +185,7 @@ export class BucketsPageHelper extends PageHelper { async testInvalidEdit(name) { await this.navigateTo(); - await this.waitClickableAndClick( - this.getFirstTableCellWithText(name), - 'Failed waiting for bucket to be present in table' - ); // wait for table to load + await this.waitClickableAndClick(this.getFirstTableCellWithText(name)); // wait for table to load and click await element(by.cssContainingText('button', 'Edit')).click(); // click button to move to edit page await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit'); diff --git a/src/pybind/mgr/dashboard/frontend/e2e/rgw/users.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/rgw/users.e2e-spec.ts index 4caaa4df17587..2e93046e0dc6b 100644 --- a/src/pybind/mgr/dashboard/frontend/e2e/rgw/users.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/e2e/rgw/users.e2e-spec.ts @@ -12,8 +12,8 @@ describe('RGW users page', () => { await UsersPageHelper.checkConsole(); }); - describe('breadcrumb test', () => { - beforeAll(async () => { + describe('breadcrumb tests', () => { + beforeEach(async () => { await users.navigateTo(); }); @@ -22,19 +22,20 @@ describe('RGW users page', () => { }); }); - describe('create, edit & delete user test', () => { - beforeAll(async () => { + describe('create, edit & delete user tests', () => { + beforeEach(async () => { await users.navigateTo(); + await users.uncheckAllTableRows(); }); it('should create user', async () => { + await users.navigateTo('create'); await users.create(user_name, 'Some Name', 'original@website.com', '1200'); await expect(users.getFirstTableCellWithText(user_name).isPresent()).toBe(true); }); it('should edit users full name, email and max buckets', async () => { await users.edit(user_name, 'Another Identity', 'changed@othersite.com', '1969'); - // checks for succsessful editing are done within edit function }); it('should delete user', async () => { @@ -42,11 +43,7 @@ describe('RGW users page', () => { }); }); - describe('Invalid input test', () => { - beforeAll(async () => { - await users.navigateTo(); - }); - + describe('Invalid input tests', () => { it('should put invalid input into user creation form and check fields are marked invalid', async () => { await users.invalidCreate(); }); diff --git a/src/pybind/mgr/dashboard/frontend/e2e/rgw/users.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/rgw/users.po.ts index fc2a6d6c5840e..b742aa84a0eb2 100644 --- a/src/pybind/mgr/dashboard/frontend/e2e/rgw/users.po.ts +++ b/src/pybind/mgr/dashboard/frontend/e2e/rgw/users.po.ts @@ -2,15 +2,16 @@ import { $, by, element } from 'protractor'; import { protractor } from 'protractor/built/ptor'; import { PageHelper } from '../page-helper.po'; +const pages = { + index: '/#/rgw/user', + create: '/#/rgw/user/create' +}; + export class UsersPageHelper extends PageHelper { - pages = { - index: '/#/rgw/user', - create: '/#/rgw/user/create' - }; + pages = pages; + @PageHelper.restrictTo(pages.create) async create(username, fullname, email, maxbuckets) { - await this.navigateTo('create'); - // Enter in username await element(by.id('uid')).sendKeys(username); @@ -32,9 +33,8 @@ export class UsersPageHelper extends PageHelper { await this.waitPresence(this.getFirstTableCellWithText(username)); } + @PageHelper.restrictTo(pages.index) async edit(name, new_fullname, new_email, new_maxbuckets) { - await this.navigateTo(); - await this.waitClickableAndClick(this.getFirstTableCellWithText(name)); // wait for table to load and click await element(by.cssContainingText('button', 'Edit')).click(); // click button to move to edit page @@ -67,6 +67,7 @@ export class UsersPageHelper extends PageHelper { async invalidCreate() { const uname = '000invalid_create_user'; // creating this user in order to check that you can't give two users the same name + await this.navigateTo('create'); await this.create(uname, 'xxx', 'xxx@xxx', '1'); await this.navigateTo('create'); @@ -134,6 +135,7 @@ export class UsersPageHelper extends PageHelper { async invalidEdit() { const uname = '000invalid_edit_user'; // creating this user to edit for the test + await this.navigateTo('create'); await this.create(uname, 'xxx', 'xxx@xxx', '1'); await this.navigateTo(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html index 6fed099f7908e..1bada27a7252f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html @@ -5,7 +5,7 @@ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts index a26a34d713ad1..faec289fc7f3a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts @@ -113,7 +113,11 @@ describe('OsdListComponent', () => { it('should have columns that are sortable', () => { fixture.detectChanges(); - expect(component.columns.every((column) => Boolean(column.prop))).toBeTruthy(); + expect( + component.columns + .filter((column) => !column.checkboxable) + .every((column) => Boolean(column.prop)) + ).toBeTruthy(); }); describe('getOsdList', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html index 3dcff15fcacfe..37797e96e0039 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html @@ -3,7 +3,7 @@ [data]="buckets" [columns]="columns" columnMode="flex" - selectionType="multi" + selectionType="multiClick" (updateSelection)="updateSelection($event)" identifier="bid" (fetchData)="getBucketList($event)"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts index bb8fcf71918f8..3f0bf0bcca526 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts @@ -50,7 +50,7 @@ describe('RgwBucketListComponent', () => { expect(tableActions).toEqual({ 'create,update,delete': { actions: ['Create', 'Edit', 'Delete'], - primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' } + primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Create' } }, 'create,update': { actions: ['Create', 'Edit'], @@ -58,7 +58,7 @@ describe('RgwBucketListComponent', () => { }, 'create,delete': { actions: ['Create', 'Delete'], - primary: { multiple: 'Create', executing: 'Delete', single: 'Delete', no: 'Create' } + primary: { multiple: 'Delete', executing: 'Create', single: 'Create', no: 'Create' } }, create: { actions: ['Create'], @@ -66,7 +66,7 @@ describe('RgwBucketListComponent', () => { }, 'update,delete': { actions: ['Edit', 'Delete'], - primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' } + primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Edit' } }, update: { actions: ['Edit'], diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts index 02065b5ddeca9..24050ebb74aa4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts @@ -62,7 +62,8 @@ export class RgwBucketListComponent { permission: 'create', icon: Icons.add, routerLink: () => this.urlBuilder.getCreate(), - name: this.actionLabels.CREATE + name: this.actionLabels.CREATE, + canBePrimary: (selection: CdTableSelection) => !selection.hasSelection }; const editAction: CdTableAction = { permission: 'update', @@ -74,7 +75,9 @@ export class RgwBucketListComponent { permission: 'delete', icon: Icons.destroy, click: () => this.deleteAction(), - name: this.actionLabels.DELETE + disable: () => !this.selection.hasSelection, + name: this.actionLabels.DELETE, + canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection }; this.tableActions = [addAction, editAction, deleteAction]; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html index 8bb40fc9d6386..a154f1182e34a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html @@ -3,7 +3,7 @@ [data]="users" [columns]="columns" columnMode="flex" - selectionType="multi" + selectionType="multiClick" (updateSelection)="updateSelection($event)" identifier="uid" (fetchData)="getUserList($event)"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts index 431bd4696782d..4c41b420bb4dd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts @@ -44,7 +44,7 @@ describe('RgwUserListComponent', () => { expect(tableActions).toEqual({ 'create,update,delete': { actions: ['Create', 'Edit', 'Delete'], - primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' } + primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Create' } }, 'create,update': { actions: ['Create', 'Edit'], @@ -52,7 +52,7 @@ describe('RgwUserListComponent', () => { }, 'create,delete': { actions: ['Create', 'Delete'], - primary: { multiple: 'Create', executing: 'Delete', single: 'Delete', no: 'Create' } + primary: { multiple: 'Delete', executing: 'Create', single: 'Create', no: 'Create' } }, create: { actions: ['Create'], @@ -60,7 +60,7 @@ describe('RgwUserListComponent', () => { }, 'update,delete': { actions: ['Edit', 'Delete'], - primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' } + primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Edit' } }, update: { actions: ['Edit'], diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts index be0c6377f888a..5b388049dd293 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts @@ -80,7 +80,8 @@ export class RgwUserListComponent { permission: 'create', icon: Icons.add, routerLink: () => this.urlBuilder.getCreate(), - name: this.actionLabels.CREATE + name: this.actionLabels.CREATE, + canBePrimary: (selection: CdTableSelection) => !selection.hasSelection }; const editAction: CdTableAction = { permission: 'update', @@ -92,7 +93,9 @@ export class RgwUserListComponent { permission: 'delete', icon: Icons.destroy, click: () => this.deleteAction(), - name: this.actionLabels.DELETE + disable: () => !this.selection.hasSelection, + name: this.actionLabels.DELETE, + canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection }; this.tableActions = [addAction, editAction, deleteAction]; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html index c7a8f894af06a..6042330023df6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html @@ -119,9 +119,6 @@ let-offset="offset" let-isVisible="isVisible">
- - Press and hold control button to select multiple rows to execute a common action on. - {{ selectedCount }} selected / diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss index 5a6e0d4db77e5..2dd838528d27d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss @@ -255,6 +255,10 @@ } } } + + .cd-datatable-checkbox { + text-align: center; + } } @keyframes progress-loading { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts index 997fcf811e3e0..bd00af4e7e692 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts @@ -207,6 +207,8 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O c.resizeable = false; } }); + + this.initCheckboxColumn(); this.filterHiddenColumns(); // Load the data table content every N ms or at least once. // Force showing the loading indicator if there are subscribers to the fetchData @@ -303,6 +305,24 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O })); } + /** + * Add a column containing a checkbox if selectionType is 'multiClick'. + */ + initCheckboxColumn() { + if (this.selectionType === 'multiClick') { + this.columns.unshift({ + prop: undefined, + resizeable: false, + sortable: false, + draggable: false, + checkboxable: true, + canAutoResize: false, + cellClass: 'cd-datatable-checkbox', + width: 30 + }); + } + } + filterHiddenColumns() { this.tableColumns = this.columns.filter((c) => !c.isHidden); } -- 2.39.5