From: pujashahu Date: Mon, 29 Jun 2026 19:42:04 +0000 (+0530) Subject: mgr/dashboard: NVMe-oF – Subsystem Resource page enhancements X-Git-Tag: testing/wip-vshankar-testing-20260708.090602~38^2~1 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=ef6af190a765c2d771b61a4fcbf6e96d85b8c327;p=ceph-ci.git mgr/dashboard: NVMe-oF – Subsystem Resource page enhancements fixes:https://tracker.ceph.com/issues/77789 Signed-off-by: pujaoshahu --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html index 7acb2ea574c..f38f1fe0d4a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html @@ -8,10 +8,10 @@ [narrow]="true" [fullWidth]="true">
+ [columnNumbers]="{ sm: 4, md: 8 }">
- +
@@ -32,8 +32,7 @@ autofocus formControlName="groupName" cdRequiredField="Gateway group name" - [invalid]="groupName.isInvalid" - /> + [invalid]="groupName.isInvalid" /> Special characters are not allowed. -
-
Enable encryption
- - @if (groupForm.controls.enableEncryption.value) { + @if (groupForm.controls.enable_auth.value) {
@@ -79,39 +75,33 @@ cdValidate #encryptionConfigRef="cdValidate" [invalid]="encryptionConfigRef.isInvalid" - formControlName="encryptionConfig" + formControlName="encryptionKey" cols="100" - rows="4"> - + rows="4"> This field is required.
} - -
-
-

Select target nodes

- - - Gateway nodes to run NVMe-oF target pods/services - - -
-
- -
+ +
+
+

Select target nodes

+ + + Gateway nodes to run NVMe-oF target pods/services + + +
+
+ +
@@ -120,13 +110,13 @@
- Recommended
- @if (!groupForm.controls.enableEncryption.value) { -
- Encryption is required if you plan to use DH-CHAP or mTLS authentication. + Encryption is required if you plan to use DH-CHAP or mTLS authentication.
@@ -205,7 +195,7 @@ @if (groupForm.controls.enableMtls.value) {
- + wrappingClass="text-right">
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 index f781372b9b7..e873691c077 100644 --- 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 @@ -1,62 +1,106 @@ -

Subsystem details

- -
-
- Serial number - {{ subsystem.serial_number }} -
-
- Model Number - {{ subsystem.model_number }} -
-
- Gateway group - {{ subsystem.gw_group || groupName }} +
+
+
+

Subsystem details

+
-
- Subsystem Type - {{ subsystem.subtype }} -
-
- HA Enabled - {{ subsystem.enable_ha ? 'Yes' : 'No' }} -
-
- Hosts allowed - {{ subsystem.allow_any_host ? 'Any host' : 'Restricted' }} -
+ @for (row of getRows(); track row) { +
-
- Maximum Controller Identifier - {{ subsystem.max_cntlid }} -
-
- Minimum Controller Identifier - {{ subsystem.min_cntlid }} -
-
+ @for (detail of getDetailsForRow(row); track detail.label) { + @if (detail.type === 'auth') { +
+ Authentication +
+ + + {{ detail.value ? 'Bi-directional' : 'No authentication' }} + + + Edit + + +
+
+ } -
- Namespaces - {{ subsystem.namespace_count }} -
-
- Maximum allowed namespaces - {{ subsystem.max_namespaces }} + @else if (detail.type === 'listeners') { +
+
+ {{ detail.label }} + @if (detail.tooltip) { + + } +
+ {{ detail.value }} +
+ } + + @else if (detail.type === 'host-access') { +
+ {{ detail.label }} +
+ {{ detail.value ? 'Allow all hosts' : 'Restrict to specific hosts' }} + + Edit + + +
+
+ } + + + @else { +
+ {{ detail.label }} + {{ getDisplayValue(detail.value) }} +
+ } + + } + + @if (getDetailsForRow(row).length < 3 && !isFullWidthRow(row)) { + @for (filler of getFillerCount(row); track filler) { +
+ } + }
-
+ }
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 index 6270f1ecb84..e22ab988885 100644 --- 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 @@ -1,18 +1 @@ @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 index d76456422fd..364ab36502a 100644 --- 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 @@ -28,7 +28,8 @@ describe('NvmeofSubsystemOverviewComponent', () => { enable_ha: true, allow_any_host: true, gw_group: 'gateway-prod', - psk: 'some-key' + has_dhchap_key: true, + network_mask: [] }; beforeEach(async () => { @@ -143,38 +144,23 @@ describe('NvmeofSubsystemOverviewComponent', () => { expect(labelTexts).toContain('Serial number'); expect(labelTexts).toContain('Model Number'); expect(labelTexts).toContain('Gateway group'); + expect(labelTexts).toContain('Host access'); + expect(labelTexts).toContain('Authentication'); + expect(labelTexts).toContain('Listeners'); expect(labelTexts).toContain('Maximum Controller Identifier'); expect(labelTexts).toContain('Minimum Controller Identifier'); expect(labelTexts).toContain('Namespaces'); expect(labelTexts).toContain('Maximum allowed namespaces'); })); - it('should not display MTLS label in overview details', fakeAsync(() => { + it('should display host access and auth state 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).not.toContain('MTLS'); - })); - - 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 not render Edit link for Hosts allowed', fakeAsync(() => { - component.ngOnInit(); - tick(); - fixture.detectChanges(); - - const editLink = fixture.nativeElement.querySelector('a[cdsLink]'); - expect(editLink).toBeFalsy(); + const hostAccessText = fixture.nativeElement.textContent; + expect(hostAccessText).toContain('Allow all hosts'); + expect(hostAccessText).toContain('Bi-directional'); + expect(hostAccessText).toContain('Edit'); })); }); 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 index a414dcf6c96..5b9643065d9 100644 --- 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 @@ -2,6 +2,15 @@ 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'; +import { ICON_TYPE } from '~/app/shared/enum/icons.enum'; + +export interface SubsystemDetail { + label: string; + value: string | number | boolean; + type: 'text' | 'host-access' | 'auth' | 'listeners'; + tooltip?: string; + row: number; +} @Component({ selector: 'cd-nvmeof-subsystem-overview', @@ -10,9 +19,10 @@ import { NvmeofSubsystem } from '~/app/shared/models/nvmeof'; standalone: false }) export class NvmeofSubsystemOverviewComponent implements OnInit { - subsystemNQN: string; - groupName: string; - subsystem: NvmeofSubsystem; + subsystemNQN!: string; + groupName!: string; + subsystem!: NvmeofSubsystem; + details: SubsystemDetail[] = []; constructor(private route: ActivatedRoute, private nvmeofService: NvmeofService) {} @@ -34,10 +44,100 @@ export class NvmeofSubsystemOverviewComponent implements OnInit { } fetchSubsystem() { - this.nvmeofService - .getSubsystem(this.subsystemNQN, this.groupName) - .subscribe((subsystem: NvmeofSubsystem) => { - this.subsystem = subsystem; - }); + this.nvmeofService.getSubsystem(this.subsystemNQN, this.groupName).subscribe((subsystem) => { + this.subsystem = subsystem as NvmeofSubsystem; + this.buildDetails(); + }); + } + + private buildDetails() { + this.details = [ + { + label: $localize`Serial number`, + value: this.subsystem.serial_number, + type: 'text', + row: 1 + }, + { label: $localize`Model Number`, value: this.subsystem.model_number, type: 'text', row: 1 }, + { + label: $localize`Gateway group`, + value: this.subsystem.gw_group || this.groupName, + type: 'text', + row: 1 + }, + { + label: $localize`Host access`, + value: this.subsystem.allow_any_host ?? false, + type: 'host-access', + row: 2 + }, + { + label: $localize`Authentication`, + value: this.subsystem.has_dhchap_key, + type: 'auth', + row: 2 + }, + { + label: $localize`Listeners`, + value: + (this.subsystem.network_mask?.length ?? 0) > 0 + ? $localize`Auto-fetched` + : $localize`Manually selected`, + type: 'listeners', + tooltip: $localize`Listeners are automatically fetched from the gateway`, + row: 3 + }, + { + label: $localize`Maximum Controller Identifier`, + value: this.subsystem.max_cntlid, + type: 'text', + row: 3 + }, + { + label: $localize`Minimum Controller Identifier`, + value: this.subsystem.min_cntlid, + type: 'text', + row: 3 + }, + { label: $localize`Namespaces`, value: this.subsystem.namespace_count, type: 'text', row: 4 }, + { + label: $localize`Maximum allowed namespaces`, + value: this.subsystem.max_namespaces, + type: 'text', + row: 4 + } + ]; + } + + getRows(): number[] { + return [...new Set(this.details.map((d) => d.row))]; + } + + getDetailsForRow(row: number): SubsystemDetail[] { + return this.details.filter((d) => d.row === row); + } + + getDisplayValue(value: string | number | boolean): string { + if (typeof value === 'boolean') { + return value ? $localize`Enabled` : $localize`Disabled`; + } + return String(value); + } + + getStatusIcon(detail: SubsystemDetail): keyof typeof ICON_TYPE { + return detail.value ? 'success' : 'error'; + } + + getColNumbers(detail: SubsystemDetail): { sm: number; md: number; lg: number } { + return detail.type === 'auth' ? { sm: 4, md: 8, lg: 12 } : { sm: 4, md: 4, lg: 4 }; + } + + isFullWidthRow(row: number): boolean { + return this.getDetailsForRow(row).some((d) => d.type === 'auth'); + } + + getFillerCount(row: number): number[] { + const needed = 3 - this.getDetailsForRow(row).length; + return Array.from({ length: needed }); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts index e177f8198eb..912cd4d3cbe 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts @@ -24,6 +24,7 @@ export interface NvmeofSubsystem { gw_group?: string; initiator_count?: number; has_dhchap_key: boolean; + network_mask?: string[]; } export interface NvmeofSubsystemData extends NvmeofSubsystem {