]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Fix nvmeof namespace list and delete modal
authorpujaoshahu <pshahu@redhat.com>
Tue, 20 Jan 2026 06:14:44 +0000 (11:44 +0530)
committerpujaoshahu <pshahu@redhat.com>
Mon, 23 Feb 2026 04:34:19 +0000 (10:04 +0530)
Fixes: https://tracker.ceph.com/issues/74451
Signed-off-by: pujaoshahu <pshahu@redhat.com>
 Conflicts:
src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts

Signed-off-by: pujaoshahu <pshahu@redhat.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.ts

index 4446a2de9cbdc38ed76420771479294e123f5c8d..b0316782099a852d77b0d21b7a26566fda42d333 100644 (file)
@@ -12,6 +12,8 @@ import { FinishedTask } from '~/app/shared/models/finished-task';
 import { ActivatedRoute, Router } from '@angular/router';
 import { InitiatorRequest, NvmeofService } from '~/app/shared/api/nvmeof.service';
 
+import { Icons } from '~/app/shared/enum/icons.enum';
+
 @Component({
   selector: 'cd-nvmeof-initiators-form',
   templateUrl: './nvmeof-initiators-form.component.html',
@@ -19,6 +21,7 @@ import { InitiatorRequest, NvmeofService } from '~/app/shared/api/nvmeof.service
   standalone: false
 })
 export class NvmeofInitiatorsFormComponent implements OnInit {
+  icons = Icons;
   permission: Permission;
   initiatorForm: CdFormGroup;
   action: string;
index 369c9b96acd18392a4b8dd4309ba33f71866cad0..a600eadc5821fa8c34287fcad5cec6761b5cb61f 100644 (file)
@@ -1,24 +1,44 @@
-<legend>
-  <cd-help-text>
-    An NVMe namespace is a quantity of non-volatile storage that can be formatted into logical blocks and presented to a host as a standard block device.
-  </cd-help-text>
-</legend>
-<cd-table [data]="namespaces"
-          columnMode="flex"
-          (fetchData)="listNamespaces()"
-          [columns]="namespacesColumns"
-          selectionType="single"
-          (updateSelection)="updateSelection($event)"
-          emptyStateTitle="No namespaces created."
-          i18n-emptyStateTitle
-          emptyStateMessage="Namespaces are storage volumes mapped to subsystems for host access. Create a namespace to start provisioning storage within a subsystem."
-          i18n-emptyStateMessage>
-
-  <div class="table-actions">
-    <cd-table-actions [permission]="permission"
-                      [selection]="selection"
-                      class="btn-group"
-                      [tableActions]="tableActions">
-    </cd-table-actions>
+<div cdsGrid
+     [useCssGrid]="true"
+     [narrow]="true"
+     [fullWidth]="true">
+<div cdsCol
+     [columnNumbers]="{sm: 4, md: 8}">
+  <div class="pb-3 form-item"
+       cdsRow>
+    <cds-combo-box
+        type="single"
+        label="Selected Gateway Group"
+        i18n-label
+        [placeholder]="gwGroupPlaceholder"
+        [items]="gwGroups"
+        (selected)="onGroupSelection($event)"
+        (clear)="onGroupClear()"
+        [disabled]="gwGroupsEmpty">
+      <cds-dropdown-list></cds-dropdown-list>
+    </cds-combo-box>
   </div>
-</cd-table>
+</div>
+</div>
+
+<ng-container *ngIf="namespaces$ | async as namespaces">
+  <cd-table [data]="namespaces"
+            columnMode="flex"
+            (fetchData)="fetchData()"
+            [columns]="namespacesColumns"
+            selectionType="single"
+            (updateSelection)="updateSelection($event)"
+            emptyStateTitle="No namespaces created."
+            i18n-emptyStateTitle
+            emptyStateMessage="Namespaces are storage volumes mapped to subsystems for host access. Create a namespace to start provisioning storage within a subsystem."
+            i18n-emptyStateMessage>
+
+    <div class="table-actions">
+      <cd-table-actions [permission]="permission"
+                        [selection]="selection"
+                        class="btn-group"
+                        [tableActions]="tableActions">
+      </cd-table-actions>
+    </div>
+  </cd-table>
+</ng-container>
index 30775d2156916ac3cca51204d3b496db83b831a6..2345d29074155ea916040a89418ba647d4f217fd 100644 (file)
@@ -1,12 +1,13 @@
-import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { HttpClientModule } from '@angular/common/http';
+import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
 import { of } from 'rxjs';
 import { RouterTestingModule } from '@angular/router/testing';
 import { SharedModule } from '~/app/shared/shared.module';
 
 import { NvmeofService } from '../../../shared/api/nvmeof.service';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
-import { ModalService } from '~/app/shared/services/modal.service';
+import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { NvmeofSubsystemsDetailsComponent } from '../nvmeof-subsystems-details/nvmeof-subsystems-details.component';
 import { NvmeofNamespacesListComponent } from './nvmeof-namespaces-list.component';
@@ -29,8 +30,16 @@ const mockNamespaces = [
 ];
 
 class MockNvmeOfService {
-  listNamespaces() {
-    return of(mockNamespaces);
+  listGatewayGroups() {
+    return of([[{ id: 'g1' }]]);
+  }
+
+  formatGwGroupsList(_response: any) {
+    return [{ content: 'g1', selected: false }];
+  }
+
+  listNamespaces(_group?: string) {
+    return of({ namespaces: mockNamespaces });
   }
 }
 
@@ -40,7 +49,9 @@ class MockAuthStorageService {
   }
 }
 
-class MockModalService {}
+class MockModalCdsService {
+  show = jasmine.createSpy('show');
+}
 
 class MockTaskWrapperService {}
 
@@ -48,6 +59,8 @@ describe('NvmeofNamespacesListComponent', () => {
   let component: NvmeofNamespacesListComponent;
   let fixture: ComponentFixture<NvmeofNamespacesListComponent>;
 
+  let modalService: MockModalCdsService;
+
   beforeEach(async () => {
     await TestBed.configureTestingModule({
       declarations: [NvmeofNamespacesListComponent, NvmeofSubsystemsDetailsComponent],
@@ -55,9 +68,10 @@ describe('NvmeofNamespacesListComponent', () => {
       providers: [
         { provide: NvmeofService, useClass: MockNvmeOfService },
         { provide: AuthStorageService, useClass: MockAuthStorageService },
-        { provide: ModalService, useClass: MockModalService },
+        { provide: ModalCdsService, useClass: MockModalCdsService },
         { provide: TaskWrapperService, useClass: MockTaskWrapperService }
-      ]
+      ],
+      schemas: [CUSTOM_ELEMENTS_SCHEMA]
     }).compileComponents();
 
     fixture = TestBed.createComponent(NvmeofNamespacesListComponent);
@@ -65,15 +79,36 @@ describe('NvmeofNamespacesListComponent', () => {
     component.ngOnInit();
     component.subsystemNQN = 'nqn.2001-07.com.ceph:1721040751436';
     fixture.detectChanges();
+    modalService = TestBed.inject(ModalCdsService) as any;
   });
 
   it('should create', () => {
     expect(component).toBeTruthy();
   });
 
-  it('should retrieve namespaces', fakeAsync(() => {
+  it('should retrieve namespaces', (done) => {
+    component.group = 'g1';
+    component.namespaces$.subscribe((namespaces) => {
+      expect(namespaces).toEqual(mockNamespaces);
+      done();
+    });
     component.listNamespaces();
-    tick();
-    expect(component.namespaces).toEqual(mockNamespaces);
-  }));
+  });
+
+  it('should open delete modal with correct data', () => {
+    // Mock selection
+    const namespace = {
+      nsid: 1,
+      ns_subsystem_nqn: 'nqn.2001-07.com.ceph:1721040751436'
+    };
+    component.selection = {
+      first: () => namespace
+    } as any;
+    component.deleteNamespaceModal();
+    expect(modalService.show).toHaveBeenCalled();
+    const args = modalService.show.calls.mostRecent().args[1];
+    expect(args.itemNames).toEqual([1]);
+    expect(args.itemDescription).toBeDefined();
+    expect(typeof args.submitActionObservable).toBe('function');
+  });
 });
index 39f1cdc91a7949feff058e9c54054b1d81160c99..58853ad8cd5429c853899a8cb5928fbe722a7e9f 100644 (file)
@@ -1,22 +1,25 @@
-import { Component, Input, OnInit } from '@angular/core';
-import { Router } from '@angular/router';
-import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { Component, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { NvmeofService, GroupsComboboxItem } from '~/app/shared/api/nvmeof.service';
 import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { DeletionImpact } from '~/app/shared/enum/delete-confirmation-modal-impact.enum';
 import { Icons } from '~/app/shared/enum/icons.enum';
 import { CdTableAction } from '~/app/shared/models/cd-table-action';
 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
 import { FinishedTask } from '~/app/shared/models/finished-task';
 import { NvmeofSubsystemNamespace } from '~/app/shared/models/nvmeof';
 import { Permission } from '~/app/shared/models/permissions';
+import { CephServiceSpec } from '~/app/shared/models/service.interface';
 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
-import { IopsPipe } from '~/app/shared/pipes/iops.pipe';
-import { MbpersecondPipe } from '~/app/shared/pipes/mbpersecond.pipe';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
+import { catchError, map, switchMap, takeUntil } from 'rxjs/operators';
 
 const BASE_URL = 'block/nvmeof/subsystems';
+const DEFAULT_PLACEHOLDER = $localize`Enter group name`;
 
 @Component({
   selector: 'cd-nvmeof-namespaces-list',
@@ -24,94 +27,66 @@ const BASE_URL = 'block/nvmeof/subsystems';
   styleUrls: ['./nvmeof-namespaces-list.component.scss'],
   standalone: false
 })
-export class NvmeofNamespacesListComponent implements OnInit {
+export class NvmeofNamespacesListComponent implements OnInit, OnDestroy {
   @Input()
   subsystemNQN: string;
   @Input()
   group: string;
-
+  @ViewChild('deleteTpl', { static: true })
+  deleteTpl: TemplateRef<any>;
   namespacesColumns: any;
   tableActions: CdTableAction[];
   selection = new CdTableSelection();
   permission: Permission;
-  namespaces: NvmeofSubsystemNamespace[];
+  namespaces$: Observable<NvmeofSubsystemNamespace[]>;
+  private namespaceSubject = new BehaviorSubject<void>(undefined);
+
+  // Gateway group dropdown properties
+  gwGroups: GroupsComboboxItem[] = [];
+  gwGroupsEmpty: boolean = false;
+  gwGroupPlaceholder: string = DEFAULT_PLACEHOLDER;
+
+  private destroy$ = new Subject<void>();
 
   constructor(
     public actionLabels: ActionLabelsI18n,
     private router: Router,
+    private route: ActivatedRoute,
     private modalService: ModalCdsService,
     private authStorageService: AuthStorageService,
     private taskWrapper: TaskWrapperService,
     private nvmeofService: NvmeofService,
-    private dimlessBinaryPipe: DimlessBinaryPipe,
-    private mbPerSecondPipe: MbpersecondPipe,
-    private iopsPipe: IopsPipe
+    private dimlessBinaryPipe: DimlessBinaryPipe
   ) {
     this.permission = this.authStorageService.getPermissions().nvmeof;
   }
 
   ngOnInit() {
+    this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
+      if (params?.['group']) this.onGroupSelection({ content: params?.['group'] });
+    });
+    this.setGatewayGroups();
     this.namespacesColumns = [
       {
-        name: $localize`ID`,
+        name: $localize`Namespace ID`,
         prop: 'nsid'
       },
       {
-        name: $localize`Bdev Name`,
-        prop: 'bdev_name'
-      },
-      {
-        name: $localize`Pool `,
-        prop: 'rbd_pool_name',
-        flexGrow: 2
-      },
-      {
-        name: $localize`Image`,
-        prop: 'rbd_image_name',
-        flexGrow: 3
-      },
-      {
-        name: $localize`Image Size`,
+        name: $localize`Size`,
         prop: 'rbd_image_size',
         pipe: this.dimlessBinaryPipe
       },
       {
-        name: $localize`Block Size`,
-        prop: 'block_size',
-        pipe: this.dimlessBinaryPipe
-      },
-      {
-        name: $localize`IOPS`,
-        prop: 'rw_ios_per_second',
-        sortable: false,
-        pipe: this.iopsPipe,
-        flexGrow: 1.5
-      },
-      {
-        name: $localize`R/W Throughput`,
-        prop: 'rw_mbytes_per_second',
-        sortable: false,
-        pipe: this.mbPerSecondPipe,
-        flexGrow: 1.5
-      },
-      {
-        name: $localize`Read Throughput`,
-        prop: 'r_mbytes_per_second',
-        sortable: false,
-        pipe: this.mbPerSecondPipe,
-        flexGrow: 1.5
+        name: $localize`Pool`,
+        prop: 'rbd_pool_name'
       },
       {
-        name: $localize`Write Throughput`,
-        prop: 'w_mbytes_per_second',
-        sortable: false,
-        pipe: this.mbPerSecondPipe,
-        flexGrow: 1.5
+        name: $localize`Image`,
+        prop: 'rbd_image_name'
       },
       {
-        name: $localize`Load Balancing Group`,
-        prop: 'load_balancing_group',
-        flexGrow: 1.5
+        name: $localize`Subsystem`,
+        prop: 'ns_subsystem_nqn'
       }
     ];
     this.tableActions = [
@@ -124,7 +99,8 @@ export class NvmeofNamespacesListComponent implements OnInit {
             [BASE_URL, { outlets: { modal: [URLVerbs.CREATE, this.subsystemNQN, 'namespace'] } }],
             { queryParams: { group: this.group } }
           ),
-        canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+        canBePrimary: (selection: CdTableSelection) => !selection.hasSelection,
+        disable: () => !this.group
       },
       {
         name: this.actionLabels.EDIT,
@@ -155,6 +131,21 @@ export class NvmeofNamespacesListComponent implements OnInit {
         click: () => this.deleteNamespaceModal()
       }
     ];
+
+    this.namespaces$ = this.namespaceSubject.pipe(
+      switchMap(() => {
+        if (!this.group) {
+          return of([]);
+        }
+        return this.nvmeofService.listNamespaces(this.group).pipe(
+          map((res: NvmeofSubsystemNamespace[] | { namespaces: NvmeofSubsystemNamespace[] }) => {
+            return Array.isArray(res) ? res : res.namespaces || [];
+          }),
+          catchError(() => of([]))
+        );
+      }),
+      takeUntil(this.destroy$)
+    );
   }
 
   updateSelection(selection: CdTableSelection) {
@@ -162,25 +153,89 @@ export class NvmeofNamespacesListComponent implements OnInit {
   }
 
   listNamespaces() {
-    this.nvmeofService.listNamespaces(this.group).subscribe((res: NvmeofSubsystemNamespace[]) => {
-      this.namespaces = res;
-    });
+    this.namespaceSubject.next();
+  }
+
+  fetchData() {
+    this.namespaceSubject.next();
+  }
+
+  // Gateway groups methods
+  onGroupSelection(selected: GroupsComboboxItem) {
+    selected.selected = true;
+    this.group = selected.content;
+    this.listNamespaces();
+  }
+
+  onGroupClear() {
+    this.group = null;
+    this.listNamespaces();
+  }
+
+  setGatewayGroups() {
+    this.nvmeofService
+      .listGatewayGroups()
+      .pipe(takeUntil(this.destroy$))
+      .subscribe({
+        next: (response: CephServiceSpec[][]) => this.handleGatewayGroupsSuccess(response),
+        error: (error) => this.handleGatewayGroupsError(error)
+      });
+  }
+
+  handleGatewayGroupsSuccess(response: CephServiceSpec[][]) {
+    if (response?.[0]?.length) {
+      this.gwGroups = this.nvmeofService.formatGwGroupsList(response);
+    } else {
+      this.gwGroups = [];
+    }
+    this.updateGroupSelectionState();
+  }
+
+  updateGroupSelectionState() {
+    if (!this.group && this.gwGroups.length) {
+      this.onGroupSelection(this.gwGroups[0]);
+      this.gwGroupsEmpty = false;
+      this.gwGroupPlaceholder = DEFAULT_PLACEHOLDER;
+    } else if (!this.gwGroups.length) {
+      this.gwGroupsEmpty = true;
+      this.gwGroupPlaceholder = $localize`No groups available`;
+    }
+  }
+
+  handleGatewayGroupsError(error: any) {
+    this.gwGroups = [];
+    this.gwGroupsEmpty = true;
+    this.gwGroupPlaceholder = $localize`Unable to fetch Gateway groups`;
+    if (error?.preventDefault) {
+      error?.preventDefault?.();
+    }
   }
 
   deleteNamespaceModal() {
     const namespace = this.selection.first();
+    const subsystemNqn = namespace.ns_subsystem_nqn;
     this.modalService.show(DeleteConfirmationModalComponent, {
-      itemDescription: 'Namespace',
+      itemDescription: $localize`Namespace`,
+      impact: DeletionImpact.high,
+      bodyTemplate: this.deleteTpl,
       itemNames: [namespace.nsid],
       actionDescription: 'delete',
+      bodyContext: {
+        deletionMessage: $localize`Deleting the namespace <strong>${namespace.nsid}</strong> will permanently remove all resources, services, and configurations within it. This action cannot be undone.`
+      },
       submitActionObservable: () =>
         this.taskWrapper.wrapTaskAroundCall({
           task: new FinishedTask('nvmeof/namespace/delete', {
-            nqn: this.subsystemNQN,
+            nqn: subsystemNqn,
             nsid: namespace.nsid
           }),
-          call: this.nvmeofService.deleteNamespace(this.subsystemNQN, namespace.nsid, this.group)
+          call: this.nvmeofService.deleteNamespace(subsystemNqn, namespace.nsid, this.group)
         })
     });
   }
+
+  ngOnDestroy() {
+    this.destroy$.next();
+    this.destroy$.complete();
+  }
 }
index f06bec2543f7e5176690e0a8d265ab58f8fe3184..737ba752660ee441c9b006c7f6c890e8c4b0b2b9 100644 (file)
@@ -1,46 +1,55 @@
-<div class="pb-3"
-     cdsCol
-     [columnNumbers]="{md: 6}">
-  <cds-combo-box
-      type="single"
-      label="Selected Gateway Group"
-      i18n-label
-      [placeholder]="gwGroupPlaceholder"
-      [items]="gwGroups"
-      (selected)="onGroupSelection($event)"
-      (clear)="onGroupClear()"
-      [disabled]="gwGroupsEmpty">
-    <cds-dropdown-list></cds-dropdown-list>
-  </cds-combo-box>
+<div cdsGrid
+     [useCssGrid]="true"
+     [narrow]="true"
+     [fullWidth]="true">
+<div cdsCol
+     [columnNumbers]="{sm: 4, md: 8}">
+  <div class="pb-3 form-item"
+       cdsRow>
+    <cds-combo-box
+        type="single"
+        label="Selected Gateway Group"
+        i18n-label
+        [placeholder]="gwGroupPlaceholder"
+        [items]="gwGroups"
+        (selected)="onGroupSelection($event)"
+        (clear)="onGroupClear()"
+        [disabled]="gwGroupsEmpty">
+      <cds-dropdown-list></cds-dropdown-list>
+    </cds-combo-box>
+  </div>
 </div>
+</div>
+<ng-container *ngIf="subsystems$ | async as subsystems">
+  <cd-table #table
+            [data]="subsystems"
+            [columns]="subsystemsColumns"
+            columnMode="flex"
+            selectionType="single"
+            [hasDetails]="true"
+            (setExpandedRow)="setExpandedRow($event)"
+            (updateSelection)="updateSelection($event)"
+            (fetchData)="fetchData()"
+            emptyStateTitle="No subsystems created"
+            i18n-emptyStateTitle
+            emptyStateMessage="Subsystems group NVMe namespaces and manage host access. Create a subsystem to start mapping NVMe volumes to hosts."
+            i18n-emptyStateMessage>
 
-<cd-table #table
-          [data]="subsystems"
-          [columns]="subsystemsColumns"
-          columnMode="flex"
-          selectionType="single"
-          [hasDetails]="true"
-          (setExpandedRow)="setExpandedRow($event)"
-          (updateSelection)="updateSelection($event)"
-          emptyStateTitle="No subsystems created"
-          i18n-emptyStateTitle
-          emptyStateMessage="Subsystems group NVMe namespaces and manage host access. Create a subsystem to start mapping NVMe volumes to hosts."
-          i18n-emptyStateMessage>
-
-  <div class="table-actions">
-    <cd-table-actions [permission]="permissions.nvmeof"
-                      [selection]="selection"
-                      class="btn-group"
-                      [tableActions]="tableActions">
-    </cd-table-actions>
-  </div>
+    <div class="table-actions">
+      <cd-table-actions [permission]="permissions.nvmeof"
+                        [selection]="selection"
+                        class="btn-group"
+                        [tableActions]="tableActions">
+      </cd-table-actions>
+    </div>
 
-  <cd-nvmeof-subsystems-details *cdTableDetail
-                                [selection]="expandedRow"
-                                [permissions]="permissions"
-                                [group]="expandedRow?.gw_group">
-  </cd-nvmeof-subsystems-details>
-</cd-table>
+    <cd-nvmeof-subsystems-details *cdTableDetail
+                                  [selection]="expandedRow"
+                                  [permissions]="permissions"
+                                  [group]="expandedRow?.gw_group">
+    </cd-nvmeof-subsystems-details>
+  </cd-table>
+</ng-container>
 
 <ng-template #authenticationTpl
              let-row="data.row">
index bc3edd515229687d6f89853aad8653dfc54c79ad..a3532ec1e12e420d08409a8bbbf9a9246d7833e4 100644 (file)
@@ -1,4 +1,4 @@
-import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { HttpClientModule } from '@angular/common/http';
 import { of } from 'rxjs';
 import { RouterTestingModule } from '@angular/router/testing';
@@ -112,17 +112,19 @@ describe('NvmeofSubsystemsComponent', () => {
     expect(component).toBeTruthy();
   });
 
-  it('should retrieve subsystems', fakeAsync(() => {
-    component.getSubsystems();
-    tick();
+  it('should retrieve subsystems', (done) => {
     const expected = mockSubsystems.map((s) => ({
       ...s,
       gw_group: component.group,
       auth: 'No authentication',
       initiator_count: 0
     }));
-    expect(component.subsystems).toEqual(expected);
-  }));
+    component.subsystems$.subscribe((subsystems) => {
+      expect(subsystems).toEqual(expected);
+      done();
+    });
+    component.getSubsystems();
+  });
 
   it('should load gateway groups correctly', () => {
     expect(component.gwGroups.length).toBe(2);
index d2207c2f57750131e67fbb197feb698ccd535684..468cf64de52df47374f1952cb0c6e9d5be5711a1 100644 (file)
@@ -23,7 +23,7 @@ import { NotificationService } from '~/app/shared/services/notification.service'
 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
 import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
 import { CephServiceSpec } from '~/app/shared/models/service.interface';
-import { forkJoin, of, Subject } from 'rxjs';
+import { BehaviorSubject, forkJoin, Observable, of, Subject } from 'rxjs';
 import { catchError, map, switchMap, takeUntil } from 'rxjs/operators';
 import { DeletionImpact } from '~/app/shared/enum/delete-confirmation-modal-impact.enum';
 
@@ -46,7 +46,6 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit
   @ViewChild('deleteTpl', { static: true })
   deleteTpl: TemplateRef<any>;
 
-  subsystems: (NvmeofSubsystem & { gw_group?: string; initiator_count?: number })[] = [];
   subsystemsColumns: any;
   permissions: Permissions;
   selection = new CdTableSelection();
@@ -58,6 +57,8 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit
   gwGroupsEmpty: boolean = false;
   gwGroupPlaceholder: string = DEFAULT_PLACEHOLDER;
   authType = NvmeofSubsystemAuthType;
+  subsystems$: Observable<(NvmeofSubsystem & { gw_group?: string; initiator_count?: number })[]>;
+  private subsystemSubject = new BehaviorSubject<void>(undefined);
 
   private destroy$ = new Subject<void>();
 
@@ -130,42 +131,43 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit
         click: () => this.deleteSubsystemModal()
       }
     ];
-  }
-
-  updateSelection(selection: CdTableSelection) {
-    this.selection = selection;
-  }
 
-  getSubsystems() {
-    if (this.group) {
-      this.nvmeofService
-        .listSubsystems(this.group)
-        .pipe(
+    this.subsystems$ = this.subsystemSubject.pipe(
+      switchMap(() => {
+        if (!this.group) {
+          return of([]);
+        }
+        return this.nvmeofService.listSubsystems(this.group).pipe(
           switchMap((subsystems: NvmeofSubsystem[] | NvmeofSubsystem) => {
             const subs = Array.isArray(subsystems) ? subsystems : [subsystems];
             if (subs.length === 0) return of([]);
-
             return forkJoin(subs.map((sub) => this.enrichSubsystemWithInitiators(sub)));
-          })
-        )
-        .pipe(takeUntil(this.destroy$))
-        .subscribe({
-          next: (subsystems: NvmeofSubsystem[]) => {
-            this.subsystems = subsystems;
-          },
-          error: (error) => {
-            this.subsystems = [];
+          }),
+          catchError((error) => {
             this.notificationService.show(
               NotificationType.error,
               $localize`Unable to fetch Gateway group`,
               $localize`Gateway group does not exist`
             );
             this.handleError(error);
-          }
-        });
-    } else {
-      this.subsystems = [];
-    }
+            return of([]);
+          })
+        );
+      }),
+      takeUntil(this.destroy$)
+    );
+  }
+
+  updateSelection(selection: CdTableSelection) {
+    this.selection = selection;
+  }
+
+  getSubsystems() {
+    this.subsystemSubject.next();
+  }
+
+  fetchData() {
+    this.subsystemSubject.next();
   }
 
   deleteSubsystemModal() {