]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add expand/collapse datatable feature 35270/head
authorSebastian Krah <skrah@suse.com>
Tue, 7 Jan 2020 15:56:40 +0000 (16:56 +0100)
committerLaura Paduano <lpaduano@suse.com>
Mon, 8 Jun 2020 14:36:07 +0000 (16:36 +0200)
Adds expand/collapse feature to every datatable with details.

Fixes: https://tracker.ceph.com/issues/40702
Signed-off-by: Sebastian Krah <skrah@suse.com>
(cherry picked from commit d2d0efdc053b9818144b685be0d7593db8dbf398)

Conflicts:
- src/pybind/mgr/dashboard/frontend/e2e/block/images.po.ts
- src/pybind/mgr/dashboard/frontend/e2e/cluster/configuration.e2e-spec.ts
- src/pybind/mgr/dashboard/frontend/e2e/cluster/configuration.po.ts
- src/pybind/mgr/dashboard/frontend/e2e/cluster/mgr-modules.po.ts
- src/pybind/mgr/dashboard/frontend/e2e/cluster/osds.e2e-spec.ts
- src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts
- src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.po.ts
- src/pybind/mgr/dashboard/frontend/e2e/rgw/users.po.ts

All files have been moved to a different directory structure
(src/pybind/mgr/dashboard/frontend/cypress/integration/[...])
due to the change of the e2e framework from protractor to cypress
(PR #34910)

Conflicts:
- src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts
mgr/dashboard: Reset getExpandCollapseElement
to align with master since the functionality of expanding
a list item is included in this backport

76 files changed:
src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-details/configuration-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.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/cluster/osd/osd-list/osd-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/rules-list/rules-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-details/pool-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.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.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.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.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/classes/list-with-details.class.ts [new file with mode: 0644]
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.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts
src/pybind/mgr/dashboard/frontend/src/styles/defaults.scss

index 789fbb12ab26e79ccf36a279266a96da9beda2c0..07d772cc2188dee08a3333871c4a28ff18d6954b 100644 (file)
@@ -185,15 +185,15 @@ export abstract class PageHelper {
     }
   }
 
-  /**
-   * This was adapted for octopus, because row expansion doesn't exist in octopus.
-   * By changing the method code, we prevent making more changes in the actual
-   * test files.
-   */
   getExpandCollapseElement(content?: string) {
-    return this.getFirstTableCell(content);
-  }
+    this.waitDataTableToLoad();
 
+    if (content) {
+      return cy.contains('.datatable-body-row', content).find('.tc_expand-collapse');
+    } else {
+      return cy.get('.tc_expand-collapse').first();
+    }
+  }
   /**
    * Gets column headers of table
    */
index 26d726732cc97a5cb5fa0379807b9bcfc169db32..9a4c9c2612d37ff516d9d8e8fce4a9787861ca3e 100644 (file)
@@ -5,7 +5,6 @@ import * as _ from 'lodash';
 
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { SharedModule } from '../../../shared/shared.module';
 import { IscsiTargetDetailsComponent } from './iscsi-target-details.component';
 
@@ -42,37 +41,35 @@ describe('IscsiTargetDetailsComponent', () => {
       backstores: ['backstore:1', 'backstore:2'],
       default_backstore: 'backstore:1'
     };
-    component.selection = new CdTableSelection();
-    component.selection.selected = [
-      {
-        target_iqn: 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw',
-        portals: [{ host: 'node1', ip: '192.168.100.201' }],
-        disks: [
-          {
-            pool: 'rbd',
-            image: 'disk_1',
-            backstore: 'backstore:1',
-            controls: { hw_max_sectors: 1 }
-          }
-        ],
-        clients: [
-          {
-            client_iqn: 'iqn.1994-05.com.redhat:rh7-client',
-            luns: [{ pool: 'rbd', image: 'disk_1' }],
-            auth: {
-              user: 'myiscsiusername'
-            },
-            info: {
-              alias: 'myhost',
-              ip_address: ['192.168.200.1'],
-              state: { LOGGED_IN: ['node1'] }
-            }
+    component.selection = undefined;
+    component.selection = {
+      target_iqn: 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw',
+      portals: [{ host: 'node1', ip: '192.168.100.201' }],
+      disks: [
+        {
+          pool: 'rbd',
+          image: 'disk_1',
+          backstore: 'backstore:1',
+          controls: { hw_max_sectors: 1 }
+        }
+      ],
+      clients: [
+        {
+          client_iqn: 'iqn.1994-05.com.redhat:rh7-client',
+          luns: [{ pool: 'rbd', image: 'disk_1' }],
+          auth: {
+            user: 'myiscsiusername'
+          },
+          info: {
+            alias: 'myhost',
+            ip_address: ['192.168.200.1'],
+            state: { LOGGED_IN: ['node1'] }
           }
-        ],
-        groups: [],
-        target_controls: { dataout_timeout: 2 }
-      }
-    ];
+        }
+      ],
+      groups: [],
+      target_controls: { dataout_timeout: 2 }
+    };
 
     fixture.detectChanges();
   });
index 893007bd8ec1ca4cd105c45b8c50143e682d9c56..41bc99d9aacd2de38a2460e253ec238ea98df956 100644 (file)
@@ -13,7 +13,6 @@ import * as _ from 'lodash';
 import { TableComponent } from '../../../shared/datatable/table/table.component';
 import { Icons } from '../../../shared/enum/icons.enum';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { BooleanTextPipe } from '../../../shared/pipes/boolean-text.pipe';
 import { IscsiBackstorePipe } from '../../../shared/pipes/iscsi-backstore.pipe';
 
@@ -24,7 +23,7 @@ import { IscsiBackstorePipe } from '../../../shared/pipes/iscsi-backstore.pipe';
 })
 export class IscsiTargetDetailsComponent implements OnChanges, OnInit {
   @Input()
-  selection: CdTableSelection;
+  selection: any;
   @Input()
   settings: any;
   @Input()
@@ -91,8 +90,8 @@ export class IscsiTargetDetailsComponent implements OnChanges, OnInit {
   }
 
   ngOnChanges() {
-    if (this.selection.hasSelection) {
-      this.selectedItem = this.selection.first();
+    if (this.selection) {
+      this.selectedItem = this.selection;
       this.generateTree();
     }
 
index 1b8eeb83e4aea66ad5fd99c59b09eb725a55f738..276629cd413b2490d87a73282dc58265d8e7dd82 100644 (file)
@@ -23,6 +23,8 @@
           identifier="target_iqn"
           forceIdentifier="true"
           selectionType="single"
+          [hasDetails]="true"
+          (setExpandedRow)="setExpandedRow($event)"
           (updateSelection)="updateSelection($event)">
   <div class="table-actions btn-toolbar">
     <cd-table-actions class="btn-group"
@@ -42,8 +44,8 @@
   </div>
 
   <cd-iscsi-target-details cdTableDetail
-                           *ngIf="selection.hasSingleSelection"
+                           *ngIf="expandedRow"
                            [cephIscsiConfigVersion]="cephIscsiConfigVersion"
-                           [selection]="selection"
+                           [selection]="expandedRow"
                            [settings]="settings"></cd-iscsi-target-details>
 </cd-table>
index f3911d58a9dc889f095724d56e9971e1cce0f717..026cf343240ef74e84b0deecf4d5cf7bbe412309 100644 (file)
@@ -6,6 +6,7 @@ import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
 import { Subscription } from 'rxjs';
 
 import { IscsiService } from '../../../shared/api/iscsi.service';
+import { ListWithDetails } from '../../../shared/classes/list-with-details.class';
 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
 import { TableComponent } from '../../../shared/datatable/table/table.component';
@@ -31,7 +32,7 @@ import { IscsiTargetDiscoveryModalComponent } from '../iscsi-target-discovery-mo
   styleUrls: ['./iscsi-target-list.component.scss'],
   providers: [TaskListService]
 })
-export class IscsiTargetListComponent implements OnInit, OnDestroy {
+export class IscsiTargetListComponent extends ListWithDetails implements OnInit, OnDestroy {
   @ViewChild(TableComponent, { static: false })
   table: TableComponent;
 
@@ -69,6 +70,7 @@ export class IscsiTargetListComponent implements OnInit, OnDestroy {
     private taskWrapper: TaskWrapperService,
     public actionLabels: ActionLabelsI18n
   ) {
+    super();
     this.permission = this.authStorageService.getPermissions().iscsi;
 
     this.tableActions = [
index c5c776613a3ce6cbbab08882aea61062a4567f37..fee853628f3dbad8acdb55d4e774c2383d6a9c4b 100644 (file)
@@ -2,7 +2,7 @@
   <ng-container i18n>Only available for RBD images with <strong>fast-diff</strong> enabled</ng-container>
 </ng-template>
 
-<tabset *ngIf="selection?.hasSingleSelection">
+<tabset *ngIf="selection">
   <tab i18n-heading
        heading="Details">
     <table class="table table-striped table-bordered">
         <tr>
           <td i18n
               class="bold w-25">Name</td>
-          <td class="w-75">{{ selectedItem.name }}</td>
+          <td class="w-75">{{ selection.name }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Pool</td>
-          <td>{{ selectedItem.pool_name }}</td>
+          <td>{{ selection.pool_name }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Data Pool</td>
-          <td>{{ selectedItem.data_pool | empty }}</td>
+          <td>{{ selection.data_pool | empty }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Created</td>
-          <td>{{ selectedItem.timestamp | cdDate }}</td>
+          <td>{{ selection.timestamp | cdDate }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Size</td>
-          <td>{{ selectedItem.size | dimlessBinary }}</td>
+          <td>{{ selection.size | dimlessBinary }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Objects</td>
-          <td>{{ selectedItem.num_objs | dimless }}</td>
+          <td>{{ selection.num_objs | dimless }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Object size</td>
-          <td>{{ selectedItem.obj_size | dimlessBinary }}</td>
+          <td>{{ selection.obj_size | dimlessBinary }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Features</td>
           <td>
-            <span *ngFor="let feature of selectedItem.features_name">
+            <span *ngFor="let feature of selection.features_name">
               <span class="badge badge-dark mr-2">{{ feature }}</span>
             </span>
           </td>
           <td i18n
               class="bold">Provisioned</td>
           <td>
-            <span *ngIf="selectedItem.features_name?.indexOf('fast-diff') === -1">
+            <span *ngIf="selection.features_name?.indexOf('fast-diff') === -1">
               <span class="form-text text-muted"
                     [tooltip]="usageNotAvailableTooltipTpl"
                     placement="right"
                     i18n>N/A</span>
             </span>
-            <span *ngIf="selectedItem.features_name?.indexOf('fast-diff') !== -1">
-              {{ selectedItem.disk_usage | dimlessBinary }}
+            <span *ngIf="selection.features_name?.indexOf('fast-diff') !== -1">
+              {{ selection.disk_usage | dimlessBinary }}
             </span>
           </td>
         </tr>
           <td i18n
               class="bold">Total provisioned</td>
           <td>
-            <span *ngIf="selectedItem.features_name?.indexOf('fast-diff') === -1">
+            <span *ngIf="selection.features_name?.indexOf('fast-diff') === -1">
               <span class="form-text text-muted"
                     [tooltip]="usageNotAvailableTooltipTpl"
                     placement="right"
                     i18n>N/A</span>
             </span>
-            <span *ngIf="selectedItem.features_name?.indexOf('fast-diff') !== -1">
-              {{ selectedItem.total_disk_usage | dimlessBinary }}
+            <span *ngIf="selection.features_name?.indexOf('fast-diff') !== -1">
+              {{ selection.total_disk_usage | dimlessBinary }}
             </span>
           </td>
         </tr>
         <tr>
           <td i18n
               class="bold">Striping unit</td>
-          <td>{{ selectedItem.stripe_unit | dimlessBinary }}</td>
+          <td>{{ selection.stripe_unit | dimlessBinary }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Striping count</td>
-          <td>{{ selectedItem.stripe_count }}</td>
+          <td>{{ selection.stripe_count }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Parent</td>
           <td>
-            <span *ngIf="selectedItem.parent">{{ selectedItem.parent.pool_name }}<span *ngIf="selectedItem.parent.pool_namespace">/{{ selectedItem.parent.pool_namespace }}</span>/{{ selectedItem.parent.image_name }}@{{ selectedItem.parent.snap_name }}</span>
-            <span *ngIf="!selectedItem.parent">-</span>
+            <span *ngIf="selection.parent">{{ selection.parent.pool_name }}<span *ngIf="selection.parent.pool_namespace">/{{ selection.parent.pool_namespace }}</span>/{{ selection.parent.image_name }}@{{ selection.parent.snap_name }}</span>
+            <span *ngIf="!selection.parent">-</span>
           </td>
         </tr>
         <tr>
           <td i18n
               class="bold">Block name prefix</td>
-          <td>{{ selectedItem.block_name_prefix }}</td>
+          <td>{{ selection.block_name_prefix }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Order</td>
-          <td>{{ selectedItem.order }}</td>
+          <td>{{ selection.order }}</td>
         </tr>
       </tbody>
     </table>
   </tab>
   <tab i18n-heading
        heading="Snapshots">
-    <cd-rbd-snapshot-list [snapshots]="selectedItem.snapshots"
-                          [featuresName]="selectedItem.features_name"
-                          [poolName]="selectedItem.pool_name"
-                          [namespace]="selectedItem.namespace"
-                          [rbdName]="selectedItem.name"></cd-rbd-snapshot-list>
+    <cd-rbd-snapshot-list [snapshots]="selection.snapshots"
+                          [featuresName]="selection.features_name"
+                          [poolName]="selection.pool_name"
+                          [namespace]="selection.namespace"
+                          [rbdName]="selection.name"></cd-rbd-snapshot-list>
   </tab>
   <tab i18n-heading
        heading="Configuration">
-    <cd-rbd-configuration-table [data]="selectedItem['configuration']"></cd-rbd-configuration-table>
+    <cd-rbd-configuration-table [data]="selection['configuration']"></cd-rbd-configuration-table>
   </tab>
 </tabset>
 
index 0463230ac01e0e3334adcf7a0871574755c8cf04..0916391e06633a5e64e67d99c6f60821e7de954b 100644 (file)
@@ -1,6 +1,5 @@
-import { Component, Input, OnChanges, TemplateRef, ViewChild } from '@angular/core';
+import { Component, Input, TemplateRef, ViewChild } from '@angular/core';
 
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { RbdFormModel } from '../rbd-form/rbd-form.model';
 
 @Component({
@@ -8,20 +7,13 @@ import { RbdFormModel } from '../rbd-form/rbd-form.model';
   templateUrl: './rbd-details.component.html',
   styleUrls: ['./rbd-details.component.scss']
 })
-export class RbdDetailsComponent implements OnChanges {
+export class RbdDetailsComponent {
   @Input()
-  selection: CdTableSelection;
-  selectedItem: RbdFormModel;
+  selection: RbdFormModel;
   @Input()
   images: any;
   @ViewChild('poolConfigurationSourceTpl', { static: true })
   poolConfigurationSourceTpl: TemplateRef<any>;
 
   constructor() {}
-
-  ngOnChanges() {
-    if (this.selection.hasSelection) {
-      this.selectedItem = this.selection.first();
-    }
-  }
 }
index fbf6aa0a19ac16289ee09aea46aa8b9253ebe88a..098b297e1bbb663e9c2858f1c753b330613d7a79 100644 (file)
@@ -12,6 +12,8 @@
           [searchableObjects]="true"
           forceIdentifier="true"
           selectionType="single"
+          [hasDetails]="true"
+          (setExpandedRow)="setExpandedRow($event)"
           (updateSelection)="updateSelection($event)">
   <cd-table-actions class="table-actions"
                     [permission]="permission"
@@ -19,7 +21,7 @@
                     [tableActions]="tableActions">
   </cd-table-actions>
   <cd-rbd-details cdTableDetail
-                  [selection]="selection">
+                  [selection]="expandedRow">
   </cd-rbd-details>
 </cd-table>
 
index aaea7eb89055490059fb0cc5cf50b0f51cad9f41..f69945b6e7eb465696bd8b084e8a56d8eaaef919 100644 (file)
@@ -5,6 +5,7 @@ import * as _ from 'lodash';
 import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
 
 import { RbdService } from '../../../shared/api/rbd.service';
+import { ListWithDetails } from '../../../shared/classes/list-with-details.class';
 import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component';
 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
@@ -40,7 +41,7 @@ const BASE_URL = 'block/rbd';
     { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }
   ]
 })
-export class RbdListComponent implements OnInit {
+export class RbdListComponent extends ListWithDetails implements OnInit {
   @ViewChild(TableComponent, { static: true })
   table: TableComponent;
   @ViewChild('usageTpl', { static: false })
@@ -108,6 +109,7 @@ export class RbdListComponent implements OnInit {
     private urlBuilder: URLBuilderService,
     public actionLabels: ActionLabelsI18n
   ) {
+    super();
     this.permission = this.authStorageService.getPermissions().rbdImage;
     const getImageUri = () =>
       this.selection.first() &&
index f5a8ac952b1def95d007cd9bc4f52697d67466b8..05960e87fa1975065b17cd2621dd4f636ca7a58e 100644 (file)
@@ -5,8 +5,10 @@
           identifier="id"
           forceIdentifier="true"
           selectionType="single"
+          [hasDetails]="true"
+          (setExpandedRow)="setExpandedRow($event)"
           (updateSelection)="updateSelection($event)">
   <cd-cephfs-tabs cdTableDetail
-                  [selection]="selection">
+                  [selection]="expandedRow">
   </cd-cephfs-tabs>
 </cd-table>
index 04657592e71c479a17c889ac65cd24e83fb5c5ae..9b4ece957fda175f2ac424813df6d6ee4862ff56 100644 (file)
@@ -3,6 +3,7 @@ import { Component, OnInit } from '@angular/core';
 import { I18n } from '@ngx-translate/i18n-polyfill';
 
 import { CephfsService } from '../../../shared/api/cephfs.service';
+import { ListWithDetails } from '../../../shared/classes/list-with-details.class';
 import { CellTemplate } from '../../../shared/enum/cell-template.enum';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
 import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
@@ -14,7 +15,7 @@ import { CdDatePipe } from '../../../shared/pipes/cd-date.pipe';
   templateUrl: './cephfs-list.component.html',
   styleUrls: ['./cephfs-list.component.scss']
 })
-export class CephfsListComponent implements OnInit {
+export class CephfsListComponent extends ListWithDetails implements OnInit {
   columns: CdTableColumn[];
   filesystems: any = [];
   selection = new CdTableSelection();
@@ -23,7 +24,9 @@ export class CephfsListComponent implements OnInit {
     private cephfsService: CephfsService,
     private cdDatePipe: CdDatePipe,
     private i18n: I18n
-  ) {}
+  ) {
+    super();
+  }
 
   ngOnInit() {
     this.columns = [
index bef5caf7ee9d44a008e9d3c6793d15327d1c3dcc..cd3800af5e161bfd72a074b47598dab8f3866e30 100644 (file)
@@ -1,4 +1,4 @@
-<tabset *ngIf="selectedItem">
+<tabset *ngIf="selection">
   <tab i18n-heading
        (selectTab)="softRefresh()"
        heading="Details">
index 93853889a674b1d2ad72fab19ee5d0e316a96600..288863374673ca6ffcb94fe7d8592097d683418f 100644 (file)
@@ -11,7 +11,6 @@ import { of } from 'rxjs';
 import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
 import { CephfsService } from '../../../shared/api/cephfs.service';
 import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { SharedModule } from '../../../shared/shared.module';
 import { CephfsClientsComponent } from '../cephfs-clients/cephfs-clients.component';
 import { CephfsDetailComponent } from '../cephfs-detail/cephfs-detail.component';
@@ -49,24 +48,22 @@ describe('CephfsTabsComponent', () => {
     };
   };
 
-  const setSelection = (selection: object[]) => {
-    component.selection.selected = selection;
+  const setSelection = (selection: any) => {
+    component.selection = selection;
     component.ngOnChanges();
   };
 
   const selectFs = (id: number, name: string) => {
-    setSelection([
-      {
-        id,
-        mdsmap: {
-          info: {
-            something: {
-              name
-            }
+    setSelection({
+      id,
+      mdsmap: {
+        info: {
+          something: {
+            name
           }
         }
       }
-    ]);
+    });
   };
 
   const updateData = () => {
@@ -101,7 +98,7 @@ describe('CephfsTabsComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(CephfsTabsComponent);
     component = fixture.componentInstance;
-    component.selection = new CdTableSelection();
+    component.selection = undefined;
     data = {
       standbys: 'b',
       pools: [{}, {}],
@@ -126,14 +123,12 @@ describe('CephfsTabsComponent', () => {
   });
 
   it('should resist invalid mds info', () => {
-    setSelection([
-      {
-        id: 3,
-        mdsmap: {
-          info: {}
-        }
+    setSelection({
+      id: 3,
+      mdsmap: {
+        info: {}
       }
-    ]);
+    });
     expect(component.grafanaId).toBe(undefined);
   });
 
@@ -212,7 +207,7 @@ describe('CephfsTabsComponent', () => {
     });
 
     it('should should unsubscribe on deselect', () => {
-      setSelection([]);
+      setSelection(undefined);
       expect(old.unsubscribed).toBe(true);
       expect(getReload()).toBe(undefined); // Cleared timer subscription
     });
index cd5e28a84b23c25641fc6c0f067f2c1355a8ff86..454922c00a0e6b8c2b862426d22e9dfb1a4984d3 100644 (file)
@@ -5,7 +5,6 @@ import { Subscription, timer } from 'rxjs';
 
 import { CephfsService } from '../../../shared/api/cephfs.service';
 import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { Permission } from '../../../shared/models/permissions';
 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
 
@@ -16,8 +15,7 @@ import { AuthStorageService } from '../../../shared/services/auth-storage.servic
 })
 export class CephfsTabsComponent implements OnChanges, OnDestroy {
   @Input()
-  selection: CdTableSelection;
-  selectedItem: any;
+  selection: any;
 
   // Grafana tab
   grafanaId: any;
@@ -54,13 +52,12 @@ export class CephfsTabsComponent implements OnChanges, OnDestroy {
   }
 
   ngOnChanges() {
-    this.selectedItem = this.selection.first();
-    if (!this.selectedItem) {
+    if (!this.selection) {
       this.unsubscribeInterval();
       return;
     }
-    if (this.selectedItem.id !== this.id) {
-      this.setupSelected(this.selectedItem.id, this.selectedItem.mdsmap.info);
+    if (this.selection.id !== this.id) {
+      this.setupSelected(this.selection.id, this.selection.mdsmap.info);
     }
   }
 
index 1ceac7b25da452ea28a82c9257cebabddf33a011..e690b4a91c10d705b5f7157c58ea418fc4453afa 100755 (executable)
@@ -1,4 +1,4 @@
-<tabset *ngIf="selection?.hasSingleSelection">
+<tabset *ngIf="selection">
   <tab i18n-heading
        heading="Details">
     <table class="table table-striped table-bordered">
@@ -6,23 +6,23 @@
         <tr>
           <td i18n
               class="bold w-25">Name</td>
-          <td class="w-75">{{ selectedItem.name }}</td>
+          <td class="w-75">{{ selection.name }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Description</td>
-          <td>{{ selectedItem.desc }}</td>
+          <td>{{ selection.desc }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Long description</td>
-          <td>{{ selectedItem.long_desc }}</td>
+          <td>{{ selection.long_desc }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Current values</td>
           <td>
-            <span *ngFor="let conf of selectedItem.value; last as isLast">
+            <span *ngFor="let conf of selection.value; last as isLast">
               {{ conf.section }}: {{ conf.value }}{{ !isLast ? "," : "" }}<br />
             </span>
           </td>
         <tr>
           <td i18n
               class="bold">Default</td>
-          <td>{{ selectedItem.default }}</td>
+          <td>{{ selection.default }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Daemon default</td>
-          <td>{{ selectedItem.daemon_default }}</td>
+          <td>{{ selection.daemon_default }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Type</td>
-          <td>{{ selectedItem.type }}</td>
+          <td>{{ selection.type }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Min</td>
-          <td>{{ selectedItem.min }}</td>
+          <td>{{ selection.min }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Max</td>
-          <td>{{ selectedItem.max }}</td>
+          <td>{{ selection.max }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Flags</td>
           <td>
-            <span *ngFor="let flag of selectedItem.flags">
+            <span *ngFor="let flag of selection.flags">
               <span title="{{ flags[flag] }}">
                 <span class="badge badge-dark mr-2">{{ flag | uppercase }}</span>
               </span>
@@ -67,7 +67,7 @@
           <td i18n
               class="bold">Services</td>
           <td>
-            <span *ngFor="let service of selectedItem.services">
+            <span *ngFor="let service of selection.services">
               <span class="badge badge-dark mr-2">{{ service }}</span>
             </span>
           </td>
         <tr>
           <td i18n
               class="bold">Source</td>
-          <td>{{ selectedItem.source }}</td>
+          <td>{{ selection.source }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Level</td>
-          <td>{{ selectedItem.level }}</td>
+          <td>{{ selection.level }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Can be updated at runtime (editable)</td>
-          <td>{{ selectedItem.can_update_at_runtime | booleanText }}</td>
+          <td>{{ selection.can_update_at_runtime | booleanText }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Tags</td>
-          <td>{{ selectedItem.tags }}</td>
+          <td>{{ selection.tags }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">Enum values</td>
-          <td>{{ selectedItem.enum_values }}</td>
+          <td>{{ selection.enum_values }}</td>
         </tr>
         <tr>
           <td i18n
               class="bold">See also</td>
-          <td>{{ selectedItem.see_also }}</td>
+          <td>{{ selection.see_also }}</td>
         </tr>
       </tbody>
     </table>
index 25876369667461ec45ca5d4a9fc835a363c4d91b..d80544bd02c7faf90308b3be04dd086830e3e157 100755 (executable)
@@ -3,8 +3,6 @@ import { Component, Input, OnChanges } from '@angular/core';
 import { I18n } from '@ngx-translate/i18n-polyfill';
 import * as _ from 'lodash';
 
-import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
-
 @Component({
   selector: 'cd-configuration-details',
   templateUrl: './configuration-details.component.html',
@@ -12,8 +10,7 @@ import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
 })
 export class ConfigurationDetailsComponent implements OnChanges {
   @Input()
-  selection: CdTableSelection;
-  selectedItem: any;
+  selection: any;
   flags = {
     runtime: this.i18n('The value can be updated at runtime.'),
     no_mon_update: this.i18n(`Daemons/clients do not pull this value from the
@@ -28,9 +25,8 @@ export class ConfigurationDetailsComponent implements OnChanges {
   constructor(private i18n: I18n) {}
 
   ngOnChanges() {
-    if (this.selection.hasSelection) {
-      this.selectedItem = this.selection.first();
-      this.selectedItem.services = _.split(this.selectedItem.services, ',');
+    if (this.selection) {
+      this.selection.services = _.split(this.selection.services, ',');
     }
   }
 }
index f4b6401a24662f8f1f95d6e7f236dc831d8095a1..a1eb64963395b73ff65bb23505cb73101e064670 100644 (file)
@@ -3,6 +3,8 @@
           [columns]="columns"
           [extraFilterableColumns]="filters"
           selectionType="single"
+          [hasDetails]="true"
+          (setExpandedRow)="setExpandedRow($event)"
           (updateSelection)="updateSelection($event)">
   <cd-table-actions class="table-actions"
                     [permission]="permission"
@@ -10,7 +12,7 @@
                     [tableActions]="tableActions">
   </cd-table-actions>
   <cd-configuration-details cdTableDetail
-                            [selection]="selection">
+                            [selection]="expandedRow">
   </cd-configuration-details>
 </cd-table>
 
index f1dcdc4fa1c8f513e97edf08c3d8c3c87b8b79bc..0c6863b9996e67d79d96e70d43cf6bec8e0c7421 100644 (file)
@@ -3,6 +3,7 @@ import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
 import { I18n } from '@ngx-translate/i18n-polyfill';
 
 import { ConfigurationService } from '../../../shared/api/configuration.service';
+import { ListWithDetails } from '../../../shared/classes/list-with-details.class';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
 import { CellTemplate } from '../../../shared/enum/cell-template.enum';
 import { Icons } from '../../../shared/enum/icons.enum';
@@ -18,7 +19,7 @@ import { AuthStorageService } from '../../../shared/services/auth-storage.servic
   templateUrl: './configuration.component.html',
   styleUrls: ['./configuration.component.scss']
 })
-export class ConfigurationComponent implements OnInit {
+export class ConfigurationComponent extends ListWithDetails implements OnInit {
   permission: Permission;
   tableActions: CdTableAction[];
   data: any[] = [];
@@ -91,6 +92,7 @@ export class ConfigurationComponent implements OnInit {
     private i18n: I18n,
     public actionLabels: ActionLabelsI18n
   ) {
+    super();
     this.permission = this.authStorageService.getPermissions().configOpt;
     const getConfigOptUri = () =>
       this.selection.first() && `${encodeURIComponent(this.selection.first().name)}`;
index a77f3bcd222688ef234bf4043120e596c943da24..ef072c8af68085392881e6dc3183731ed2873069 100644 (file)
@@ -1,7 +1,7 @@
-<tabset *ngIf="selection.hasSingleSelection">
+<tabset *ngIf="selection">
   <tab i18n-heading
        heading="Devices">
-    <cd-device-list [hostname]="selection.first()['hostname']"></cd-device-list>
+    <cd-device-list [hostname]="selection['hostname']"></cd-device-list>
   </tab>
   <tab i18n-heading
        heading="Inventory"
index 9bca99a9657957155e88f22afa4dece67223936a..865386bc6cda56dc34b135261d30edda79200ce0 100644 (file)
@@ -10,7 +10,6 @@ import { ToastrModule } from 'ngx-toastr';
 
 import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
 import { CoreModule } from '../../../../core/core.module';
-import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
 import { Permissions } from '../../../../shared/models/permissions';
 import { SharedModule } from '../../../../shared/shared.module';
 import { CephModule } from '../../../ceph.module';
@@ -42,7 +41,7 @@ describe('HostDetailsComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(HostDetailsComponent);
     component = fixture.componentInstance;
-    component.selection = new CdTableSelection();
+    component.selection = undefined;
     component.permissions = new Permissions({
       hosts: ['read'],
       grafana: ['read']
@@ -55,7 +54,7 @@ describe('HostDetailsComponent', () => {
 
   describe('Host details tabset', () => {
     beforeEach(() => {
-      component.selection.selected = [{ hostname: 'localhost' }];
+      component.selection = { hostname: 'localhost' };
       fixture.detectChanges();
     });
 
index 469c4c3ab7ed1b42fe5a2187ad8429f6abd3f268..f98fa9a093abb9ce3b62d4de74dbd3ab5f303733 100644 (file)
@@ -2,7 +2,6 @@ import { Component, Input, ViewChild } from '@angular/core';
 
 import { TabsetComponent } from 'ngx-bootstrap/tabs';
 
-import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
 import { Permissions } from '../../../../shared/models/permissions';
 
 @Component({
@@ -15,13 +14,13 @@ export class HostDetailsComponent {
   permissions: Permissions;
 
   @Input()
-  selection: CdTableSelection;
+  selection: any;
 
   @ViewChild(TabsetComponent, { static: false })
   tabsetChild: TabsetComponent;
 
   get selectedHostname(): string {
-    return this.selection.hasSelection ? this.selection.first()['hostname'] : null;
+    return this.selection !== undefined ? this.selection['hostname'] : null;
   }
 
   constructor() {}
index ab63cc90b51388fc19a8c7a05676ab2594f1b6b0..fa67d8c59deca2ec9db40aa9645802ada150598b 100644 (file)
@@ -6,6 +6,8 @@
               columnMode="flex"
               (fetchData)="getHosts($event)"
               selectionType="single"
+              [hasDetails]="true"
+              (setExpandedRow)="setExpandedRow($event)"
               (updateSelection)="updateSelection($event)">
       <div class="table-actions btn-toolbar">
         <cd-table-actions [permission]="permissions.hosts"
@@ -31,7 +33,7 @@
       </ng-template>
       <cd-host-details cdTableDetail
                        [permissions]="permissions"
-                       [selection]="selection">
+                       [selection]="expandedRow">
       </cd-host-details>
     </cd-table>
   </tab>
index d6812be0d69d80809195846cc7aeeb5a75ff28e6..de4a7069fabacabeed0bba930326f49208f1a425 100644 (file)
@@ -5,6 +5,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill';
 import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
 
 import { HostService } from '../../../shared/api/host.service';
+import { ListWithDetails } from '../../../shared/classes/list-with-details.class';
 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
 import { Icons } from '../../../shared/enum/icons.enum';
@@ -28,7 +29,7 @@ const BASE_URL = 'hosts';
   styleUrls: ['./hosts.component.scss'],
   providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
 })
-export class HostsComponent implements OnInit {
+export class HostsComponent extends ListWithDetails implements OnInit {
   permissions: Permissions;
   columns: Array<CdTableColumn> = [];
   hosts: Array<object> = [];
@@ -53,6 +54,7 @@ export class HostsComponent implements OnInit {
     private router: Router,
     private depCheckerService: DepCheckerService
   ) {
+    super();
     this.permissions = this.authStorageService.getPermissions();
     this.tableActions = [
       {
index 7913fd40745178057c5bb74def36bef117199e9f..6ed9b65da3996c4a6ee26390181a1409abb8ea03 100644 (file)
@@ -1,4 +1,4 @@
-<tabset *ngIf="selection.hasSingleSelection">
+<tabset *ngIf="selection">
   <tab i18n-heading
        heading="Details">
     <cd-table-key-value [data]="module_config">
index 236683404554acda6e0cca797182b7474401c4a0..1e0575e71a6929d3e789adc783f2860387ab166f 100644 (file)
@@ -4,7 +4,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { TabsModule } from 'ngx-bootstrap/tabs';
 
 import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
-import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
 import { SharedModule } from '../../../../shared/shared.module';
 import { MgrModuleDetailsComponent } from './mgr-module-details.component';
 
@@ -21,7 +20,7 @@ describe('MgrModuleDetailsComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(MgrModuleDetailsComponent);
     component = fixture.componentInstance;
-    component.selection = new CdTableSelection();
+    component.selection = undefined;
     fixture.detectChanges();
   });
 
index dd166779d8af64d8a4907b2c3ba688acc8185e77..dbe2066e4a37f0e910bfd3043e05902bdf0c09b3 100644 (file)
@@ -1,7 +1,6 @@
 import { Component, Input, OnChanges } from '@angular/core';
 
 import { MgrModuleService } from '../../../../shared/api/mgr-module.service';
-import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
 
 @Component({
   selector: 'cd-mgr-module-details',
@@ -12,14 +11,13 @@ export class MgrModuleDetailsComponent implements OnChanges {
   module_config: any;
 
   @Input()
-  selection: CdTableSelection;
+  selection: any;
 
   constructor(private mgrModuleService: MgrModuleService) {}
 
   ngOnChanges() {
-    if (this.selection.hasSelection) {
-      const selectedItem = this.selection.first();
-      this.mgrModuleService.getConfig(selectedItem.name).subscribe((resp: any) => {
+    if (this.selection) {
+      this.mgrModuleService.getConfig(this.selection.name).subscribe((resp: any) => {
         this.module_config = resp;
       });
     }
index 967ae6612f7ef30f2a1155c038df69201e6730eb..29b287de8bfe95de2ae296a9b20e6ea885b4af79 100644 (file)
@@ -4,6 +4,8 @@
           [columns]="columns"
           columnMode="flex"
           selectionType="single"
+          [hasDetails]="true"
+          (setExpandedRow)="setExpandedRow($event)"
           (updateSelection)="updateSelection($event)"
           identifier="module"
           (fetchData)="getModuleList($event)">
@@ -13,6 +15,6 @@
                     [tableActions]="tableActions">
   </cd-table-actions>
   <cd-mgr-module-details cdTableDetail
-                         [selection]="selection">
+                         [selection]="expandedRow">
   </cd-mgr-module-details>
 </cd-table>
index 5d93b6c17899872b9304230baf6e693536dcdfc7..75042749a92c255df5687364f1f3cb4efdba4eea 100644 (file)
@@ -5,6 +5,7 @@ import { BlockUI, NgBlockUI } from 'ng-block-ui';
 import { timer as observableTimer } from 'rxjs';
 
 import { MgrModuleService } from '../../../../shared/api/mgr-module.service';
+import { ListWithDetails } from '../../../../shared/classes/list-with-details.class';
 import { TableComponent } from '../../../../shared/datatable/table/table.component';
 import { CellTemplate } from '../../../../shared/enum/cell-template.enum';
 import { Icons } from '../../../../shared/enum/icons.enum';
@@ -21,7 +22,7 @@ import { NotificationService } from '../../../../shared/services/notification.se
   templateUrl: './mgr-module-list.component.html',
   styleUrls: ['./mgr-module-list.component.scss']
 })
-export class MgrModuleListComponent {
+export class MgrModuleListComponent extends ListWithDetails {
   @ViewChild(TableComponent, { static: true })
   table: TableComponent;
   @BlockUI()
@@ -39,6 +40,7 @@ export class MgrModuleListComponent {
     private notificationService: NotificationService,
     private i18n: I18n
   ) {
+    super();
     this.permission = this.authStorageService.getPermissions().configOpt;
     this.columns = [
       {
index 67ca9683be9ef3a62e5f8e14d9b8dcab935f8705..3430d00b48afc6d5c165c6739b5bc0401e4d39a1 100644 (file)
@@ -1,4 +1,4 @@
-<tabset *ngIf="selection.hasSingleSelection"
+<tabset *ngIf="selection"
         id="tabset-osd-details">
   <tab heading="Devices"
        i18n-heading>
index 16b7f20e331a1719a8486a382c67b5e2c8744f46..0a9bc9d49a0e6dc53e47dce0ca57db967e0a5443 100644 (file)
@@ -7,7 +7,6 @@ import { of } from 'rxjs';
 
 import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
 import { OsdService } from '../../../../shared/api/osd.service';
-import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
 import { SharedModule } from '../../../../shared/shared.module';
 import { TablePerformanceCounterComponent } from '../../../performance-counter/table-performance-counter/table-performance-counter.component';
 import { DeviceListComponent } from '../../../shared/device-list/device-list.component';
@@ -38,7 +37,7 @@ describe('OsdDetailsComponent', () => {
     fixture = TestBed.createComponent(OsdDetailsComponent);
     component = fixture.componentInstance;
 
-    component.selection = new CdTableSelection();
+    component.selection = undefined;
     debugElement = fixture.debugElement;
     osdService = debugElement.injector.get(OsdService);
 
index 87c4d1fa9a609bccb124e3f44fdffbffd1b2a769..9cac703d2622692b3cd24554df3cbb9e87265af6 100644 (file)
@@ -3,7 +3,6 @@ import { Component, Input, OnChanges } from '@angular/core';
 import * as _ from 'lodash';
 
 import { OsdService } from '../../../../shared/api/osd.service';
-import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
 import { Permission } from '../../../../shared/models/permissions';
 import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
 
@@ -14,7 +13,7 @@ import { AuthStorageService } from '../../../../shared/services/auth-storage.ser
 })
 export class OsdDetailsComponent implements OnChanges {
   @Input()
-  selection: CdTableSelection;
+  selection: any;
 
   osd: {
     id?: number;
@@ -33,8 +32,8 @@ export class OsdDetailsComponent implements OnChanges {
     this.osd = {
       loaded: false
     };
-    if (this.selection.hasSelection) {
-      this.osd = this.selection.first();
+    if (this.selection) {
+      this.osd = this.selection;
       this.refresh();
     }
   }
index f939dc7231756c0a205b5009d2b05d7e9550d6dd..196a07e2ffb1d4247a44d9a632b5c40430dc6e5a 100644 (file)
@@ -2,10 +2,13 @@
   <tab i18n-heading
        heading="OSDs List">
 
-    <cd-table [data]="osds"
+    <cd-table [autoReload]="false"
+              [data]="osds"
               (fetchData)="getOsdList()"
               [columns]="columns"
               selectionType="multiClick"
+              [hasDetails]="true"
+              (setExpandedRow)="setExpandedRow($event)"
               (updateSelection)="updateSelection($event)"
               [updateSelectionOnRefresh]="'never'">
 
@@ -27,7 +30,7 @@
       </div>
 
       <cd-osd-details cdTableDetail
-                      [selection]="selection">
+                      [selection]="expandedRow">
       </cd-osd-details>
     </cd-table>
 
index 20d953f9d3af1c23a9564fea17a984f7b24871d7..22ac804fa8294b7f08fa7dcb9451aeda124c995d 100644 (file)
@@ -126,7 +126,7 @@ describe('OsdListComponent', () => {
     fixture.detectChanges();
     expect(
       component.columns
-        .filter((column) => !column.checkboxable)
+        .filter((column) => !(column.prop === undefined))
         .every((column) => Boolean(column.prop))
     ).toBeTruthy();
   });
index bd5844b0f958caf3c1bf399fa1e17388083459de..76a0130749afd6906613bfb91c22bca5e02f7289 100644 (file)
@@ -7,6 +7,7 @@ import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
 import { forkJoin as observableForkJoin, Observable } from 'rxjs';
 
 import { OsdService } from '../../../../shared/api/osd.service';
+import { ListWithDetails } from '../../../../shared/classes/list-with-details.class';
 import { ConfirmationModalComponent } from '../../../../shared/components/confirmation-modal/confirmation-modal.component';
 import { CriticalConfirmationModalComponent } from '../../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { FormModalComponent } from '../../../../shared/components/form-modal/form-modal.component';
@@ -40,7 +41,7 @@ const BASE_URL = 'osd';
   styleUrls: ['./osd-list.component.scss'],
   providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
 })
-export class OsdListComponent implements OnInit {
+export class OsdListComponent extends ListWithDetails implements OnInit {
   @ViewChild('osdUsageTpl', { static: true })
   osdUsageTpl: TemplateRef<any>;
   @ViewChild('markOsdConfirmationTpl', { static: true })
@@ -89,6 +90,7 @@ export class OsdListComponent implements OnInit {
     public actionLabels: ActionLabelsI18n,
     public notificationService: NotificationService
   ) {
+    super();
     this.permissions = this.authStorageService.getPermissions();
     this.tableActions = [
       {
index 64a446c87fb8626bf74b2a4c95a0eebd8828caa8..e9ad6760a897731aacdf8470466aa0e1e127bc0f 100644 (file)
@@ -4,25 +4,24 @@
           [forceIdentifier]="true"
           [customCss]="customCss"
           selectionType="single"
+          [hasDetails]="true"
+          (setExpandedRow)="setExpandedRow($event)"
           (updateSelection)="updateSelection($event)">
   <cd-table-actions class="table-actions"
                     [permission]="permission"
                     [selection]="selection"
                     [tableActions]="tableActions">
   </cd-table-actions>
-  <tabset cdTableDetail
-          *ngIf="selection.hasSingleSelection">
-    <tab i18n-heading
-         heading="Details">
-      <cd-table-key-value [renderObjects]="true"
-                          [hideEmpty]="true"
-                          [appendParentKey]="false"
-                          [data]="selection.first()"
-                          [customCss]="customCss"
-                          [autoReload]="false">
-      </cd-table-key-value>
-    </tab>
-  </tabset>
+
+  <cd-table-key-value cdTableDetail
+                      *ngIf="expandedRow"
+                      [renderObjects]="true"
+                      [hideEmpty]="true"
+                      [appendParentKey]="false"
+                      [data]="expandedRow"
+                      [customCss]="customCss"
+                      [autoReload]="false">
+  </cd-table-key-value>
 </cd-table>
 
 <ng-template #externalLinkTpl
index 2f09b1dcb945374afe43af0712466318697a3112..0296f66486b2f2c25a4d19bb0a95649b07562798 100644 (file)
@@ -1,5 +1,6 @@
 import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
 import { I18n } from '@ngx-translate/i18n-polyfill';
+import { ListWithDetails } from '../../../../shared/classes/list-with-details.class';
 import { CellTemplate } from '../../../../shared/enum/cell-template.enum';
 import { Icons } from '../../../../shared/enum/icons.enum';
 import { CdTableAction } from '../../../../shared/models/cd-table-action';
@@ -19,7 +20,7 @@ const BASE_URL = 'silence'; // as only silence actions can be used
   templateUrl: './active-alert-list.component.html',
   styleUrls: ['./active-alert-list.component.scss']
 })
-export class ActiveAlertListComponent implements OnInit {
+export class ActiveAlertListComponent extends ListWithDetails implements OnInit {
   @ViewChild('externalLinkTpl', { static: true })
   externalLinkTpl: TemplateRef<any>;
   columns: CdTableColumn[];
@@ -41,6 +42,7 @@ export class ActiveAlertListComponent implements OnInit {
     private i18n: I18n,
     private cdDatePipe: CdDatePipe
   ) {
+    super();
     this.permission = this.authStorageService.getPermissions().prometheus;
     this.tableActions = [
       {
index ef80573a2c892c67309afad96c61fb2c3d1c550c..c0050ac7dae7aec97a391f50c55a9c39f0eb2c43 100644 (file)
@@ -1,12 +1,13 @@
 <cd-table [data]="data"
           [columns]="columns"
-          (updateSelection)="selectionUpdated($event)"
+          [hasDetails]="true"
+          (updateSelection)="setExpandedRow($event)"
           [selectionType]="'single'">
   <tabset cdTableDetail
-          *ngIf="selectedRule">
+          *ngIf="expandedRow">
     <tab i18n-heading
          heading="Details">
-      <cd-table-key-value [data]="selectedRule"
+      <cd-table-key-value [data]="expandedRow"
                           [renderObjects]="true"
                           [hideKeys]="hideKeys"></cd-table-key-value>
     </tab>
index 41577da9d9e4d9cacc93fc556efae32d79a83e67..ee5f3a0e49bd9c5bd1d539f25e96b45560ad5d9b 100644 (file)
@@ -2,8 +2,8 @@ import { Component, Input, OnInit } from '@angular/core';
 
 import { I18n } from '@ngx-translate/i18n-polyfill';
 
+import { ListWithDetails } from '../../../../shared/classes/list-with-details.class';
 import { CdTableColumn } from '../../../../shared/models/cd-table-column';
-import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
 import { PrometheusRule } from '../../../../shared/models/prometheus-alerts';
 import { DurationPipe } from '../../../../shared/pipes/duration.pipe';
 
@@ -12,11 +12,11 @@ import { DurationPipe } from '../../../../shared/pipes/duration.pipe';
   templateUrl: './rules-list.component.html',
   styleUrls: ['./rules-list.component.scss']
 })
-export class RulesListComponent implements OnInit {
+export class RulesListComponent extends ListWithDetails implements OnInit {
   @Input()
   data: any;
   columns: CdTableColumn[];
-  selectedRule: PrometheusRule;
+  expandedRow: PrometheusRule;
 
   /**
    * Hide active alerts in details of alerting rules as they are already shown
@@ -25,7 +25,9 @@ export class RulesListComponent implements OnInit {
    */
   hideKeys = ['alerts', 'type'];
 
-  constructor(private i18n: I18n) {}
+  constructor(private i18n: I18n) {
+    super();
+  }
 
   ngOnInit() {
     this.columns = [
@@ -37,8 +39,4 @@ export class RulesListComponent implements OnInit {
       { prop: 'annotations.description', name: this.i18n('Description') }
     ];
   }
-
-  selectionUpdated(selection: CdTableSelection) {
-    this.selectedRule = selection.first();
-  }
 }
index 40dc8ffc8e541f43fe14d84fda49556c72fe2ba3..5a25ec686c1ee3b1ca76463c69fe9dc53bf2f0c3 100644 (file)
@@ -4,6 +4,8 @@
           [customCss]="customCss"
           [sorts]="sorts"
           selectionType="single"
+          [hasDetails]="true"
+          (setExpandedRow)="setExpandedRow($event)"
           (fetchData)="refresh()"
           (updateSelection)="updateSelection($event)">
   <cd-table-actions class="table-actions"
                     [selection]="selection"
                     [tableActions]="tableActions">
   </cd-table-actions>
-  <tabset cdTableDetail
-          *ngIf="selection.hasSingleSelection">
-    <tab i18n-heading
-         heading="Details">
-      <cd-table-key-value [renderObjects]="true"
-                          [hideEmpty]="true"
-                          [appendParentKey]="false"
-                          [data]="selection.first()"
-                          [customCss]="customCss"
-                          [autoReload]="false">
-      </cd-table-key-value>
-    </tab>
-  </tabset>
+  <cd-table-key-value cdTableDetail
+                      *ngIf="expandedRow"
+                      [renderObjects]="true"
+                      [hideEmpty]="true"
+                      [appendParentKey]="false"
+                      [data]="expandedRow"
+                      [customCss]="customCss"
+                      [autoReload]="false">
+  </cd-table-key-value>
 </cd-table>
index 76de16a6b49a5e9b0b1ab0ce79051d5816f55fc9..3d27ac484d90e8a6b91f28b4b947c4902def2b17 100644 (file)
@@ -6,6 +6,7 @@ import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
 import { Observable, Subscriber } from 'rxjs';
 
 import { PrometheusService } from '../../../../shared/api/prometheus.service';
+import { ListWithDetails } from '../../../../shared/classes/list-with-details.class';
 import { CriticalConfirmationModalComponent } from '../../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import {
   ActionLabelsI18n,
@@ -32,7 +33,7 @@ const BASE_URL = 'monitoring/silence';
   templateUrl: './silence-list.component.html',
   styleUrls: ['./silence-list.component.scss']
 })
-export class SilenceListComponent {
+export class SilenceListComponent extends ListWithDetails {
   silences: AlertmanagerSilence[] = [];
   columns: CdTableColumn[];
   tableActions: CdTableAction[];
@@ -57,6 +58,7 @@ export class SilenceListComponent {
     private actionLabels: ActionLabelsI18n,
     private succeededLabels: SucceededActionLabelsI18n
   ) {
+    super();
     this.permission = this.authStorageService.getPermissions().prometheus;
     const selectionExpired = (selection: CdTableSelection) =>
       selection.first() && selection.first().status && selection.first().status.state === 'expired';
index b8f10391a11ba6c49f45384dcf0f17e33b319ae0..5bb97e5eb221d04ffb0cf4e73055b483b3f44ba6 100644 (file)
@@ -1,4 +1,4 @@
-<tabset *ngIf="selection?.hasSingleSelection">
+<tabset *ngIf="selection">
   <tab heading="Details"
        i18n-heading>
     <cd-table-key-value [data]="data">
index 065cb69412cf7cb868236d737fb00401f514933e..f1f3b49b683842f166e924dd54844d501262523c 100644 (file)
@@ -7,7 +7,6 @@ import * as _ from 'lodash';
 import { TabsModule } from 'ngx-bootstrap/tabs';
 
 import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { SharedModule } from '../../../shared/shared.module';
 import { NfsDetailsComponent } from './nfs-details.component';
 
@@ -27,31 +26,29 @@ describe('NfsDetailsComponent', () => {
     fixture = TestBed.createComponent(NfsDetailsComponent);
     component = fixture.componentInstance;
 
-    component.selection = new CdTableSelection();
-    component.selection.selected = [
-      {
-        export_id: 1,
-        path: '/qwe',
-        fsal: { name: 'CEPH', user_id: 'fs', fs_name: 1 },
-        cluster_id: 'cluster1',
-        daemons: ['node1', 'node2'],
-        pseudo: '/qwe',
-        tag: 'asd',
-        access_type: 'RW',
-        squash: 'no_root_squash',
-        protocols: [3, 4],
-        transports: ['TCP', 'UDP'],
-        clients: [
-          {
-            addresses: ['192.168.0.10', '192.168.1.0/8'],
-            access_type: 'RW',
-            squash: 'root_id_squash'
-          }
-        ],
-        id: 'cluster1:1',
-        state: 'LOADING'
-      }
-    ];
+    component.selection = undefined;
+    component.selection = {
+      export_id: 1,
+      path: '/qwe',
+      fsal: { name: 'CEPH', user_id: 'fs', fs_name: 1 },
+      cluster_id: 'cluster1',
+      daemons: ['node1', 'node2'],
+      pseudo: '/qwe',
+      tag: 'asd',
+      access_type: 'RW',
+      squash: 'no_root_squash',
+      protocols: [3, 4],
+      transports: ['TCP', 'UDP'],
+      clients: [
+        {
+          addresses: ['192.168.0.10', '192.168.1.0/8'],
+          access_type: 'RW',
+          squash: 'root_id_squash'
+        }
+      ],
+      id: 'cluster1:1',
+      state: 'LOADING'
+    };
     component.ngOnChanges();
     fixture.detectChanges();
   });
@@ -78,13 +75,13 @@ describe('NfsDetailsComponent', () => {
   });
 
   it('should prepare data if RGW', () => {
-    const newData = _.assignIn(component.selection.first(), {
+    const newData = _.assignIn(component.selection, {
       fsal: {
         name: 'RGW',
         rgw_user_id: 'rgw_user_id'
       }
     });
-    component.selection.selected = [newData];
+    component.selection = newData;
     component.ngOnChanges();
     expect(component.data).toEqual({
       'Access Type': 'RW',
index db91c7d53388c267419f97edbee2fd171db721a8..c6a3ba0eca905cf61642305ec748bbbdca88afe1 100644 (file)
@@ -1,10 +1,8 @@
 import { Component, Input, OnChanges } from '@angular/core';
 
 import { I18n } from '@ngx-translate/i18n-polyfill';
-import * as _ from 'lodash';
 
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 
 @Component({
   selector: 'cd-nfs-details',
@@ -13,7 +11,7 @@ import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 })
 export class NfsDetailsComponent implements OnChanges {
   @Input()
-  selection: CdTableSelection;
+  selection: any;
 
   selectedItem: any;
   data: any;
@@ -42,8 +40,8 @@ export class NfsDetailsComponent implements OnChanges {
   }
 
   ngOnChanges() {
-    if (this.selection.hasSelection) {
-      this.selectedItem = this.selection.first();
+    if (this.selection) {
+      this.selectedItem = this.selection;
 
       this.clients = this.selectedItem.clients;
 
index ef42aa2929cebd4b47a8059ab60b3931588b1d28..79304265e7ea3a1a317d2fd68f4194dd4b9cf922 100644 (file)
@@ -5,6 +5,8 @@
           identifier="id"
           forceIdentifier="true"
           selectionType="single"
+          [hasDetails]="true"
+          (setExpandedRow)="setExpandedRow($event)"
           (updateSelection)="updateSelection($event)">
   <div class="table-actions btn-toolbar">
     <cd-table-actions class="btn-group"
@@ -15,7 +17,7 @@
   </div>
 
   <cd-nfs-details cdTableDetail
-                  [selection]="selection">
+                  [selection]="expandedRow">
   </cd-nfs-details>
 </cd-table>
 
index 2a5c874051d88d5467e0db6f9ba7273a4236c395..7de9a5100742b6899b12a6fe42948783809e8951 100644 (file)
@@ -6,6 +6,7 @@ import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
 import { Subscription } from 'rxjs';
 
 import { NfsService } from '../../../shared/api/nfs.service';
+import { ListWithDetails } from '../../../shared/classes/list-with-details.class';
 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
 import { TableComponent } from '../../../shared/datatable/table/table.component';
@@ -28,7 +29,7 @@ import { TaskWrapperService } from '../../../shared/services/task-wrapper.servic
   styleUrls: ['./nfs-list.component.scss'],
   providers: [TaskListService]
 })
-export class NfsListComponent implements OnInit, OnDestroy {
+export class NfsListComponent extends ListWithDetails implements OnInit, OnDestroy {
   @ViewChild('nfsState', { static: false })
   nfsState: TemplateRef<any>;
   @ViewChild('nfsFsal', { static: true })
@@ -67,6 +68,7 @@ export class NfsListComponent implements OnInit, OnDestroy {
     private taskWrapper: TaskWrapperService,
     public actionLabels: ActionLabelsI18n
   ) {
+    super();
     this.permission = this.authStorageService.getPermissions().nfs;
     const getNfsUri = () =>
       this.selection.first() &&
index 78d23ac28c864e77d5a47f284ac7c0b01e20f38c..a1515e02b7840d6367e3f18e1ed11593b52c8783 100644 (file)
@@ -1,10 +1,10 @@
 <tabset #tabsetChild
         cdTableDetail
-        *ngIf="selection.hasSingleSelection">
+        *ngIf="selection">
   <tab i18n-heading
        heading="Details">
     <cd-table-key-value [renderObjects]="true"
-                        [data]="filterNonPoolData(selection.first())"
+                        [data]="filterNonPoolData(selection)"
                         [autoReload]="false">
     </cd-table-key-value>
   </tab>
        *ngIf="permissions.grafana.read"
        heading="Performance Details">
     <cd-grafana [grafanaPath]="'ceph-pool-detail?var-pool_name='
-                + selection.first()['pool_name']"
+                + selection['pool_name']"
                 uid="-xyV8KCiz"
                 grafanaStyle="one">
     </cd-grafana>
   </tab>
-  <tab *ngIf="selection.first().type === 'replicated'"
+  <tab *ngIf="selection.type === 'replicated'"
        i18n-heading
        heading="Configuration">
     <cd-rbd-configuration-table [data]="selectedPoolConfiguration"></cd-rbd-configuration-table>
   </tab>
   <tab i18n-heading
-       *ngIf="selection.first()['tiers']?.length > 0"
+       *ngIf="selection['tiers']?.length > 0"
        heading="Cache Tiers Details">
     <cd-table [data]="cacheTiers"
               [columns]="cacheTierColumns"
index 172eeb1b93c7edb5bd7aace1458483ec815e45f6..f6330b2d034d6ca019ff81ebd9c0f59d132427e2 100644 (file)
@@ -6,7 +6,6 @@ import { RouterTestingModule } from '@angular/router/testing';
 import { TabsetComponent, TabsModule } from 'ngx-bootstrap/tabs';
 
 import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { Permissions } from '../../../shared/models/permissions';
 import { SharedModule } from '../../../shared/shared.module';
 import { RbdConfigurationListComponent } from '../../block/rbd-configuration-list/rbd-configuration-list.component';
@@ -31,7 +30,7 @@ describe('PoolDetailsComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(PoolDetailsComponent);
     poolDetailsComponent = fixture.componentInstance;
-    poolDetailsComponent.selection = new CdTableSelection();
+    poolDetailsComponent.selection = undefined;
     poolDetailsComponent.permissions = new Permissions({
       grafana: ['read']
     });
@@ -44,12 +43,10 @@ describe('PoolDetailsComponent', () => {
 
   describe('Pool details tabset', () => {
     beforeEach(() => {
-      poolDetailsComponent.selection.selected = [
-        {
-          tiers: [0],
-          pool: 0
-        }
-      ];
+      poolDetailsComponent.selection = {
+        tiers: [0],
+        pool: 0
+      };
     });
 
     it('should recognize a tabset child', () => {
@@ -67,11 +64,9 @@ describe('PoolDetailsComponent', () => {
     });
 
     it('should not show "Cache Tiers Details" tab if selected pool has no "tiers"', () => {
-      poolDetailsComponent.selection.selected = [
-        {
-          tiers: []
-        }
-      ];
+      poolDetailsComponent.selection = {
+        tiers: []
+      };
       fixture.detectChanges();
       const tabs = poolDetailsComponent.tabsetChild.tabs;
       expect(tabs.length).toEqual(2);
index 76365843e9cfa5db3e9f22b151145b73b933503e..8e7c395967ef21311b7605c1956a65f75625b080 100644 (file)
@@ -6,7 +6,6 @@ import { TabsetComponent } from 'ngx-bootstrap/tabs';
 
 import { PoolService } from '../../../shared/api/pool.service';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { RbdConfigurationEntry } from '../../../shared/models/configuration';
 import { Permissions } from '../../../shared/models/permissions';
 
@@ -19,7 +18,7 @@ export class PoolDetailsComponent implements OnChanges {
   cacheTierColumns: Array<CdTableColumn> = [];
 
   @Input()
-  selection: CdTableSelection;
+  selection: any;
   @Input()
   permissions: Permissions;
   @Input()
@@ -64,8 +63,8 @@ export class PoolDetailsComponent implements OnChanges {
   }
 
   ngOnChanges() {
-    if (this.selection.hasSingleSelection) {
-      this.poolService.getConfiguration(this.selection.first().pool_name).subscribe((poolConf) => {
+    if (this.selection) {
+      this.poolService.getConfiguration(this.selection.pool_name).subscribe((poolConf) => {
         this.selectedPoolConfiguration = poolConf;
       });
     }
index b2c31564acb0e24352fbca155c8ec11274866d42..8d6205817d81cf50b1e4bab81db7da9b89e95005 100644 (file)
@@ -10,6 +10,8 @@
               [data]="pools"
               [columns]="columns"
               selectionType="single"
+              [hasDetails]="true"
+              (setExpandedRow)="setExpandedRow($event)"
               (updateSelection)="updateSelection($event)">
       <cd-table-actions id="pool-list-actions"
                         class="table-actions"
@@ -19,9 +21,9 @@
       </cd-table-actions>
       <cd-pool-details cdTableDetail
                        id="pool-list-details"
-                       [selection]="selection"
+                       [selection]="expandedRow"
                        [permissions]="permissions"
-                       [cacheTiers]="selectionCacheTiers">
+                       [cacheTiers]="cacheTiers">
       </cd-pool-details>
     </cd-table>
 
index e5b1791df9a636d363035ea882db8c75aa66cf61..73d0925bc58e1a70b11eef111bd6809f190c1546 100644 (file)
@@ -74,7 +74,11 @@ describe('PoolListComponent', () => {
   });
 
   it('should have columns that are sortable', () => {
-    expect(component.columns.every((column) => Boolean(column.prop))).toBeTruthy();
+    expect(
+      component.columns
+        .filter((column) => !(column.prop === undefined))
+        .every((column) => Boolean(column.prop))
+    ).toBeTruthy();
   });
 
   describe('monAllowPoolDelete', () => {
@@ -429,7 +433,7 @@ describe('PoolListComponent', () => {
 
   describe('getSelectionTiers', () => {
     const setSelectionTiers = (tiers: number[]) => {
-      component.selection.selected = [{ tiers }];
+      component.expandedRow = { tiers };
       component.getSelectionTiers();
     };
 
@@ -439,31 +443,31 @@ describe('PoolListComponent', () => {
 
     it('should select multiple existing cache tiers', () => {
       setSelectionTiers([0, 1, 2]);
-      expect(component.selectionCacheTiers).toEqual(getPoolList());
+      expect(component.cacheTiers).toEqual(getPoolList());
     });
 
     it('should select correct existing cache tier', () => {
       setSelectionTiers([0]);
-      expect(component.selectionCacheTiers).toEqual([createPool('a', 0)]);
+      expect(component.cacheTiers).toEqual([createPool('a', 0)]);
     });
 
     it('should not select cache tier if id is invalid', () => {
       setSelectionTiers([-1]);
-      expect(component.selectionCacheTiers).toEqual([]);
+      expect(component.cacheTiers).toEqual([]);
     });
 
     it('should not select cache tier if empty', () => {
       setSelectionTiers([]);
-      expect(component.selectionCacheTiers).toEqual([]);
+      expect(component.cacheTiers).toEqual([]);
     });
 
     it('should be able to selected one pool with multiple tiers, than with a single tier, than with no tiers', () => {
       setSelectionTiers([0, 1, 2]);
-      expect(component.selectionCacheTiers).toEqual(getPoolList());
+      expect(component.cacheTiers).toEqual(getPoolList());
       setSelectionTiers([0]);
-      expect(component.selectionCacheTiers).toEqual([createPool('a', 0)]);
+      expect(component.cacheTiers).toEqual([createPool('a', 0)]);
       setSelectionTiers([]);
-      expect(component.selectionCacheTiers).toEqual([]);
+      expect(component.cacheTiers).toEqual([]);
     });
   });
 
index 303391d23a6f5d76e4536850f1e7ba8899c99496..61fa3585183f4f363213b2383c988b94d10b784b 100644 (file)
@@ -6,6 +6,7 @@ import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
 
 import { ConfigurationService } from '../../../shared/api/configuration.service';
 import { PoolService } from '../../../shared/api/pool.service';
+import { ListWithDetails } from '../../../shared/classes/list-with-details.class';
 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants';
 import { TableComponent } from '../../../shared/datatable/table/table.component';
@@ -38,7 +39,7 @@ const BASE_URL = 'pool';
   ],
   styleUrls: ['./pool-list.component.scss']
 })
-export class PoolListComponent implements OnInit {
+export class PoolListComponent extends ListWithDetails implements OnInit {
   @ViewChild(TableComponent, { static: true })
   table: TableComponent;
   @ViewChild('poolUsageTpl', { static: true })
@@ -55,7 +56,7 @@ export class PoolListComponent implements OnInit {
   permissions: Permissions;
   tableActions: CdTableAction[];
   viewCacheStatusList: any[];
-  selectionCacheTiers: any[] = [];
+  cacheTiers: any[] = [];
   monAllowPoolDelete = false;
 
   constructor(
@@ -71,6 +72,7 @@ export class PoolListComponent implements OnInit {
     private configurationService: ConfigurationService,
     public actionLabels: ActionLabelsI18n
   ) {
+    super();
     this.permissions = this.authStorageService.getPermissions();
     this.tableActions = [
       {
@@ -214,7 +216,6 @@ export class PoolListComponent implements OnInit {
 
   updateSelection(selection: CdTableSelection) {
     this.selection = selection;
-    this.getSelectionTiers();
   }
 
   deletePoolModal() {
@@ -279,10 +280,10 @@ export class PoolListComponent implements OnInit {
   }
 
   getSelectionTiers() {
-    const cacheTierIds = this.selection.hasSingleSelection
-      ? this.selection.first()['tiers'] || []
-      : [];
-    this.selectionCacheTiers = this.pools.filter((pool) => cacheTierIds.includes(pool.pool));
+    if (typeof this.expandedRow !== 'undefined') {
+      const cacheTierIds = this.expandedRow['tiers'];
+      this.cacheTiers = this.pools.filter((pool) => cacheTierIds.includes(pool.pool));
+    }
   }
 
   getDisableDesc(): string | undefined {
@@ -294,4 +295,9 @@ export class PoolListComponent implements OnInit {
 
     return undefined;
   }
+
+  setExpandedRow(expandedRow: any) {
+    super.setExpandedRow(expandedRow);
+    this.getSelectionTiers();
+  }
 }
index 3d0cb7b482908db728070cc875239e6fe13049d9..761c41b32b0ddda4203814917e3145a2d38eb7b9 100644 (file)
-<tabset *ngIf="selection.hasSingleSelection">
+<tabset *ngIf="selection">
   <tab i18n-heading
        heading="Details">
-    <div *ngIf="bucket">
+    <div *ngIf="selection">
       <table class="table table-striped table-bordered">
         <tbody>
           <tr>
             <td i18n
                 class="bold w-25">Name</td>
-            <td class="w-75">{{ bucket.bid }}</td>
+            <td class="w-75">{{ selection.bid }}</td>
           </tr>
           <tr>
             <td i18n
                 class="bold">ID</td>
-            <td>{{ bucket.id }}</td>
+            <td>{{ selection.id }}</td>
           </tr>
           <tr>
             <td i18n
                 class="bold">Owner</td>
-            <td>{{ bucket.owner }}</td>
+            <td>{{ selection.owner }}</td>
           </tr>
           <tr>
             <td i18n
                 class="bold">Index type</td>
-            <td>{{ bucket.index_type }}</td>
+            <td>{{ selection.index_type }}</td>
           </tr>
           <tr>
             <td i18n
                 class="bold">Placement rule</td>
-            <td>{{ bucket.placement_rule }}</td>
+            <td>{{ selection.placement_rule }}</td>
           </tr>
           <tr>
             <td i18n
                 class="bold">Marker</td>
-            <td>{{ bucket.marker }}</td>
+            <td>{{ selection.marker }}</td>
           </tr>
           <tr>
             <td i18n
                 class="bold">Maximum marker</td>
-            <td>{{ bucket.max_marker }}</td>
+            <td>{{ selection.max_marker }}</td>
           </tr>
           <tr>
             <td i18n
                 class="bold">Version</td>
-            <td>{{ bucket.ver }}</td>
+            <td>{{ selection.ver }}</td>
           </tr>
           <tr>
             <td i18n
                 class="bold">Master version</td>
-            <td>{{ bucket.master_ver }}</td>
+            <td>{{ selection.master_ver }}</td>
           </tr>
           <tr>
             <td i18n
                 class="bold">Modification time</td>
-            <td>{{ bucket.mtime | cdDate }}</td>
+            <td>{{ selection.mtime | cdDate }}</td>
           </tr>
           <tr>
             <td i18n
                 class="bold">Zonegroup</td>
-            <td>{{ bucket.zonegroup }}</td>
+            <td>{{ selection.zonegroup }}</td>
           </tr>
           <tr>
             <td i18n
                 class="bold">Versioning</td>
-            <td>{{ bucket.versioning }}</td>
+            <td>{{ selection.versioning }}</td>
           </tr>
           <tr>
             <td i18n
                 class="bold">MFA Delete</td>
-            <td>{{ bucket.mfa_delete }}</td>
+            <td>{{ selection.mfa_delete }}</td>
           </tr>
         </tbody>
       </table>
 
       <!-- Bucket quota -->
-      <div *ngIf="bucket.bucket_quota">
+      <div *ngIf="selection.bucket_quota">
         <legend i18n>Bucket quota</legend>
         <table class="table table-striped table-bordered">
           <tbody>
             <tr>
               <td i18n
                   class="bold w-25">Enabled</td>
-              <td class="w-75">{{ bucket.bucket_quota.enabled | booleanText }}</td>
+              <td class="w-75">{{ selection.bucket_quota.enabled | booleanText }}</td>
             </tr>
             <tr>
               <td i18n
                   class="bold">Maximum size</td>
-              <td *ngIf="bucket.bucket_quota.max_size <= -1"
+              <td *ngIf="selection.bucket_quota.max_size <= -1"
                   i18n>Unlimited</td>
-              <td *ngIf="bucket.bucket_quota.max_size > -1">
-                {{ bucket.bucket_quota.max_size | dimless }}
+              <td *ngIf="selection.bucket_quota.max_size > -1">
+                {{ selection.bucket_quota.max_size | dimless }}
               </td>
             </tr>
             <tr>
               <td i18n
                   class="bold">Maximum objects</td>
-              <td *ngIf="bucket.bucket_quota.max_objects <= -1"
+              <td *ngIf="selection.bucket_quota.max_objects <= -1"
                   i18n>Unlimited</td>
-              <td *ngIf="bucket.bucket_quota.max_objects > -1">
-                {{ bucket.bucket_quota.max_objects }}
+              <td *ngIf="selection.bucket_quota.max_objects > -1">
+                {{ selection.bucket_quota.max_objects }}
               </td>
             </tr>
           </tbody>
           <tr>
             <td i18n
                 class="bold w-25">Enabled</td>
-            <td class="w-75">{{ bucket.lock_enabled | booleanText }}</td>
+            <td class="w-75">{{ selection.lock_enabled | booleanText }}</td>
           </tr>
-          <ng-container *ngIf="bucket.lock_enabled">
+          <ng-container *ngIf="selection.lock_enabled">
             <tr>
               <td i18n
                   class="bold">Mode</td>
-              <td>{{ bucket.lock_mode }}</td>
+              <td>{{ selection.lock_mode }}</td>
             </tr>
             <tr>
               <td i18n
                   class="bold">Days</td>
-              <td>{{ bucket.lock_retention_period_days }}</td>
+              <td>{{ selection.lock_retention_period_days }}</td>
             </tr>
             <tr>
               <td i18n
                   class="bold">Years</td>
-              <td>{{ bucket.lock_retention_period_years }}</td>
+              <td>{{ selection.lock_retention_period_years }}</td>
             </tr>
           </ng-container>
         </tbody>
index 0b4fa50626c8ce5de5881901f494439fe251ff6a..48f2554003826c8d7dc8883ac3239b12ef2776ff 100644 (file)
@@ -1,23 +1,13 @@
-import { Component, Input, OnChanges } from '@angular/core';
-
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { Component, Input } from '@angular/core';
 
 @Component({
   selector: 'cd-rgw-bucket-details',
   templateUrl: './rgw-bucket-details.component.html',
   styleUrls: ['./rgw-bucket-details.component.scss']
 })
-export class RgwBucketDetailsComponent implements OnChanges {
-  bucket: any;
-
+export class RgwBucketDetailsComponent {
   @Input()
-  selection: CdTableSelection;
+  selection: any;
 
   constructor() {}
-
-  ngOnChanges() {
-    if (this.selection.hasSelection) {
-      this.bucket = this.selection.first();
-    }
-  }
 }
index 1a2fbaeba02c4cfed5b1e9955973104abdbd2a05..a35475abec4e01613c12571ba8de182e3a947079 100644 (file)
@@ -8,6 +8,8 @@
           [columns]="columns"
           columnMode="flex"
           selectionType="multiClick"
+          [hasDetails]="true"
+          (setExpandedRow)="setExpandedRow($event)"
           (updateSelection)="updateSelection($event)"
           identifier="bid"
           (fetchData)="getBucketList($event)">
@@ -17,6 +19,6 @@
                     [tableActions]="tableActions">
   </cd-table-actions>
   <cd-rgw-bucket-details cdTableDetail
-                         [selection]="selection">
+                         [selection]="expandedRow">
   </cd-rgw-bucket-details>
 </cd-table>
index 8573c7395570d38eff8597a2b4c7bd46a81c06a5..1a99482b5bbe5f0a0b9fec97d5405398cbdf6b33 100644 (file)
@@ -5,6 +5,7 @@ import { BsModalService } from 'ngx-bootstrap/modal';
 import { forkJoin as observableForkJoin, Observable, Subscriber } from 'rxjs';
 
 import { RgwBucketService } from '../../../shared/api/rgw-bucket.service';
+import { ListWithDetails } from '../../../shared/classes/list-with-details.class';
 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
 import { TableComponent } from '../../../shared/datatable/table/table.component';
@@ -25,7 +26,7 @@ const BASE_URL = 'rgw/bucket';
   styleUrls: ['./rgw-bucket-list.component.scss'],
   providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
 })
-export class RgwBucketListComponent {
+export class RgwBucketListComponent extends ListWithDetails {
   @ViewChild(TableComponent, { static: true })
   table: TableComponent;
 
@@ -46,6 +47,7 @@ export class RgwBucketListComponent {
     public actionLabels: ActionLabelsI18n,
     private ngZone: NgZone
   ) {
+    super();
     this.permission = this.authStorageService.getPermissions().rgw;
     this.columns = [
       {
index d46bbb8c2cceff2b5125c0591db7474867a18e33..b7cea04f21cdaf5a2035a18458bf14a92d5c4c14 100644 (file)
@@ -1,4 +1,4 @@
-<tabset *ngIf="selection.hasSingleSelection">
+<tabset *ngIf="selection">
   <tab i18n-heading
        heading="Details">
     <cd-table-key-value [data]="metadata"
@@ -14,7 +14,7 @@
   <tab i18n-heading
        *ngIf="grafanaPermission.read"
        heading="Performance Details">
-    <cd-grafana [grafanaPath]="'rgw-instance-detail?var-rgw_servers=rgw.' + this.selection.first().id"
+    <cd-grafana [grafanaPath]="'rgw-instance-detail?var-rgw_servers=rgw.' + this.serviceId"
                 uid="x5ARzZtmk"
                 grafanaStyle="one">
     </cd-grafana>
index 96b6d81a8f6f7a3ea68515ea3b0aed5c5eea1f82..f185f2133d9624ac6ab8e93cf7ee7c8ce677831e 100644 (file)
@@ -4,7 +4,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { TabsModule } from 'ngx-bootstrap/tabs';
 
 import { configureTestBed } from '../../../../testing/unit-test-helper';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { SharedModule } from '../../../shared/shared.module';
 import { PerformanceCounterModule } from '../../performance-counter/performance-counter.module';
 import { RgwDaemonDetailsComponent } from './rgw-daemon-details.component';
@@ -21,7 +20,7 @@ describe('RgwDaemonDetailsComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(RgwDaemonDetailsComponent);
     component = fixture.componentInstance;
-    component.selection = new CdTableSelection();
+    component.selection = undefined;
     fixture.detectChanges();
   });
 
index c78754890dbc5afd3d6b3ec94e809390af84b230..e338572aae378faa1ce3bec15f74a9f24dc354fc 100644 (file)
@@ -3,7 +3,6 @@ import { Component, Input, OnChanges } from '@angular/core';
 import * as _ from 'lodash';
 
 import { RgwDaemonService } from '../../../shared/api/rgw-daemon.service';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { Permission } from '../../../shared/models/permissions';
 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
 
@@ -18,7 +17,7 @@ export class RgwDaemonDetailsComponent implements OnChanges {
   grafanaPermission: Permission;
 
   @Input()
-  selection: CdTableSelection;
+  selection: any;
 
   constructor(
     private rgwDaemonService: RgwDaemonService,
@@ -29,8 +28,8 @@ export class RgwDaemonDetailsComponent implements OnChanges {
 
   ngOnChanges() {
     // Get the service id of the first selected row.
-    if (this.selection.hasSelection) {
-      this.serviceId = this.selection.first().id;
+    if (this.selection) {
+      this.serviceId = this.selection.id;
     }
   }
 
index cb485c6a648ad2bd3259818667488edf77e132e7..51c0fd95a5ac0f23c893bfbf784ee54e3f92ac36 100644 (file)
@@ -4,11 +4,11 @@
     <cd-table [data]="daemons"
               [columns]="columns"
               columnMode="flex"
-              selectionType="single"
-              (updateSelection)="updateSelection($event)"
+              [hasDetails]="true"
+              (setExpandedRow)="setExpandedRow($event)"
               (fetchData)="getDaemonList($event)">
       <cd-rgw-daemon-details cdTableDetail
-                             [selection]="selection">
+                             [selection]="expandedRow">
       </cd-rgw-daemon-details>
     </cd-table>
   </tab>
index 7f689615527821602682b6fbd7eaf99a6a4338ad..9f22e860b4de5f9294b160de1f067d0a5d409437 100644 (file)
@@ -3,9 +3,9 @@ import { Component } from '@angular/core';
 import { I18n } from '@ngx-translate/i18n-polyfill';
 
 import { RgwDaemonService } from '../../../shared/api/rgw-daemon.service';
+import { ListWithDetails } from '../../../shared/classes/list-with-details.class';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
 import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { Permission } from '../../../shared/models/permissions';
 import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
@@ -15,10 +15,9 @@ import { AuthStorageService } from '../../../shared/services/auth-storage.servic
   templateUrl: './rgw-daemon-list.component.html',
   styleUrls: ['./rgw-daemon-list.component.scss']
 })
-export class RgwDaemonListComponent {
+export class RgwDaemonListComponent extends ListWithDetails {
   columns: CdTableColumn[] = [];
   daemons: object[] = [];
-  selection: CdTableSelection = new CdTableSelection();
   grafanaPermission: Permission;
 
   constructor(
@@ -27,6 +26,7 @@ export class RgwDaemonListComponent {
     cephShortVersionPipe: CephShortVersionPipe,
     private i18n: I18n
   ) {
+    super();
     this.grafanaPermission = this.authStorageService.getPermissions().grafana;
     this.columns = [
       {
@@ -58,8 +58,4 @@ export class RgwDaemonListComponent {
       }
     );
   }
-
-  updateSelection(selection: CdTableSelection) {
-    this.selection = selection;
-  }
 }
index 4a87754f7ecf393e4e56352f5084dd7745651293..e4080a26362c27bf5351e3af0ab24b769cc0d890 100644 (file)
@@ -1,4 +1,4 @@
-<tabset *ngIf="selection.hasSingleSelection">
+<tabset *ngIf="selection">
   <tab i18n-heading
        heading="Details">
     <div *ngIf="user">
index 0b9d68bfe86147c6d4bf180ace9b38aa612795db..1b29228c38cabc511e98c849b4816d134e654714 100644 (file)
@@ -6,7 +6,6 @@ import { BsModalService } from 'ngx-bootstrap/modal';
 import { TabsModule } from 'ngx-bootstrap/tabs';
 
 import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { SharedModule } from '../../../shared/shared.module';
 import { RgwUserS3Key } from '../models/rgw-user-s3-key';
 import { RgwUserDetailsComponent } from './rgw-user-details.component';
@@ -24,7 +23,7 @@ describe('RgwUserDetailsComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(RgwUserDetailsComponent);
     component = fixture.componentInstance;
-    component.selection = new CdTableSelection();
+    component.selection = {};
     fixture.detectChanges();
   });
 
@@ -32,13 +31,13 @@ describe('RgwUserDetailsComponent', () => {
     expect(component).toBeTruthy();
 
     const detailsTab = fixture.debugElement.nativeElement.querySelector('tab[heading="Details"]');
-    expect(detailsTab).toBeFalsy();
+    expect(detailsTab).toBeTruthy();
     const keysTab = fixture.debugElement.nativeElement.querySelector('tab[heading="Keys"]');
     expect(keysTab).toBeFalsy();
   });
 
   it('should show "Details" tab', () => {
-    component.selection.selected = [{ uid: 'myUsername' }];
+    component.selection = { uid: 'myUsername' };
     fixture.detectChanges();
 
     const detailsTab = fixture.debugElement.nativeElement.querySelector('tab[heading="Details"]');
@@ -49,7 +48,7 @@ describe('RgwUserDetailsComponent', () => {
 
   it('should show "Keys" tab', () => {
     const s3Key = new RgwUserS3Key();
-    component.selection.selected = [{ keys: [s3Key] }];
+    component.selection = { keys: [s3Key] };
     component.ngOnChanges();
     fixture.detectChanges();
 
@@ -60,9 +59,8 @@ describe('RgwUserDetailsComponent', () => {
   });
 
   it('should show correct "System" info', () => {
-    component.selection.selected = [
-      { uid: '', email: '', system: 'true', keys: [], swift_keys: [] }
-    ];
+    component.selection = { uid: '', email: '', system: 'true', keys: [], swift_keys: [] };
+
     component.ngOnChanges();
     fixture.detectChanges();
 
@@ -72,7 +70,7 @@ describe('RgwUserDetailsComponent', () => {
     expect(detailsTab[6].textContent).toEqual('System');
     expect(detailsTab[7].textContent).toEqual('Yes');
 
-    component.selection.selected[0].system = 'false';
+    component.selection.system = 'false';
     component.ngOnChanges();
     fixture.detectChanges();
 
index 290822c9afbeb13703e051836dae2371b15dffcb..b2c95901f4ed4eb74e0a40b13374edadf974431c 100644 (file)
@@ -25,7 +25,7 @@ export class RgwUserDetailsComponent implements OnChanges, OnInit {
   public secretKeyTpl: TemplateRef<any>;
 
   @Input()
-  selection: CdTableSelection;
+  selection: any;
 
   // Details tab
   user: any;
@@ -64,8 +64,8 @@ export class RgwUserDetailsComponent implements OnChanges, OnInit {
   }
 
   ngOnChanges() {
-    if (this.selection.hasSelection) {
-      this.user = this.selection.first();
+    if (this.selection) {
+      this.user = this.selection;
 
       // Sort subusers and capabilities.
       this.user.subusers = _.sortBy(this.user.subusers, 'id');
index f1d603536d4f229b7b20367491ae0e44abd9521c..e274e8214d6f2752ca1f22904dba6b23d04be5b5 100644 (file)
@@ -8,6 +8,8 @@
           [columns]="columns"
           columnMode="flex"
           selectionType="multiClick"
+          [hasDetails]="true"
+          (setExpandedRow)="setExpandedRow($event)"
           (updateSelection)="updateSelection($event)"
           identifier="uid"
           (fetchData)="getUserList($event)">
@@ -17,6 +19,6 @@
                     [tableActions]="tableActions">
   </cd-table-actions>
   <cd-rgw-user-details cdTableDetail
-                       [selection]="selection">
+                       [selection]="expandedRow">
   </cd-rgw-user-details>
 </cd-table>
index 8775220401b63f10acfc455015c008e5a721cb43..b920ecd7d30fdbe3d434d00c7d27aa113715408b 100644 (file)
@@ -5,6 +5,7 @@ import { BsModalService } from 'ngx-bootstrap/modal';
 import { forkJoin as observableForkJoin, Observable, Subscriber } from 'rxjs';
 
 import { RgwUserService } from '../../../shared/api/rgw-user.service';
+import { ListWithDetails } from '../../../shared/classes/list-with-details.class';
 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
 import { TableComponent } from '../../../shared/datatable/table/table.component';
@@ -26,7 +27,7 @@ const BASE_URL = 'rgw/user';
   styleUrls: ['./rgw-user-list.component.scss'],
   providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
 })
-export class RgwUserListComponent {
+export class RgwUserListComponent extends ListWithDetails {
   @ViewChild(TableComponent, { static: true })
   table: TableComponent;
   permission: Permission;
@@ -46,6 +47,7 @@ export class RgwUserListComponent {
     public actionLabels: ActionLabelsI18n,
     private ngZone: NgZone
   ) {
+    super();
     this.permission = this.authStorageService.getPermissions().rgw;
     this.columns = [
       {
index 9a4cb7436a7795a46655aa69f7d4a1c01f375d1f..ec054c67fbb25b005917ae00e9fafc673d2af7b7 100644 (file)
@@ -1,4 +1,4 @@
-<tabset *ngIf="selection?.hasSingleSelection">
+<tabset *ngIf="selection">
   <tab heading="Details"
        i18n-heading>
     <cd-table [data]="scopes_permissions"
index 74f0ff400897329d8ce4619b32f70bbcd405cd57..d07ef6fe447a2b120d94cde2ec72b4b3ea4a2633 100644 (file)
@@ -5,7 +5,6 @@ import { RouterTestingModule } from '@angular/router/testing';
 import { TabsModule } from 'ngx-bootstrap/tabs';
 
 import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { SharedModule } from '../../../shared/shared.module';
 import { RoleDetailsComponent } from './role-details.component';
 
@@ -31,16 +30,14 @@ describe('RoleDetailsComponent', () => {
 
   it('should create scopes permissions [1/2]', () => {
     component.scopes = ['log', 'rgw'];
-    component.selection = new CdTableSelection([
-      {
-        description: 'RGW Manager',
-        name: 'rgw-manager',
-        scopes_permissions: {
-          rgw: ['read', 'create', 'update', 'delete']
-        },
-        system: true
-      }
-    ]);
+    component.selection = {
+      description: 'RGW Manager',
+      name: 'rgw-manager',
+      scopes_permissions: {
+        rgw: ['read', 'create', 'update', 'delete']
+      },
+      system: true
+    };
     expect(component.scopes_permissions.length).toBe(0);
     component.ngOnChanges();
     expect(component.scopes_permissions).toEqual([
@@ -51,17 +48,15 @@ describe('RoleDetailsComponent', () => {
 
   it('should create scopes permissions [2/2]', () => {
     component.scopes = ['cephfs', 'log', 'rgw'];
-    component.selection = new CdTableSelection([
-      {
-        description: 'Test',
-        name: 'test',
-        scopes_permissions: {
-          log: ['read', 'update'],
-          rgw: ['read', 'create', 'update']
-        },
-        system: false
-      }
-    ]);
+    component.selection = {
+      description: 'Test',
+      name: 'test',
+      scopes_permissions: {
+        log: ['read', 'update'],
+        rgw: ['read', 'create', 'update']
+      },
+      system: false
+    };
     expect(component.scopes_permissions.length).toBe(0);
     component.ngOnChanges();
     expect(component.scopes_permissions).toEqual([
index 1ed0568f773bff6c020730de7ea1c93a81b51dc7..6ee91494104229df26cb345fa5c9425d2c35d561 100644 (file)
@@ -5,7 +5,6 @@ import * as _ from 'lodash';
 
 import { CellTemplate } from '../../../shared/enum/cell-template.enum';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 
 @Component({
   selector: 'cd-role-details',
@@ -14,7 +13,7 @@ import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 })
 export class RoleDetailsComponent implements OnChanges, OnInit {
   @Input()
-  selection: CdTableSelection;
+  selection: any;
   @Input()
   scopes: Array<string>;
   selectedItem: any;
@@ -63,8 +62,8 @@ export class RoleDetailsComponent implements OnChanges, OnInit {
   }
 
   ngOnChanges() {
-    if (this.selection.hasSelection) {
-      this.selectedItem = this.selection.first();
+    if (this.selection) {
+      this.selectedItem = this.selection;
       // Build the scopes/permissions data used by the data table.
       const scopes_permissions: any[] = [];
       _.each(this.scopes, (scope) => {
index 75331794fc40f1553af0657657e0a91b3caf41fa..6b8a5d73e7b897c1d216bf3c0739c67862bae642 100644 (file)
@@ -5,6 +5,8 @@
           [columns]="columns"
           identifier="name"
           selectionType="single"
+          [hasDetails]="true"
+          (setExpandedRow)="setExpandedRow($event)"
           (fetchData)="getRoles()"
           (updateSelection)="updateSelection($event)">
   <cd-table-actions class="table-actions"
@@ -13,7 +15,7 @@
                     [tableActions]="tableActions">
   </cd-table-actions>
   <cd-role-details cdTableDetail
-                   [selection]="selection"
+                   [selection]="expandedRow"
                    [scopes]="scopes">
   </cd-role-details>
 </cd-table>
index 8e966cbaa56727818e20ceea49c04cb033c9e5f2..a945b79fae4d9751a9083ae333bfba71ed8c29c1 100644 (file)
@@ -6,6 +6,7 @@ import { forkJoin } from 'rxjs';
 
 import { RoleService } from '../../../shared/api/role.service';
 import { ScopeService } from '../../../shared/api/scope.service';
+import { ListWithDetails } from '../../../shared/classes/list-with-details.class';
 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { FormModalComponent } from '../../../shared/components/form-modal/form-modal.component';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
@@ -29,7 +30,7 @@ const BASE_URL = 'user-management/roles';
   styleUrls: ['./role-list.component.scss'],
   providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
 })
-export class RoleListComponent implements OnInit {
+export class RoleListComponent extends ListWithDetails implements OnInit {
   permission: Permission;
   tableActions: CdTableAction[];
   columns: CdTableColumn[];
@@ -50,6 +51,7 @@ export class RoleListComponent implements OnInit {
     private urlBuilder: URLBuilderService,
     public actionLabels: ActionLabelsI18n
   ) {
+    super();
     this.permission = this.authStorageService.getPermissions().user;
     const addAction: CdTableAction = {
       permission: 'create',
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/list-with-details.class.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/list-with-details.class.ts
new file mode 100644 (file)
index 0000000..5857426
--- /dev/null
@@ -0,0 +1,7 @@
+export class ListWithDetails {
+  expandedRow: any;
+
+  setExpandedRow(expandedRow: any) {
+    this.expandedRow = expandedRow;
+  }
+}
index 51c5363e8830a717d891af89e11a3c91f8ae4236..131977cceadb49b49d2ea0d05f9a249a38344e8f 100644 (file)
         </a>
         <ul *dropdownMenu
             class="dropdown-menu px-3">
-          <li *ngFor="let column of columns">
-
-            <div class="custom-control custom-checkbox">
-              <input class="custom-control-input"
-                     type="checkbox"
-                     (change)="toggleColumn($event)"
-                     [name]="column.prop"
-                     [id]="column.prop"
-                     [checked]="!column.isHidden">
-              <label class="custom-control-label"
-                     [for]="column.prop">{{ column.name }}</label>
-            </div>
-          </li>
+          <ng-container *ngFor="let column of columns">
+            <li *ngIf="column.name !== ''" >
+              <div class="custom-control custom-checkbox">
+                <input class="custom-control-input"
+                       type="checkbox"
+                       (change)="toggleColumn($event)"
+                       [name]="column.prop"
+                       [id]="column.prop"
+                       [checked]="!column.isHidden">
+                <label class="custom-control-label"
+                       [for]="column.prop">{{ column.name }}</label>
+              </div>
+            </li>
+          </ng-container>
         </ul>
       </div>
     </div>
                  [loadingIndicator]="loadingIndicator"
                  [rowIdentity]="rowIdentity()"
                  [rowHeight]="'auto'">
+
+    <!-- Row Detail Template -->
+    <ngx-datatable-row-detail rowHeight="auto"
+                              #detailRow>
+      <ng-template let-row="row"
+                   let-expanded="expanded"
+                   ngx-datatable-row-detail-template>
+        <!-- Table Details -->
+        <ng-content select="[cdTableDetail]"></ng-content>
+      </ng-template>
+    </ngx-datatable-row-detail>
+
     <ngx-datatable-footer>
       <ng-template ngx-datatable-footer-template
                    let-rowCount="rowCount"
   </ngx-datatable>
 </div>
 
-<!-- Table Details -->
-<ng-content select="[cdTableDetail]"></ng-content>
-
 <!-- cell templates that can be accessed from outside -->
 <ng-template #tableCellBoldTpl
              let-value="value">
   <span data-toggle="tooltip"
         [title]="value">{{ value | truncate:column?.customTemplateConfig?.length:column?.customTemplateConfig?.omission }}</span>
 </ng-template>
+
+<ng-template #rowDetailsTpl
+             let-row="row"
+             let-isExpanded="expanded"
+             ngx-datatable-cell-template>
+  <a href="javascript:void(0)"
+     [class.expand-collapse-icon-right]="!isExpanded"
+     [class.expand-collapse-icon-down]="isExpanded"
+     class="expand-collapse-icon tc_expand-collapse"
+     title="Expand/Collapse Row"
+     i18n-title
+     (click)="toggleExpandRow(row, isExpanded)">
+  </a>
+</ng-template>
index e358a9d2fa766d1565233750cfe3d761fb9501b3..08f407b357c1c1eee475875a5d9815a4b5b5f6f2 100644 (file)
@@ -1,5 +1,12 @@
 @import 'styles';
 
+@mixin row-details-icon {
+  font-family: 'ForkAwesome', sans-serif;
+  font-size: 1rem;
+  color: $gray-900;
+  line-height: 1;
+}
+
 .dataTables_wrapper {
   margin-bottom: 25px;
   .separator {
 
   .dropdown-menu {
     white-space: nowrap;
-    & li {
-      cursor: pointer;
-      & label {
+    & li {
+      cursor: default;
+      & label {
         width: 100%;
         margin-bottom: 0;
-        padding-left: 20px;
-        padding-right: 20px;
+        padding-left: 0;
+        padding-right: 0;
         cursor: pointer;
         &:hover {
           background-color: $color-table-dropdown-bg;
         }
-        & input {
+        & input {
           cursor: pointer;
         }
       }
         }
         .datatable-body-cell-label {
           display: block;
+          height: 100%;
         }
       }
     }
+    .datatable-row-detail {
+      padding: 20px;
+      border-bottom: 2px solid $color-table-header-border;
+    }
+    .expand-collapse-icon {
+      display: block;
+      height: 100%;
+      text-align: center;
+      &:hover {
+        text-decoration: none;
+      }
+    }
+    .expand-collapse-icon-right:before {
+      @include row-details-icon;
+      content: '\f105';
+    }
+    .expand-collapse-icon-down:before {
+      @include row-details-icon;
+      content: '\f107';
+    }
   }
   .datatable-footer {
     @extend .p-2;
index 7030b67bdc58d96ab0f1cf58fa32d4b75310041e..a3ac199a4ba16f23502fb2dce0de90785b9feeb1 100644 (file)
@@ -622,4 +622,72 @@ describe('TableComponent', () => {
       expect(component.useCustomClass('https://secure.it')).toBe('btn secure');
     });
   });
+
+  describe('test expand and collapse feature', () => {
+    beforeEach(() => {
+      spyOn(component.setExpandedRow, 'emit');
+      component.table = {
+        rowDetail: { collapseAllRows: jest.fn(), toggleExpandRow: jest.fn() }
+      } as any;
+
+      // Setup table
+      component.identifier = 'a';
+      component.data = createFakeData(10);
+
+      // Select item
+      component.expanded = _.clone(component.data[1]);
+    });
+
+    describe('update expanded on refresh', () => {
+      const updateExpendedOnState = (state: 'always' | 'never' | 'onChange') => {
+        component.updateExpandedOnRefresh = state;
+        component.updateExpanded();
+      };
+
+      beforeEach(() => {
+        // Mock change
+        component.data[1].b = 'test';
+      });
+
+      it('refreshes "always"', () => {
+        updateExpendedOnState('always');
+        expect(component.expanded.b).toBe('test');
+        expect(component.setExpandedRow.emit).toHaveBeenCalled();
+      });
+
+      it('refreshes "onChange"', () => {
+        updateExpendedOnState('onChange');
+        expect(component.expanded.b).toBe('test');
+        expect(component.setExpandedRow.emit).toHaveBeenCalled();
+      });
+
+      it('does not refresh "onChange" if data is equal', () => {
+        component.data[1].b = 10; // Reverts change
+        updateExpendedOnState('onChange');
+        expect(component.expanded.b).toBe(10);
+        expect(component.setExpandedRow.emit).not.toHaveBeenCalled();
+      });
+
+      it('"never" refreshes', () => {
+        updateExpendedOnState('never');
+        expect(component.expanded.b).toBe(10);
+        expect(component.setExpandedRow.emit).not.toHaveBeenCalled();
+      });
+    });
+
+    it('should open the table details and close other expanded rows', () => {
+      component.toggleExpandRow(component.expanded, false);
+      expect(component.expanded).toEqual({ a: 1, b: 10, c: true });
+      expect(component.table.rowDetail.collapseAllRows).toHaveBeenCalled();
+      expect(component.setExpandedRow.emit).toHaveBeenCalledWith(component.expanded);
+      expect(component.table.rowDetail.toggleExpandRow).toHaveBeenCalled();
+    });
+
+    it('should close the current table details expansion', () => {
+      component.toggleExpandRow(component.expanded, true);
+      expect(component.expanded).toBeUndefined();
+      expect(component.setExpandedRow.emit).toHaveBeenCalledWith(undefined);
+      expect(component.table.rowDetail.toggleExpandRow).toHaveBeenCalled();
+    });
+  });
 });
index fedd05890bfbf4c5374a43c7f8e004e0bec1e21b..9efb7c91915951901b31d1c887291347cd1c19c2 100644 (file)
@@ -63,6 +63,8 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   mapTpl: TemplateRef<any>;
   @ViewChild('truncateTpl', { static: true })
   truncateTpl: TemplateRef<any>;
+  @ViewChild('rowDetailsTpl', { static: true })
+  rowDetailsTpl: TemplateRef<any>;
 
   // This is the array with the items to be shown.
   @Input()
@@ -94,6 +96,9 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   // Page size to show. Set to 0 to show unlimited number of rows.
   @Input()
   limit? = 10;
+  // Has the row details?
+  @Input()
+  hasDetails = false;
 
   /**
    * Auto reload time in ms - per default every 5s
@@ -120,6 +125,9 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   // By default selected item details will be updated on table refresh, if data has changed
   @Input()
   updateSelectionOnRefresh: 'always' | 'never' | 'onChange' = 'onChange';
+  // By default expanded item details will be updated on table refresh, if data has changed
+  @Input()
+  updateExpandedOnRefresh: 'always' | 'never' | 'onChange' = 'onChange';
 
   @Input()
   autoSave = true;
@@ -159,6 +167,9 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   @Output()
   updateSelection = new EventEmitter();
 
+  @Output()
+  setExpandedRow = new EventEmitter();
+
   /**
    * This should be defined if you need access to the applied column filters.
    *
@@ -174,6 +185,11 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
    */
   selection = new CdTableSelection();
 
+  /**
+   * Use this variable to access the expanded row
+   */
+  expanded: any = undefined;
+
   tableColumns: CdTableColumn[];
   icons = Icons;
   cellTemplates: {
@@ -239,6 +255,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
         this.identifier = this.columns[0].prop + '';
       }
     }
+
     this.initUserConfig();
     this.columns.forEach((c) => {
       if (c.cellTransformation) {
@@ -252,6 +269,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
       }
     });
 
+    this.initExpandCollapseColumn(); // If rows have details, add a column to expand or collapse the rows
     this.initCheckboxColumn();
     this.filterHiddenColumns();
     this.initColumnFilters();
@@ -371,6 +389,25 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
     }
   }
 
+  /**
+   * Add a column to expand and collapse the table row if it 'hasDetails'
+   */
+  initExpandCollapseColumn() {
+    if (this.hasDetails) {
+      this.columns.unshift({
+        prop: undefined,
+        resizeable: false,
+        sortable: false,
+        draggable: false,
+        isHidden: false,
+        canAutoResize: false,
+        cellClass: 'cd-datatable-expand-collapse',
+        width: 40,
+        cellTemplate: this.rowDetailsTpl
+      });
+    }
+  }
+
   filterHiddenColumns() {
     this.tableColumns = this.columns.filter((c) => !c.isHidden);
   }
@@ -587,6 +624,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
     this.updateFilter();
     this.reset();
     this.updateSelected();
+    this.updateExpanded();
   }
 
   /**
@@ -626,6 +664,22 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
     this.onSelect(this.selection);
   }
 
+  updateExpanded() {
+    if (_.isUndefined(this.expanded) || this.updateExpandedOnRefresh === 'never') {
+      return;
+    }
+
+    const expandedId = this.expanded[this.identifier];
+    const newExpanded = _.find(this.data, (row) => expandedId === row[this.identifier]);
+
+    if (this.updateExpandedOnRefresh === 'onChange' && _.isEqual(this.expanded, newExpanded)) {
+      return;
+    }
+
+    this.expanded = newExpanded;
+    this.setExpandedRow.emit(newExpanded);
+  }
+
   onSelect($event: any) {
     this.selection.selected = $event['selected'];
     this.updateSelection.emit(_.clone(this.selection));
@@ -751,4 +805,18 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
       };
     };
   }
+
+  toggleExpandRow(row: any, isExpanded: boolean) {
+    if (!isExpanded) {
+      // If current row isn't expanded, collapse others
+      this.expanded = row;
+      this.table.rowDetail.collapseAllRows();
+      this.setExpandedRow.emit(row);
+    } else {
+      // If all rows are closed, emit undefined
+      this.expanded = undefined;
+      this.setExpandedRow.emit(undefined);
+    }
+    this.table.rowDetail.toggleExpandRow(row);
+  }
 }
index 7865cb29a044e129ae6483de7ccb76f52ecb1073..c6c853e6dd8a7509f3a316f3649a2030bc03531a 100644 (file)
@@ -2,6 +2,10 @@
 
 @import 'vendor.variables';
 
+// Bootstrap defaults
+
+$gray-900: #212529 !default;
+
 $screen-sm-min: 576px !default;
 $screen-md-min: 768px !default;
 $screen-lg-min: 992px !default;