]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Implement support for editing existing nvme-oF gateway groups through UI 69441/head
authorpujashahu <pshahu@redhat.com>
Thu, 25 Jun 2026 19:12:45 +0000 (00:42 +0530)
committerpujashahu <pshahu@redhat.com>
Fri, 3 Jul 2026 09:41:52 +0000 (15:11 +0530)
Fixes: https://tracker.ceph.com/issues/77386
Signed-off-by: pujaoshahu <pshahu@redhat.com>
16 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.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component.ts [deleted file]
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.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.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/constants/app.constants.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts

index 7dbdfbbd518ee8c04ff226a938914a4525adf29b..368a7de95b71de598041a50992f43d9b8d3bf511 100644 (file)
@@ -10,7 +10,6 @@ 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,9 +147,7 @@ import { NvmeofGatewayGroupFilterComponent } from './nvmeof-gateway-group-filter
     LayoutModule,
     ThemeModule,
     NvmeofSetupCardsComponent,
-    NvmeofGatewayGroupFilterComponent,
-    TextLabelListComponent
-    // ProductiveCardComponent
+    NvmeofGatewayGroupFilterComponent
   ],
   declarations: [
     RbdListComponent,
@@ -381,17 +378,6 @@ 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 435410a6acaac020f5d2d1d6a66c2816d5e488f2..926e32678839003bb4bf7236608cd5b53ef609ba 100644 (file)
@@ -1,15 +1,4 @@
 <ng-container *ngIf="gatewayGroup$ | async as gateways">
-  <!-- Details Card Section -->
-  <ng-container *ngIf="selection.hasSelection && showDetailsCard">
-    <cd-details-card
-      cardTitle="Details"
-      i18n-cardTitle
-      [details]="selectedGatewayDetails"
-      [showEditButton]="false"
-      [columns]="4">
-    </cd-details-card>
-  </ng-container>
-
   <cd-table
     #table
     [data]="gateways"
@@ -43,8 +32,8 @@
              let-value="data.value"
              let-row="data.row">
   <a cdsLink
-     href="#"
-     (click)="onNameClick(row, $event)">
+     [routerLink]="[viewUrl, value]"
+     (click)="$event.stopPropagation()">
     {{ value }}
   </a>
 </ng-template>
index 9b00fff75333bb709e0139f6ef3acba7390c3bf2..4ca09129451a1429daabf4d627f9eded7167eec4 100644 (file)
@@ -76,7 +76,6 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
   gatewayCount = 0;
   selectedGatewayDetails: DetailItem[] = [];
   private lastGroupCount = 0;
-  showDetailsCard = false;
 
   viewUrl = `/${BASE_URL}/view`;
   icons = Icons;
@@ -130,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,
@@ -146,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(() =>
@@ -210,7 +217,6 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
   updateSelection(selection: CdTableSelection): void {
     this.selection = selection;
     this.selectedGatewayDetails = this.buildGatewayDetails(selection.first());
-    this.showDetailsCard = false;
   }
 
   deleteGatewayGroupModal() {
@@ -269,11 +275,6 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
           })
           .pipe(
             tap({
-              next: () => {
-                this.table.data = this.table.data.filter(
-                  (row: CephServiceSpec) => row.spec?.group !== selectedGroup.spec.group
-                );
-              },
               complete: () => {
                 this.nvmeofStateService.requestRefresh();
               }
@@ -291,14 +292,6 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
     });
   }
 
-  onNameClick(row: any, event?: Event): void {
-    event?.preventDefault();
-    event?.stopPropagation();
-    this.selection = new CdTableSelection([row]);
-    this.selectedGatewayDetails = this.buildGatewayDetails(row);
-    this.showDetailsCard = true;
-  }
-
   private checkNodesAvailability(): void {
     forkJoin([this.nvmeofService.listGatewayGroups(), this.hostService.getAllHosts()]).subscribe(
       ([groups, hosts]: [GatewayGroup[][], any[]]) => {
@@ -342,6 +335,19 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
     this.router.navigate([this.viewUrl, groupName]);
   }
 
+  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 [];
@@ -367,14 +373,9 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
       },
       {
         label: $localize`mTLS`,
-        value: selectedGroup.spec?.enable_auth ? $localize`Enabled` : $localize`Disabled`,
+        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
deleted file mode 100644 (file)
index e69de29..0000000
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
deleted file mode 100644 (file)
index e69de29..0000000
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
deleted file mode 100644 (file)
index e69de29..0000000
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
deleted file mode 100644 (file)
index e69de29..0000000
index 98e7190faf206a92733d3974096a600fc1218a42..89fe4865356f6b92c97d84f23dbfe1ced6655fe4 100644 (file)
@@ -2,9 +2,11 @@ import {
   Component,
   EventEmitter,
   Input,
+  OnChanges,
   OnDestroy,
   OnInit,
   Output,
+  SimpleChanges,
   TemplateRef,
   ViewChild
 } from '@angular/core';
@@ -46,7 +48,7 @@ import { DetailItem } from '~/app/shared/components/details-card/details-card.co
   styleUrls: ['./nvmeof-gateway-node.component.scss'],
   standalone: false
 })
-export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy {
+export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy, OnChanges {
   @ViewChild(TableComponent, { static: true })
   table!: TableComponent;
 
@@ -67,6 +69,7 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy {
 
   @Input() groupName: string | undefined;
   @Input() mode: 'selector' | 'details' = NvmeofGatewayNodeMode.SELECTOR;
+  @Input() preSelectedHostnames: string[] = [];
 
   usedHostnames: Set<string> = new Set();
   serviceSpec: CephServiceSpec | undefined;
@@ -144,6 +147,43 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy {
     ];
   }
 
+  ngOnChanges(changes: SimpleChanges): void {
+    if (
+      changes['preSelectedHostnames'] &&
+      !changes['preSelectedHostnames'].firstChange &&
+      changes['preSelectedHostnames'].currentValue?.length > 0
+    ) {
+      this.table.refreshBtn();
+    }
+  }
+
+  private applyPreSelection(): void {
+    if (!this.preSelectedHostnames?.length || !this.table) {
+      return;
+    }
+
+    const hostsToSelect = this.hosts.filter((host) =>
+      this.preSelectedHostnames.includes(host.hostname)
+    );
+
+    if (hostsToSelect.length > 0) {
+      this.selection.selected = hostsToSelect;
+      this.selectionChange.emit(this.selection);
+
+      setTimeout(() => {
+        hostsToSelect.forEach((host) => {
+          const rowIndex = this.table.model?.data?.findIndex(
+            (row: any) => _.get(row, [0, 'selected', 'hostname']) === host.hostname
+          );
+          if (rowIndex > -1) {
+            this.table.model.selectRow(rowIndex, true);
+          }
+        });
+        this.table.updateSelection.emit(this.selection);
+      });
+    }
+  }
+
   private setTableActions() {
     this.tableActions = [
       {
@@ -273,8 +313,11 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy {
       this.sub.unsubscribe();
     }
 
+    const useEditMode =
+      this.mode === NvmeofGatewayNodeMode.SELECTOR && this.preSelectedHostnames?.length > 0;
+
     const fetchData$: Observable<any> =
-      this.mode === NvmeofGatewayNodeMode.DETAILS
+      this.mode === NvmeofGatewayNodeMode.DETAILS || useEditMode
         ? this.nvmeofService.fetchHostsAndGroups()
         : this.nvmeofService.getAvailableHosts(this.tableContext?.toParams());
 
@@ -288,6 +331,8 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy {
         next: (result: any) => {
           if (this.mode === NvmeofGatewayNodeMode.DETAILS) {
             this.processDetailsData(result.groups, result.hosts);
+          } else if (useEditMode) {
+            this.processEditModeData(result.groups, result.hosts);
           } else {
             this.hosts = result;
             this.count = this.hosts.length;
@@ -298,6 +343,41 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy {
       });
   }
 
+  private processEditModeData(groups: any[][], hostList: Host[]) {
+    const groupList = groups?.[0] ?? [];
+    const preSelectedSet = new Set(this.preSelectedHostnames);
+
+    const usedByOtherGroups = new Set<string>();
+    groupList.forEach((group: CephServiceSpec) => {
+      const hosts = group.placement?.hosts || [];
+      const isCurrentGroup =
+        hosts.every((h: string) => preSelectedSet.has(h)) && hosts.length === preSelectedSet.size;
+
+      if (!isCurrentGroup) {
+        hosts.forEach((hostname: string) => usedByOtherGroups.add(hostname));
+
+        const label = group.placement?.label;
+        if (label) {
+          (hostList || []).forEach((host: Host) => {
+            if (host.labels?.includes(label as string)) {
+              usedByOtherGroups.add(host.hostname);
+            }
+          });
+        }
+      }
+    });
+
+    this.hosts = (hostList || []).filter((host: Host) => {
+      const isPreSelected = preSelectedSet.has(host.hostname);
+      const isAvailable = !usedByOtherGroups.has(host.hostname);
+      return isPreSelected || isAvailable;
+    });
+
+    this.count = this.hosts.length;
+    this.hostsLoaded.emit(this.count);
+    this.applyPreSelection();
+  }
+
   private processDetailsData(groups: any[][], hostList: Host[]) {
     const groupList = groups?.[0] ?? [];
 
index 8cd803d79aa277719cee3ea7a1ff6c0fc349ced8..7acb2ea574cc6530d9443246c496d57ab9d6f5d4 100644 (file)
 
         </div>
       </div>
-      <!-- Pool -->
+
+      <!-- CDS Checkbox -->
+      <div cdsRow
+           class="form-item">
+        <div cdsCol>
+          <cds-checkbox id="enableCds"
+                        formControlName="enableEncryption"
+                        i18n>Enable encryption
+          </cds-checkbox>
+        </div>
+      </div>
+
+      <!-- Encryption Configuration (shown when checkbox is checked) -->
+      @if (groupForm.controls.enableEncryption.value) {
       <div cdsRow
            class="form-item">
         <div cdsCol>
-          <cds-select
-            label="Block pool"
-            helperText="An RBD application-enabled pool in which the gateway configuration can be managed."
-            labelInputID="pool"
-            id="pool"
-            formControlName="pool"
-            cdRequiredField="Block pool"
-            [invalid]="groupForm.controls.pool.invalid && groupForm.controls.pool.dirty"
-            [invalidText]="poolError"
-            i18n-label
+          <cds-textarea-label
+            labelInputID="encryptionConfig"
+            helperText="Provide the encryption configuration details."
+            cdRequiredField="Encryption configuration"
+            i18n
             i18n-helperText
-          >
-            <option *ngIf="poolsLoading"
-                    [ngValue]="null"
-                    i18n>Loading...</option>
-            <option *ngIf="!poolsLoading && pools.length === 0"
-                    [ngValue]="null"
-                    i18n>-- No block pools available --</option>
-            <option *ngFor="let pool of pools"
-                    [value]="pool.pool_name">{{ pool.pool_name }}</option>
-          </cds-select>
-          <ng-template #poolError>
-            <span class="invalid-feedback"
-                  *ngIf="groupForm.showError('pool', formDir, 'required')"
-                  i18n>This field is required.</span>
-          </ng-template>
+            [invalid]="encryptionConfigRef.isInvalid">Encryption configuration
+            <textarea cdsTextArea
+                      id="encryptionConfig"
+                      cdValidate
+                      #encryptionConfigRef="cdValidate"
+                      [invalid]="encryptionConfigRef.isInvalid"
+                      formControlName="encryptionConfig"
+                      cols="100"
+                      rows="4">
+            </textarea>
+          </cds-textarea-label>
+          <span
+            class="invalid-feedback"
+            *ngIf="groupForm.showError('encryptionConfig', formDir, 'required')"
+            i18n>This field is required.</span>
         </div>
       </div>
-        <!-- Target Nodes Selection -->
-        <div
+      }
+      <!-- Target Nodes Selection -->
+      <div
         cdsRow
         class="form-item"
       >
             <!-- Encryption -->
             <div cdsRow class="form-item">
               <div cdsCol>
-                <div [cdsStack]="'horizontal'" 
-                      gap="1" 
-                      alignment="center">
+                <div class="cds--stack cds--stack-horizontal">
                   <label class="cds--type-label-01" 
                          i18n>Enable encryption</label>
                   <cds-tag type="green"
                     class="cds-mt-3">
             <div cdsRow class="form-item">
               <div cdsCol>
-                <div [cdsStack]="'horizontal'" gap="1" alignment="center" class="cds-mb-3">
+                <div class="cds--stack cds--stack-horizontal cds-mb-3">
                   <label class="cds--type-label-01" i18n>Enable Mutual TLS (mTLS)</label>
                 </div>
                 <cds-checkbox id="enableMtls" formControlName="enableMtls">
index f27cbec917dcb5cf7b806d3f78efc25a32837f69..ac97aa3a3b8ee54c14c4c5bb786cdb8384d9f294 100644 (file)
@@ -1,6 +1,5 @@
 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';
@@ -11,13 +10,11 @@ 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';
-
-enum CertificateType {
-  internal = 'internal',
-  external = 'external'
-}
+import { Pool } from '../../pool/pool';
+import { CertificateType } from '~/app/shared/models/service.interface';
 
 @Component({
   selector: 'cd-nvmeof-group-form',
@@ -28,19 +25,16 @@ enum CertificateType {
 export class NvmeofGroupFormComponent extends CdForm implements OnInit {
   @ViewChild(NvmeofGatewayNodeComponent) gatewayNodeComponent!: NvmeofGatewayNodeComponent;
 
+  readonly CertificateType = CertificateType;
   permission: Permission;
   groupForm!: CdFormGroup;
   action!: string;
   resource: string;
-  group: string;
-  pageURL: string;
-  hasAvailableNodes = true;
-  CertificateType = CertificateType;
-  editing = false;
-  gatewayGroupName = '';
-  existingServiceData: any = null;
-  pools: Array<{ pool_name: string }> = [{ pool_name: 'rbd' }];
+  group = '';
+  pageURL = '';
+  pools: Pool[] = [];
   poolsLoading = false;
+  hasAvailableNodes = true;
 
   constructor(
     private authStorageService: AuthStorageService,
@@ -48,8 +42,7 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
     private taskWrapperService: TaskWrapperService,
     private cephServiceService: CephServiceService,
     private nvmeofService: NvmeofService,
-    private router: Router,
-    private route: ActivatedRoute
+    private router: Router
   ) {
     super();
     this.permission = this.authStorageService.getPermissions().nvmeof;
@@ -57,155 +50,52 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
   }
 
   ngOnInit() {
-    this.route.params.subscribe((params) => {
-      if (params['name']) {
-        this.editing = true;
-        this.gatewayGroupName = params['name'];
-        this.action = this.actionLabels.EDIT;
-        this.createForm();
-        this.loadGatewayGroupData(params['name']);
-      } else {
-        this.editing = false;
-        this.action = this.actionLabels.CREATE;
-        this.gatewayGroupName = '';
-        this.createForm();
-      }
-    });
+    this.action = this.actionLabels.CREATE;
+    this.createForm();
   }
 
   createForm() {
-    const groupNameValidators = this.editing
-      ? [
-          Validators.required,
-          (control) => {
-            const value = control.value;
-            return value && /[^a-zA-Z0-9_-]/.test(value) ? { invalidChars: true } : null;
-          }
-        ]
-      : [
+    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;
           }
-        ];
-
-    const groupNameAsyncValidators = this.editing
-      ? []
-      : [CdValidators.unique(this.nvmeofService.exists, this.nvmeofService)];
-
-    this.groupForm = new CdFormGroup({
-      groupName: new UntypedFormControl(null, groupNameValidators, groupNameAsyncValidators),
-      pool: new UntypedFormControl('rbd', {
-        validators: [Validators.required]
-      }),
+        ],
+        [CdValidators.unique(this.nvmeofService.exists, this.nvmeofService)]
+      ),
       unmanaged: new UntypedFormControl(false),
       enableEncryption: new UntypedFormControl(false),
+      encryptionConfig: new UntypedFormControl(null),
+      encryptionKey: new UntypedFormControl(null),
       enableMtls: new UntypedFormControl(false),
-      encryptionKey: new UntypedFormControl('', [
-        CdValidators.composeIf(
-          {
-            enableEncryption: true
-          },
-          [Validators.required]
-        )
-      ]),
       certificateType: new UntypedFormControl(CertificateType.internal),
+      pool: new UntypedFormControl('rbd'),
       custom_sans: new UntypedFormControl([]),
-      rootCACert: new UntypedFormControl(null, [
-        CdValidators.composeIf(
-          {
-            enableEncryption: true,
-            certificateType: 'external'
-          },
-          [Validators.required]
-        )
-      ]),
-      clientCert: new UntypedFormControl(null, [
-        CdValidators.composeIf(
-          {
-            enableEncryption: true,
-            certificateType: 'external'
-          },
-          [Validators.required]
-        )
-      ]),
-      clientKey: new UntypedFormControl(null, [
-        CdValidators.composeIf(
-          {
-            enableEncryption: true,
-            certificateType: 'external'
-          },
-          [Validators.required]
-        )
-      ]),
-      serverCert: new UntypedFormControl(null, [
-        CdValidators.composeIf(
-          {
-            enableEncryption: true,
-            certificateType: 'external'
-          },
-          [Validators.required]
-        )
-      ]),
-      serverKey: new UntypedFormControl(null, [
-        CdValidators.composeIf(
-          {
-            enableEncryption: true,
-            certificateType: CertificateType.external
-          },
-          [Validators.required]
-        )
-      ])
+      rootCACert: new UntypedFormControl(null),
+      clientCert: new UntypedFormControl(null),
+      clientKey: new UntypedFormControl(null),
+      serverCert: new UntypedFormControl(null),
+      serverKey: new UntypedFormControl(null)
     });
 
-    // Disable group name field in edit mode
-    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 || {};
-
-          // Prefill form with existing data
-          this.groupForm.patchValue({
-            groupName: groupName,
-            pool: spec.pool || 'rbd',
-            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 external certificates, load them
-          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
+    this.groupForm.get('enableEncryption')?.valueChanges.subscribe((enabled) => {
+      // Keep both legacy and new encryption fields in sync.
+      if (!enabled) {
+        return;
+      }
+      const encryptionConfigControl = this.groupForm.get('encryptionConfig');
+      const encryptionKeyControl = this.groupForm.get('encryptionKey');
+      if (!encryptionKeyControl?.value && encryptionConfigControl?.value) {
+        encryptionKeyControl.setValue(encryptionConfigControl.value, { emitEvent: false });
+      }
+      if (!encryptionConfigControl?.value && encryptionKeyControl?.value) {
+        encryptionConfigControl.setValue(encryptionKeyControl.value, { emitEvent: false });
       }
-    );
+    });
   }
 
   onHostsLoaded(count: number): void {
@@ -249,41 +139,51 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
       return;
     }
 
-    const formValues = this.groupForm.getRawValue(); // Use getRawValue to get disabled fields too
+    const formValues = this.groupForm.value;
     const selectedHostnames = this.gatewayNodeComponent?.getSelectedHostnames() || [];
     if (selectedHostnames.length === 0) {
       this.groupForm.setErrors({ cdSubmitButton: true });
       return;
     }
-
-    const groupName = this.editing ? this.gatewayGroupName : formValues.groupName;
-    const pool = formValues.pool;
-    const serviceName = `${pool}.${groupName}`;
-
-    let taskUrl = this.editing ? `service/${URLVerbs.EDIT}` : `service/${URLVerbs.CREATE}`;
+    let taskUrl = `service/${URLVerbs.CREATE}`;
+    const serviceName = `${formValues.groupName}`;
 
     const serviceSpec: Record<string, any> = {
       service_type: 'nvmeof',
       service_id: serviceName,
-      pool: pool,
-      group: groupName,
+      group: formValues.groupName,
       placement: {
         hosts: selectedHostnames
       },
       unmanaged: formValues.unmanaged
     };
 
+    if (formValues.enableEncryption) {
+      const encryptionKey = formValues.encryptionKey || formValues.encryptionConfig;
+      if (encryptionKey) {
+        serviceSpec['encryption_key'] = encryptionKey;
+      }
+    }
+
     if (formValues.enableMtls) {
       serviceSpec['ssl'] = true;
       serviceSpec['enable_auth'] = true;
       serviceSpec['certificate_source'] =
         formValues.certificateType === CertificateType.internal ? 'cephadm-signed' : 'inline';
 
-      if (formValues.certificateType === CertificateType.internal) {
-        if (formValues.custom_sans && formValues.custom_sans.length > 0) {
-          serviceSpec['custom_sans'] = formValues.custom_sans;
-        }
-      } else {
+      if (formValues.pool) {
+        serviceSpec['pool'] = formValues.pool;
+        serviceSpec['service_id'] = `${formValues.pool}.${serviceName}`;
+      }
+
+      if (
+        formValues.certificateType === CertificateType.internal &&
+        formValues.custom_sans?.length > 0
+      ) {
+        serviceSpec['custom_sans'] = formValues.custom_sans;
+      }
+
+      if (formValues.certificateType === CertificateType.external) {
         serviceSpec['root_ca_cert'] = formValues.rootCACert;
         serviceSpec['client_cert'] = formValues.clientCert;
         serviceSpec['client_key'] = formValues.clientKey;
@@ -292,16 +192,12 @@ 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.${serviceName}`
         }),
-        call: apiCall
+        call: this.cephServiceService.create(serviceSpec)
       })
       .subscribe({
         complete: () => {
@@ -317,31 +213,20 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
     this.router.navigateByUrl('/block/nvmeof/gateways');
   }
 
-  onFileUpload(event: any, controlName: string) {
-    const file = event.target.files[0];
-    if (file) {
-      const reader = new FileReader();
-      reader.onload = (e: any) => {
-        this.groupForm.get(controlName)?.setValue(e.target.result);
-      };
-      reader.readAsText(file);
-    }
+  onCertificateTypeChange(type: CertificateType): void {
+    this.groupForm.get('certificateType')?.setValue(type);
   }
 
-  onCertificateTypeChange(type: CertificateType) {
-    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: []
-      });
+  onFileUpload(event: Event, controlName: string): void {
+    const target = event.target as HTMLInputElement;
+    const file = target?.files?.[0];
+    const control = this.groupForm.get(controlName);
+    if (!file || !control) {
+      return;
     }
+
+    const reader = new FileReader();
+    reader.onload = () => control.setValue(reader.result);
+    reader.readAsText(file, 'utf8');
   }
 }
index 48b6326e937ba18807da2890613de8c53f947c90..61de7d2648db097c384b8f9aecb7caf7970f349c 100644 (file)
       <div cdsRow
            class="form-item">
         <cds-combo-box i18n
-                       cdValidate
-                       #listenersRef="cdValidate"
-                       [invalid]="listenersRef.isInvalid"
+                       [invalid]="formGroup.get('listeners')?.invalid && (formGroup.get('listeners')?.dirty || formGroup.get('listeners')?.touched)"
                        [invalidText]="listenersInvalidText"
                        [label]="listenersLabel"
                        [helperText]="listenersHelperText"
index 1fc6559d020accd6c9eae235f862f3a8a0ced3fa..d88531a22c2f67d538d1cf5c9387a1a385bf0e8b 100644 (file)
                     [invalid]="serviceForm.controls.realm_name.invalid && serviceForm.controls.realm_name.dirty"
                     [invalidText]="authModeError"
                     helperText="Active-directory authentication for domain member servers and User authentication for Stand-alone servers configuration."
-                    i18n-helperText
-                    [disabled]="realmList.length === 0  || editing ? true : false">
+                    i18n-helperText>
           @if (realmList.length === 0) {
           <option i18n
                   selected>-- No realm available --
       <div class="form-item">
         <cds-select id="zonegroup_name"
                     formControlName="zonegroup_name"
-                    label="Zonegroup"
-                    [disabled]="zonegroupList.length === 0  || editing ? true : null">
+                    label="Zonegroup">
           @for (zonegroup of zonegroupList; track zonegroup) {
           <option [value]="zonegroup.name">
             {{ zonegroup.name }}
       <div class="form-item">
         <cds-select id="zone_name"
                     formControlName="zone_name"
-                    label="Zone"
-                    [disabled]="zoneList.length === 0  || editing ? true : null">
+                    label="Zone">
           @for (zone of zoneList; track zone) {
           <option [value]="zone.name">
             {{ zone.name }}
           <input cdsPassword
                  type="password"
                  id="grafana_admin_password"
-                 formControlName="grafana_admin_password"
-                 [disabled]="editing ? true:null" />
+                 formControlName="grafana_admin_password" />
         </cds-password-label>
         <cd-copy-2-clipboard-button source="grafana_admin_password"
                                     class="mt-4">
index b4fbab508ea2c5c3331d5921e5f7d577dcb35c18..3b56e8df9bd56fa0f5ca898a35ae2a1410894f2f 100644 (file)
@@ -618,10 +618,9 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
         component.onSubmit();
         expect(cephServiceService.create).toHaveBeenCalledWith({
           service_type: 'nvmeof',
-          service_id: 'rbd.default',
+          service_id: 'default',
           placement: {},
           unmanaged: false,
-          pool: 'rbd',
           group: 'default',
           enable_auth: true,
           ssl: true,
index 6c10ff5a2ce0e60eaec64e81309f41ef07cbf1db..c52f488b031727f7651cdb248df5e480eda72784 100644 (file)
@@ -1151,6 +1151,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         } else {
           this.showRealmCreationForm = false;
         }
+        this.updateRgwControlStates();
       },
       (_error) => {
         const defaultZone = new RgwZone();
@@ -1159,6 +1160,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         defaultZonegroup.name = 'default';
         this.zoneList.push(defaultZone);
         this.zonegroupList.push(defaultZonegroup);
+        this.updateRgwControlStates();
       }
     );
   }
@@ -1229,6 +1231,33 @@ export class ServiceFormComponent extends CdForm implements OnInit {
       case 'nvmeof':
         this.serviceForm.get('group').disable();
         break;
+      case 'grafana':
+        this.serviceForm.get('grafana_admin_password').disable();
+        break;
+    }
+  }
+
+  private updateRgwControlStates(): void {
+    const realmControl = this.serviceForm.get('realm_name');
+    const zonegroupControl = this.serviceForm.get('zonegroup_name');
+    const zoneControl = this.serviceForm.get('zone_name');
+
+    if (this.editing || this.realmList.length === 0) {
+      realmControl.disable({ emitEvent: false });
+    } else {
+      realmControl.enable({ emitEvent: false });
+    }
+
+    if (this.editing || this.zonegroupList.length === 0) {
+      zonegroupControl.disable({ emitEvent: false });
+    } else {
+      zonegroupControl.enable({ emitEvent: false });
+    }
+
+    if (this.editing || this.zoneList.length === 0) {
+      zoneControl.disable({ emitEvent: false });
+    } else {
+      zoneControl.enable({ emitEvent: false });
     }
   }
 
index 22673b0ac2d46e3cd969cd11d3a410234d4fec17..d5352edea15696e047ecb528d716982c6d235a01 100644 (file)
@@ -165,6 +165,7 @@ export class ActionLabelsI18n {
   SETUP_MULTISITE_REPLICATION: string;
   NFS_EXPORT: string;
   VIEW: string;
+  EDIT_GATEWAYS_GROUP: string;
   constructor() {
     /* Create a new item */
     this.CREATE = $localize`Create`;
@@ -257,6 +258,7 @@ export class ActionLabelsI18n {
 
     this.NFS_EXPORT = $localize`Create NFS Share`;
     this.VIEW = $localize`View`;
+    this.EDIT_GATEWAYS_GROUP = $localize`Edit gateways group`;
   }
 }
 
index eaca956d3adcb6a0d42ce7166e7da4ca4c71b94c..3ee767018308b834ddc076356ecec60e1c3d6e76 100644 (file)
@@ -111,6 +111,7 @@ export interface CephServiceAdditionalSpec {
   client_secret: string;
   oidc_issuer_url: string;
   enable_auth: boolean;
+  encryption_key?: string;
   qat: QatSepcs;
 }