]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: NVme-Subsystem list 66751/head
authorPuja Shahu <pshahu@li-4dbc3fcc-2cf0-11b2-a85c-8cca2743bba1.ibm.com>
Mon, 29 Dec 2025 07:57:06 +0000 (13:27 +0530)
committerpujaoshahu <pshahu@redhat.com>
Wed, 21 Jan 2026 04:24:07 +0000 (09:54 +0530)
Fixes: https://tracker.ceph.com/issues/74284
Signed-off-by:pujaoshahu <pshahu@redhat.com>
Signed-off-by: Puja Shahu <pshahu@li-4dbc3fcc-2cf0-11b2-a85c-8cca2743bba1.ibm.com>
Signed-off-by: pujaoshahu <pshahu@redhat.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.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 5dcca32b006dd43e5e4ee42bb42c60944719e6b1..b17aa751ce5551708d6877981836e9b44cbdfac0 100644 (file)
@@ -140,7 +140,14 @@ export class NvmeofGatewayGroupComponent implements OnInit {
             );
           }),
           catchError((error) => {
-            this.context?.error?.(error);
+            this.notificationService.show(
+              NotificationType.error,
+              $localize`Unable to fetch Gateway group`,
+              $localize`Gateway group does not exist`
+            );
+            if (error?.preventDefault) {
+              error.preventDefault();
+            }
             return of([]);
           })
         )
index 84a1c8763b1b1c7d2b0f32bff5cf482d885dd374..c9956b82e8ccea17d067975f4bc23c62c79066ff 100644 (file)
   </cds-combo-box>
 </div>
 
-<legend i18n>
-  Subsystems
-  <cd-help-text>
-    A subsystem provides access control to which hosts can access the namespaces within the subsystem.
-  </cd-help-text>
-</legend>
-<cd-table [data]="subsystems"
-          columnMode="flex"
-          (fetchData)="getSubsystems()"
+<cd-table #table
+          [data]="subsystems"
           [columns]="subsystemsColumns"
+          columnMode="flex"
           selectionType="single"
           [hasDetails]="true"
           (setExpandedRow)="setExpandedRow($event)"
   <cd-nvmeof-subsystems-details *cdTableDetail
                                 [selection]="expandedRow"
                                 [permissions]="permissions"
-                                [group]="group">
+                                [group]="expandedRow?.gw_group">
   </cd-nvmeof-subsystems-details>
 </cd-table>
 
+<ng-template #authenticationTpl
+             let-row="data.row">
+  <div [cdsStack]="'horizontal'"
+       gap="4">
+  @if (row.enable_ha === false) {
+    <cd-icon type="warning"></cd-icon>
+    <span class="cds-ml-3"
+          i18n>No authentication</span>
+  } @else if (row.allow_any_host) {
+    <cd-icon type="success"></cd-icon>
+    <span class="cds-ml-3"
+          i18n>Unidirectional</span>
+  } @else {
+    <cd-icon type="success"></cd-icon>
+    <span class="cds-ml-3"
+          i18n>Bidirectional</span>
+  }
+  </div>
+</ng-template>
+
+<ng-template #encryptionTpl
+             let-row="data.row">
+  <div [cdsStack]="'horizontal'"
+       gap="4">
+  @if (row.enable_ha) {
+    <cd-icon type="success"></cd-icon>
+    <span class="cds-ml-3"
+          i18n>Enabled</span>
+  } @else {
+    <cd-icon type="error"></cd-icon>
+    <span class="cds-ml-3"
+          i18n>Disabled</span>
+  }
+  </div>
+</ng-template>
+
 <router-outlet name="modal"></router-outlet>
index 413cec31ce346543376382a5400eb31890a99183..d2de1c4090577437aed50a308d298cedd96f6dd3 100644 (file)
@@ -6,7 +6,7 @@ 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 { NvmeofSubsystemsComponent } from './nvmeof-subsystems.component';
 import { NvmeofSubsystemsDetailsComponent } from '../nvmeof-subsystems-details/nvmeof-subsystems-details.component';
@@ -63,6 +63,10 @@ class MockNvmeOfService {
     return of(mockSubsystems);
   }
 
+  getInitiators() {
+    return of([]);
+  }
+
   formatGwGroupsList(_data: CephServiceSpec[][]) {
     return mockformattedGwGroups;
   }
@@ -93,7 +97,7 @@ describe('NvmeofSubsystemsComponent', () => {
       providers: [
         { provide: NvmeofService, useClass: MockNvmeOfService },
         { provide: AuthStorageService, useClass: MockAuthStorageService },
-        { provide: ModalService, useClass: MockModalService },
+        { provide: ModalCdsService, useClass: MockModalService },
         { provide: TaskWrapperService, useClass: MockTaskWrapperService }
       ]
     }).compileComponents();
@@ -111,7 +115,12 @@ describe('NvmeofSubsystemsComponent', () => {
   it('should retrieve subsystems', fakeAsync(() => {
     component.getSubsystems();
     tick();
-    expect(component.subsystems).toEqual(mockSubsystems);
+    const expected = mockSubsystems.map((s) => ({
+      ...s,
+      gw_group: component.group,
+      initiator_count: 0
+    }));
+    expect(component.subsystems).toEqual(expected);
   }));
 
   it('should load gateway groups correctly', () => {
index c725199d13f13029dff4f8cfb0635dde230280d6..dc29124710205489740cf90e8cf872c1f73a8fb6 100644 (file)
@@ -1,20 +1,25 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
-
 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
-import { NvmeofSubsystem } from '~/app/shared/models/nvmeof';
+import { NvmeofSubsystem, NvmeofSubsystemInitiator } from '~/app/shared/models/nvmeof';
 import { Permissions } from '~/app/shared/models/permissions';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
 import { CdTableAction } from '~/app/shared/models/cd-table-action';
+
 import { Icons } from '~/app/shared/enum/icons.enum';
 import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
 import { FinishedTask } from '~/app/shared/models/finished-task';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { NvmeofService, GroupsComboboxItem } from '~/app/shared/api/nvmeof.service';
+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 { catchError, map, switchMap, takeUntil } from 'rxjs/operators';
 
 const BASE_URL = 'block/nvmeof/subsystems';
 const DEFAULT_PLACEHOLDER = $localize`Enter group name`;
@@ -25,18 +30,27 @@ const DEFAULT_PLACEHOLDER = $localize`Enter group name`;
   styleUrls: ['./nvmeof-subsystems.component.scss'],
   standalone: false
 })
-export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit {
-  subsystems: NvmeofSubsystem[] = [];
+export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit, OnDestroy {
+  @ViewChild('authenticationTpl', { static: true })
+  authenticationTpl: TemplateRef<any>;
+
+  @ViewChild('encryptionTpl', { static: true })
+  encryptionTpl: TemplateRef<any>;
+
+  subsystems: (NvmeofSubsystem & { gw_group?: string; initiator_count?: number })[] = [];
   subsystemsColumns: any;
   permissions: Permissions;
   selection = new CdTableSelection();
   tableActions: CdTableAction[];
   subsystemDetails: any[];
+  context: CdTableFetchDataContext;
   gwGroups: GroupsComboboxItem[] = [];
   group: string = null;
   gwGroupsEmpty: boolean = false;
   gwGroupPlaceholder: string = DEFAULT_PLACEHOLDER;
 
+  private destroy$ = new Subject<void>();
+
   constructor(
     private nvmeofService: NvmeofService,
     private authStorageService: AuthStorageService,
@@ -44,29 +58,46 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit
     private router: Router,
     private modalService: ModalCdsService,
     private taskWrapper: TaskWrapperService,
-    private route: ActivatedRoute
+    private route: ActivatedRoute,
+    private notificationService: NotificationService
   ) {
     super();
     this.permissions = this.authStorageService.getPermissions();
   }
 
   ngOnInit() {
-    this.route.queryParams.subscribe((params) => {
+    this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
       if (params?.['group']) this.onGroupSelection({ content: params?.['group'] });
     });
     this.setGatewayGroups();
     this.subsystemsColumns = [
       {
-        name: $localize`NQN`,
-        prop: 'nqn'
+        name: $localize`Subsystem NQN`,
+        prop: 'nqn',
+        flexGrow: 2
       },
       {
-        name: $localize`# Namespaces`,
+        name: $localize`Gateway group`,
+        prop: 'gw_group'
+      },
+      {
+        name: $localize`Initiators`,
+        prop: 'initiator_count'
+      },
+
+      {
+        name: $localize`Namespaces`,
         prop: 'namespace_count'
       },
       {
-        name: $localize`# Maximum Allowed Namespaces`,
-        prop: 'max_namespaces'
+        name: $localize`Authentication`,
+        prop: 'authentication',
+        cellTemplate: this.authenticationTpl
+      },
+      {
+        name: $localize`Traffic encryption`,
+        prop: 'encryption',
+        cellTemplate: this.encryptionTpl
       }
     ];
     this.tableActions = [
@@ -90,7 +121,6 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit
     ];
   }
 
-  // Subsystems
   updateSelection(selection: CdTableSelection) {
     this.selection = selection;
   }
@@ -99,9 +129,28 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit
     if (this.group) {
       this.nvmeofService
         .listSubsystems(this.group)
-        .subscribe((subsystems: NvmeofSubsystem[] | NvmeofSubsystem) => {
-          if (Array.isArray(subsystems)) this.subsystems = subsystems;
-          else this.subsystems = [subsystems];
+        .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 = [];
+            this.notificationService.show(
+              NotificationType.error,
+              $localize`Unable to fetch Gateway group`,
+              $localize`Gateway group does not exist`
+            );
+            this.handleError(error);
+          }
         });
     } else {
       this.subsystems = [];
@@ -135,19 +184,70 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit
   }
 
   setGatewayGroups() {
-    this.nvmeofService.listGatewayGroups().subscribe((response: CephServiceSpec[][]) => {
-      if (response?.[0]?.length) {
-        this.gwGroups = this.nvmeofService.formatGwGroupsList(response);
-      } else this.gwGroups = [];
-      // Select first group if no group is selected
-      if (!this.group && this.gwGroups.length) {
-        this.onGroupSelection(this.gwGroups[0]);
-        this.gwGroupsEmpty = false;
-        this.gwGroupPlaceholder = DEFAULT_PLACEHOLDER;
-      } else {
-        this.gwGroupsEmpty = true;
-        this.gwGroupPlaceholder = $localize`No groups available`;
-      }
-    });
+    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 {
+      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`;
+    this.handleError(error);
+  }
+
+  private handleError(error: any): void {
+    if (error?.preventDefault) {
+      error?.preventDefault?.();
+    }
+    this.context?.error?.(error);
+  }
+
+  private enrichSubsystemWithInitiators(sub: NvmeofSubsystem) {
+    return this.nvmeofService.getInitiators(sub.nqn, this.group).pipe(
+      catchError(() => of([])),
+      map((initiators: NvmeofSubsystemInitiator[] | { hosts?: NvmeofSubsystemInitiator[] }) => {
+        let count = 0;
+        if (Array.isArray(initiators)) count = initiators.length;
+        else if (initiators?.hosts && Array.isArray(initiators.hosts)) {
+          count = initiators.hosts.length;
+        }
+
+        return {
+          ...sub,
+          gw_group: this.group,
+          initiator_count: count
+        } as NvmeofSubsystem & { initiator_count?: number };
+      })
+    );
+  }
+
+  ngOnDestroy() {
+    this.destroy$.next();
+    this.destroy$.complete();
   }
 }