]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: NVme-gateway-resource gateway-resources
authorSagar Gopale <sagar.gopale@ibm.com>
Tue, 13 Jan 2026 07:29:32 +0000 (12:59 +0530)
committerSagar Gopale <sagar.gopale@ibm.com>
Wed, 28 Jan 2026 09:29:23 +0000 (14:59 +0530)
Fixes: https://tracker.ceph.com/issues/74334
Signed-off-by: Sagar Gopale <sagar.gopale@ibm.com>
24 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view-breadcrumb.resolver.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view-breadcrumb.resolver.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.html
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-gateway-node/nvmeof-gateway-node.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/nvmeof.enum.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/url-builder.service.ts
src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss

index 9725b2ab6bd4cf50772a1434f621e8771f797ed1..ab2a019a32858a82a754bc27a88f4fd16202e249 100644 (file)
@@ -1,5 +1,5 @@
 import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
+import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { RouterModule, Routes } from '@angular/router';
 
@@ -63,7 +63,9 @@ import {
   SelectModule,
   UIShellModule,
   TreeviewModule,
+  SideNavModule,
   TabsModule,
+  ThemeModule,
   TagModule
 } from 'carbon-components-angular';
 
@@ -75,9 +77,14 @@ import SubtractFilled from '@carbon/icons/es/subtract--filled/32';
 import Reset from '@carbon/icons/es/reset/32';
 import SubtractAlt from '@carbon/icons/es/subtract--alt/20';
 import ProgressBarRound from '@carbon/icons/es/progress-bar--round/32';
+import Search from '@carbon/icons/es/search/32';
 import { NvmeofGatewayGroupComponent } from './nvmeof-gateway-group/nvmeof-gateway-group.component';
 import { NvmeofGroupFormComponent } from './nvmeof-group-form /nvmeof-group-form.component';
+
 import { NvmeofGatewayNodeComponent } from './nvmeof-gateway-node/nvmeof-gateway-node.component';
+import { NvmeofGatewaySubsystemComponent } from './nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component';
+import { NvmeGatewayViewComponent } from './nvme-gateway-view/nvme-gateway-view.component';
+import { NvmeGatewayViewBreadcrumbResolver } from './nvme-gateway-view/nvme-gateway-view-breadcrumb.resolver';
 
 @NgModule({
   imports: [
@@ -105,6 +112,10 @@ import { NvmeofGatewayNodeComponent } from './nvmeof-gateway-node/nvmeof-gateway
     DatePickerModule,
     ComboBoxModule,
     TabsModule,
+    TabsModule,
+    SideNavModule,
+    ThemeModule,
+
     TagModule,
     GridModule
   ],
@@ -145,8 +156,12 @@ import { NvmeofGatewayNodeComponent } from './nvmeof-gateway-node/nvmeof-gateway
     NvmeofInitiatorsListComponent,
     NvmeofInitiatorsFormComponent,
     NvmeofGatewayNodeComponent,
-    NvmeofGroupFormComponent
+    NvmeofGroupFormComponent,
+    NvmeGatewayViewComponent,
+    NvmeofGatewaySubsystemComponent
+
   ],
+  schemas: [CUSTOM_ELEMENTS_SCHEMA],
   exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
 })
 export class BlockModule {
@@ -158,7 +173,8 @@ export class BlockModule {
       SubtractFilled,
       Reset,
       ProgressBarRound,
-      SubtractAlt
+      SubtractAlt,
+      Search
     ]);
   }
 }
@@ -300,11 +316,30 @@ const routes: Routes = [
     children: [
       { path: '', redirectTo: 'gateways', pathMatch: 'full' },
       { path: 'gateways', component: NvmeofGatewayComponent, data: { breadcrumbs: 'Gateways' } },
+      { path: 'gateways', component: NvmeofGatewayComponent, data: { breadcrumbs: 'Gateways' } },
       {
         path: `gateways/${URLVerbs.CREATE}`,
         component: NvmeofGroupFormComponent,
         data: { breadcrumbs: `${ActionLabels.CREATE}${URLVerbs.GATEWAY_GROUP}` }
       },
+
+      {
+        path: `gateways/${URLVerbs.VIEW}/:group`,
+        component: NvmeGatewayViewComponent,
+        data: { breadcrumbs: `${ActionLabels.VIEW}${URLVerbs.GATEWAY_GROUP}` },
+        children: [
+          {
+            path: '',
+            component: NvmeofGatewayNodeComponent,
+            data: { breadcrumbs: NvmeGatewayViewBreadcrumbResolver }
+          },
+          {
+            path: 'subsystems',
+            component: NvmeofSubsystemsDetailsComponent,
+            data: { breadcrumbs: 'Subsystems' }
+          }
+        ]
+      },
       {
         path: 'subsystems',
         component: NvmeofSubsystemsComponent,
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view-breadcrumb.resolver.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view-breadcrumb.resolver.spec.ts
new file mode 100644 (file)
index 0000000..cd33e36
--- /dev/null
@@ -0,0 +1,48 @@
+import { TestBed } from '@angular/core/testing';
+import { ActivatedRouteSnapshot } from '@angular/router';
+
+import { NvmeGatewayViewBreadcrumbResolver } from './nvme-gateway-view-breadcrumb.resolver';
+
+describe('NvmeGatewayViewBreadcrumbResolver', () => {
+  let resolver: NvmeGatewayViewBreadcrumbResolver;
+  let route: ActivatedRouteSnapshot;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [NvmeGatewayViewBreadcrumbResolver]
+    });
+    resolver = TestBed.inject(NvmeGatewayViewBreadcrumbResolver);
+    route = new ActivatedRouteSnapshot();
+  });
+
+  it('should be created', () => {
+    expect(resolver).toBeTruthy();
+  });
+
+  it('should resolve breadcrumb with group name from parent params', () => {
+    route.params = {};
+    Object.defineProperty(route, 'parent', {
+      value: { params: { group: 'test-group' } },
+      writable: true
+    });
+
+    spyOn(resolver, 'getFullPath').and.returnValue('full/path/test-group');
+
+    const result = resolver.resolve(route);
+
+    expect(result).toEqual([{ text: 'test-group', path: 'full/path/test-group' }]);
+  });
+
+  it('should resolve breadcrumb with group name from current params', () => {
+    route.params = { group: 'test-group' };
+    Object.defineProperty(route, 'parent', {
+      value: { params: {} },
+      writable: true
+    });
+    spyOn(resolver, 'getFullPath').and.returnValue('full/path/test-group');
+
+    const result = resolver.resolve(route);
+
+    expect(result).toEqual([{ text: 'test-group', path: 'full/path/test-group' }]);
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view-breadcrumb.resolver.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view-breadcrumb.resolver.ts
new file mode 100644 (file)
index 0000000..4bce58c
--- /dev/null
@@ -0,0 +1,14 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot } from '@angular/router';
+
+import { BreadcrumbsResolver, IBreadcrumb } from '~/app/shared/models/breadcrumbs';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class NvmeGatewayViewBreadcrumbResolver extends BreadcrumbsResolver {
+  resolve(route: ActivatedRouteSnapshot): IBreadcrumb[] {
+    const group = route.parent?.params?.group || route.params?.group;
+    return [{ text: group, path: this.getFullPath(route) }];
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view.component.html
new file mode 100644 (file)
index 0000000..cc9f58b
--- /dev/null
@@ -0,0 +1,30 @@
+<header class="cds-mb-5">
+  <h2 class="cds--type-heading-05">{{ groupName }}</h2>
+</header>
+<div class="nvme-shell">
+  <div>
+    <cds-sidenav [expanded]="true">
+      <cds-sidenav-item
+        [active]="selectedTab === 'gateways'"
+        (click)="selectTab('gateways')">
+        <span class="cds--type-heading-compact-01">Gateway nodes</span>
+      </cds-sidenav-item>
+      <cds-sidenav-item
+        [active]="selectedTab === 'subsystems'"
+        (click)="selectTab('subsystems')">
+        <span class="cds--type-heading-compact-01">Subsystems</span>
+      </cds-sidenav-item>
+    </cds-sidenav>
+  </div>
+
+  <main class="nvme-main">
+  @if (selectedTab === 'gateways') {
+    <cd-nvmeof-gateway-node></cd-nvmeof-gateway-node>
+  }
+  @if (selectedTab === 'subsystems') {
+    <cd-nvmeof-gateway-subsystem
+      [groupName]="groupName">
+    </cd-nvmeof-gateway-subsystem>
+  }
+  </main>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view.component.scss
new file mode 100644 (file)
index 0000000..f237cb0
--- /dev/null
@@ -0,0 +1,13 @@
+@use '@carbon/colors';
+@use '@carbon/layout';
+
+.nvme-shell {
+  min-height: calc(100vh - #{layout.rem(157px)});
+  background-color: colors.$gray-10;
+  transform: translate(0);
+  position: relative;
+}
+
+.nvme-main {
+  margin-left: layout.rem(272px);
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view.component.spec.ts
new file mode 100644 (file)
index 0000000..5893c72
--- /dev/null
@@ -0,0 +1,31 @@
+import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { SideNavModule, ThemeModule } from 'carbon-components-angular';
+
+import { RouterTestingModule } from '@angular/router/testing';
+import { NvmeGatewayViewComponent } from './nvme-gateway-view.component';
+
+describe('NvmeGatewayViewComponent', () => {
+  let component: NvmeGatewayViewComponent;
+  let fixture: ComponentFixture<NvmeGatewayViewComponent>;
+
+  beforeEach(
+    waitForAsync(() => {
+      TestBed.configureTestingModule({
+        declarations: [NvmeGatewayViewComponent],
+        imports: [RouterTestingModule, SideNavModule, ThemeModule],
+        schemas: [CUSTOM_ELEMENTS_SCHEMA]
+      }).compileComponents();
+    })
+  );
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(NvmeGatewayViewComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-gateway-view/nvme-gateway-view.component.ts
new file mode 100644 (file)
index 0000000..ecf7eb5
--- /dev/null
@@ -0,0 +1,28 @@
+import { Component, OnInit, ViewEncapsulation } from '@angular/core';
+import { ActivatedRoute, ParamMap } from '@angular/router';
+import { Observable, of } from 'rxjs';
+import { NvmeofSubsystem } from '~/app/shared/models/nvmeof';
+
+@Component({
+  selector: 'cd-nvme-gateway-view',
+  templateUrl: './nvme-gateway-view.component.html',
+  styleUrls: ['./nvme-gateway-view.component.scss'],
+  encapsulation: ViewEncapsulation.None,
+  standalone: false
+})
+export class NvmeGatewayViewComponent implements OnInit {
+  groupName: string;
+  subsystems$: Observable<NvmeofSubsystem[]> = of([]);
+  selectedTab: string | null = 'gateways';
+  constructor(private route: ActivatedRoute) {}
+
+  ngOnInit() {
+    this.route.paramMap.subscribe((pm: ParamMap) => {
+      this.groupName = pm.get('group') ?? '';
+    });
+  }
+
+  selectTab(tab: string): void {
+    this.selectedTab = tab;
+  }
+}
index 8c629d76836de2e2c1ab4a7bfee4a013545aa92f..a92d454c604746b449cffed3d7e99c408ca8e801 100644 (file)
   <span *ngIf="created">{{ created | date:'EEE d MMM, yyyy' }}</span>
 </ng-template>
 
+<ng-template #customTableItemTemplate
+             let-value="data.value">
+  <a class="cds--link"
+     [routerLink]="['/block/nvmeof/gateways/view', value | encodeUri]"
+     (click)="$event.stopPropagation()">
+    {{ value }}
+  </a>
+</ng-template>
+
 <ng-template #gatewayStatusTpl
              let-gateway="data.value">
   <div [cdsStack]="'horizontal'"
index 7d6eec88134d878741fb13b729fa96adc0bdd369..13498a3b88a0044381eb9b060b342a9ba572d958 100644 (file)
@@ -1,6 +1,7 @@
 import { Component, OnInit, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core';
-import { BehaviorSubject, forkJoin, Observable, of } from 'rxjs';
-import { catchError, map, switchMap, tap } from 'rxjs/operators';
+import { Router } from '@angular/router';
+import { BehaviorSubject, forkJoin, Observable, of, timer } from 'rxjs';
+import { catchError, map, switchMap } from 'rxjs/operators';
 import { GatewayGroup, NvmeofService } from '~/app/shared/api/nvmeof.service';
 import { HostService } from '~/app/shared/api/host.service';
 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
@@ -24,6 +25,7 @@ import { NotificationService } from '~/app/shared/services/notification.service'
 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
 import { URLBuilderService } from '~/app/shared/services/url-builder.service';
 
+
 const BASE_URL = 'block/nvmeof/gateways';
 
 @Component({
@@ -41,12 +43,17 @@ export class NvmeofGatewayGroupComponent implements OnInit {
   @ViewChild('dateTpl', { static: true })
   dateTpl: TemplateRef<any>;
 
-  @ViewChild('gatewayStatusTpl', { static: true })
-  gatewayStatusTpl: TemplateRef<any>;
+  @ViewChild('customTableItemTemplate', { static: true })
+  customTableItemTemplate: TemplateRef<any>;
 
   @ViewChild('deleteTpl', { static: true })
   deleteTpl: TemplateRef<any>;
 
+  @ViewChild('gatewayStatusTpl', { static: true })
+  gatewayStatusTpl: TemplateRef<any>;
+
+
+
   permission: Permission;
   tableActions: CdTableAction[];
   nodesAvailable = false;
@@ -72,7 +79,8 @@ export class NvmeofGatewayGroupComponent implements OnInit {
     private cephServiceService: CephServiceService,
     public taskWrapper: TaskWrapperService,
     private notificationService: NotificationService,
-    private urlBuilder: URLBuilderService
+    private urlBuilder: URLBuilderService,
+    private router: Router
   ) {}
 
   ngOnInit(): void {
@@ -81,7 +89,8 @@ export class NvmeofGatewayGroupComponent implements OnInit {
     this.columns = [
       {
         name: $localize`Name`,
-        prop: 'name'
+        prop: 'name',
+        cellTemplate: this.customTableItemTemplate
       },
       {
         name: $localize`Gateways`,
@@ -107,6 +116,14 @@ export class NvmeofGatewayGroupComponent implements OnInit {
       canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
     };
 
+    const viewAction: CdTableAction = {
+      permission: 'read',
+      icon: Icons.eye,
+      click: () => this.getViewDetails(),
+      name: $localize`View details`,
+      canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
+    };
+
     const deleteAction: CdTableAction = {
       permission: 'delete',
       icon: Icons.destroy,
@@ -114,7 +131,10 @@ export class NvmeofGatewayGroupComponent implements OnInit {
       name: this.actionLabels.DELETE,
       canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
     };
-    this.tableActions = [createAction, deleteAction];
+
+    this.tableActions = [createAction, viewAction, deleteAction];
+
+
     this.gatewayGroup$ = this.subject.pipe(
       switchMap(() =>
         this.nvmeofService.listGatewayGroups().pipe(
@@ -206,7 +226,8 @@ export class NvmeofGatewayGroupComponent implements OnInit {
             call: this.cephServiceService.delete(serviceName)
           })
           .pipe(
-            tap(() => {
+            switchMap(() => timer(25000)),
+            map(() => {
               this.table.refreshBtn();
             }),
             catchError((error) => {
@@ -221,7 +242,6 @@ export class NvmeofGatewayGroupComponent implements OnInit {
       }
     });
   }
-
   private checkNodesAvailability(): void {
     forkJoin([this.nvmeofService.listGatewayGroups(), this.hostService.getAllHosts()]).subscribe(
       ([groups, hosts]: [GatewayGroup[][], any[]]) => {
@@ -244,4 +264,17 @@ export class NvmeofGatewayGroupComponent implements OnInit {
       }
     );
   }
+
+  getViewDetails() {
+    const selectedGroup = this.selection.first();
+    if (!selectedGroup) {
+      return;
+    }
+    const groupName = selectedGroup.spec?.group ?? selectedGroup.name ?? null;
+    if (!groupName) {
+      return;
+    }
+    const url = `/block/nvmeof/gateways/view/${encodeURIComponent(groupName)}`;
+    this.router.navigateByUrl(url);
+  }
 }
index 7dc017449018f65b09e4cd64ec62f40275543886..57c1d697eef3880c394db4f8f47803004b492a87 100644 (file)
@@ -1,21 +1,26 @@
-
-<cd-table
-  #table
-  [data]="hosts"
-  [columns]="columns"
-  columnMode="flex"
-  (fetchData)="getHosts($event)"
-  selectionType="multiClick"
-  [searchableObjects]="true"
-  [serverSide]="false"
-  [maxLimit]="25"
-  (updateSelection)="updateSelection($event)"
-  emptyStateTitle="No nodes available"
-  i18n-emptyStateTitle
-  emptyStateMessage="Add your first gateway node to start using NVMe over Fabrics. Nodes provide the resources required to expose NVMe/TCP block storage."
-  i18n-emptyStateMessage
->
-</cd-table>
+<div class="cds-mt-5">
+  <cd-table
+    #table
+    [data]="hosts"
+    [columns]="columns"
+    columnMode="flex"
+    (fetchData)="getHosts($event)"
+    selectionType="none"
+    [searchableObjects]="true"
+    [serverSide]="false"
+    [maxLimit]="25"
+    identifier="hostname"
+    forceIdentifier="true"
+    (updateSelection)="updateSelection($event)"
+  >
+    <cd-table-actions
+      class="table-actions"
+      [permission]="permission"
+      [selection]="selection"
+      [tableActions]="tableActions">
+    </cd-table-actions>
+  </cd-table>
+</div>
 
 <ng-template
   #addrTpl
index da5ad197af65b242f082ed9948e868ea212189e1..a2e72c20b68d0b735caae0bd3dfe13fa1a507295 100644 (file)
@@ -1,5 +1,6 @@
 import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
+
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { RouterTestingModule } from '@angular/router/testing';
 
@@ -152,12 +153,7 @@ describe('NvmeofGatewayNodeComponent', () => {
     component.selection = new CdTableSelection();
     component.selection.selected = [mockGatewayNodes[0], mockGatewayNodes[1]];
 
-    // ensure hosts list contains the selected hosts for lookup
-    component.hosts = [mockGatewayNodes[0], mockGatewayNodes[1]];
-
-    const selectedHosts = component
-      .getSelectedHostnames()
-      .map((hostname) => component.hosts.find((host) => host.hostname === hostname));
+    const selectedHosts = component.getSelectedHosts();
 
     expect(selectedHosts.length).toBe(2);
     expect(selectedHosts[0]).toEqual(mockGatewayNodes[0]);
@@ -173,7 +169,8 @@ describe('NvmeofGatewayNodeComponent', () => {
     expect(selectedHostnames).toEqual(['gateway-node-1', 'gateway-node-2']);
   });
 
-  it('should load hosts with orchestrator available and facts feature enabled', (done) => {
+  it('should load hosts with orchestrator available and facts feature enabled', fakeAsync(() => {
+
     const hostListSpy = spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
     const mockOrcStatus: any = {
       available: true,
@@ -187,18 +184,17 @@ describe('NvmeofGatewayNodeComponent', () => {
 
     component.getHosts(new CdTableFetchDataContext(() => undefined));
 
-    setTimeout(() => {
-      expect(hostListSpy).toHaveBeenCalled();
-      // Only hosts with status 'available', '' or 'running' are included (excluding 'maintenance')
-      expect(component.hosts.length).toBe(2);
-      expect(component.isLoadingHosts).toBe(false);
-      expect(component.hosts[0]['hostname']).toBe('gateway-node-1');
-      expect(component.hosts[0]['status']).toBe(HostStatus.AVAILABLE);
-      done();
-    }, 100);
-  });
+    tick(100);
+    expect(hostListSpy).toHaveBeenCalled();
+    // Only hosts with status 'available', '' or 'running' are included (excluding 'maintenance')
+    expect(component.hosts.length).toBe(2);
+    expect(component.isLoadingHosts).toBe(false);
+    expect(component.hosts[0]['hostname']).toBe('gateway-node-1');
+    expect(component.hosts[0]['status']).toBe('available');
+  }));
+
+  it('should normalize empty status to "available"', fakeAsync(() => {
 
-  it('should normalize empty status to "available"', (done) => {
     spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
     const mockOrcStatus: any = {
       available: true,
@@ -212,15 +208,14 @@ describe('NvmeofGatewayNodeComponent', () => {
 
     component.getHosts(new CdTableFetchDataContext(() => undefined));
 
-    setTimeout(() => {
-      // Host at index 1 in filtered list (gateway-node-3 has empty status which becomes 'available')
-      const nodeWithEmptyStatus = component.hosts.find((h) => h.hostname === 'gateway-node-3');
-      expect(nodeWithEmptyStatus?.['status']).toBe(HostStatus.AVAILABLE);
-      done();
-    }, 100);
-  });
+    tick(100);
+    // Host at index 1 in filtered list (gateway-node-3 has empty status which becomes 'available')
+    const nodeWithEmptyStatus = component.hosts.find((h) => h.hostname === 'gateway-node-3');
+    expect(nodeWithEmptyStatus?.['status']).toBe('available');
+  }));
+
+  it('should set count to hosts length', fakeAsync(() => {
 
-  it('should set count to hosts length', (done) => {
     spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
     const mockOrcStatus: any = {
       available: true,
@@ -234,14 +229,13 @@ describe('NvmeofGatewayNodeComponent', () => {
 
     component.getHosts(new CdTableFetchDataContext(() => undefined));
 
-    setTimeout(() => {
-      // Count should equal the filtered hosts length
-      expect(component.count).toBe(component.hosts.length);
-      done();
-    }, 100);
-  });
+    tick(100);
+    // Count should equal the filtered hosts length
+    expect(component.count).toBe(component.hosts.length);
+  }));
+
+  it('should set count to 0 when no hosts are returned', fakeAsync(() => {
 
-  it('should set count to 0 when no hosts are returned', (done) => {
     spyOn(hostService, 'list').and.returnValue(of([]));
     const mockOrcStatus: any = {
       available: true,
@@ -255,14 +249,13 @@ describe('NvmeofGatewayNodeComponent', () => {
 
     component.getHosts(new CdTableFetchDataContext(() => undefined));
 
-    setTimeout(() => {
-      expect(component.count).toBe(0);
-      expect(component.hosts.length).toBe(0);
-      done();
-    }, 100);
-  });
+    tick(100);
+    expect(component.count).toBe(0);
+    expect(component.hosts.length).toBe(0);
+  }));
+
+  it('should handle error when fetching hosts', fakeAsync(() => {
 
-  it('should handle error when fetching hosts', (done) => {
     const errorMsg = 'Failed to fetch hosts';
     spyOn(hostService, 'list').and.returnValue(throwError(() => new Error(errorMsg)));
     const mockOrcStatus: any = {
@@ -280,12 +273,11 @@ describe('NvmeofGatewayNodeComponent', () => {
 
     component.getHosts(context);
 
-    setTimeout(() => {
-      expect(component.isLoadingHosts).toBe(false);
-      expect(context.error).toHaveBeenCalled();
-      done();
-    }, 100);
-  });
+    tick(100);
+    expect(component.isLoadingHosts).toBe(false);
+    expect(context.error).toHaveBeenCalled();
+  }));
+
 
   it('should check hosts facts available when orchestrator features present', () => {
     component.orchStatus = {
@@ -295,12 +287,14 @@ describe('NvmeofGatewayNodeComponent', () => {
 
     spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
 
+
     const result = component.checkHostsFactsAvailable();
 
     expect(result).toBe(true);
   });
 
-  it('should return false when get_facts feature is not available', () => {
+  it('should return true even when get_facts feature is not available', () => {
+
     component.orchStatus = {
       available: true,
       features: new Map([['other_feature', { available: true }]])
@@ -308,10 +302,11 @@ describe('NvmeofGatewayNodeComponent', () => {
 
     const result = component.checkHostsFactsAvailable();
 
-    expect(result).toBe(false);
+    expect(result).toBe(true);
   });
 
-  it('should return false when orchestrator status features are empty', () => {
+  it('should return true even when orchestrator status features are empty', () => {
+
     component.orchStatus = {
       available: true,
       features: new Map()
@@ -319,7 +314,8 @@ describe('NvmeofGatewayNodeComponent', () => {
 
     const result = component.checkHostsFactsAvailable();
 
-    expect(result).toBe(false);
+    expect(result).toBe(true);
+
   });
 
   it('should return false when orchestrator status is undefined', () => {
@@ -330,30 +326,29 @@ describe('NvmeofGatewayNodeComponent', () => {
     expect(result).toBe(false);
   });
 
-  it('should not re-fetch if already loading', (done) => {
+  it('should not re-fetch if already loading', fakeAsync(() => {
+
     component.isLoadingHosts = true;
     const hostListSpy = spyOn(hostService, 'list');
 
     component.getHosts(new CdTableFetchDataContext(() => undefined));
 
-    setTimeout(() => {
-      expect(hostListSpy).not.toHaveBeenCalled();
-      done();
-    }, 100);
-  });
+    tick(100);
+    expect(hostListSpy).not.toHaveBeenCalled();
+  }));
+
 
   it('should unsubscribe on component destroy', () => {
-    const destroy$ = component['destroy$'];
-    spyOn(destroy$, 'next');
-    spyOn(destroy$, 'complete');
+    const sub = component['sub'];
+    spyOn(sub, 'unsubscribe');
 
     component.ngOnDestroy();
 
-    expect(destroy$.next).toHaveBeenCalled();
-    expect(destroy$.complete).toHaveBeenCalled();
+    expect(sub.unsubscribe).toHaveBeenCalled();
   });
 
-  it('should handle host list with various label types', (done) => {
+  it('should handle host list with various label types', fakeAsync(() => {
+
     const hostsWithLabels = [
       {
         ...mockGatewayNodes[0],
@@ -378,14 +373,12 @@ describe('NvmeofGatewayNodeComponent', () => {
 
     component.getHosts(new CdTableFetchDataContext(() => undefined));
 
-    setTimeout(() => {
-      expect(component.hosts[0]['labels'].length).toBe(3);
-      expect(component.hosts[1]['labels'].length).toBe(0);
-      done();
-    }, 100);
-  });
+    tick(100);
+    expect(component.hosts[0]['labels'].length).toBe(3);
+    expect(component.hosts[1]['labels'].length).toBe(0);
+  }));
 
-  it('should handle hosts with multiple services', (done) => {
+  it('should handle hosts with multiple services', fakeAsync(() => {
     const hostsWithServices = [
       {
         ...mockGatewayNodes[0],
@@ -409,13 +402,11 @@ describe('NvmeofGatewayNodeComponent', () => {
 
     component.getHosts(new CdTableFetchDataContext(() => undefined));
 
-    setTimeout(() => {
-      expect(component.hosts[0]['services'].length).toBe(2);
-      done();
-    }, 100);
-  });
+    tick(100);
+    expect(component.hosts[0]['services'].length).toBe(2);
+  }));
 
-  it('should initialize table context on first getHosts call', (done) => {
+  it('should initialize table context on first getHosts call', fakeAsync(() => {
     spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
     const mockOrcStatus: any = {
       available: true,
@@ -431,13 +422,11 @@ describe('NvmeofGatewayNodeComponent', () => {
 
     component.getHosts(new CdTableFetchDataContext(() => undefined));
 
-    setTimeout(() => {
-      expect((component as any).tableContext).not.toBeNull();
-      done();
-    }, 100);
-  });
+    tick(100);
+    expect((component as any).tableContext).not.toBeNull();
+  }));
 
-  it('should reuse table context if already set', (done) => {
+  it('should reuse table context if already set', fakeAsync(() => {
     const context = new CdTableFetchDataContext(() => undefined);
     spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
     const mockOrcStatus: any = {
@@ -452,10 +441,9 @@ describe('NvmeofGatewayNodeComponent', () => {
 
     component.getHosts(context);
 
-    setTimeout(() => {
-      const storedContext = (component as any).tableContext;
-      expect(storedContext).toBe(context);
-      done();
-    }, 100);
-  });
+    tick(100);
+    const storedContext = (component as any).tableContext;
+    expect(storedContext).toBe(context);
+  }));
+
 });
index 69d61470c553281af3689eb3ff93e21707d02a0e..7fe06c92692d1577a5acfa9a81b63a426b4352ac 100644 (file)
@@ -27,7 +27,6 @@ import { CephServiceSpec } from '~/app/shared/models/service.interface';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { NvmeofService } from '~/app/shared/api/nvmeof.service';
 
-import _ from 'lodash';
 
 @Component({
   selector: 'cd-nvmeof-gateway-node',
@@ -37,22 +36,19 @@ import _ from 'lodash';
 })
 export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy {
   @ViewChild(TableComponent, { static: true })
-  table: TableComponent;
+  table!: TableComponent;
 
   @ViewChild('hostNameTpl', { static: true })
-  hostNameTpl: TemplateRef<any>;
+  hostNameTpl!: TemplateRef<any>;
 
   @ViewChild('statusTpl', { static: true })
-  statusTpl: TemplateRef<any>;
+  statusTpl!: TemplateRef<any>;
 
   @ViewChild('addrTpl', { static: true })
-  addrTpl: TemplateRef<any>;
+  addrTpl!: TemplateRef<any>;
 
   @ViewChild('labelsTpl', { static: true })
-  labelsTpl: TemplateRef<any>;
-
-  @ViewChild('orchTmpl', { static: true })
-  orchTmpl: TemplateRef<any>;
+  labelsTpl!: TemplateRef<any>;
 
   @Output() selectionChange = new EventEmitter<CdTableSelection>();
   @Output() hostsLoaded = new EventEmitter<number>();
@@ -63,7 +59,8 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy {
   columns: CdTableColumn[] = [];
   hosts: Host[] = [];
   isLoadingHosts = false;
-  tableActions: CdTableAction[];
+  tableActions!: CdTableAction[];
+
   selection = new CdTableSelection();
   icons = Icons;
   HostStatus = HostStatus;
@@ -82,6 +79,23 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy {
   }
 
   ngOnInit(): void {
+    this.tableActions = [
+      {
+        permission: 'create',
+        icon: Icons.add,
+        click: () => this.addGateway(),
+        name: $localize`Add`,
+        canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+      },
+      {
+        permission: 'delete',
+        icon: Icons.destroy,
+        click: () => this.removeGateway(),
+        name: $localize`Remove`,
+        disable: (selection: CdTableSelection) => !selection.hasSelection
+      }
+    ];
+
     this.columns = [
       {
         name: $localize`Hostname`,
@@ -102,7 +116,8 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy {
         cellTemplate: this.statusTpl
       },
       {
-        name: $localize`Labels`,
+        name: $localize`Labels (tags)`,
+
         prop: 'labels',
         flexGrow: 1,
         cellTemplate: this.labelsTpl
@@ -110,6 +125,14 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy {
     ];
   }
 
+  addGateway(): void {
+    // TODO: Logic to open add gateway modal
+  }
+
+  removeGateway(): void {
+    // TODO: Logic to remove gateway
+  }
+
   ngOnDestroy(): void {
     this.destroy$.next();
     this.destroy$.complete();
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.html
new file mode 100644 (file)
index 0000000..475fa19
--- /dev/null
@@ -0,0 +1,30 @@
+<div class="cds-mt-5">
+  <cd-table
+    [data]="subsystems"
+    [columns]="columns"
+    columnMode="flex"
+    selectionType="none"
+    identifier="nqn"
+    [forceIdentifier]="true"
+    [serverSide]="false"
+    [maxLimit]="25"
+    (updateSelection)="updateSelection($event)"
+    emptyStateTitle="No subsystems linked yet."
+    emptyStateMessage="Once a subsystem is associated, it will appear in this list."
+    [emptyStateIcon]="iconType.emptySearch"
+  >
+  </cd-table>
+</div>
+
+<ng-template #authTpl
+             let-row="data.row">
+  <div [cdsStack]="'horizontal'"
+       gap="4">
+  @if (row.auth === authType.NO_AUTH) {
+    <cd-icon type="warning"></cd-icon>
+  } @else {
+    <cd-icon type="success"></cd-icon>
+  }
+  <span class="cds-ml-3">{{ row.auth }}</span>
+  </div>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.spec.ts
new file mode 100644 (file)
index 0000000..19c09eb
--- /dev/null
@@ -0,0 +1,91 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { of } from 'rxjs';
+
+import { NvmeofGatewaySubsystemComponent } from './nvmeof-gateway-subsystem.component';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { NvmeofSubsystem } from '~/app/shared/models/nvmeof';
+
+import { SharedModule } from '~/app/shared/shared.module';
+
+describe('NvmeofGatewaySubsystemComponent', () => {
+  let component: NvmeofGatewaySubsystemComponent;
+  let fixture: ComponentFixture<NvmeofGatewaySubsystemComponent>;
+  let nvmeofService: NvmeofService;
+
+  const mockSubsystems: NvmeofSubsystem[] = [
+    {
+      nqn: 'nqn.2014-08.org.nvmexpress:uuid:1111',
+      enable_ha: true,
+      allow_any_host: true,
+      gw_group: 'group1',
+      serial_number: 'SN001',
+      model_number: 'MN001',
+      min_cntlid: 1,
+      max_cntlid: 65519,
+      max_namespaces: 256,
+      namespace_count: 0,
+      subtype: 'NVMe',
+      namespaces: []
+    } as NvmeofSubsystem,
+    {
+      nqn: 'nqn.2014-08.org.nvmexpress:uuid:2222',
+      enable_ha: false,
+      allow_any_host: false,
+      gw_group: 'group1',
+      serial_number: 'SN002',
+      model_number: 'MN002',
+      min_cntlid: 1,
+      max_cntlid: 65519,
+      max_namespaces: 256,
+      namespace_count: 0,
+      subtype: 'NVMe',
+      namespaces: []
+    } as NvmeofSubsystem
+  ];
+
+  const mockInitiators1 = [{ nqn: 'host1' }, { nqn: 'host2' }];
+  const mockInitiators2 = [{ nqn: 'host3' }];
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [NvmeofGatewaySubsystemComponent],
+      imports: [HttpClientTestingModule, SharedModule],
+      providers: [
+        {
+          provide: NvmeofService,
+          useValue: {
+            listSubsystems: jest.fn(() => of(mockSubsystems)),
+            getInitiators: jest.fn((nqn) => {
+              if (nqn === 'nqn.2014-08.org.nvmexpress:uuid:1111') {
+                return of(mockInitiators1);
+              }
+              return of(mockInitiators2);
+            })
+          }
+        }
+      ]
+    }).compileComponents();
+
+    nvmeofService = TestBed.inject(NvmeofService);
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(NvmeofGatewaySubsystemComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should verify getData fetches and processes data correctly', () => {
+    component.groupName = 'direct-test-group';
+    component.getSubsystemsData();
+
+    expect(nvmeofService.listSubsystems).toHaveBeenCalledWith('direct-test-group');
+    expect(component.subsystems.length).toBe(2);
+    expect(component.subsystems[0].nqn).toBe(mockSubsystems[0].nqn);
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-subsystem/nvmeof-gateway-subsystem.component.ts
new file mode 100644 (file)
index 0000000..b67f8e9
--- /dev/null
@@ -0,0 +1,127 @@
+import {
+  Component,
+  Input,
+  OnInit,
+  OnChanges,
+  SimpleChanges,
+  TemplateRef,
+  ViewChild
+} from '@angular/core';
+import { forkJoin, of } from 'rxjs';
+import { catchError, map, switchMap } from 'rxjs/operators';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import {
+  NvmeofSubsystem,
+  NvmeofSubsystemData,
+  NvmeofSubsystemInitiator
+} from '~/app/shared/models/nvmeof';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+
+import { Icons, ICON_TYPE } from '~/app/shared/enum/icons.enum';
+import { NvmeofSubsystemAuthType } from '~/app/shared/enum/nvmeof.enum';
+
+@Component({
+  selector: 'cd-nvmeof-gateway-subsystem',
+  templateUrl: './nvmeof-gateway-subsystem.component.html',
+  styleUrls: ['./nvmeof-gateway-subsystem.component.scss'],
+  standalone: false
+})
+export class NvmeofGatewaySubsystemComponent implements OnInit, OnChanges {
+  @ViewChild('authTpl', { static: true })
+  authTpl!: TemplateRef<any>;
+
+  @Input() groupName: string;
+
+  columns: CdTableColumn[] = [];
+
+  subsystems: NvmeofSubsystemData[] = [];
+  selection = new CdTableSelection();
+  icons = Icons;
+  iconType = ICON_TYPE;
+  authType = NvmeofSubsystemAuthType;
+
+  constructor(private nvmeofService: NvmeofService) {}
+
+  ngOnInit(): void {
+    this.columns = [
+      {
+        name: $localize`Subsystem NQN`,
+        prop: 'nqn',
+        flexGrow: 2
+      },
+      {
+        name: $localize`Authentication`,
+        prop: 'auth',
+        flexGrow: 1.5,
+        cellTemplate: this.authTpl
+      },
+      {
+        name: $localize`Hosts (Initiators)`,
+        prop: 'hosts',
+        flexGrow: 1
+      }
+    ];
+  }
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.groupName && this.groupName) {
+      this.getSubsystemsData();
+    }
+  }
+
+  getSubsystemsData() {
+    this.nvmeofService
+      .listSubsystems(this.groupName)
+      .pipe(
+        switchMap((subsystems: NvmeofSubsystem[] | NvmeofSubsystem) => {
+          const subs = Array.isArray(subsystems) ? subsystems : [subsystems];
+          if (subs.length === 0) return of([]);
+
+          return forkJoin(
+            subs.map((sub) =>
+              this.nvmeofService.getInitiators(sub.nqn, this.groupName).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;
+                    }
+
+                    let authStatus = NvmeofSubsystemAuthType.BIDIRECTIONAL;
+                    if (sub.enable_ha === false) {
+                      authStatus = NvmeofSubsystemAuthType.NO_AUTH;
+                    } else if (sub.allow_any_host) {
+                      authStatus = NvmeofSubsystemAuthType.UNIDIRECTIONAL;
+                    }
+
+                    return {
+                      ...sub,
+                      auth: authStatus,
+                      hosts: count
+                    };
+                  }
+                )
+              )
+            )
+          );
+        })
+      )
+      .subscribe({
+        next: (subsystems: NvmeofSubsystemData[]) => {
+          this.subsystems = subsystems;
+        },
+        error: () => {
+          this.subsystems = [];
+        }
+      });
+  }
+
+  updateSelection(selection: CdTableSelection): void {
+    this.selection = selection;
+  }
+}
index 306dca8d5472529d6387ca2eef4871ae5bc60f25..b6392c79afd832e8697ebd843225c2c290b59468 100644 (file)
@@ -14,7 +14,7 @@ import { CdDevice } from '../models/devices';
 import { SmartDataResponseV1 } from '../models/smart';
 import { DeviceService } from '../services/device.service';
 import { Host } from '../models/host.interface';
-import { OrchestratorStatus } from '~/app/shared/models/orchestrator.interface';
+
 
 @Injectable({
   providedIn: 'root'
@@ -170,10 +170,10 @@ export class HostService extends ApiClient {
     return this.http.get<Host[]>(`${this.baseUIURL}/list`);
   }
 
-  checkHostsFactsAvailable(orchStatus?: OrchestratorStatus): boolean {
-    const orchFeatures = orchStatus?.features;
-    if (!_.isEmpty(orchFeatures)) {
-      return !!orchFeatures.get_facts?.available;
+  checkHostsFactsAvailable(orchStatus: any) {
+    if (orchStatus?.available) {
+      return true;
+
     }
     return false;
   }
index 2024593ec224214048a56f65a063324c892e0b5d..389fe60f454f23180cf286d13a25a6e8b255895a 100644 (file)
@@ -42,3 +42,6 @@ Using `color` in css and seyting svg will fill="currentColor does not work.
 .deploy-icon {
   fill: theme.$layer-selected-disabled !important;
 }
+.emptySearch-icon {
+  fill: theme.$layer-selected-disabled !important;
+}
index 08cc137916b937cd6c8ac3bdb9583fd1a48020bd..bf66bed88eaeb14fe026089000915c05fced9684 100644 (file)
@@ -33,6 +33,7 @@ export enum URLVerbs {
   /* Non-standard verbs */
   COPY = 'copy',
   CLONE = 'clone',
+  VIEW = 'view',
 
   /* Prometheus wording */
   RECREATE = 'recreate',
@@ -45,6 +46,7 @@ export enum URLVerbs {
   CONNECT = 'connect',
   RECONNECT = 'reconnect',
   GATEWAY_GROUP = 'Gateway group'
+
 }
 
 export enum ActionLabels {
@@ -87,7 +89,8 @@ export enum ActionLabels {
 
   /* Multi-cluster */
   CONNECT = 'connect',
-  RECONNECT = 'reconnect'
+  RECONNECT = 'reconnect',
+  VIEW = 'View'
 }
 
 @Injectable({
@@ -162,7 +165,7 @@ export class ActionLabelsI18n {
   EXPAND_CLUSTER: string;
   SETUP_MULTISITE_REPLICATION: string;
   NFS_EXPORT: string;
-
+  VIEW: string;
   constructor() {
     /* Create a new item */
     this.CREATE = $localize`Create`;
@@ -254,6 +257,7 @@ export class ActionLabelsI18n {
     this.EXPAND_CLUSTER = $localize`Expand Cluster`;
 
     this.NFS_EXPORT = $localize`Create NFS Export`;
+    this.VIEW = $localize`View`;
   }
 }
 
index 5f45acf054e62af07a5f7635f5624ebe90e45df1..fbc448735f0a9eb383b940a3714321fce3fbe33e 100644 (file)
@@ -102,7 +102,8 @@ export enum Icons {
   spin = 'fa fa-spin', //  To get any icon to rotate
   inverse = 'fa fa-inverse', // To get an alternative icon color
   notification = 'notification',
-  error = 'error--filled'
+  error = 'error--filled',
+  emptySearch = 'search'
 }
 
 export enum IconSize {
@@ -122,5 +123,6 @@ export const ICON_TYPE = {
   infoCircle: 'info-circle',
   notification: 'notification',
   success: 'success',
-  warning: 'warning'
+  warning: 'warning',
+  emptySearch: 'emptySearch'
 } as const;
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/nvmeof.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/nvmeof.enum.ts
new file mode 100644 (file)
index 0000000..1e06864
--- /dev/null
@@ -0,0 +1,5 @@
+export enum NvmeofSubsystemAuthType {
+  NO_AUTH = 'No authentication',
+  UNIDIRECTIONAL = 'Unidirectional',
+  BIDIRECTIONAL = 'Bi-directional'
+}
index 43a24a4b5ce2d0b6ec9bcd496323648505c3f4c5..19fd40111cec45d72c925a5922c0380efcdce424 100644 (file)
@@ -19,6 +19,15 @@ export interface NvmeofSubsystem {
   namespace_count: number;
   subtype: string;
   max_namespaces: number;
+  allow_any_host?: boolean;
+  enable_ha?: boolean;
+  gw_group?: string;
+  initiator_count?: number;
+}
+
+export interface NvmeofSubsystemData extends NvmeofSubsystem {
+  auth?: string;
+  hosts?: number;
 }
 
 export interface NvmeofSubsystemInitiator {
index b63489b8bcecdce83f7d8a89accc62b67a549936..9b750e7fe5cccab6b717bba7065cb356687f7f8a 100644 (file)
@@ -55,4 +55,8 @@ export class URLBuilderService {
   getReconnect(item: string, absolute = true): string {
     return this.getURL(URLVerbs.RECONNECT, absolute, item);
   }
+
+  getView(absolute = true): string {
+    return this.getURL(URLVerbs.VIEW, absolute);
+  }
 }
index 12abe38883520cfd082a8f10d10da1f4623a5475..f2038758e30a6221c94f33836da1b81c8429308d 100644 (file)
@@ -31,3 +31,6 @@
 .cds-mt-5 {
   margin-top: layout.$spacing-05;
 }
+.cds-pt-6 {
+  padding-top: layout.$spacing-06;
+}