]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Remove tabs under subsystem 67472/head
authorpujaoshahu <pshahu@redhat.com>
Mon, 23 Feb 2026 18:31:30 +0000 (00:01 +0530)
committerpujaoshahu <pshahu@redhat.com>
Wed, 25 Feb 2026 14:02:34 +0000 (19:32 +0530)
Fixes: https://tracker.ceph.com/issues/74904
Signed-off-by: pujaoshahu <pshahu@redhat.com>
29 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html
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.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.spec.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.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/breadcrumb.service.ts [new file with mode: 0644]

index edb4a1ec7d3de478f103e7bf7bf5b05026b4d060..52677a13ebfdcdd0aa04c79cdde0158631233ba0 100644 (file)
@@ -96,8 +96,10 @@ import { NvmeGatewayViewBreadcrumbResolver } from './nvme-gateway-view/nvme-gate
 import { NvmeofGatewayNodeMode } from '~/app/shared/enum/nvmeof.enum';
 import { NvmeofGatewayNodeAddModalComponent } from './nvmeof-gateway-node/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component';
 import { NvmeofSubsystemNamespacesListComponent } from './nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component';
+import { NvmeofSubsystemOverviewComponent } from './nvmeof-subsystem-overview/nvmeof-subsystem-overview.component';
 import { NvmeSubsystemViewBreadcrumbResolver } from './nvme-subsystem-view/nvme-subsystem-view-breadcrumb.resolver';
 import { NvmeSubsystemViewComponent } from './nvme-subsystem-view/nvme-subsystem-view.component';
+import { NvmeofSubsystemPerformanceComponent } from './nvmeof-subsystem-performance/nvmeof-subsystem-performance.component';
 
 @NgModule({
   imports: [
@@ -180,7 +182,9 @@ import { NvmeSubsystemViewComponent } from './nvme-subsystem-view/nvme-subsystem
     NvmeofGatewayNodeAddModalComponent,
     NvmeofNamespaceExpandModalComponent,
     NvmeSubsystemViewComponent,
-    NvmeofEditHostKeyModalComponent
+    NvmeofEditHostKeyModalComponent,
+    NvmeofSubsystemOverviewComponent,
+    NvmeofSubsystemPerformanceComponent
   ],
 
   exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
@@ -340,7 +344,6 @@ const routes: Routes = [
       {
         path: 'gateways',
         component: NvmeofGatewayComponent,
-        data: { breadcrumbs: 'Gateways' },
         children: [
           {
             path: `${URLVerbs.EDIT}/:subsystem_nqn/namespace/:nsid`,
@@ -420,7 +423,11 @@ const routes: Routes = [
         component: NvmeSubsystemViewComponent,
         data: { breadcrumbs: NvmeSubsystemViewBreadcrumbResolver },
         children: [
-          { path: '', redirectTo: 'namespaces', pathMatch: 'full' },
+          { path: '', redirectTo: 'overview', pathMatch: 'full' },
+          {
+            path: 'overview',
+            component: NvmeofSubsystemOverviewComponent
+          },
           {
             path: 'hosts',
             component: NvmeofInitiatorsListComponent
@@ -432,6 +439,10 @@ const routes: Routes = [
           {
             path: 'listeners',
             component: NvmeofListenersListComponent
+          },
+          {
+            path: 'performance',
+            component: NvmeofSubsystemPerformanceComponent
           }
         ]
       }
index 5941593bfe3fcfc1d6db97a963cbc45c03e99f43..97ef80e06e7dd88f9b850a6ad01d7235013a3e62 100644 (file)
@@ -30,6 +30,11 @@ export class NvmeSubsystemViewComponent implements OnInit {
   private buildSidebarItems() {
     const extras = { queryParams: { group: this.groupName } };
     this.sidebarItems = [
+      {
+        label: $localize`Overview`,
+        route: [this.basePath, this.subsystemNQN, 'overview'],
+        routeExtras: extras
+      },
       {
         label: $localize`Initiators`,
         route: [this.basePath, this.subsystemNQN, 'hosts'],
@@ -44,6 +49,11 @@ export class NvmeSubsystemViewComponent implements OnInit {
         label: $localize`Listeners`,
         route: [this.basePath, this.subsystemNQN, 'listeners'],
         routeExtras: extras
+      },
+      {
+        label: $localize`Performance`,
+        route: [this.basePath, this.subsystemNQN, 'performance'],
+        routeExtras: extras
       }
     ];
   }
index 9da8c7c14eaad6ab2a9c6b6013b2daffe49564f0..d586dcc67304109b08232353494c81564c8922e8 100644 (file)
@@ -11,7 +11,7 @@
     [maxLimit]="25"
     identifier="hostname"
     forceIdentifier="true"
-    [autoReload]="false"
+    [autoReload]="true"
     (updateSelection)="updateSelection($event)"
     emptyStateTitle="No nodes available"
     i18n-emptyStateTitle
index 84db49150d8cda151c3ec567650eaa6c14f47933..4e2ebf2315e7c28825f000f7ab7f2a284d17e81f 100644 (file)
@@ -2,6 +2,7 @@
   <cd-table
     [data]="subsystems"
     [columns]="columns"
+    [autoReload]="true"
     columnMode="flex"
     selectionType="none"
     identifier="nqn"
index 3553beea6d2e099017cc0375e329afb70221b3c6..81095151fcd1a31adbcb5982c6abe6ab11d598f7 100644 (file)
@@ -27,7 +27,8 @@ describe('NvmeofGatewaySubsystemComponent', () => {
       max_namespaces: 256,
       namespace_count: 0,
       subtype: 'NVMe',
-      namespaces: []
+      namespaces: [],
+      has_dhchap_key: true
     } as NvmeofSubsystem,
     {
       nqn: 'nqn.2014-08.org.nvmexpress:uuid:2222',
@@ -41,7 +42,8 @@ describe('NvmeofGatewaySubsystemComponent', () => {
       max_namespaces: 256,
       namespace_count: 0,
       subtype: 'NVMe',
-      namespaces: []
+      namespaces: [],
+      has_dhchap_key: true
     } as NvmeofSubsystem
   ];
 
index a17657ae49e9586e4d2039e4355867281c22dbe1..3af5b947f0ad8a5af004fa18c6ce0dd3709dbada 100644 (file)
@@ -10,7 +10,7 @@
             isNavigation="true"
             [cacheActive]="false">
   <cds-tab
-    heading="Gateway groups"
+    heading="Gateways"
     [tabContent]="gateways_content"
     i18n-heading
     [active]="activeTab === Tabs.gateways"
index cd8a3718a62f3de8e87f33e9176d8120e8654280..683768cf1a9a2e58b4c2303dc341c7c3a85e92c9 100644 (file)
@@ -1,5 +1,5 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
 
 import { NvmeofGatewayComponent } from './nvmeof-gateway.component';
 
@@ -8,10 +8,13 @@ import { RouterTestingModule } from '@angular/router/testing';
 import { SharedModule } from '~/app/shared/shared.module';
 import { ComboBoxModule, GridModule, TabsModule } from 'carbon-components-angular';
 import { of } from 'rxjs';
+import { BreadcrumbService } from '~/app/shared/services/breadcrumb.service';
 
 describe('NvmeofGatewayComponent', () => {
   let component: NvmeofGatewayComponent;
   let fixture: ComponentFixture<NvmeofGatewayComponent>;
+  let breadcrumbService: BreadcrumbService;
+  let router: Router;
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
@@ -25,6 +28,7 @@ describe('NvmeofGatewayComponent', () => {
         TabsModule
       ],
       providers: [
+        BreadcrumbService,
         {
           provide: ActivatedRoute,
           useValue: {
@@ -36,10 +40,36 @@ describe('NvmeofGatewayComponent', () => {
 
     fixture = TestBed.createComponent(NvmeofGatewayComponent);
     component = fixture.componentInstance;
+    breadcrumbService = TestBed.inject(BreadcrumbService);
+    router = TestBed.inject(Router);
     fixture.detectChanges();
   });
 
   it('should create', () => {
     expect(component).toBeTruthy();
   });
+
+  it('should set tab crumb on init', () => {
+    spyOn(breadcrumbService, 'setTabCrumb');
+    component.ngOnInit();
+    expect(breadcrumbService.setTabCrumb).toHaveBeenCalledWith('Gateways');
+  });
+
+  it('should update tab crumb on tab switch', () => {
+    spyOn(router, 'navigate');
+    spyOn(breadcrumbService, 'setTabCrumb');
+    component.onSelected(component.Tabs.subsystem);
+    expect(router.navigate).toHaveBeenCalledWith([], {
+      relativeTo: TestBed.inject(ActivatedRoute),
+      queryParams: { tab: component.Tabs.subsystem },
+      queryParamsHandling: 'merge'
+    });
+    expect(breadcrumbService.setTabCrumb).toHaveBeenCalledWith('Subsystem');
+  });
+
+  it('should clear tab crumb on destroy', () => {
+    spyOn(breadcrumbService, 'clearTabCrumb');
+    component.ngOnDestroy();
+    expect(breadcrumbService.clearTabCrumb).toHaveBeenCalled();
+  });
 });
index 7f2eebf1d123a3bb5e593c30b889a9bcfbcbfa8b..2927fb1615cbea6d15489dc17b02beb33c5d92c8 100644 (file)
@@ -1,10 +1,13 @@
-import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
+import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
+import { Subject } from 'rxjs';
+import { filter, takeUntil } from 'rxjs/operators';
 
 import _ from 'lodash';
 
 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { BreadcrumbService } from '~/app/shared/services/breadcrumb.service';
 
 enum TABS {
   gateways = 'gateways',
@@ -12,33 +15,68 @@ enum TABS {
   namespace = 'namespace'
 }
 
+const TAB_LABELS: Record<TABS, string> = {
+  [TABS.gateways]: $localize`Gateways`,
+  [TABS.subsystem]: $localize`Subsystem`,
+  [TABS.namespace]: $localize`Namespace`
+};
+
 @Component({
   selector: 'cd-nvmeof-gateway',
   templateUrl: './nvmeof-gateway.component.html',
   styleUrls: ['./nvmeof-gateway.component.scss'],
   standalone: false
 })
-export class NvmeofGatewayComponent implements OnInit {
+export class NvmeofGatewayComponent implements OnInit, OnDestroy {
   selectedTab: TABS;
   activeTab: TABS = TABS.gateways;
+  private readonly destroy$ = new Subject<void>();
 
   @ViewChild('statusTpl', { static: true })
   statusTpl: TemplateRef<any>;
   selection = new CdTableSelection();
 
-  constructor(public actionLabels: ActionLabelsI18n, private route: ActivatedRoute) {}
+  constructor(
+    public actionLabels: ActionLabelsI18n,
+    private route: ActivatedRoute,
+    private router: Router,
+    private breadcrumbService: BreadcrumbService
+  ) {}
 
   ngOnInit() {
-    this.route.queryParams.subscribe((params) => {
+    this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
       if (params['tab'] && Object.values(TABS).includes(params['tab'])) {
         this.activeTab = params['tab'] as TABS;
       }
+      this.breadcrumbService.setTabCrumb(TAB_LABELS[this.activeTab]);
     });
+
+    this.router.events
+      .pipe(
+        filter((event) => event instanceof NavigationEnd),
+        takeUntil(this.destroy$)
+      )
+      .subscribe(() => {
+        // Run after NavigationEnd handlers so tab crumb is not cleared by global breadcrumb reset.
+        setTimeout(() => this.breadcrumbService.setTabCrumb(TAB_LABELS[this.activeTab]));
+      });
+  }
+
+  ngOnDestroy() {
+    this.destroy$.next();
+    this.destroy$.complete();
+    this.breadcrumbService.clearTabCrumb();
   }
 
   onSelected(tab: TABS) {
     this.selectedTab = tab;
     this.activeTab = tab;
+    this.router.navigate([], {
+      relativeTo: this.route,
+      queryParams: { tab },
+      queryParamsHandling: 'merge'
+    });
+    this.breadcrumbService.setTabCrumb(TAB_LABELS[tab]);
   }
 
   public get Tabs(): typeof TABS {
index 97a520db0c6a6b36f52cf0c3508002a97d2776c7..c535005fe2b89fddc477ab3fe1e869ee13cc3cf6 100644 (file)
@@ -26,6 +26,7 @@
           (fetchData)="listInitiators()"
           [columns]="initiatorColumns"
           selectionType="multiClick"
+          [autoReload]="false"
           (updateSelection)="updateSelection($event)">
   <div class="table-actions">
     <cd-table-actions [permission]="permission"
index 5f51162e21908cb58d50f8d506eca300e902dbbe..8cebc17928c26e5a8d421fd468189388a27acff6 100644 (file)
@@ -15,14 +15,14 @@ import { NvmeofInitiatorsListComponent } from './nvmeof-initiators-list.componen
 const mockInitiators = [
   {
     nqn: '*',
-    dhchap_key: ''
+    use_dhchap: ''
   }
 ];
 
 const mockSubsystem = {
   nqn: 'nqn.2016-06.io.spdk:cnode1',
   serial_number: '12345',
-  psk: ''
+  has_dhchap_key: false
 };
 
 class MockNvmeOfService {
@@ -65,7 +65,6 @@ describe('NvmeofInitiatorsListComponent', () => {
     component.subsystemNQN = 'nqn.2016-06.io.spdk:cnode1';
     component.group = 'group1';
     component.ngOnInit();
-    fixture.detectChanges();
   });
 
   it('should create', () => {
@@ -82,7 +81,7 @@ describe('NvmeofInitiatorsListComponent', () => {
   }));
 
   it('should update authStatus when initiator has dhchap_key', fakeAsync(() => {
-    const initiatorsWithKey = [{ nqn: 'nqn1', dhchap_key: 'key1' }];
+    const initiatorsWithKey = [{ nqn: 'nqn1', use_dhchap: 'key1' }];
     spyOn(TestBed.inject(NvmeofService), 'getInitiators').and.returnValue(of(initiatorsWithKey));
     component.listInitiators();
     tick();
@@ -90,7 +89,8 @@ describe('NvmeofInitiatorsListComponent', () => {
   }));
 
   it('should update authStatus when subsystem has psk', fakeAsync(() => {
-    const subsystemWithPsk = { ...mockSubsystem, psk: 'psk1' };
+    const subsystemWithPsk = { ...mockSubsystem, has_dhchap_key: true };
+    component.initiators = [{ nqn: 'nqn1', use_dhchap: 'key1' }];
     spyOn(TestBed.inject(NvmeofService), 'getSubsystem').and.returnValue(of(subsystemWithPsk));
     component.getSubsystem();
     tick();
index 2b605c2cce5714d796237f7f1eae69af121f9bc2..d806ec5402535c2ea010b959a27f3caf8d4ed4ae 100644 (file)
@@ -11,6 +11,7 @@
           (fetchData)="listListeners()"
           [columns]="listenerColumns"
           identifier="id"
+          [autoReload]="true"
           forceIdentifier="true"
           selectionType="single"
           (updateSelection)="updateSelection($event)">
index a600eadc5821fa8c34287fcad5cec6761b5cb61f..f3e43fe6d60d5d5492ec2648949f8bbfbd4f6d78 100644 (file)
@@ -25,6 +25,7 @@
   <cd-table [data]="namespaces"
             columnMode="flex"
             (fetchData)="fetchData()"
+            [autoReload]="false"
             [columns]="namespacesColumns"
             selectionType="single"
             (updateSelection)="updateSelection($event)"
index 9f3969b7e4442591a7c8b66a599f8bf21504d880..804ecf0b8023dc28a3723cee60c857a8b47c7874 100644 (file)
@@ -207,11 +207,18 @@ export class NvmeofNamespacesListComponent implements OnInit, OnDestroy {
   }
 
   updateGroupSelectionState() {
-    if (!this.group && this.gwGroups.length) {
-      this.onGroupSelection(this.gwGroups[0]);
+    if (this.gwGroups.length) {
+      if (!this.group) {
+        this.onGroupSelection(this.gwGroups[0]);
+      } else {
+        this.gwGroups = this.gwGroups.map((g) => ({
+          ...g,
+          selected: g.content === this.group
+        }));
+      }
       this.gwGroupsEmpty = false;
       this.gwGroupPlaceholder = DEFAULT_PLACEHOLDER;
-    } else if (!this.gwGroups.length) {
+    } else {
       this.gwGroupsEmpty = true;
       this.gwGroupPlaceholder = $localize`No groups available`;
     }
index 8913b850cf0af113a3b86b4bed09d75176e2a7c2..5a8a035ed7f5581ec64bb6d7414ded96c737c876 100644 (file)
@@ -1,6 +1,7 @@
   <cd-table [data]="namespaces"
             columnMode="flex"
             (fetchData)="listNamespaces()"
+            [autoReload]="true"
             [columns]="namespacesColumns"
             selectionType="single"
             (updateSelection)="updateSelection($event)"
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.html
new file mode 100644 (file)
index 0000000..f781372
--- /dev/null
@@ -0,0 +1,62 @@
+<cds-tile *ngIf="subsystem">
+  <h4 class="cds--type-heading-03 tile-title"
+      i18n>Subsystem details</h4>
+
+  <div class="details-grid">
+    <div class="detail-item">
+      <span class="cds--type-label-01"
+            i18n>Serial number</span>
+      <span class="cds--type-body-compact-01">{{ subsystem.serial_number }}</span>
+    </div>
+    <div class="detail-item">
+      <span class="cds--type-label-01"
+            i18n>Model Number</span>
+      <span class="cds--type-body-compact-01">{{ subsystem.model_number }}</span>
+    </div>
+    <div class="detail-item">
+      <span class="cds--type-label-01"
+            i18n>Gateway group</span>
+      <span class="cds--type-body-compact-01">{{ subsystem.gw_group || groupName }}</span>
+    </div>
+
+    <div class="detail-item">
+      <span class="cds--type-label-01"
+            i18n>Subsystem Type</span>
+      <span class="cds--type-body-compact-01">{{ subsystem.subtype }}</span>
+    </div>
+    <div class="detail-item">
+      <span class="cds--type-label-01"
+            i18n>HA Enabled</span>
+      <span class="cds--type-body-compact-01">{{ subsystem.enable_ha ? 'Yes' : 'No' }}</span>
+    </div>
+    <div class="detail-item">
+      <span class="cds--type-label-01"
+            i18n>Hosts allowed</span>
+      <span class="cds--type-body-compact-01">{{ subsystem.allow_any_host ? 'Any host' : 'Restricted' }}</span>
+    </div>
+
+    <div class="detail-item">
+      <span class="cds--type-label-01"
+            i18n>Maximum Controller Identifier</span>
+      <span class="cds--type-body-compact-01">{{ subsystem.max_cntlid }}</span>
+    </div>
+    <div class="detail-item">
+      <span class="cds--type-label-01"
+            i18n>Minimum Controller Identifier</span>
+      <span class="cds--type-body-compact-01">{{ subsystem.min_cntlid }}</span>
+    </div>
+    <div class="detail-item"></div>
+
+    <div class="detail-item">
+      <span class="cds--type-label-01"
+            i18n>Namespaces</span>
+      <span class="cds--type-body-compact-01">{{ subsystem.namespace_count }}</span>
+    </div>
+    <div class="detail-item">
+      <span class="cds--type-label-01"
+            i18n>Maximum allowed namespaces</span>
+      <span class="cds--type-body-compact-01">{{ subsystem.max_namespaces }}</span>
+    </div>
+    <div class="detail-item"></div>
+  </div>
+</cds-tile>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.scss
new file mode 100644 (file)
index 0000000..6270f1e
--- /dev/null
@@ -0,0 +1,18 @@
+@use '@carbon/layout';
+
+.tile-title {
+  margin-bottom: layout.$spacing-06;
+}
+
+.details-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  row-gap: layout.$spacing-06;
+  column-gap: layout.$spacing-07;
+}
+
+.detail-item {
+  display: flex;
+  flex-direction: column;
+  gap: layout.$spacing-02;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.spec.ts
new file mode 100644 (file)
index 0000000..ad7f77d
--- /dev/null
@@ -0,0 +1,181 @@
+import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ActivatedRoute } from '@angular/router';
+import { of } from 'rxjs';
+
+import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { GridModule, TilesModule } from 'carbon-components-angular';
+
+import { NvmeofSubsystemOverviewComponent } from './nvmeof-subsystem-overview.component';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { SharedModule } from '~/app/shared/shared.module';
+
+describe('NvmeofSubsystemOverviewComponent', () => {
+  let component: NvmeofSubsystemOverviewComponent;
+  let fixture: ComponentFixture<NvmeofSubsystemOverviewComponent>;
+  let nvmeofService: NvmeofService;
+
+  const mockSubsystem = {
+    nqn: 'nqn.2016-06.io.spdk:cnode1',
+    serial_number: 'Ceph30487186726692',
+    model_number: 'Ceph bdev Controller',
+    min_cntlid: 1,
+    max_cntlid: 2040,
+    subtype: 'NVMe',
+    namespace_count: 3,
+    max_namespaces: 256,
+    enable_ha: true,
+    allow_any_host: true,
+    gw_group: 'gateway-prod',
+    psk: 'some-key'
+  };
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [NvmeofSubsystemOverviewComponent],
+      imports: [
+        HttpClientTestingModule,
+        RouterTestingModule,
+        SharedModule,
+        NgbTooltipModule,
+        TilesModule,
+        GridModule
+      ],
+      providers: [
+        {
+          provide: ActivatedRoute,
+          useValue: {
+            parent: {
+              params: of({ subsystem_nqn: 'nqn.2016-06.io.spdk:cnode1' })
+            },
+            queryParams: of({ group: 'group1' })
+          }
+        },
+        {
+          provide: NvmeofService,
+          useValue: {
+            getSubsystem: jest.fn().mockReturnValue(of(mockSubsystem))
+          }
+        }
+      ]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(NvmeofSubsystemOverviewComponent);
+    component = fixture.componentInstance;
+    nvmeofService = TestBed.inject(NvmeofService);
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should fetch subsystem on init', fakeAsync(() => {
+    component.ngOnInit();
+    tick();
+    expect(nvmeofService.getSubsystem).toHaveBeenCalledWith('nqn.2016-06.io.spdk:cnode1', 'group1');
+  }));
+
+  it('should store subsystem data', fakeAsync(() => {
+    component.ngOnInit();
+    tick();
+    expect(component.subsystem).toEqual(mockSubsystem);
+    expect(component.subsystem.serial_number).toBe('Ceph30487186726692');
+    expect(component.subsystem.model_number).toBe('Ceph bdev Controller');
+    expect(component.subsystem.max_cntlid).toBe(2040);
+    expect(component.subsystem.min_cntlid).toBe(1);
+    expect(component.subsystem.namespace_count).toBe(3);
+    expect(component.subsystem.max_namespaces).toBe(256);
+    expect(component.subsystem.gw_group).toBe('gateway-prod');
+  }));
+
+  it('should not fetch when subsystemNQN is missing', fakeAsync(() => {
+    TestBed.resetTestingModule();
+    TestBed.configureTestingModule({
+      declarations: [NvmeofSubsystemOverviewComponent],
+      imports: [
+        HttpClientTestingModule,
+        RouterTestingModule,
+        SharedModule,
+        NgbTooltipModule,
+        TilesModule,
+        GridModule
+      ],
+      providers: [
+        {
+          provide: ActivatedRoute,
+          useValue: {
+            parent: {
+              params: of({})
+            },
+            queryParams: of({ group: 'group1' })
+          }
+        },
+        {
+          provide: NvmeofService,
+          useValue: {
+            getSubsystem: jest.fn().mockReturnValue(of(mockSubsystem))
+          }
+        }
+      ]
+    }).compileComponents();
+
+    const newFixture = TestBed.createComponent(NvmeofSubsystemOverviewComponent);
+    const newComponent = newFixture.componentInstance;
+    const newService = TestBed.inject(NvmeofService);
+    newFixture.detectChanges();
+    tick();
+    expect(newService.getSubsystem).not.toHaveBeenCalled();
+    expect(newComponent.subsystem).toBeUndefined();
+  }));
+
+  it('should render detail labels in the template', fakeAsync(() => {
+    component.ngOnInit();
+    tick();
+    fixture.detectChanges();
+
+    const compiled = fixture.nativeElement;
+    const labels = compiled.querySelectorAll('.cds--type-label-01');
+    const labelTexts = Array.from(labels).map((el: HTMLElement) => el.textContent.trim());
+    expect(labelTexts).toContain('Serial number');
+    expect(labelTexts).toContain('Model Number');
+    expect(labelTexts).toContain('Gateway group');
+    expect(labelTexts).toContain('Maximum Controller Identifier');
+    expect(labelTexts).toContain('Minimum Controller Identifier');
+    expect(labelTexts).toContain('Namespaces');
+    expect(labelTexts).toContain('Maximum allowed namespaces');
+  }));
+
+  it('should display subsystem type from subsystem data', fakeAsync(() => {
+    component.ngOnInit();
+    tick();
+    fixture.detectChanges();
+
+    const values = fixture.nativeElement.querySelectorAll('.cds--type-body-compact-01');
+    const valueTexts = Array.from(values).map((el: HTMLElement) => el.textContent.trim());
+    expect(valueTexts).toContain('NVMe');
+  }));
+
+  it('should display hosts allowed from subsystem data', fakeAsync(() => {
+    component.ngOnInit();
+    tick();
+    fixture.detectChanges();
+
+    const values = fixture.nativeElement.querySelectorAll('.cds--type-body-compact-01');
+    const valueTexts = Array.from(values).map((el: HTMLElement) => el.textContent.trim());
+    expect(valueTexts).toContain('Any host');
+  }));
+
+  it('should display HA status from subsystem data', fakeAsync(() => {
+    component.ngOnInit();
+    tick();
+    fixture.detectChanges();
+
+    const values = fixture.nativeElement.querySelectorAll('.cds--type-body-compact-01');
+    const valueTexts = Array.from(values).map((el: HTMLElement) => el.textContent.trim());
+    expect(valueTexts).toContain('Yes');
+  }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.ts
new file mode 100644 (file)
index 0000000..a414dcf
--- /dev/null
@@ -0,0 +1,43 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { NvmeofSubsystem } from '~/app/shared/models/nvmeof';
+
+@Component({
+  selector: 'cd-nvmeof-subsystem-overview',
+  templateUrl: './nvmeof-subsystem-overview.component.html',
+  styleUrls: ['./nvmeof-subsystem-overview.component.scss'],
+  standalone: false
+})
+export class NvmeofSubsystemOverviewComponent implements OnInit {
+  subsystemNQN: string;
+  groupName: string;
+  subsystem: NvmeofSubsystem;
+
+  constructor(private route: ActivatedRoute, private nvmeofService: NvmeofService) {}
+
+  ngOnInit() {
+    this.route.parent?.params.subscribe((params) => {
+      this.subsystemNQN = params['subsystem_nqn'];
+      this.fetchIfReady();
+    });
+    this.route.queryParams.subscribe((qp) => {
+      this.groupName = qp['group'];
+      this.fetchIfReady();
+    });
+  }
+
+  private fetchIfReady() {
+    if (this.subsystemNQN && this.groupName) {
+      this.fetchSubsystem();
+    }
+  }
+
+  fetchSubsystem() {
+    this.nvmeofService
+      .getSubsystem(this.subsystemNQN, this.groupName)
+      .subscribe((subsystem: NvmeofSubsystem) => {
+        this.subsystem = subsystem;
+      });
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.html
new file mode 100644 (file)
index 0000000..1a40ac4
--- /dev/null
@@ -0,0 +1,13 @@
+<cd-grafana *ngIf="permissions.grafana.read && subsystemNQN && groupName"
+            i18n-title
+            title="Subsystem details"
+            grafanaPath="ceph-nvme-of-gateways-performance?var-group={{groupName}}&var-subsystem={{subsystemNQN}}"
+            [type]="'metrics'"
+            uid="feeuv1dno43r4deed"
+            grafanaStyle="three">
+</cd-grafana>
+
+<cd-helper *ngIf="!permissions.grafana.read"
+           i18n>
+  Grafana permissions are required to view performance details.
+</cd-helper>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.spec.ts
new file mode 100644 (file)
index 0000000..a05e91c
--- /dev/null
@@ -0,0 +1,62 @@
+import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ActivatedRoute } from '@angular/router';
+import { of } from 'rxjs';
+
+import { NvmeofSubsystemPerformanceComponent } from './nvmeof-subsystem-performance.component';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { Permissions } from '~/app/shared/models/permissions';
+import { SharedModule } from '~/app/shared/shared.module';
+
+describe('NvmeofSubsystemPerformanceComponent', () => {
+  let component: NvmeofSubsystemPerformanceComponent;
+  let fixture: ComponentFixture<NvmeofSubsystemPerformanceComponent>;
+
+  const mockPermissions = new Permissions({ grafana: ['read'] });
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [NvmeofSubsystemPerformanceComponent],
+      imports: [HttpClientTestingModule, RouterTestingModule, SharedModule],
+      providers: [
+        {
+          provide: ActivatedRoute,
+          useValue: {
+            parent: {
+              params: of({ subsystem_nqn: 'nqn.2016-06.io.spdk:cnode1' })
+            },
+            queryParams: of({ group: 'group1' })
+          }
+        },
+        {
+          provide: AuthStorageService,
+          useValue: {
+            getPermissions: jest.fn().mockReturnValue(mockPermissions)
+          }
+        }
+      ]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(NvmeofSubsystemPerformanceComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should set subsystemNQN and groupName from route params', fakeAsync(() => {
+    component.ngOnInit();
+    tick();
+    expect(component.subsystemNQN).toBe('nqn.2016-06.io.spdk:cnode1');
+    expect(component.groupName).toBe('group1');
+  }));
+
+  it('should have grafana read permission', () => {
+    expect(component.permissions.grafana.read).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-performance/nvmeof-subsystem-performance.component.ts
new file mode 100644 (file)
index 0000000..e54087c
--- /dev/null
@@ -0,0 +1,29 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+
+@Component({
+  selector: 'cd-nvmeof-subsystem-performance',
+  templateUrl: './nvmeof-subsystem-performance.component.html',
+  styleUrls: ['./nvmeof-subsystem-performance.component.scss'],
+  standalone: false
+})
+export class NvmeofSubsystemPerformanceComponent implements OnInit {
+  subsystemNQN: string;
+  groupName: string;
+  permissions: Permissions;
+
+  constructor(private route: ActivatedRoute, private authStorageService: AuthStorageService) {
+    this.permissions = this.authStorageService.getPermissions();
+  }
+
+  ngOnInit() {
+    this.route.parent?.params.subscribe((params) => {
+      this.subsystemNQN = params['subsystem_nqn'];
+    });
+    this.route.queryParams.subscribe((qp) => {
+      this.groupName = qp['group'];
+    });
+  }
+}
index ca9f7d7ef1901b1854aff86987c80db903de6a8c..14a75e15bcc348a622bfccf725b87831bcf67d91 100644 (file)
         </cd-table-key-value>
       </ng-template>
     </ng-container>
-    <ng-container ngbNavItem="listeners">
-      <a ngbNavLink
-         i18n>Listeners</a>
-      <ng-template ngbNavContent>
-      <cd-nvmeof-listeners-list
-        [subsystemNQN]="subsystemNQN"
-        [group]="group">
-      </cd-nvmeof-listeners-list>
-      </ng-template>
-    </ng-container>
-    <ng-container ngbNavItem="namespaces">
-      <a ngbNavLink
-         i18n>Namespaces</a>
-      <ng-template ngbNavContent>
-    <cd-nvmeof-namespaces-list
-      [subsystemNQN]="subsystemNQN"
-      [group]="group">
-    </cd-nvmeof-namespaces-list>
-      </ng-template>
-    </ng-container>
-    <ng-container ngbNavItem="initiators">
-      <a ngbNavLink
-         i18n>Initiators</a>
-      <ng-template ngbNavContent>
-        <cd-nvmeof-initiators-list [subsystemNQN]="subsystemNQN"
-                                   [group]="group">
-        </cd-nvmeof-initiators-list>
-      </ng-template>
-    </ng-container>
     <ng-container ngbNavItem="performance-details"
                   *ngIf="permissions.grafana.read">
       <a ngbNavLink
index 12ba23ed7206822bed997d84feb2fbaab7027eef..0f5190080fbcd79754fd1b3254c32e041bfbbde4 100644 (file)
@@ -28,6 +28,7 @@ describe('NvmeofSubsystemsDetailsComponent', () => {
       subtype: 'NVMe',
       nqn: 'nqn.2001-07.com.ceph:1720603703820',
       namespace_count: 1,
+      has_dhchap_key: false,
       max_namespaces: DEFAULT_MAX_NAMESPACE_PER_SUBSYSTEM
     };
     component.permissions = new Permissions({
index 20c797391b2bea935198a67ab694e078988b6faa..f61913a2a07d51b57ac72057fbe704bdcd03c65e 100644 (file)
@@ -26,8 +26,6 @@
             [columns]="subsystemsColumns"
             columnMode="flex"
             selectionType="single"
-            [hasDetails]="true"
-            (setExpandedRow)="setExpandedRow($event)"
             (updateSelection)="updateSelection($event)"
             (fetchData)="fetchData()"
             emptyStateTitle="No subsystems created"
index 67ec2448363fd7eddfb2fab60a17df27823f63b4..91bdf4e9bf7125623b0e6f40c3570788e43a3cb5 100644 (file)
@@ -119,11 +119,6 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit
         name: $localize`Authentication`,
         prop: 'authentication',
         cellTemplate: this.authenticationTpl
-      },
-      {
-        name: $localize`Traffic encryption`,
-        prop: 'encryption',
-        cellTemplate: this.encryptionTpl
       }
     ];
 
index 4e6a4338a6277d311b98c4a03e1b2e6eadc20d3e..0b4cd23a09b22831660ade4fae312b0d7208b3c4 100644 (file)
@@ -31,6 +31,7 @@ import { distinct, filter, first, mergeMap, toArray } from 'rxjs/operators';
 import { AppConstants } from '~/app/shared/constants/app.constants';
 
 import { BreadcrumbsResolver, IBreadcrumb } from '~/app/shared/models/breadcrumbs';
+import { BreadcrumbService } from '~/app/shared/services/breadcrumb.service';
 
 @Component({
   selector: 'cd-breadcrumbs',
@@ -49,9 +50,17 @@ export class BreadcrumbsComponent implements OnDestroy {
    */
   finished = false;
   subscription: Subscription;
+  private tabCrumbSubscription: Subscription;
   private defaultResolver = new BreadcrumbsResolver();
-
-  constructor(private router: Router, private injector: Injector, private titleService: Title) {
+  private baseCrumbs: IBreadcrumb[] = [];
+  private currentTabCrumb: IBreadcrumb = null;
+
+  constructor(
+    private router: Router,
+    private injector: Injector,
+    private titleService: Title,
+    private breadcrumbService: BreadcrumbService
+  ) {
     this.subscription = this.router.events
       .pipe(filter((x) => x instanceof NavigationStart))
       .subscribe(() => {
@@ -61,6 +70,7 @@ export class BreadcrumbsComponent implements OnDestroy {
     this.subscription = this.router.events
       .pipe(filter((x) => x instanceof NavigationEnd))
       .subscribe(() => {
+        this.breadcrumbService.clearTabCrumb();
         const currentRoot = router.routerState.snapshot.root;
 
         this._resolveCrumbs(currentRoot)
@@ -75,15 +85,28 @@ export class BreadcrumbsComponent implements OnDestroy {
           )
           .subscribe((x) => {
             this.finished = true;
-            this.crumbs = x;
+            this.baseCrumbs = x;
+            this.crumbs = this.currentTabCrumb ? [...x, this.currentTabCrumb] : [...x];
             const title = this.getTitleFromCrumbs(this.crumbs);
             this.titleService.setTitle(title);
           });
       });
+
+    this.tabCrumbSubscription = this.breadcrumbService.tabCrumb$.subscribe((tabCrumb) => {
+      this.currentTabCrumb = tabCrumb;
+      if (tabCrumb) {
+        this.crumbs = [...this.baseCrumbs, tabCrumb];
+      } else {
+        this.crumbs = [...this.baseCrumbs];
+      }
+      const title = this.getTitleFromCrumbs(this.crumbs);
+      this.titleService.setTitle(title);
+    });
   }
 
   ngOnDestroy(): void {
     this.subscription.unsubscribe();
+    this.tabCrumbSubscription.unsubscribe();
   }
 
   private _resolveCrumbs(route: ActivatedRouteSnapshot): Observable<IBreadcrumb[]> {
index c7cfec1209e447fcc9ee8a01b061b4a34c93e05c..26d17b81d6b81129b8bbdcd26f6bf58011bf5a26 100644 (file)
@@ -23,7 +23,7 @@ export interface NvmeofSubsystem {
   enable_ha?: boolean;
   gw_group?: string;
   initiator_count?: number;
-  psk?: string;
+  has_dhchap_key: boolean;
 }
 
 export interface NvmeofSubsystemData extends NvmeofSubsystem {
@@ -33,7 +33,7 @@ export interface NvmeofSubsystemData extends NvmeofSubsystem {
 
 export interface NvmeofSubsystemInitiator {
   nqn: string;
-  dhchap_key?: string;
+  use_dhchap?: string;
 }
 
 export interface NvmeofListener {
@@ -96,10 +96,6 @@ export function getSubsystemAuthStatus(
   const UNIDIRECTIONAL = 'Unidirectional';
   const BIDIRECTIONAL = 'Bi-directional';
 
-  if (subsystem.psk) {
-    return BIDIRECTIONAL;
-  }
-
   let hostsList: NvmeofSubsystemInitiator[] = [];
   if (_initiators && 'hosts' in _initiators && Array.isArray(_initiators.hosts)) {
     hostsList = _initiators.hosts;
@@ -107,12 +103,19 @@ export function getSubsystemAuthStatus(
     hostsList = _initiators as NvmeofSubsystemInitiator[];
   }
 
-  const hasDhchapKey = hostsList.some((host) => !!host.dhchap_key);
-  if (hasDhchapKey) {
-    return UNIDIRECTIONAL;
+  let auth = NO_AUTH;
+
+  const hostHasDhchapKey = hostsList.some((host) => !!host.use_dhchap);
+
+  if (hostHasDhchapKey) {
+    auth = UNIDIRECTIONAL;
+  }
+
+  if (subsystem.has_dhchap_key && hostHasDhchapKey) {
+    auth = BIDIRECTIONAL;
   }
 
-  return NO_AUTH;
+  return auth;
 }
 
 // Form control names for NvmeofNamespacesFormComponent
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/breadcrumb.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/breadcrumb.service.ts
new file mode 100644 (file)
index 0000000..8ef1a89
--- /dev/null
@@ -0,0 +1,20 @@
+import { Injectable } from '@angular/core';
+import { BehaviorSubject } from 'rxjs';
+
+import { IBreadcrumb } from '~/app/shared/models/breadcrumbs';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class BreadcrumbService {
+  private tabCrumbSubject = new BehaviorSubject<IBreadcrumb>(null);
+  tabCrumb$ = this.tabCrumbSubject.asObservable();
+
+  setTabCrumb(text: string, path: string = null): void {
+    this.tabCrumbSubject.next({ text, path });
+  }
+
+  clearTabCrumb(): void {
+    this.tabCrumbSubject.next(null);
+  }
+}