]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Implement NVMe-oF Gateway Group Resource Overview Page
authorpujashahu <pshahu@redhat.com>
Fri, 12 Jun 2026 22:05:13 +0000 (03:35 +0530)
committerpujashahu <pshahu@redhat.com>
Fri, 3 Jul 2026 08:06:44 +0000 (13:36 +0530)
Fixes: https://tracker.ceph.com/issues/77383
Signed-off-by: pujaoshahu <pshahu@redhat.com>
(cherry picked from commit 254ab5383505962460d06b483dd931bf91c1f0b8)

12 files changed:
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.scss
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-add-modal/nvmeof-gateway-node-add-modal.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/details-card/details-card.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/details-card/details-card.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/details-card/details-card.component.ts

index 8d4007806bcf981c3ff9dcb8482420e63525f78d..5084e98a14b72bf9faa13c884405c9c3f1161350 100644 (file)
@@ -1,4 +1,17 @@
 <ng-container *ngIf="gatewayGroup$ | async as gateways">
+  <!-- Details Card Section -->
+  <ng-container *ngIf="selection.hasSelection">
+    <cd-details-card
+      cardTitle="Details"
+      i18n-cardTitle
+      [details]="selectedGatewayDetails"
+      [showEditButton]="true"
+      [editButtonLabel]="actionLabels.EDIT"
+      [columns]="4"
+      (editClicked)="editSelectedGatewayGroup()">
+    </cd-details-card>
+  </ng-container>
+
   <cd-table
     #table
     [data]="gateways"
index b59f235e8affaa80c3d69780f1464c54d33c03f1..b89d8bc9a51437ae2459645f03caaa19e93ca6f5 100644 (file)
@@ -26,6 +26,7 @@ import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
 import { CephServiceService } from '~/app/shared/api/ceph-service.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
+import { DetailItem } from '~/app/shared/components/details-card/details-card.component';
 import { FinishedTask } from '~/app/shared/models/finished-task';
 import { DeletionImpact } from '~/app/shared/enum/delete-confirmation-modal-impact.enum';
 import { NotificationService } from '~/app/shared/services/notification.service';
@@ -69,10 +70,11 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
   selection: CdTableSelection = new CdTableSelection();
   gatewayGroup$: Observable<CephServiceSpec[]> = of([]);
   subject = new BehaviorSubject<CephServiceSpec[]>([]);
-  context!: CdTableFetchDataContext;
+  context?: CdTableFetchDataContext;
   gatewayGroupName = '';
   subsystemCount = 0;
   gatewayCount = 0;
+  selectedGatewayDetails: DetailItem[] = [];
   private lastGroupCount = 0;
 
   viewUrl = `/${BASE_URL}/view`;
@@ -127,6 +129,14 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
       canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
     };
 
+    const editAction: CdTableAction = {
+      permission: 'update',
+      icon: Icons.edit,
+      routerLink: () => this.urlBuilder.getEdit(this.selection.first()?.name),
+      name: this.actionLabels.EDIT,
+      canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection
+    };
+
     const viewAction: CdTableAction = {
       permission: 'read',
       icon: Icons.eye,
@@ -143,7 +153,7 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
       canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
     };
 
-    this.tableActions = [createAction, viewAction, deleteAction];
+    this.tableActions = [createAction, editAction, viewAction, deleteAction];
 
     this.gatewayGroup$ = this.subject.pipe(
       switchMap(() =>
@@ -206,6 +216,7 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
 
   updateSelection(selection: CdTableSelection): void {
     this.selection = selection;
+    this.selectedGatewayDetails = this.buildGatewayDetails(selection.first());
   }
 
   deleteGatewayGroupModal() {
@@ -324,6 +335,45 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
     this.router.navigate([this.viewUrl, groupName]);
   }
 
+  editSelectedGatewayGroup(): void {
+    const selectedGroup = this.selection.first();
+    if (!selectedGroup) {
+      return;
+    }
+    this.router.navigate([this.urlBuilder.getEdit(selectedGroup.name)]);
+  }
+
+  private buildGatewayDetails(selectedGroup: any): DetailItem[] {
+    if (!selectedGroup) {
+      return [];
+    }
+
+    const runningGateways = selectedGroup.statusCount?.running ?? 0;
+    const errorGateways = selectedGroup.statusCount?.error ?? 0;
+    const totalGateways = runningGateways + errorGateways;
+
+    return [
+      {
+        label: $localize`Gateway name`,
+        value: selectedGroup.name
+      },
+      {
+        label: $localize`Gateway nodes`,
+        value: totalGateways
+      },
+      {
+        label: $localize`Encryption`,
+        value: selectedGroup.spec?.enable_auth ? $localize`Enabled` : $localize`Disabled`,
+        type: 'status'
+      },
+      {
+        label: $localize`mTLS`,
+        value: selectedGroup.spec?.enable_mtls ? $localize`Enabled` : $localize`Disabled`,
+        type: 'status'
+      }
+    ];
+  }
+
   ngOnDestroy(): void {
     this.destroy$.next();
     this.destroy$.complete();
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.html
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.spec.ts
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.ts
new file mode 100644 (file)
index 0000000..e69de29
index 15452fcf264ff9ce775bfc695aeb2d07897094cc..294dd0ad5f28545c76c3a61118dd71bc6bfa5e65 100644 (file)
@@ -45,6 +45,7 @@ import {
   LayerModule,
   ThemeModule
 } from 'carbon-components-angular';
+import { ProductiveCardComponent } from './productive-card/productive-card.component';
 import EditIcon from '@carbon/icons/es/edit/16';
 import CodeIcon from '@carbon/icons/es/code/16';
 import { MotdComponent } from '~/app/shared/components/motd/motd.component';
@@ -186,7 +187,8 @@ import { ProductiveCardComponent } from './productive-card/productive-card.compo
     TagModule,
     LinkModule,
     LayerModule,
-    ThemeModule
+    ThemeModule,
+    ProductiveCardComponent
   ],
   declarations: [
     SparklineComponent,
index dd302459d61d6173c3fb6a3cb11579c879ff414e..d60b1f3b3454ea2ab286df4149c7f672334ec874 100644 (file)
@@ -2,23 +2,31 @@
   <ng-template #header>
     @if (cardTitle) {
     <h3 class="cds--type-heading-03" i18n>{{ cardTitle }}</h3>
+    @if (showEditButton) {
+    <button
+      cdsButton="ghost"
+      size="sm"
+      [disabled]="editButtonDisabled"
+      (click)="onEditClick()"
+      [attr.aria-label]="editButtonLabel"
+    >
+      {{ editButtonLabel }}
+      <cd-icon class="cds-ml-5" type="edit"></cd-icon>
+    </button>
+    }
     }
   </ng-template>
 
-  <div [cdsStack]="'vertical'" 
-       [gap]="0">
+  <div [cdsStack]="'vertical'" [gap]="0">
     @if (details && details.length > 0) {
-    <div [cdsStack]="'horizontal'" 
-         [gap]="6">
+    <div [cdsStack]="'horizontal'" [gap]="6">
       @for (detail of getVisibleDetails(); track detail.label) {
-      <div [cdsStack]="'vertical'" 
-           [gap]="2">
+      <div [cdsStack]="'vertical'" [gap]="2">
         <label class="cds--type-label-01">{{ detail.label }}</label>
         <div class="cds--type-body-01">
           @switch (detail.type) {
             @case ('status') {
-              <div [cdsStack]="'horizontal'" 
-                   [gap]="3">
+              <div [cdsStack]="'horizontal'" [gap]="3">
                 <cd-icon [type]="getStatusIcon(detail)"></cd-icon>
                 <span>{{ getDisplayValue(detail.value) }}</span>
               </div>
index 8e1912a5da04e83b696c018c9c1fe2a5e0c2f15b..a8eb9996aa84c97dd97f84d73bf29ab615630e66 100644 (file)
@@ -36,6 +36,11 @@ describe('DetailsCardComponent', () => {
     expect(component.isStatusDisabled('Enabled')).toBe(false);
   });
 
+  it('should emit editClicked event', () => {
+    spyOn(component.editClicked, 'emit');
+    component.onEditClick();
+    expect(component.editClicked.emit).toHaveBeenCalled();
+  });
   it('should handle empty details array', () => {
     component.details = [];
     const visible = component.getVisibleDetails();
index 41d69190d5bb5e20e0549ad3c979d79ef0fab1d1..7cfed7235e0a16d3c462f5be5df9e72f88b09239 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, Input, TemplateRef } from '@angular/core';
+import { Component, EventEmitter, Input, Output, TemplateRef } from '@angular/core';
 
 type DetailValue = any;
 type DetailTemplateContext = { $implicit: DetailValue; detail: DetailItem };
@@ -25,6 +25,25 @@ export class DetailsCardComponent {
   @Input()
   details?: DetailItem[] = [];
 
+  @Input()
+  showEditButton = false;
+
+  @Input()
+  editButtonLabel = $localize`Edit`;
+
+  @Input()
+  editButtonDisabled = false;
+
+  @Input()
+  columns = 4;
+
+  @Output()
+  editClicked = new EventEmitter<void>();
+
+  onEditClick(): void {
+    this.editClicked.emit();
+  }
+  
   @Input()
   columns = 4;