]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Use checkbox for multi selection. 30495/head
authorVolker Theile <vtheile@suse.com>
Fri, 20 Sep 2019 14:27:39 +0000 (16:27 +0200)
committerVolker Theile <vtheile@suse.com>
Wed, 23 Oct 2019 12:56:11 +0000 (14:56 +0200)
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 <vtheile@suse.com>
16 files changed:
src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts
src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.po.ts
src/pybind/mgr/dashboard/frontend/e2e/rgw/users.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/e2e/rgw/users.po.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts

index c19aefaaa8b10e006bc2a32590baa93efe468fa2..e142d869e122eac9e08c5cd99ccdf5c53f6035fc 100644 (file)
@@ -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()
+    );
+  }
 }
index a7e127f1e2e96b4348497887aa15f1d298b1ae11..8dbb80ead3e144315eff17d9054b69259d1655a1 100644 (file)
@@ -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');
     });
   });
index b2cd3c45ad62f8afcfe45d010115adcdeac69a86..cf3f604560363668915db092c3b5c64adf783458 100644 (file)
@@ -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');
index 4caaa4df17587252cd2ba4092c0592baafef4652..2e93046e0dc6b5fa89fbe26df10084ed8b6b9ffe 100644 (file)
@@ -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();
     });
index fc2a6d6c5840e37ae5fc13b4708daffad64c6eb4..b742aa84a0eb2c42ff191b5e948707eb009ca07a 100644 (file)
@@ -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();
index 6fed099f7908e9a34d36df7c65f634e115e140c0..1bada27a7252f92d8ed874d275e1767230ec4ba7 100644 (file)
@@ -5,7 +5,7 @@
     <cd-table [data]="osds"
               (fetchData)="getOsdList()"
               [columns]="columns"
-              selectionType="multi"
+              selectionType="multiClick"
               (updateSelection)="updateSelection($event)"
               [updateSelectionOnRefresh]="'never'">
 
index a26a34d713ad1ca1967c0d52fbc43d848410bb19..faec289fc7f3a1145a72b0da07f4a6db292818c7 100644 (file)
@@ -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', () => {
index 3dcff15fcacfe83ed01cbf53d429e79de958b06a..37797e96e003992e1f9c7ef3e4559c69d8b1d6ad 100644 (file)
@@ -3,7 +3,7 @@
           [data]="buckets"
           [columns]="columns"
           columnMode="flex"
-          selectionType="multi"
+          selectionType="multiClick"
           (updateSelection)="updateSelection($event)"
           identifier="bid"
           (fetchData)="getBucketList($event)">
index bb8fcf71918f89f76892aa9cd8d054ae544f276d..3f0bf0bcca5266d7c0bdd42783c287c6799f6d02 100644 (file)
@@ -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'],
index 02065b5ddeca938c9b2f41f3d2665de31cba12a8..24050ebb74aa46d55e78e788f41362b57a97dc04 100644 (file)
@@ -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];
   }
index 8bb40fc9d63866902417aa71aec7f8fc7414593d..a154f1182e34a4d823716df8903c991df91389d9 100644 (file)
@@ -3,7 +3,7 @@
           [data]="users"
           [columns]="columns"
           columnMode="flex"
-          selectionType="multi"
+          selectionType="multiClick"
           (updateSelection)="updateSelection($event)"
           identifier="uid"
           (fetchData)="getUserList($event)">
index 431bd4696782dd754f7e7f81c1a81ca6d12efbbb..4c41b420bb4ddf08f4a834e894618a8fe0abffe5 100644 (file)
@@ -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'],
index be0c6377f888a2985753f5b75731e03d44f03288..5b388049dd29387627b399c07f53680c5e325b4e 100644 (file)
@@ -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];
   }
index c7a8f894af06a2f6289f6e7e0c6d8e24f065145d..6042330023df6245240a6a587e9b34f48abff97e 100644 (file)
                    let-offset="offset"
                    let-isVisible="isVisible">
         <div class="page-count">
-          <span *ngIf="selectionType == 'multi'">
-            <cd-helper i18n>Press and hold control button to select multiple rows to execute a common action on.</cd-helper>
-          </span>
           <span *ngIf="selectionType">
             {{ selectedCount }} <ng-container i18n="X selected">selected</ng-container> /
           </span>
index 5a6e0d4db77e518586fc58085271c6908b40017e..2dd838528d27d218257e4e2e0fe4e9b4ea1213cb 100644 (file)
       }
     }
   }
+
+  .cd-datatable-checkbox {
+    text-align: center;
+  }
 }
 
 @keyframes progress-loading {
index 997fcf811e3e03386ad8fe82c0e75f2e446ab892..bd00af4e7e69213ac8b6deb2b255406d9ecf698a 100644 (file)
@@ -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);
   }