]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Implement backword support for Create existing nvme-oF gateway groups... 69693/head
authorpujashahu <pshahu@redhat.com>
Wed, 24 Jun 2026 08:08:54 +0000 (13:38 +0530)
committerpujashahu <pshahu@redhat.com>
Mon, 6 Jul 2026 14:59:39 +0000 (20:29 +0530)
Fixes : https://tracker.ceph.com/issues/77632

Signed-off-by: pujaoshahu <pshahu@redhat.com>
13 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
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/nvmeof-gateway-node.component.html
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-group-form/nvmeof-group-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.spec.ts
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/cluster/services/service-form/service-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/details-card/details-card.component.html

index 722832ae977ab149d2007974461a090284c30b47..2291a091997f632691235991432c27091343ea62 100644 (file)
@@ -10,6 +10,7 @@ import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants';
 import { FeatureTogglesGuardService } from '~/app/shared/services/feature-toggles-guard.service';
 import { ModuleStatusGuardService } from '~/app/shared/services/module-status-guard.service';
 import { SharedModule } from '~/app/shared/shared.module';
+import { TextLabelListComponent } from '~/app/shared/components/text-label-list/text-label-list.component';
 import { IscsiSettingComponent } from './iscsi-setting/iscsi-setting.component';
 import { IscsiTabsComponent } from './iscsi-tabs/iscsi-tabs.component';
 import { IscsiTargetDetailsComponent } from './iscsi-target-details/iscsi-target-details.component';
@@ -148,7 +149,8 @@ import { NvmeofEditAuthenticationComponent } from './nvmeof-edit-authentication/
     LayoutModule,
     ThemeModule,
     NvmeofSetupCardsComponent,
-    NvmeofGatewayGroupFilterComponent
+    NvmeofGatewayGroupFilterComponent,
+    TextLabelListComponent
   ],
   declarations: [
     RbdListComponent,
@@ -380,6 +382,17 @@ const routes: Routes = [
               }
             }
           },
+          {
+            path: `${URLVerbs.EDIT}/:name`,
+            component: NvmeofGroupFormComponent,
+            data: {
+              breadcrumbs: ActionLabels.EDIT,
+              pageHeader: {
+                title: $localize`Edit Gateway Group`,
+                description: $localize`Modify gateway group configuration.`
+              }
+            }
+          },
           {
             path: `${URLVerbs.VIEW}/:group`,
             component: NvmeGatewayViewComponent,
index 287361a41dc23618903307ca37b5ed7b99656ae6..8d4007806bcf981c3ff9dcb8482420e63525f78d 100644 (file)
@@ -1,43 +1,27 @@
-<div class="nvmeof-content-layout">
-  <div class="nvmeof-content-main">
-    <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>
+<ng-container *ngIf="gatewayGroup$ | async as gateways">
+  <cd-table
+    #table
+    [data]="gateways"
+    [columns]="columns"
+    columnMode="flex"
+    selectionType="single"
+    identifier="name"
+    (updateSelection)="updateSelection($event)"
+    (fetchData)="fetchData()"
+    emptyStateTitle="No gateway group created"
+    i18n-emptyStateTitle
+    emptyStateMessage="Set up your first gateway group to start using NVMe over Fabrics. This will allow you to create high-performance block storage with NVMe/TCP protocol."
+    i18n-emptyStateMessage>
 
-      <cd-table
-        #table
-        [data]="gateways"
-        [columns]="columns"
-        columnMode="flex"
-        selectionType="single"
-        identifier="name"
-        (updateSelection)="updateSelection($event)"
-        (fetchData)="fetchData()"
-        emptyStateTitle="No gateway group created"
-        i18n-emptyStateTitle
-        emptyStateMessage="Set up your first gateway group to start using NVMe over Fabrics. This will allow you to create high-performance block storage with NVMe/TCP protocol."
-        i18n-emptyStateMessage>
-        <div class="table-actions">
-          <cd-table-actions
-            [permission]="permission"
-            [selection]="selection"
-            [tableActions]="tableActions">
-          </cd-table-actions>
-        </div>
-      </cd-table>
-    </ng-container>
-  </div>
-</div>
+    <div class="table-actions">
+      <cd-table-actions [permission]="permission"
+                        [selection]="selection"
+                        class="btn-group"
+                        [tableActions]="tableActions">
+      </cd-table-actions>
+    </div>
+  </cd-table>
+</ng-container>
 
 <ng-template #dateTpl
              let-created="data.value">
 </ng-template>
 
 <ng-template #customTableItemTemplate
-             let-value="data.value"
-             let-row="data.row">
+             let-value="data.value">
   <a cdsLink
-     [routerLink]="[viewUrl, value]"
+     [routerLink]="[viewUrl, value | encodeUri]"
      (click)="$event.stopPropagation()">
     {{ value }}
   </a>
index 04c3bf4fbe7dcb5ee8f42972291b0daeb04b07e4..0c436fa10d6c27827e4e468f7b2580108ffcadbe 100644 (file)
@@ -26,7 +26,6 @@ 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';
@@ -74,11 +73,11 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
   gatewayGroupName = '';
   subsystemCount = 0;
   gatewayCount = 0;
-  selectedGatewayDetails: DetailItem[] = [];
   private lastGroupCount = 0;
 
   viewUrl = `/${BASE_URL}/view`;
   icons = Icons;
+
   iconSize = IconSize;
 
   constructor(
@@ -191,8 +190,7 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
           }),
           catchError(() => {
             return of([]);
-          }),
-          finalize(() => this.setTableLoading(false))
+          })
         )
       ),
       shareReplay({ bufferSize: 1, refCount: true }),
@@ -210,20 +208,12 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
       .subscribe(() => this.fetchData());
   }
   fetchData(): void {
-    this.setTableLoading(true);
     this.subject.next([]);
     this.checkNodesAvailability();
   }
 
-  private setTableLoading(loading: boolean): void {
-    if (this.table) {
-      this.table.loadingIndicator = loading;
-    }
-  }
-
   updateSelection(selection: CdTableSelection): void {
     this.selection = selection;
-    this.selectedGatewayDetails = this.buildGatewayDetails(selection.first());
   }
 
   deleteGatewayGroupModal() {
@@ -350,78 +340,8 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
     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();
   }
-
-  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'
-      }
-    ];
-  }
 }
index b8025d08f0689d9cb7d2ff19d5d7673a631f7acd..0a97671fa3b1d685b415f32dfbb99a90c317b487 100644 (file)
@@ -1,7 +1,7 @@
 <div class="content-theme">
   @if (mode === 'details' && groupName) {
-  <cd-details-card cardTitle="Details" i18n-cardTitle [details]="gatewayDetails" [showEditButton]="true"
-    editButtonLabel="Edit" i18n-editButtonLabel (editClicked)="onEditGateway()" class="cds-mb-5">
+  <cd-details-card cardTitle="Details" i18n-cardTitle [details]="gatewayDetails" [showEditButton]="false"
+    class="cds-mb-5">
   </cd-details-card>
   }
 
index fecbbe14a476cf2f533a37c18838600678cd7db3..89fe4865356f6b92c97d84f23dbfe1ced6655fe4 100644 (file)
@@ -439,8 +439,6 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy, OnChanges
     this.hostsLoaded.emit(this.count);
   }
 
-  onEditGateway(): void {}
-
   private buildGatewayDetails(
     serviceSpec: CephServiceSpec,
     gatewayNodeCount: number
index 94cf5fc5b587b7be4e79cdd40ba9acbdc393f79d..70dfb6453fba03bb22304307d74ff22a6e1fbbc9 100644 (file)
             i18n>Special characters are not allowed.</span>
         </div>
       </div>
-      <div cdsRow
-           class="form-item">
-        <div cdsCol>
-          <cds-checkbox id="enableCds"
-                        formControlName="enable_auth"
-                        i18n>Enable encryption
-          </cds-checkbox>
-        </div>
-      </div>
 
-      @if (groupForm.controls.enableEncryption.value) {
-      <div cdsRow
-           class="form-item">
-        <div cdsCol>
-          <cds-textarea-label
-            labelInputID="encryptionConfig"
-            helperText="Provide the encryption configuration details."
-            cdRequiredField="Encryption configuration"
-            i18n
-            i18n-helperText
-            [invalid]="encryptionConfigRef.isInvalid">Encryption configuration
-            <textarea cdsTextArea
-                      id="encryptionConfig"
-                      cdValidate
-                      #encryptionConfigRef="cdValidate"
-                      [invalid]="encryptionConfigRef.isInvalid"
-                      formControlName="encryptionKey"
-                      cols="100"
-                      rows="4"></textarea>
-          </cds-textarea-label>
-          <span
-            class="invalid-feedback"
-            *ngIf="groupForm.showError('encryptionKey', formDir, 'required')"
-            i18n>This field is required.</span>
-        </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>
                     id="encryptionKey"
                     formControlName="encryptionKey"
                     rows="4"
-                    cols="100">
+                    cols="100"
+                    cdsTheme="g10">
                   </textarea>
                 </cds-textarea-label>
                 <span
index 5b77ae89ceb200b68e3f481c58332f1577befa4e..6a86280d935cb561fe548eb9299f4b2421851f32 100644 (file)
@@ -16,6 +16,7 @@ import { NvmeofGroupFormComponent } from './nvmeof-group-form.component';
 import { CheckboxModule, GridModule, InputModule, SelectModule } from 'carbon-components-angular';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { CephServiceService } from '~/app/shared/api/ceph-service.service';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
 import { FormHelper } from '~/testing/unit-test-helper';
 
 describe('NvmeofGroupFormComponent', () => {
@@ -25,6 +26,7 @@ describe('NvmeofGroupFormComponent', () => {
   let formHelper: FormHelper;
   let taskWrapperService: TaskWrapperService;
   let cephServiceService: CephServiceService;
+  let nvmeofService: NvmeofService;
   let router: Router;
 
   beforeEach(async () => {
@@ -51,8 +53,12 @@ describe('NvmeofGroupFormComponent', () => {
     component = fixture.componentInstance;
     taskWrapperService = TestBed.inject(TaskWrapperService);
     cephServiceService = TestBed.inject(CephServiceService);
+    nvmeofService = TestBed.inject(NvmeofService);
     router = TestBed.inject(Router);
 
+    // Mock NvmeofService.exists so the async unique validator resolves immediately
+    spyOn(nvmeofService, 'exists').and.returnValue(of(false));
+
     component.ngOnInit();
     form = component.groupForm;
     formHelper = new FormHelper(form);
@@ -126,7 +132,7 @@ describe('NvmeofGroupFormComponent', () => {
 
       expect(cephServiceService.create).toHaveBeenCalledWith({
         service_type: 'nvmeof',
-        service_id: 'default',
+        service_id: 'nvmeof.default',
         group: 'default',
         placement: {
           hosts: ['host1', 'host2']
@@ -153,7 +159,7 @@ describe('NvmeofGroupFormComponent', () => {
       );
     });
 
-    it('should create service with encryption when enabled', () => {
+    it('should create service with encryption key when enabled', () => {
       component.gatewayNodeComponent = {
         getSelectedHosts: (): any[] => [{ hostname: 'host1' }],
         getSelectedHostnames: (): string[] => ['host1']
@@ -179,7 +185,6 @@ describe('NvmeofGroupFormComponent', () => {
       } as any;
 
       component.groupForm.get('groupName').setValue('mtls-internal');
-      component.groupForm.get('pool').setValue('rbd');
       component.groupForm.get('enableEncryption').setValue(true);
       component.groupForm.get('encryptionKey').setValue('test-encryption-key');
       component.groupForm.get('enableMtls').setValue(true);
@@ -191,7 +196,7 @@ describe('NvmeofGroupFormComponent', () => {
       expect(cephServiceService.create).toHaveBeenCalledWith(
         jasmine.objectContaining({
           service_type: 'nvmeof',
-          service_id: 'rbd.mtls-internal',
+          service_id: 'nvmeof.mtls-internal',
           ssl: true,
           enable_auth: true,
           certificate_source: 'cephadm-signed',
@@ -207,7 +212,6 @@ describe('NvmeofGroupFormComponent', () => {
       } as any;
 
       component.groupForm.get('groupName').setValue('mtls-external');
-      component.groupForm.get('pool').setValue('rbd');
       component.groupForm.get('enableEncryption').setValue(true);
       component.groupForm.get('encryptionKey').setValue('test-encryption-key');
       component.groupForm.get('enableMtls').setValue(true);
@@ -222,7 +226,7 @@ describe('NvmeofGroupFormComponent', () => {
 
       expect(cephServiceService.create).toHaveBeenCalledWith(
         jasmine.objectContaining({
-          service_id: 'rbd.mtls-external',
+          service_id: 'nvmeof.mtls-external',
           ssl: true,
           enable_auth: true,
           certificate_source: 'inline',
index 7053bbbe9f9a99555707279f7c757cc7effffdac..dd54c824f1994cc5178187780e3c0c188cedcaa8 100644 (file)
@@ -1,5 +1,6 @@
 import { Component, OnInit, ViewChild } from '@angular/core';
 import { UntypedFormControl, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
 import { CdForm } from '~/app/shared/forms/cd-form';
 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
@@ -10,10 +11,8 @@ import { NvmeofGatewayNodeComponent } from '../nvmeof-gateway-node/nvmeof-gatewa
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { CephServiceService } from '~/app/shared/api/ceph-service.service';
 import { FinishedTask } from '~/app/shared/models/finished-task';
-import { Router } from '@angular/router';
 import { CdValidators } from '~/app/shared/forms/cd-validators';
 import { NvmeofService } from '~/app/shared/api/nvmeof.service';
-import { Pool } from '../../pool/pool';
 import { CertificateType } from '~/app/shared/models/service.interface';
 
 @Component({
@@ -32,9 +31,10 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
   resource: string;
   group = '';
   pageURL = '';
-  pools: Pool[] = [];
-  poolsLoading = false;
   hasAvailableNodes = true;
+  editing = false;
+  gatewayGroupName = '';
+  existingServiceData: any = null;
 
   constructor(
     private authStorageService: AuthStorageService,
@@ -42,7 +42,8 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
     private taskWrapperService: TaskWrapperService,
     private cephServiceService: CephServiceService,
     private nvmeofService: NvmeofService,
-    private router: Router
+    private router: Router,
+    private route: ActivatedRoute
   ) {
     super();
     this.permission = this.authStorageService.getPermissions().nvmeof;
@@ -50,30 +51,42 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
   }
 
   ngOnInit() {
-    this.action = this.actionLabels.CREATE;
     this.createForm();
+
+    this.route.params.subscribe((params) => {
+      if (params['name']) {
+        this.editing = true;
+        this.gatewayGroupName = params['name'];
+        this.action = this.actionLabels.EDIT;
+        this.loadGatewayGroupData(params['name']);
+      } else {
+        this.editing = false;
+        this.action = this.actionLabels.CREATE;
+      }
+    });
   }
 
   createForm() {
+    const groupNameValidators = [
+      Validators.required,
+      (control: any) => {
+        const value = control.value;
+        return value && /[^a-zA-Z0-9_-]/.test(value) ? { invalidChars: true } : null;
+      }
+    ];
+
+    const groupNameAsyncValidators = this.editing
+      ? []
+      : [CdValidators.unique(this.nvmeofService.exists, this.nvmeofService)];
+
     this.groupForm = new CdFormGroup({
-      groupName: new UntypedFormControl(
-        null,
-        [
-          Validators.required,
-          (control) => {
-            const value = control.value;
-            return value && /[^a-zA-Z0-9_-]/.test(value) ? { invalidChars: true } : null;
-          }
-        ],
-        [CdValidators.unique(this.nvmeofService.exists, this.nvmeofService)]
-      ),
+      groupName: new UntypedFormControl(null, groupNameValidators, groupNameAsyncValidators),
       unmanaged: new UntypedFormControl(false),
       enableEncryption: new UntypedFormControl(false),
       encryptionConfig: new UntypedFormControl(null),
       encryptionKey: new UntypedFormControl(null),
       enableMtls: new UntypedFormControl(false),
       certificateType: new UntypedFormControl(CertificateType.internal),
-      pool: new UntypedFormControl('rbd'),
       custom_sans: new UntypedFormControl([]),
       rootCACert: new UntypedFormControl(null),
       clientCert: new UntypedFormControl(null),
@@ -83,7 +96,6 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
     });
 
     this.groupForm.get('enableEncryption')?.valueChanges.subscribe((enabled) => {
-      // Keep both legacy and new encryption fields in sync.
       if (!enabled) {
         return;
       }
@@ -96,6 +108,50 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
         encryptionConfigControl.setValue(encryptionKeyControl.value, { emitEvent: false });
       }
     });
+
+    if (this.editing) {
+      this.groupForm.get('groupName')?.disable();
+    }
+  }
+
+  loadGatewayGroupData(groupName: string) {
+    this.nvmeofService.listGatewayGroups().subscribe(
+      (gatewayGroups: any) => {
+        const groups = gatewayGroups?.[0] ?? [];
+        const group = groups.find((g: any) => g.spec?.group === groupName);
+
+        if (group) {
+          this.existingServiceData = group;
+          const spec = group.spec || {};
+
+          this.groupForm.patchValue({
+            groupName: groupName,
+            unmanaged: spec.unmanaged || false,
+            enableEncryption: spec.ssl || false,
+            enableMtls: spec.ssl || false,
+            certificateType:
+              spec.certificate_source === 'inline'
+                ? CertificateType.external
+                : CertificateType.internal,
+            encryptionKey: spec.encryption_key || '',
+            custom_sans: spec.custom_sans || []
+          });
+
+          if (spec.certificate_source === 'inline') {
+            this.groupForm.patchValue({
+              rootCACert: spec.root_ca_cert || null,
+              clientCert: spec.client_cert || null,
+              clientKey: spec.client_key || null,
+              serverCert: spec.server_cert || null,
+              serverKey: spec.server_key || null
+            });
+          }
+        }
+      },
+      (_error) => {
+        // Error loading gateway group data
+      }
+    );
   }
 
   onHostsLoaded(count: number): void {
@@ -139,19 +195,21 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
       return;
     }
 
-    const formValues = this.groupForm.value;
+    const formValues = this.groupForm.getRawValue();
     const selectedHostnames = this.gatewayNodeComponent?.getSelectedHostnames() || [];
     if (selectedHostnames.length === 0) {
       this.groupForm.setErrors({ cdSubmitButton: true });
       return;
     }
-    const taskUrl = `service/${URLVerbs.CREATE}`;
-    const serviceId = `${formValues.groupName}`;
+
+    const groupName = this.editing ? this.gatewayGroupName : formValues.groupName;
+    const serviceName = `nvmeof.${groupName}`;
+    const taskUrl = this.editing ? `service/${URLVerbs.EDIT}` : `service/${URLVerbs.CREATE}`;
 
     const serviceSpec: Record<string, any> = {
       service_type: 'nvmeof',
-      service_id: serviceId,
-      group: formValues.groupName,
+      service_id: serviceName,
+      group: groupName,
       placement: {
         hosts: selectedHostnames
       },
@@ -171,11 +229,6 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
       serviceSpec['certificate_source'] =
         formValues.certificateType === CertificateType.internal ? 'cephadm-signed' : 'inline';
 
-      if (formValues.pool) {
-        serviceSpec['pool'] = formValues.pool;
-        serviceSpec['service_id'] = `${formValues.pool}.${formValues.groupName}`;
-      }
-
       if (
         formValues.certificateType === CertificateType.internal &&
         formValues.custom_sans?.length > 0
@@ -192,12 +245,16 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
       }
     }
 
+    const apiCall = this.editing
+      ? this.cephServiceService.update(serviceSpec)
+      : this.cephServiceService.create(serviceSpec);
+
     this.taskWrapperService
       .wrapTaskAroundCall({
         task: new FinishedTask(taskUrl, {
-          service_name: `nvmeof.${serviceId}`
+          service_name: serviceName
         }),
-        call: this.cephServiceService.create(serviceSpec)
+        call: apiCall
       })
       .subscribe({
         complete: () => {
@@ -213,10 +270,6 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
     this.router.navigateByUrl('/block/nvmeof/gateways');
   }
 
-  onCertificateTypeChange(type: CertificateType): void {
-    this.groupForm.get('certificateType')?.setValue(type);
-  }
-
   onFileUpload(event: Event, controlName: string): void {
     const target = event.target as HTMLInputElement;
     const file = target?.files?.[0];
@@ -229,4 +282,21 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
     reader.onload = () => control.setValue(reader.result);
     reader.readAsText(file, 'utf8');
   }
+
+  onCertificateTypeChange(type: CertificateType): void {
+    this.groupForm.get('certificateType')?.setValue(type);
+    if (type === CertificateType.internal) {
+      this.groupForm.patchValue({
+        rootCACert: null,
+        clientCert: null,
+        clientKey: null,
+        serverCert: null,
+        serverKey: null
+      });
+    } else {
+      this.groupForm.patchValue({
+        custom_sans: []
+      });
+    }
+  }
 }
index e49ba58057fefd2eb2866e2807d53963530e8940..48021924dd8d66290f02cffc0215250882799d7c 100644 (file)
@@ -194,4 +194,19 @@ describe('NvmeofNamespacesListComponent', () => {
 
     component.fetchData();
   });
+
+  it('should update group and trigger namespace fetch on group change', () => {
+    const fetchDataSpy = jest.spyOn(component, 'fetchData');
+
+    component.onGroupChange('g1');
+
+    expect(component.group).toBe('g1');
+    expect(fetchDataSpy).not.toHaveBeenCalled(); // onGroupChange calls namespaceSubject.next() directly
+  });
+
+  it('should clear group on onGroupChange with null', () => {
+    component.group = 'g1';
+    component.onGroupChange(null);
+    expect(component.group).toBeNull();
+  });
 });
index 71b05f17cde96cd862990e45a43f978d05215ac9..427dd6068947596d76ad7bbb167be7d905764578 100644 (file)
@@ -223,6 +223,18 @@ export class NvmeofNamespacesListComponent implements OnInit, OnDestroy {
     });
   }
 
+  get group(): string | null {
+    return this.groupHandler.group;
+  }
+
+  set group(value: string | null) {
+    this.groupHandler.group = value;
+  }
+
+  onGroupChange(group: string | null): void {
+    this.groupHandler.onGroupChange(group);
+  }
+
   updateSelection(selection: CdTableSelection) {
     this.selection = selection;
   }
index 26b6a0b27219c9bf7e846a5fa799c6dd3429a8d7..1e213685305d51b47148808619ecff33789bc946 100644 (file)
@@ -1153,7 +1153,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         } else {
           this.showRealmCreationForm = false;
         }
-         this.updateRgwControlStates();
+        this.updateRgwControlStates();
       },
       (_error) => {
         const defaultZone = new RgwZone();
@@ -1162,7 +1162,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         defaultZonegroup.name = 'default';
         this.zoneList.push(defaultZone);
         this.zonegroupList.push(defaultZonegroup);
-         this.updateRgwControlStates();
+        this.updateRgwControlStates();
       }
     );
   }
@@ -1226,7 +1226,9 @@ export class ServiceFormComponent extends CdForm implements OnInit {
     this.toggleFormControlState('zone_name', this.editing || this.zoneList.length === 0);
   }
 
-  private updateGrafanaPasswordControlState(serviceType = this.serviceForm?.get('service_type')?.value): void {
+  private updateGrafanaPasswordControlState(
+    serviceType = this.serviceForm?.get('service_type')?.value
+  ): void {
     this.toggleFormControlState(
       'grafana_admin_password',
       this.editing && serviceType === 'grafana'
index 3d558fb8c56ebe1035af1498eb1d29ea275cead7..b0d356e099b7bd671c8ea8592c8a59304a7a22f9 100644 (file)
@@ -2,47 +2,34 @@
   <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>
+  <div [cdsStack]="'vertical'"
+       [gap]="0">
     @if (details && details.length > 0) {
-    <div class="cds--grid cds--no-gutter">
-      <div class="cds--row">
-        @for (detail of getVisibleDetails(); track detail.label) {
-        <div class="cds--col">
-          <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">
-                    <cd-icon [type]="getStatusIcon(detail)"></cd-icon>
-                    <span>{{ getDisplayValue(detail.value) }}</span>
-                  </div>
-                }
-                
-                @default {
-                  <span>{{ getDisplayValue(detail.value) }}</span>
-                }
-              }
-            </div>
-          </div>
+    <div [cdsStack]="'horizontal'"
+         [gap]="5">
+      @for (detail of getVisibleDetails(); track detail.label) {
+      <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">
+                <cd-icon [type]="getStatusIcon(detail)"></cd-icon>
+                <span>{{ getDisplayValue(detail.value) }}</span>
+              </div>
+            }
+
+            @default {
+              <span>{{ getDisplayValue(detail.value) }}</span>
+            }
+          }
         </div>
-        }
       </div>
+      }
     </div>
     }
     <ng-content></ng-content>