]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: NVMe-oF – Subsystem Resource page enhancements
authorpujashahu <pshahu@redhat.com>
Mon, 29 Jun 2026 19:42:04 +0000 (01:12 +0530)
committerpujashahu <pshahu@redhat.com>
Fri, 3 Jul 2026 15:55:08 +0000 (21:25 +0530)
fixes:https://tracker.ceph.com/issues/77789

Signed-off-by: pujaoshahu <pshahu@redhat.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystem-overview/nvmeof-subsystem-overview.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts

index 7acb2ea574cc6530d9443246c496d57ab9d6f5d4..f38f1fe0d4a2ccdd3147aacf31ab0587e23cd502 100644 (file)
@@ -8,10 +8,10 @@
        [narrow]="true"
        [fullWidth]="true">
     <div cdsCol
-         [columnNumbers]="{sm: 4, md: 8}">
+         [columnNumbers]="{ sm: 4, md: 8 }">
       <div cdsRow
            class="form-heading form-item cds-mt-5">
-      <cd-help-text [formAllFieldsRequired]="true"></cd-help-text>
+        <cd-help-text [formAllFieldsRequired]="true"></cd-help-text>
       </div>
       <div cdsRow
            class="form-item">
@@ -32,8 +32,7 @@
               autofocus
               formControlName="groupName"
               cdRequiredField="Gateway group name"
-              [invalid]="groupName.isInvalid"
-            />
+              [invalid]="groupName.isInvalid" />
           </cds-text-label>
           <span
             class="invalid-feedback"
             class="invalid-feedback"
             *ngIf="groupForm.get('groupName')?.hasError('invalidChars') && (groupForm.get('groupName')?.dirty || groupForm.get('groupName')?.touched)"
             i18n>Special characters are not allowed.</span>
-
         </div>
       </div>
 
-      <!-- CDS Checkbox -->
       <div cdsRow
            class="form-item">
         <div cdsCol>
           <cds-checkbox id="enableCds"
-                        formControlName="enableEncryption"
+                        formControlName="enable_auth"
                         i18n>Enable encryption
           </cds-checkbox>
         </div>
       </div>
 
-      <!-- Encryption Configuration (shown when checkbox is checked) -->
-      @if (groupForm.controls.enableEncryption.value) {
+      @if (groupForm.controls.enable_auth.value) {
       <div cdsRow
            class="form-item">
         <div cdsCol>
                       cdValidate
                       #encryptionConfigRef="cdValidate"
                       [invalid]="encryptionConfigRef.isInvalid"
-                      formControlName="encryptionConfig"
+                      formControlName="encryptionKey"
                       cols="100"
-                      rows="4">
-            </textarea>
+                      rows="4"></textarea>
           </cds-textarea-label>
           <span
             class="invalid-feedback"
-            *ngIf="groupForm.showError('encryptionConfig', formDir, 'required')"
+            *ngIf="groupForm.showError('encryptionKey', formDir, 'required')"
             i18n>This field is required.</span>
         </div>
       </div>
       }
-      <!-- Target Nodes Selection -->
-      <div
-        cdsRow
-        class="form-item"
-      >
-      <div cdsCol>
-        <h1 class="cds--type-heading-02">Select target nodes</h1>
-      <cd-help-text>
-        <span i18n>
-          Gateway nodes to run NVMe-oF target pods/services
-        </span>
-      </cd-help-text>
-      </div>
-      <div
-        cdsCol
-        class="cds-pt-3 cds-pb-3"
-      >
-        <cd-nvmeof-gateway-node
-          (hostsLoaded)="onHostsLoaded($event)"
-        ></cd-nvmeof-gateway-node>
-      </div>
+
+      <div cdsRow
+           class="form-item">
+        <div cdsCol>
+          <h1 class="cds--type-heading-02">Select target nodes</h1>
+          <cd-help-text>
+            <span i18n>
+              Gateway nodes to run NVMe-oF target pods/services
+            </span>
+          </cd-help-text>
+        </div>
+        <div cdsCol
+             class="cds-pt-3 cds-pb-3">
+          <cd-nvmeof-gateway-node
+            (hostsLoaded)="onHostsLoaded($event)"></cd-nvmeof-gateway-node>
+        </div>
       </div>
       <div cdsRow class="form-item cds-mb-0">
         <div cdsCol>
             <div cdsRow class="form-item">
               <div cdsCol>
                 <div class="cds--stack cds--stack-horizontal">
-                  <label class="cds--type-label-01" 
+                  <label class="cds--type-label-01"
                          i18n>Enable encryption</label>
                   <cds-tag type="green"
                            size="sm"
                            i18n>Recommended</cds-tag>
                 </div>
-                <cds-checkbox id="enableEncryption" 
+                <cds-checkbox id="enableEncryption"
                               formControlName="enableEncryption">
                   <span
                   class="cds--type-body-01"
             </div>
 
         @if (!groupForm.controls.enableEncryption.value) {
-        <div cdsRow 
+        <div cdsRow
              class="cds-mt-2">
           <div cdsCol>
             <cd-alert-panel type="warning"
                             spacingClass="mb-0"
                             i18n>
-             <span class="cds--type-heading-01">Encryption is required if you plan to use DH-CHAP or mTLS authentication.</span> 
+             <span class="cds--type-heading-01">Encryption is required if you plan to use DH-CHAP or mTLS authentication.</span>
             </cd-alert-panel>
           </div>
         </div>
             @if (groupForm.controls.enableMtls.value) {
             <div cdsRow class="form-item">
               <div cdsCol>
-                <label class="cds--label fw-bold" 
+                <label class="cds--label fw-bold"
                 i18n>Choose Certificate Authority</label>
                 <cds-radio-group
                   formControlName="certificateType"
           [form]="groupForm"
           [submitText]="(action | titlecase) + ' ' + (resource)"
           [disabled]="isCreateDisabled"
-          wrappingClass="text-right"
-        >
+          wrappingClass="text-right">
         </cd-form-button-panel>
       </div>
     </div>
index f781372b9b7d3b3fcc770a3e654bd14b93679989..e873691c07795d2eecd85e61c9080093c0a1f444 100644 (file)
 <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 [cdsStack]="'vertical'"
+       [gap]="6">
+    <div cdsGrid
+         [useCssGrid]="true"
+         [condensed]="true"
+         [fullWidth]="true">
+      <div cdsCol
+           [columnNumbers]="{ sm: 4, md: 8, lg: 12 }">
+        <h3 class="cds--type-heading-03"
+            i18n>Subsystem details</h3>
+      </div>
     </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>
+    @for (row of getRows(); track row) {
+    <div cdsGrid
+         [useCssGrid]="true"
+         [condensed]="true"
+         [fullWidth]="true">
 
-    <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>
+      @for (detail of getDetailsForRow(row); track detail.label) {
+        @if (detail.type === 'auth') {
+          <div cdsCol
+               [columnNumbers]="{ sm: 4, md: 4, lg: 4 }"
+               [cdsStack]="'vertical'"
+               [gap]="2">
+            <span class="cds--type-label-01"
+                  i18n>Authentication</span>
+            <div>
+              <cd-icon [type]="getStatusIcon(detail)"></cd-icon>
+              <span class="cds--type-label-02">
+                {{ detail.value ? 'Bi-directional' : 'No authentication' }}
+              </span>
+              <a cdsLink
+                 [routerLink]="['../hosts']"
+                 [queryParams]="{ group: groupName }"
+                 [cdsStack]="'horizontal'"
+                 [gap]="1">
+                <span i18n>Edit</span>
+                <cd-icon type="edit"></cd-icon>
+              </a>
+            </div>
+          </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>
+        @else if (detail.type === 'listeners') {
+          <div cdsCol
+               [columnNumbers]="{ sm: 4, md: 4, lg: 4 }"
+               [cdsStack]="'vertical'"
+               [gap]="2">
+            <div>
+              <span class="cds--type-label-01">{{ detail.label }}</span>
+              @if (detail.tooltip) {
+                <cd-icon type="infoCircle"
+                         class="cds-ml-2"
+                         [ngbTooltip]="detail.tooltip"></cd-icon>
+              }
+            </div>
+            <span class="cds--type-label-02">{{ detail.value }}</span>
+          </div>
+        }
+
+        @else if (detail.type === 'host-access') {
+          <div cdsCol
+               [columnNumbers]="getColNumbers(detail)"
+               [cdsStack]="'vertical'"
+               [gap]="2">
+            <span class="cds--type-label-01">{{ detail.label }}</span>
+            <div class="cds--type-label-02">
+              <span>{{ detail.value ? 'Allow all hosts' : 'Restrict to specific hosts' }}</span>
+              <a cdsLink
+                 [routerLink]="['../hosts']"
+                 [queryParams]="{ group: groupName }"
+                 [cdsStack]="'horizontal'"
+                 class="cds-ml-2"
+                 [gap]="1">
+                <span i18n>Edit</span>
+                <cd-icon type="edit"></cd-icon>
+              </a>
+            </div>
+          </div>
+        }
+
+        <!-- Default: plain text row -->
+        @else {
+          <div cdsCol
+               [columnNumbers]="getColNumbers(detail)"
+               [cdsStack]="'vertical'"
+               [gap]="2">
+            <span class="cds--type-label-01">{{ detail.label }}</span>
+            <span class="cds--type-label-02">{{ getDisplayValue(detail.value) }}</span>
+          </div>
+        }
+
+      }
+
+      @if (getDetailsForRow(row).length < 3 && !isFullWidthRow(row)) {
+        @for (filler of getFillerCount(row); track filler) {
+          <div cdsCol
+               [columnNumbers]="{ sm: 4, md: 4, lg: 4 }"></div>
+        }
+      }
     </div>
-    <div class="detail-item"></div>
+    }
   </div>
 </cds-tile>
index 6270f1ecb847f2259a8789862d75321a7f22dc99..e22ab988885a7454a402674681b46982fb8c394e 100644 (file)
@@ -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;
-}
index d76456422fdfac10ec829eb7a05249ec997fcaa4..364ab36502afe0979eb7042b29ea59d75a6b3e68 100644 (file)
@@ -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');
   }));
 });
index a414dcf6c96414204941fc85769aca6ce0e97e08..5b9643065d930d7acca98313dcf1c0a3ff0bc9ec 100644 (file)
@@ -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 });
   }
 }
index e177f8198eba376f914a7a99fcf8606b84b4bd30..912cd4d3cbe13ca7ba579bba5603cc5716d7bb7a 100644 (file)
@@ -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 {