]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: carbonize service form 65751/head
authorPedro Gonzalez Gomez <pegonzal@ibm.com>
Tue, 16 Sep 2025 14:00:22 +0000 (16:00 +0200)
committerPedro Gonzalez Gomez <pegonzal@ibm.com>
Tue, 13 Jan 2026 08:56:00 +0000 (09:56 +0100)
Also updates service controller to fix snmp-gateway creation issue where service_id is not wanted on the request

Fixes: https://tracker.ceph.com/issues/73098
Fixes: https://tracker.ceph.com/issues/74216
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@ibm.com>
src/pybind/mgr/dashboard/controllers/service.py
src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/inventory.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/services.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/08-hosts.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
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/ceph/cluster/services/services.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts

index b75f417361498fdfffb26257726f11ef7520bd4b..4e419b413844097e8f49d23088b527e203bef1f0 100644 (file)
@@ -66,6 +66,8 @@ class Service(RESTController):
         :return: None
         """
 
+        if service_spec.get('service_type') not in ServiceSpec.REQUIRES_SERVICE_ID:
+            service_spec.pop('service_id', None)
         OrchClient.instance().services.apply(service_spec, no_overwrite=True)
 
     @UpdatePermission
index 9230a26720a4ad02f7d7392bfb5f3ab949f7357c..629510862534e4c1b6bdfa382d185c6bc866a041 100644 (file)
@@ -10,7 +10,11 @@ export class InventoryPageHelper extends PageHelper {
   identify() {
     // Nothing we can do, just verify the form is there
     this.getFirstTableCell().click();
-    cy.contains('[data-testid="primary-action"]', 'Identify').should('not.be.disabled').click();
+    cy.wait(500);
+    cy.contains('[data-testid="primary-action"]', 'Identify')
+      .should('exist')
+      .should('not.be.disabled', { timeout: 15000 })
+      .click();
     cy.get('cds-modal').within(() => {
       cy.get('#duration').select('15 minutes');
       cy.get('#duration').select('10 minutes');
index 5286676634e3383c7c6ca3fcaade8adc5b2d38dd..72eeb37a2efbd60d96a0c70a3113edd695388a14 100644 (file)
@@ -50,13 +50,13 @@ export class ServicesPageHelper extends PageHelper {
         case 'rgw':
           cy.get('#service_id').type('foo');
           unmanaged
-            ? cy.get('label[for=unmanaged]').click()
+            ? cy.get('cds-checkbox#unmanaged input[type="checkbox"]').check({ force: true })
             : cy.get('#count').clear().type(String(count));
           break;
 
         case 'ingress':
           if (unmanaged) {
-            cy.get('label[for=unmanaged]').click();
+            cy.get('cds-checkbox#unmanaged input[type="checkbox"]').check({ force: true });
           }
           this.selectOption('backend_service', 'rgw.foo');
           cy.get('#service_id').should('have.value', 'rgw.foo');
@@ -68,14 +68,14 @@ export class ServicesPageHelper extends PageHelper {
         case 'nfs':
           cy.get('#service_id').type('testnfs');
           unmanaged
-            ? cy.get('label[for=unmanaged]').click()
+            ? cy.get('cds-checkbox#unmanaged input[type="checkbox"]').check({ force: true })
             : cy.get('#count').clear().type(String(count));
           break;
 
         case 'smb':
           cy.get('#service_id').type('testsmb');
           unmanaged
-            ? cy.get('label[for=unmanaged]').click()
+            ? cy.get('cds-checkbox#unmanaged input[type="checkbox"]').check({ force: true })
             : cy.get('#count').clear().type(String(count));
           cy.get('#cluster_id').type('cluster_foo');
           cy.get('#config_uri').type('rados://.smb/foo/scc.toml');
@@ -108,19 +108,20 @@ export class ServicesPageHelper extends PageHelper {
           cy.get('#oidc_issuer_url').type('http://127.0.0.0:8080/realms/ceph');
           break;
 
+        case 'mgmt-gateway':
+          cy.get('#port').clear().type('8443');
+          cy.get('cds-checkbox#enable_auth input[type="checkbox"]').check({ force: true });
+          break;
+
         default:
           cy.get('#service_id').type('test');
           unmanaged
-            ? cy.get('label[for=unmanaged]').click()
+            ? cy.get('cds-checkbox#unmanaged input[type="checkbox"]').check({ force: true })
             : cy.get('#count').clear().type(String(count));
           break;
       }
       cy.wait(1000);
-      if (serviceType === 'snmp-gateway') {
-        cy.get('cd-submit-button').dblclick();
-      } else {
-        cy.get('cd-submit-button').click();
-      }
+      cy.get('cd-submit-button').click();
     });
     if (exist) {
       cy.get('#service_id').should('have.class', 'ng-invalid');
@@ -133,7 +134,7 @@ export class ServicesPageHelper extends PageHelper {
   editService(name: string, daemonCount: string) {
     this.navigateEdit(name, true, false);
     cy.get(`${this.pages.create.id}`).within(() => {
-      cy.get('#service_type').should('be.disabled');
+      cy.get('cds-select#service_type select').should('be.disabled');
       cy.get('#service_id').should('be.disabled');
       cy.get('#count').clear().type(daemonCount);
       cy.get('cd-submit-button').click();
@@ -203,9 +204,8 @@ export class ServicesPageHelper extends PageHelper {
   deleteService(serviceName: string) {
     this.clickRowActionButton(serviceName, 'delete', 3 * 1000);
 
-    // Confirms deletion
-    cy.get('cds-modal input#confirmation_input').click({ force: true });
-    cy.contains('cds-modal button', 'Delete').click();
+    cy.get('cds-modal [aria-label="confirmation"]').click({ force: true });
+    cy.get('cds-modal button').contains('Delete', { matchCase: false }).click({ force: true });
 
     // Wait for modal to close
     cy.get('cds-modal').should('not.exist');
index 6deff9fdc1fec5c3c00b9fd0ad2b9b50bc4ebffc..41a50cb2e2873924f76a690a966137c3516620cb 100644 (file)
@@ -29,6 +29,7 @@ describe('Host Page', () => {
   it('should create rgw services', () => {
     services.navigateTo('create');
     services.addService('rgw', false, 4);
+    services.navigateTo('index');
     services.checkExist('rgw.foo', true);
   });
 
index b2ffca0a87813f3ef13defd26358d9cd743f0665..b05b726e5f600d5092e612478d078c4dd7bbade8 100644 (file)
@@ -19,7 +19,9 @@ import {
   IconService,
   TagModule,
   SelectModule,
-  LayoutModule
+  LayoutModule,
+  NumberModule,
+  FileUploaderModule
 } from 'carbon-components-angular';
 import Analytics from '@carbon/icons/es/analytics/16';
 import CloseFilled from '@carbon/icons/es/close--filled/16';
@@ -124,7 +126,9 @@ import { TextLabelListComponent } from '~/app/shared/components/text-label-list/
     TagModule,
     TextLabelListComponent,
     SelectModule,
-    LayoutModule
+    LayoutModule,
+    NumberModule,
+    FileUploaderModule
   ],
   declarations: [
     MonitorComponent,
index 4b5dd8b3301834585b03dc46e5e2d2b5e632da76..8a158c229f441bfb06f4efe917f4c96c62f56330 100644 (file)
-<cd-modal [pageURL]="pageURL"
-          [modalRef]="activeModal">
-  <span class="modal-title"
-        i18n>{{ action | titlecase }} {{ resource | upperFirst }}</span>
-  <ng-container class="modal-content">
+<cds-modal size="lg"
+           [open]="open"
+           (overlaySelected)="closeModal()">
+  <cds-modal-header (closeSelect)="closeModal()">
+    <h3 cdsModalHeaderHeading
+        i18n>{{ action | titlecase }} {{ resource }}</h3>
+    <cd-help-text></cd-help-text>
+  </cds-modal-header>
+  <section cdsModalContent>
     <form #frm="ngForm"
           [formGroup]="serviceForm"
           novalidate>
-      <div class="modal-body">
-        <cd-alert-panel *ngIf="serviceForm.controls.service_type.value === 'rgw' && showRealmCreationForm"
-                        type="info"
-                        spacingClass="mb-3"
-                        i18n>
-          <a class="text-decoration-underline"
-             (click)="createMultisiteSetup()">
-             Click here</a> to create a new Realm/Zonegroup/Zone
-        </cd-alert-panel>
+      @if (serviceForm.controls.service_type.value === 'rgw' && showRealmCreationForm) {
+      <cd-alert-panel type="info"
+                      spacingClass="mb-3"
+                      i18n>
+        <a class="text-decoration-underline"
+           (click)="createMultisiteSetup()">
+          Click here</a> to create a new Realm/Zonegroup/Zone
+      </cd-alert-panel>
+      }
 
-        <cd-alert-panel *ngIf="serviceForm.controls.service_type.value === 'oauth2-proxy'"
-                        type="info"
-                        spacingClass="mb-3"
-                        i18n>
-          Authentication must be enabled in an active `mgtm-gateway` service to enable Single Sign-On(SSO) with `oauth2-proxy`
-        </cd-alert-panel>
-        <cd-alert-panel *ngIf="serviceForm.controls.service_type.value === 'mgmt-gateway'"
-                        type="info"
-                        spacingClass="mb-3"
-                        i18n>
-          With an active mgmt-gateway service, the dashboard will continue to be served on {{currentURL}}:{{port}} and all other services will be accessible from {{currentURL}}:{{port}}/service_name
-        </cd-alert-panel>
+      @if (serviceForm.controls.service_type.value === 'oauth2-proxy') {
+      <cd-alert-panel type="info"
+                      spacingClass="mb-3"
+                      i18n>
+        Authentication must be enabled in an active `mgmt-gateway` service to enable Single Sign-On(SSO) with
+        `oauth2-proxy`
+      </cd-alert-panel>
+      }
+      @if (serviceForm.controls.service_type.value === 'mgmt-gateway') {
+      <cd-alert-panel type="info"
+                      spacingClass="mb-3"
+                      i18n>
+        With an active mgmt-gateway service, the dashboard will continue to be served on {{currentURL}}:{{port}} and all
+        other services will be accessible from {{currentURL}}:{{port}}/service_name
+      </cd-alert-panel>
+      }
 
-        <cd-alert-panel *ngIf="serviceForm.controls.service_type.value === 'smb'"
-                        type="warning"
-                        spacingClass="mb-3"
-                        i18n>
-          SMB service management is intended for advanced users only.
-          For most scenarios, it is  recommended to use the SMB module instead.
-          To manage SMB clusters and shares, please visit the <a routerLink="/cephfs/smb">SMB page</a>.
-        </cd-alert-panel>
+      @if (serviceForm.controls.service_type.value === 'smb') {
+      <cd-alert-panel type="warning"
+                      spacingClass="mb-3"
+                      i18n>
+        SMB service management is intended for advanced users only.
+        For most scenarios, it is recommended to use the SMB module instead.
+        To manage SMB clusters and shares, please visit the <a routerLink="/cephfs/smb">SMB page</a>.
+      </cd-alert-panel>
+      }
 
-        <!-- Service type -->
-        <div class="form-group row">
-          <label class="cd-col-form-label required"
-                 for="service_type"
-                 i18n>Type</label>
-          <div class="cd-col-form-input">
-            <select id="service_type"
-                    name="service_type"
-                    class="form-select"
-                    formControlName="service_type"
+      <!-- Service type -->
+      <div class="form-item">
+        <cds-select formControlName="service_type"
+                    label="Type"
+                    cdRequiredField="Type"
+                    id="service_type"
+                    [invalid]="serviceForm.controls.service_type.invalid && serviceForm.controls.service_type.dirty"
+                    [invalidText]="invalidServiceTypeError"
                     (change)="onServiceTypeChange($event.target.value)">
-              <option i18n
-                      [ngValue]="null">-- Select a service type --</option>
-              <option *ngFor="let serviceType of serviceTypes"
-                      [value]="serviceType">
-                {{ serviceType }}
-              </option>
-            </select>
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('service_type', frm, 'required')"
-                  i18n>This field is required.</span>
-          </div>
-        </div>
+          <option i18n
+                  [ngValue]="null">
+            -- Select a service type --
+          </option>
+          @for (serviceType of serviceTypes; track serviceType) {
+          <option [value]="serviceType">
+            {{ serviceType }}
+          </option>
+          }
+        </cds-select>
 
-        <!-- backend_service -->
-        <div *ngIf="serviceForm.controls.service_type.value === 'ingress'"
-             class="form-group row">
-          <label i18n
-                 class="cd-col-form-label"
-                 [ngClass]="{'required': ['ingress'].includes(serviceForm.controls.service_type.value)}"
-                 for="backend_service">Backend Service</label>
-          <div class="cd-col-form-input">
-            <select id="backend_service"
-                    name="backend_service"
-                    class="form-select"
+        <ng-template #invalidServiceTypeError>
+          @if (serviceForm.showError('service_type', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+
+      <!-- backend_service -->
+      @if (serviceForm.controls.service_type.value === 'ingress') {
+      <div class="form-item">
+        <cds-select id="backend_service"
                     formControlName="backend_service"
+                    label="Backend service"
+                    cdRequiredField="Backend service"
+                    [invalid]="serviceForm.controls.backend_service.invalid && serviceForm.controls.backend_service.dirty"
+                    [invalidText]="invalidBackendServiceError"
                     (change)="prePopulateId()">
-              <option *ngIf="services === null"
-                      [ngValue]="null"
-                      i18n>Loading...</option>
-              <option *ngIf="services !== null && services.length === 0"
-                      [ngValue]="null"
-                      i18n>-- No service available --</option>
-              <option *ngIf="services !== null && services.length > 0"
-                      [ngValue]="null"
-                      i18n>-- Select an existing service --</option>
-              <option *ngFor="let service of services"
-                      [value]="service.service_name">{{ service.service_name }}</option>
-            </select>
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('backend_service', frm, 'required')"
-                  i18n>This field is required.</span>
-          </div>
-        </div>
+          @if (services === null) {
+          <option [ngValue]="null"
+                  i18n>Loading...</option>
+          }
+          @if (services !== null && services.length === 0) {
+          <option [ngValue]="null"
+                  i18n>-- No service available --</option>
+          }
+          @if (services !== null && services.length > 0) {
+          <option [ngValue]="null"
+                  i18n>-- Select an existing service --</option>
+          }
+          @for (service of services; track service) {
+          <option [value]="service.service_name">{{ service.service_name }}</option>
+          }
+        </cds-select>
+        <ng-template #invalidBackendServiceError>
+          @if (serviceForm.showError('backend_service', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+      }
 
-        <!-- NVMe/TCP -->
-        <!-- Block Pool -->
-        <div class="form-group row"
-             *ngIf="serviceForm.controls.service_type.value === 'nvmeof'">
-          <label i18n
-                 class="cd-col-form-label required"
-                 for="pool">Block Pool</label>
-          <div class="cd-col-form-input">
-            <select id="pool"
-                    name="pool"
-                    class="form-select"
+      <!-- NVMe/TCP -->
+      <!-- Block Pool -->
+      @if (serviceForm.controls.service_type.value === 'nvmeof') {
+      <div class="form-item">
+        <cds-select id="pool"
                     formControlName="pool"
+                    label="Block Pool"
+                    cdRequiredField="Block Pool"
+                    helperText="An RBD application-enabled pool in which the gateway configuration can be managed."
+                    i18n-helperText
+                    [invalid]="serviceForm.controls.pool.invalid && serviceForm.controls.pool.dirty"
+                    [invalidText]="requiredFieldPool"
                     (change)="setNvmeServiceId()">
-              <option *ngIf="rbdPools === null"
-                      [ngValue]="null"
-                      i18n>Loading...</option>
-              <option *ngIf="rbdPools && rbdPools.length === 0"
-                      [ngValue]="null"
-                      i18n>-- No block pools available --</option>
-              <option *ngIf="rbdPools && rbdPools.length > 0"
-                      [ngValue]="null"
-                      i18n>-- Select a pool --</option>
-              <option *ngFor="let pool of rbdPools"
-                      [value]="pool.pool_name">{{ pool.pool_name }}</option>
-            </select>
-            <cd-help-text i18n>
-              An RBD application-enabled pool in which the gateway configuration can be managed.
-            </cd-help-text>
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('pool', frm, 'required')"
-                  i18n>This field is required.</span>
-          </div>
-        </div>
+          @if (rbdPools === null) {
+          <option [ngValue]="null"
+                  i18n>Loading...</option>
+          }
+          @if (rbdPools && rbdPools.length === 0) {
+          <option [ngValue]="null"
+                  i18n>-- No block pools available --</option>
+          }
+          @if (rbdPools && rbdPools.length > 0) {
+          <option [ngValue]="null"
+                  i18n>-- Select a pool --</option>
+          }
+          @for (pool of rbdPools; track pool) {
+          <option [value]="pool.pool_name">{{ pool.pool_name }}</option>
+          }
+        </cds-select>
+        <ng-template #requiredFieldPool>
 
-        <!-- Group Name -->
-        <div class="form-group row"
-             *ngIf="serviceForm.controls.service_type.value === 'nvmeof'">
-          <label class="cd-col-form-label required"
-                 for="group">
-            <span i18n>Group Name</span>
-          </label>
-          <div class="cd-col-form-input">
-            <div class="input-group">
-              <input id="group"
-                     class="form-control"
-                     type="text"
-                     formControlName="group"
-                     (change)="setNvmeServiceId()">
-            </div>
-            <cd-help-text i18n>
-              The name of the gateway group.
-            </cd-help-text>
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('service_id', frm, 'required')"
-                  i18n>This field is required.</span>
-          </div>
-        </div>
+          @if (serviceForm.showError('pool', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+      }
 
-        <!-- Service id -->
-        <div class="form-group row"
-             *ngIf="serviceForm.controls.service_type.value !== 'snmp-gateway'">
-          <label class="cd-col-form-label"
-                 [ngClass]="{'required': ['mds', 'rgw', 'nfs', 'iscsi', 'nvmeof', 'smb', 'ingress'].includes(serviceForm.controls.service_type.value)}"
-                 for="service_id">
-            <span i18n>Service Name</span>
-          </label>
-          <div class="cd-col-form-input">
-            <div class="input-group">
-              <span class="input-group-text"
-                    *ngIf="serviceForm.controls.service_type.value && ['mds', 'rgw', 'nfs', 'iscsi', 'nvmeof', 'smb', 'ingress'].includes(serviceForm.controls.service_type.value)"
-                    for="userId"
-                    i18n>{{serviceForm.controls.service_type.value}}.
-              </span>
-              <input id="service_id"
-                     class="form-control"
+      <!-- Group Name -->
+      @if (serviceForm.controls.service_type.value === 'nvmeof') {
+      <div class="form-item">
+        <cds-text-label helperText="The name of the gateway group."
+                        i18n-helperText
+                        label="Group name"
+                        cdRequiredField="Group name"
+                        [invalid]="serviceForm.controls.group.invalid && serviceForm.controls.group.dirty"
+                        [invalidText]="requiredFieldGroup">
+          <input cdsText
+                 type="text"
+                 id="group"
+                 formControlName="group"
+                 [invalid]="serviceForm.controls.group.invalid && serviceForm.controls.group.dirty"
+                 (change)="setNvmeServiceId()" />
+        </cds-text-label>
+        <ng-template #requiredFieldGroup>
+          @if (serviceForm.showError('service_id', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+      }
+
+      <!-- Service id -->
+      @if (serviceForm.controls.service_type.value !== 'snmp-gateway') {
+      <div class="form-item">
+        @if (isPrefixedNamedService) {
+        <div class="cds-input-group">
+          <div class="fit-content">
+            <cds-text-label cdRequiredField="Service name"
+                            i18n>
+              <input cdsText
                      type="text"
-                     formControlName="service_id">
-            </div>
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('service_id', frm, 'required')"
-                  i18n>This field is required.</span>
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('service_id', frm, 'uniqueName')"
-                  i18n>This service id is already in use.</span>
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('service_id', frm, 'mdsPattern')"
-                  i18n>MDS service id must start with a letter and contain alphanumeric characters or '.', '-', and '_'</span>
+                     [value]="serviceForm.controls.service_type.value + '.'"
+                     readonly="true">
+            </cds-text-label>
           </div>
+          <cds-text-label [invalid]="serviceForm.controls.service_id.invalid && serviceForm.controls.service_id.dirty"
+                          [invalidText]="serviceIdError">
+            <!-- Invisible character to set input fields on same line -->
+            &#8203;
+            <input cdsText
+                   type="text"
+                   id="service_id"
+                   formControlName="service_id"
+                   [invalid]="serviceForm.controls.service_id.invalid && serviceForm.controls.service_id.dirty" />
+          </cds-text-label>
         </div>
+        } @else {
+        <cds-text-label [invalid]="serviceForm.controls.service_id.invalid && serviceForm.controls.service_id.dirty"
+                        [invalidText]="serviceIdError">
+          Service name
+          <input cdsText
+                 type="text"
+                 id="service_id"
+                 formControlName="service_id"
+                 [invalid]="serviceForm.controls.service_id.invalid && serviceForm.controls.service_id.dirty" />
+        </cds-text-label>
+        }
 
-        <div class="form-group row"
-             *ngIf="serviceForm.controls.service_type.value === 'rgw'">
-          <label class="cd-col-form-label"
-                 for="realm_name"
-                 i18n>Realm</label>
-          <div class="cd-col-form-input">
-            <select class="form-select"
-                    id="realm_name"
-                    formControlName="realm_name"
-                    name="realm_name"
-                    [attr.disabled]="realmList.length === 0  || editing ? true : null">
-            <option *ngIf="realmList.length === 0"
-                    i18n
-                    selected>-- No realm available --</option>
-            <option *ngFor="let realm of realmList"
-                    [value]="realm.name">
-                  {{ realm.name }}
-            </option>
-            </select>
-          </div>
-        </div>
+        <ng-template #serviceIdError>
+          @if (serviceForm.showError('service_id', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+          @if (serviceForm.showError('service_id', frm, 'uniqueName')) {
+          <span class="invalid-feedback"
+                i18n>This service id is already in use.</span>
+          }
+          @if (serviceForm.showError('service_id', frm, 'mdsPattern')) {
+          <span class="invalid-feedback"
+                i18n>MDS service id must start with a letter and contain alphanumeric characters or '.', '-', and
+            '_'</span>
+          }
+        </ng-template>
+      </div>
+      }
 
-        <div class="form-group row"
-             *ngIf="serviceForm.controls.service_type.value === 'rgw'">
-          <label class="cd-col-form-label"
-                 for="zonegroup_name"
-                 i18n>Zonegroup</label>
-          <div class="cd-col-form-input">
-            <select class="form-select"
-                    id="zonegroup_name"
+      @if (serviceForm.controls.service_type.value === 'rgw') {
+      <div class="form-item">
+        <cds-select formControlName="realm_name"
+                    label="Realm"
+                    [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">
+          @if (realmList.length === 0) {
+          <option i18n
+                  selected>-- No realm available --
+          </option>
+          }
+          @for (realm of realmList; track realm) {
+          <option [value]="realm.name">
+            {{ realm.name }}
+          </option>
+          }
+        </cds-select>
+        <ng-template #authModeError>
+          @if (serviceForm.showError('realm_name', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+
+      <div class="form-item">
+        <cds-select id="zonegroup_name"
                     formControlName="zonegroup_name"
-                    name="zonegroup_name"
-                    [attr.disabled]="zonegroupList.length === 0  || editing ? true : null">
-              <option *ngFor="let zonegroup of zonegroupList"
-                      [value]="zonegroup.name">
-              {{ zonegroup.name }}
-              </option>
-            </select>
-          </div>
-        </div>
+                    label="Zonegroup"
+                    [disabled]="zonegroupList.length === 0  || editing ? true : null">
+          @for (zonegroup of zonegroupList; track zonegroup) {
+          <option [value]="zonegroup.name">
+            {{ zonegroup.name }}
+          </option>
+          }
+        </cds-select>
+      </div>
 
-        <div class="form-group row"
-             *ngIf="serviceForm.controls.service_type.value === 'rgw'">
-          <label class="cd-col-form-label"
-                 for="zone_name"
-                 i18n>Zone</label>
-          <div class="cd-col-form-input">
-            <select class="form-select"
-                    id="zone_name"
+      <div class="form-item">
+        <cds-select id="zone_name"
                     formControlName="zone_name"
-                    name="zone_name"
-                    [attr.disabled]="zoneList.length === 0  || editing ? true : null">
-              <option *ngFor="let zone of zoneList"
-                      [value]="zone.name">
-              {{ zone.name }}
-              </option>
-            </select>
-          </div>
-        </div>
+                    label="Zone"
+                    [disabled]="zoneList.length === 0  || editing ? true : null">
+          @for (zone of zoneList; track zone) {
+          <option [value]="zone.name">
+            {{ zone.name }}
+          </option>
+          }
+        </cds-select>
+      </div>
+      }
 
-        <!-- unmanaged -->
-        <div class="form-group row">
-          <div class="cd-col-form-offset">
-            <div class="custom-control custom-checkbox">
-              <input class="custom-control-input"
-                     id="unmanaged"
-                     type="checkbox"
-                     formControlName="unmanaged">
-              <label class="custom-control-label m-0"
-                     for="unmanaged"
-                     i18n>Unmanaged</label>
-              <cd-help-text i18n>If Unmanaged is selected, the orchestrator will not start or stop any daemons associated with this service. Placement and all other properties will be ignored.</cd-help-text>
-            </div>
-          </div>
-        </div>
+      <!-- unmanaged -->
+      <div class="form-item">
+        <fieldset>
+          <label class="cds--label"
+                 for="unmanaged"
+                 i18n>Unmanaged</label>
+          <cds-checkbox i18n-label
+                        id="unmanaged"
+                        formControlName="unmanaged">
+            Enable
+            <cd-help-text i18n>
+              If Unmanaged is selected, the orchestrator will not start or stop any daemons associated with this
+              service.
+              Placement and all other properties will be ignored.
+            </cd-help-text>
+          </cds-checkbox>
+        </fieldset>
+      </div>
 
-        <!-- Placement -->
-        <div *ngIf="!serviceForm.controls.unmanaged.value"
-             class="form-group row">
-          <label class="cd-col-form-label"
-                 for="placement"
-                 i18n>Placement</label>
-          <div class="cd-col-form-input">
-            <select id="placement"
-                    class="form-select"
+      <!-- Placement -->
+      @if (!serviceForm.controls.unmanaged.value) {
+      <div class="form-item">
+        <cds-select id="placement"
                     formControlName="placement"
+                    label="Placement"
                     (change)="onPlacementChange($event.target.value)">
-              <option i18n
-                      value="hosts">Hosts</option>
-              <option i18n
-                      value="label">Label</option>
-            </select>
-          </div>
-        </div>
-
-        <!-- Label -->
-        <div *ngIf="!serviceForm.controls.unmanaged.value && serviceForm.controls.placement.value === 'label'"
-             class="form-group row">
-          <label i18n
-                 class="cd-col-form-label"
-                 for="label">Label</label>
-          <div class="cd-col-form-input">
-            <input id="label"
-                   class="form-control"
-                   type="text"
-                   formControlName="label"
-                   [ngbTypeahead]="searchLabels"
-                   (focus)="labelFocus.next($any($event).target.value)"
-                   (click)="labelClick.next($any($event).target.value)">
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('label', frm, 'required')"
-                  i18n>This field is required.</span>
-          </div>
-        </div>
-
-        <!-- Hosts -->
-        <div *ngIf="!serviceForm.controls.unmanaged.value && serviceForm.controls.placement.value === 'hosts'"
-             class="form-group row">
-          <label class="cd-col-form-label"
-                 for="hosts"
-                 i18n>Hosts</label>
-          <div class="cd-col-form-input">
-            <cd-select-badges id="hosts"
-                              [data]="serviceForm.controls.hosts.value"
-                              [options]="hosts.options"
-                              [messages]="hosts.messages">
-            </cd-select-badges>
-          </div>
-        </div>
-
-        <!-- Count -->
-        <div *ngIf="!serviceForm.controls.unmanaged.value && serviceForm.controls.service_type.value !== 'nvmeof'"
-             class="form-group row">
-          <label class="cd-col-form-label"
-                 for="count">
-            <span i18n>Count</span>
-          </label>
-          <div class="cd-col-form-input">
-            <input id="count"
-                   class="form-control"
-                   type="number"
-                   formControlName="count"
-                   min="1">
-            <cd-help-text i18n>Number of deamons that will be deployed</cd-help-text>
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('count', frm, 'min')"
-                  i18n>The value must be at least 1.</span>
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('count', frm, 'pattern')"
-                  i18n>The entered value needs to be a number.</span>
-          </div>
-        </div>
-
-        <!-- RGW -->
-        <ng-container *ngIf="!serviceForm.controls.unmanaged.value && serviceForm.controls.service_type.value === 'rgw'">
-          <!-- rgw_frontend_port -->
-          <div class="form-group row">
-            <label i18n
-                   class="cd-col-form-label"
-                   for="rgw_frontend_port">Port</label>
-            <div class="cd-col-form-input">
-              <input id="rgw_frontend_port"
-                     class="form-control"
-                     type="number"
-                     formControlName="rgw_frontend_port"
-                     min="1"
-                     max="65535">
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('rgw_frontend_port', frm, 'pattern')"
-                    i18n>The entered value needs to be a number.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('rgw_frontend_port', frm, 'min')"
-                    i18n>The value must be at least 1.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('rgw_frontend_port', frm, 'max')"
-                    i18n>The value cannot exceed 65535.</span>
-            </div>
-          </div>
-        </ng-container>
+          <option i18n
+                  value="hosts">Hosts</option>
+          <option i18n
+                  value="label">Label</option>
+        </cds-select>
+      </div>
+      }
 
-        <!-- iSCSI -->
-        <!-- pool -->
-        <div class="form-group row"
-             *ngIf="serviceForm.controls.service_type.value === 'iscsi'">
-          <label i18n
-                 class="cd-col-form-label required"
-                 for="pool">Pool</label>
-          <div class="cd-col-form-input">
-            <select id="pool"
-                    name="pool"
-                    class="form-select"
-                    formControlName="pool">
-              <option *ngIf="pools === null"
-                      [ngValue]="null"
-                      i18n>Loading...</option>
-              <option *ngIf="pools && pools.length === 0"
-                      [ngValue]="null"
-                      i18n>-- No pools available --</option>
-              <option *ngIf="pools && pools.length > 0"
-                      [ngValue]="null"
-                      i18n>-- Select a pool --</option>
-              <option *ngFor="let pool of pools"
-                      [value]="pool.pool_name">{{ pool.pool_name }}</option>
-            </select>
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('pool', frm, 'required')"
-                  i18n>This field is required.</span>
-          </div>
-        </div>
+      <!-- Label -->
+      @if (hostsAndLabels$ | async; as data) {
+      @if (!serviceForm.controls.unmanaged.value && serviceForm.controls.placement.value === 'label') {
+      <div class="form-item">
+        <cds-combo-box type="multi"
+                       selectionFeedback="top-after-reopen"
+                       label="Label"
+                       cdRequiredField="Label"
+                       formControlName="label"
+                       id="label"
+                       [appendInline]="true"
+                       [items]="data.labels"
+                       [invalid]="serviceForm.controls.label.invalid && serviceForm.controls.label.dirty"
+                       [invalidText]="requiredFieldLabel"
+                       (selected)="multiSelector($event, 'label')"
+                       i18n>
+          <cds-dropdown-list></cds-dropdown-list>
+        </cds-combo-box>
+        <ng-template #requiredFieldLabel>
+          @if (serviceForm.showError('label', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+      }
+      <!-- Hosts -->
+      @if (!serviceForm.controls.unmanaged.value && serviceForm.controls.placement.value === 'hosts') {
+      <div class="form-item">
+        <cds-combo-box type="multi"
+                       selectionFeedback="top-after-reopen"
+                       label="Hosts"
+                       formControlName="hosts"
+                       id="hosts"
+                       [appendInline]="true"
+                       [items]="data.hosts"
+                       (selected)="multiSelector($event, 'hosts')"
+                       i18n>
+          <cds-dropdown-list></cds-dropdown-list>
+        </cds-combo-box>
+      </div>
+      }
+      }
 
-        <!-- fields in iSCSI which are hidden when unmanaged is true -->
-        <ng-container *ngIf="!serviceForm.controls.unmanaged.value && serviceForm.controls.service_type.value === 'iscsi'">
-          <!-- trusted_ip_list -->
-          <div class="form-group row">
-            <label class="cd-col-form-label"
-                   for="trusted_ip_list">
-              <span i18n>Trusted IPs</span>
-              <cd-helper>
-                <span i18n>Comma separated list of IP addresses.</span>
-                <br>
-                <span i18n>Please add the <b>Ceph Manager</b> IP addresses here, otherwise the iSCSI gateways can't be reached.</span>
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="trusted_ip_list"
-                     class="form-control"
-                     type="text"
-                     formControlName="trusted_ip_list">
-            </div>
-          </div>
 
-          <!-- api_port -->
-          <div class="form-group row">
-            <label i18n
-                   class="cd-col-form-label"
-                   for="api_port">Port</label>
-            <div class="cd-col-form-input">
-              <input id="api_port"
-                     class="form-control"
-                     type="number"
-                     formControlName="api_port"
-                     min="1"
-                     max="65535">
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('api_port', frm, 'pattern')"
-                    i18n>The entered value needs to be a number.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('api_port', frm, 'min')"
-                    i18n>The value must be at least 1.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('api_port', frm, 'max')"
-                    i18n>The value cannot exceed 65535.</span>
-            </div>
-          </div>
+      <!-- Count -->
+      @if (!serviceForm.controls.unmanaged.value && serviceForm.controls.service_type.value !== 'nvmeof') {
+      <div class="form-item">
+        <cds-number label="Count"
+                    formControlName="count"
+                    id="count"
+                    min="1"
+                    helperText="Number of deamons that will be deployed"
+                    i18n-helperText>
+        </cds-number>
+      </div>
+      }
 
-          <!-- api_user -->
-          <div class="form-group row">
-            <label i18n
-                   class="cd-col-form-label"
-                   [ngClass]="{'required': ['iscsi'].includes(serviceForm.controls.service_type.value)}"
-                   for="api_user">User</label>
-            <div class="cd-col-form-input">
-              <input id="api_user"
-                     class="form-control"
-                     type="text"
-                     formControlName="api_user">
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('api_user', frm, 'required')"
-                    i18n>This field is required.</span>
-            </div>
-          </div>
+      <!-- RGW -->
+      @if (!serviceForm.controls.unmanaged.value && serviceForm.controls.service_type.value === 'rgw') {
+      <!-- rgw_frontend_port -->
+      <div class="form-item">
+        <cds-number label="Port"
+                    formControlName="rgw_frontend_port"
+                    id="rgw_frontend_port"
+                    min="1"
+                    max="65535"
+                    helperText="Number of deamons that will be deployed"
+                    i18n-helperText
+                    [invalid]="serviceForm.controls.rgw_frontend_port.invalid && serviceForm.controls.rgw_frontend_port.dirty"
+                    [invalidText]="invalidRgwFrontendPortError">
+        </cds-number>
+        <ng-template #invalidRgwFrontendPortError>
+          @if (serviceForm.showError('rgw_frontend_port', frm, 'min')) {
+          <span class="invalid-feedback"
+                i18n>The value must be at least 1.</span>
+          }
+          @if (serviceForm.showError('rgw_frontend_port', frm, 'max')) {
+          <span class="invalid-feedback"
+                i18n>The value cannot exceed 65535.</span>
+          }
+        </ng-template>
+      </div>
+      }
 
-          <!-- api_password -->
-          <div class="form-group row">
-            <label i18n
-                   class="cd-col-form-label"
-                   [ngClass]="{'required': ['iscsi'].includes(serviceForm.controls.service_type.value)}"
-                   for="api_password">Password</label>
-            <div class="cd-col-form-input">
-              <div class="input-group">
-                <input id="api_password"
-                       class="form-control"
-                       type="password"
-                       autocomplete="new-password"
-                       formControlName="api_password">
-                <button type="button"
-                        class="btn btn-light"
-                        cdPasswordButton="api_password">
-                </button>
-                <cd-copy-2-clipboard-button source="api_password">
-                </cd-copy-2-clipboard-button>
-                <span class="invalid-feedback"
-                      *ngIf="serviceForm.showError('api_password', frm, 'required')"
-                      i18n>This field is required.</span>
-              </div>
-            </div>
-          </div>
-        </ng-container>
+      <!-- iSCSI -->
+      <!-- pool -->
+      @if (serviceForm.controls.service_type.value === 'iscsi') {
+      <div class="form-item">
+        <cds-select label="Pool"
+                    formControlName="pool"
+                    cdRequiredField="Pool"
+                    id="pool"
+                    [invalid]="serviceForm.controls.pool.invalid && serviceForm.controls.pool.dirty"
+                    [invalidText]="invalidPoolError">
+          @if (pools === null) {
+          <option [ngValue]="null"
+                  i18n>Loading...</option>
+          }
+          @if (pools && pools.length === 0) {
+          <option [ngValue]="null"
+                  i18n>-- No pools available --</option>
+          }
+          @if (pools && pools.length > 0) {
+          <option [ngValue]="null"
+                  i18n>-- Select a pool --</option>
+          }
+          @for (pool of pools; track pool) {
+          <option [value]="pool.pool_name">{{ pool.pool_name }}</option>
+          }
+        </cds-select>
+        <ng-template #invalidPoolError>
+          @if (serviceForm.showError('pool', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+      }
 
-        <!-- smb -->
-        <ng-container *ngIf="serviceForm.controls.service_type.value === 'smb'">
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="cluster_id"
-                   i18n>
-              Cluster id
-              <cd-helper>
-                <span>A short name identifying the SMB “cluster”. In this case a cluster is simply a management unit of one or more Samba services sharing a common configuration,
-                   and may not provide actual clustering or availability mechanisms.</span>
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="cluster_id"
-                     class="form-control"
-                     type="text"
-                     formControlName="cluster_id"
-                     placeholder="foo"
-                     i18n-placeholder>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('cluster_id', frm, 'required')"
-                    i18n>This field is required.</span>
-            </div>
-          </div>
+      <!-- fields in iSCSI which are hidden when unmanaged is true -->
+      @if (!serviceForm.controls.unmanaged.value && serviceForm.controls.service_type.value === 'iscsi') {
+      <!-- trusted_ip_list -->
+      <div class="form-item">
+        <cd-text-label-list formControlName="trusted_ip_list"
+                            label="Trusted IPs"
+                            helperText="List of trusted IP addresses. Add the Ceph Manager IP addresses so the iSCSI gateways can be reached.">
+        </cd-text-label-list>
+      </div>
+      <!-- api_port -->
+      <div class="form-item">
+        <cds-number label="Port"
+                    formControlName="api_port"
+                    id="api_port"
+                    min="1"
+                    max="65535"
+                    [invalid]="serviceForm.controls.api_port.invalid && serviceForm.controls.api_port.dirty"
+                    [invalidText]="invalidApiPortError">
+        </cds-number>
+        <ng-template #invalidApiPortError>
+          @if (serviceForm.showError('api_port', frm, 'min')) {
+          <span class="invalid-feedback"
+                i18n>The value must be at least 1.</span>
+          }
+          @if (serviceForm.showError('api_port', frm, 'max')) {
+          <span class="invalid-feedback"
+                i18n>The value cannot exceed 65535.</span>
+          }
+        </ng-template>
+      </div>
+      <!-- api_user -->
+      <div class="form-item">
+        <cds-text-label cdRequiredField="User"
+                        [invalid]="serviceForm.controls.api_user.invalid && serviceForm.controls.api_user.dirty"
+                        [invalidText]="invalidUser">
+          <input cdsText
+                 type="text"
+                 id="api_user"
+                 formControlName="api_user"
+                 [invalid]="serviceForm.controls.api_user.invalid && serviceForm.controls.api_user.dirty" />
+        </cds-text-label>
+        <ng-template #invalidUser>
+          @if (serviceForm.showError('api_user', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
 
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="config_uri">
-              <span i18n>Config URI</span>
-              <cd-helper i18n>
-                Configuration source that should be loaded by the samba-container as the primary configuration file.
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="config_uri"
-                     class="form-control"
-                     type="text"
-                     formControlName="config_uri"
-                     placeholder="rados://.smb/foo/scc.toml"
-                     i18n-placeholder>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('config_uri', frm, 'required')"
-                    i18n>This field is required.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('config_uri', frm, 'configUriPattern')"
-                    i18n>The value must start with either 'http:', 'https:', 'rados:' or 'rados:mon-config-key:'</span>
-            </div>
-          </div>
+      <!-- api_password -->
+      <div class="form-item">
+        <cds-password-label cdRequiredField="Password"
+                            [invalid]="serviceForm.controls.api_password.invalid && serviceForm.controls.api_password.dirty"
+                            [invalidText]="invalidPassword">
+          <input cdsPassword
+                 type="password"
+                 id="api_password"
+                 formControlName="api_password"
+                 [invalid]="serviceForm.controls.api_password.invalid && serviceForm.controls.api_password.dirty" />
+        </cds-password-label>
+        <ng-template #invalidPassword>
+          @if (serviceForm.showError('api_password', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+      }
 
-          <div class="form-group row"
-               formGroupName="features">
-            <label class="cd-col-form-label"
-                   for="features"
-                   i18n>Features
-              <cd-helper>
-                <span>Pre-defined terms enabling specific deployment characteristics.</span>
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <div class="custom-control custom-checkbox"
-                   *ngFor="let feature of smbFeaturesList">
-                <input class="custom-control-input"
-                       type="checkbox"
-                       name="{{feature}}"
-                       id="{{feature}}"
-                       formControlName="{{feature}}">
-                <label class="custom-control-label"
-                       for="{{feature}}"
-                       i18n>{{feature}}
-                </label>
-              </div>
-            </div>
-          </div>
+      <!-- smb -->
+      @if (serviceForm.controls.service_type.value === 'smb') {
+      <div class="form-item">
+        <cds-text-label cdRequiredField="Cluster id"
+                        helperText="A short name identifying the SMB “cluster”. In this case a cluster is simply a management unit of one or more Samba services sharing a common configuration, and may not provide actual clustering or availability mechanisms."
+                        i18n-helperText
+                        [invalid]="serviceForm.controls.cluster_id.invalid && serviceForm.controls.cluster_id.dirty"
+                        [invalidText]="requiredFieldClusterId">
+          <input cdsText
+                 type="text"
+                 id="cluster_id"
+                 formControlName="cluster_id"
+                 placeholder="foo"
+                 [invalid]="serviceForm.controls.cluster_id.invalid && serviceForm.controls.cluster_id.dirty" />
+        </cds-text-label>
+        <ng-template #requiredFieldClusterId>
+          @if (serviceForm.showError('cluster_id', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
 
-          <div class="form-item">
-            <cd-text-label-list formControlName="custom_dns"
-                                label="Custom DNS"
-                                helperText="IP addresses that will be used as the DNS servers for a Samba container."
-                                placeholder="192.168.76.204">
-            </cd-text-label-list>
-          </div>
+      <div class="form-item">
+        <cds-text-label cdRequiredField="Config URI"
+                        helperText="Configuration source that should be loaded by the samba-container as the primary configuration file."
+                        i18n-helperText
+                        [invalid]="serviceForm.controls.config_uri.invalid && serviceForm.controls.config_uri.dirty"
+                        [invalidText]="invalidConfigUriError">
+          <input cdsText
+                 type="text"
+                 id="config_uri"
+                 formControlName="config_uri"
+                 placeholder="rados://.smb/foo/scc.toml"
+                 [invalid]="serviceForm.controls.config_uri.invalid && serviceForm.controls.config_uri.dirty" />
+        </cds-text-label>
+        <ng-template #invalidConfigUriError>
+          @if (serviceForm.showError('config_uri', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+          @if (serviceForm.showError('config_uri', frm, 'configUriPattern')) {
+          <span class="invalid-feedback"
+                i18n>The value must start with either 'http:', 'https:', 'rados:' or 'rados:mon-config-key:'</span>
+          }
+        </ng-template>
+      </div>
+      <div class="form-item">
+        <fieldset>
+          <label class="cds--label"
+                 for="features"
+                 i18n>Features
+            <cd-help-text>
+              Pre-defined terms enabling specific deployment characteristics.
+            </cd-help-text>
+          </label>
+          @for (feature of smbFeaturesList; track feature) {
+          <cds-checkbox i18n-label
+                        id="{{feature}}"
+                        name="{{feature}}"
+                        formControlName="{{feature}}">
+            Enable
+          </cds-checkbox>
+          }
+        </fieldset>
+      </div>
 
-          <div class="form-group row">
-            <label class="cd-col-form-label"
-                   for="join_sources">
-              <span i18n>Join sources</span>
-              <cd-helper i18n>
-                <span>Comma separated list of URIs.</span>
-                <br>
-                <span>A list of values that will be used to identify where authentication data that will be used to perform domain joins are located.</span>
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="join_sources"
-                     class="form-control"
-                     type="text"
-                     formControlName="join_sources"
-                     placeholder="rados:mon-config-key:smb/config/foo/join1.json"
-                     i18n-placeholder>
-            </div>
-          </div>
+      <div class="form-item">
+        <cd-text-label-list formControlName="custom_dns"
+                            label="Custom DNS"
+                            helperText="IP addresses that will be used as the DNS servers for a Samba container."
+                            placeholder="192.168.76.204"
+                            i18n-placeholder>
+        </cd-text-label-list>
+      </div>
 
-          <div class="form-group row">
-            <label class="cd-col-form-label"
-                   for="user_sources">
-              <span i18n>User sources</span>
-              <cd-helper i18n>
-                <span>Comma separated list of URIs.</span>
-                <br>
-                <span>A list of pseudo-uris containing data the samba-container can use to create users (and/or
-                  groups). A ceph based samba container may typically use a rados uri
-                  or a mon config-key store uri </span>
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="user_sources"
-                     class="form-control"
-                     type="text"
-                     formControlName="user_sources"
-                     placeholder="rados:mon-config-key:smb/config/foo/join2.json"
-                     i18n-placeholder>
-            </div>
-          </div>
+      <div class="form-item">
+        <cd-text-label-list formControlName="join_sources"
+                            label="Join sources"
+                            helperText="Identify where authentication data that will be used to perform domain joins is located."
+                            placeholder="rados:mon-config-key:smb/config/foo/join1.json"
+                            i18n-placeholder>
+        </cd-text-label-list>
+      </div>
 
-          <div class="form-group row">
-            <label class="cd-col-form-label"
-                   for="include_ceph_users">
-              <span i18n>Ceph users</span>
-              <cd-helper i18n>
-                <span>Comma separated list of Ceph users.</span>
-                <br>
-                <span>A list of cephx user names that the Samba Containers may use.</span>
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="include_ceph_users"
-                     class="form-control"
-                     type="text"
-                     formControlName="include_ceph_users"
-                     placeholder="client.smb.fs.cluster.foo"
-                     i18n-placeholder>
-            </div>
-          </div>
+      <div class="form-item">
+        <cd-text-label-list formControlName="user_sources"
+                            label="User sources"
+                            helperText="Pseudo-uris containing data the samba-container can use to create users. A ceph based samba container may typically use a rados uri
+                            or a mon config-key store uri "
+                            placeholder="rados:mon-config-key:smb/config/foo/join2.json"
+                            i18n-placeholder>
+        </cd-text-label-list>
+      </div>
 
-        </ng-container>
+      <div class="form-item">
+        <cd-text-label-list formControlName="include_ceph_users"
+                            label="Ceph users"
+                            helperText="Cephx user names that the Samba containers may use."
+                            placeholder="client.smb.fs.cluster.foo"
+                            i18n-placeholder>
+        </cd-text-label-list>
+      </div>
+      }
 
-        <!-- Ingress -->
-        <ng-container *ngIf="serviceForm.controls.service_type.value === 'ingress'">
-          <!-- virtual_ip -->
-          <div class="form-group row">
-            <label class="cd-col-form-label"
-                   [ngClass]="{'required': ['ingress'].includes(serviceForm.controls.service_type.value)}"
-                   for="virtual_ip">
-              <span i18n>Virtual IP</span>
-              <cd-helper>
-                <span i18n>The virtual IP address and subnet (in CIDR notation) where the ingress service will be available.</span>
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="virtual_ip"
-                     class="form-control"
-                     type="text"
-                     formControlName="virtual_ip">
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('virtual_ip', frm, 'required')"
-                    i18n>This field is required.</span>
-            </div>
-          </div>
+      <!-- Ingress -->
+      @if (serviceForm.controls.service_type.value === 'ingress') {
+      <!-- virtual_ip -->
+      <div class="form-item">
+        <cds-text-label cdRequiredField="Virtual IP"
+                        helperText="The virtual IP address and subnet (in CIDR notation) where the ingress service will be available."
+                        i18n-helperText
+                        [invalid]="serviceForm.controls.virtual_ip.invalid && serviceForm.controls.virtual_ip.dirty"
+                        [invalidText]="requiredFieldVirtualIp">
+          <input cdsText
+                 type="text"
+                 id="virtual_ip"
+                 formControlName="virtual_ip"
+                 [invalid]="serviceForm.controls.virtual_ip.invalid && serviceForm.controls.virtual_ip.dirty" />
+        </cds-text-label>
+        <ng-template #requiredFieldVirtualIp>
+          @if (serviceForm.showError('virtual_ip', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+      <!-- frontend_port -->
+      <div class="form-item">
+        <cds-number cdRequiredField="Frontend port"
+                    label="Frontend port"
+                    formControlName="frontend_port"
+                    id="frontend_port"
+                    min="1"
+                    max="65535"
+                    helperText="The port used to access the ingress service."
+                    i18n-helperText
+                    [invalid]="serviceForm.controls.frontend_port.invalid && serviceForm.controls.frontend_port.dirty"
+                    [invalidText]="invalidFrontendPortError">
+        </cds-number>
+        <ng-template #invalidFrontendPortError>
+          @if (serviceForm.showError('frontend_port', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+          @if (serviceForm.showError('frontend_port', frm, 'min')) {
+          <span class="invalid-feedback"
+                i18n>The value must be at least 1.</span>
+          }
+          @if (serviceForm.showError('frontend_port', frm, 'max')) {
+          <span class="invalid-feedback"
+                i18n>The value cannot exceed 65535.</span>
+          }
+        </ng-template>
+      </div>
+      <!-- monitor_port -->
+      <div class="form-item">
+        <cds-number cdRequiredField="Monitor port"
+                    label="Monitor port"
+                    formControlName="monitor_port"
+                    id="monitor_port"
+                    min="1"
+                    max="65535"
+                    helperText="The port used by haproxy for load balancer status."
+                    i18n-helperText
+                    [invalid]="serviceForm.controls.monitor_port.invalid && serviceForm.controls.monitor_port.dirty"
+                    [invalidText]="invalidMonitorPortError">
+        </cds-number>
+        <ng-template #invalidMonitorPortError>
+          @if (serviceForm.showError('monitor_port', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+          @if (serviceForm.showError('monitor_port', frm, 'min')) {
+          <span class="invalid-feedback"
+                i18n>The value must be at least 1.</span>
+          }
+          @if (serviceForm.showError('monitor_port', frm, 'max')) {
+          <span class="invalid-feedback"
+                i18n>The value cannot exceed 65535.</span>
+          }
+        </ng-template>
+      </div>
+      <!-- virtual_interface_networks -->
+      @if (!serviceForm.controls.unmanaged.value) {
+      <div class="form-item">
+        <cd-text-label-list formControlName="virtual_interface_networks"
+                            label="CIDR Networks"
+                            helperText="A list of networks to identify which network interface to use for the virtual IP address.">
+        </cd-text-label-list>
+      </div>
+      }
+      }
 
-          <!-- frontend_port -->
-          <div class="form-group row">
-            <label class="cd-col-form-label"
-                   [ngClass]="{'required': ['ingress'].includes(serviceForm.controls.service_type.value)}"
-                   for="frontend_port">
-              <span i18n>Frontend Port</span>
-              <cd-helper>
-                <span i18n>The port used to access the ingress service.</span>
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="frontend_port"
-                     class="form-control"
-                     type="number"
-                     formControlName="frontend_port"
-                     min="1"
-                     max="65535">
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('frontend_port', frm, 'pattern')"
-                    i18n>The entered value needs to be a number.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('frontend_port', frm, 'min')"
-                    i18n>The value must be at least 1.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('frontend_port', frm, 'max')"
-                    i18n>The value cannot exceed 65535.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('frontend_port', frm, 'required')"
-                    i18n>This field is required.</span>
-            </div>
-          </div>
+      <!-- SNMP-Gateway -->
+      @if (serviceForm.controls.service_type.value === 'snmp-gateway') {
+      <!-- snmp-version -->
+      <div class="form-item">
+        <cds-select formControlName="snmp_version"
+                    id="snmp_version"
+                    label="Version"
+                    cdRequiredField="Version"
+                    helperText="Active-directory authentication for domain member servers and User authentication for Stand-alone servers configuration."
+                    i18n-helperText
+                    [invalid]="serviceForm.controls.snmp_version.invalid && serviceForm.controls.snmp_version.dirty"
+                    [invalidText]="requiredFieldSnmpVersion"
+                    (change)="clearValidations()">
+          <option i18n
+                  [ngValue]="null">-- Select SNMP version --</option>
+          @for (snmpVersion of ['V2c', 'V3']; track snmpVersion) {
+          <option [value]="snmpVersion">{{ snmpVersion }}</option>
+          }
+        </cds-select>
+        <ng-template #requiredFieldSnmpVersion>
+          @if (serviceForm.showError('snmp_version', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+      <!-- Destination -->
+      <div class="form-item">
+        <cds-text-label cdRequiredField="Destination"
+                        helperText="Must be of the format hostname:port."
+                        i18n-helperText
+                        [invalid]="serviceForm.controls.snmp_destination.invalid && serviceForm.controls.snmp_destination.dirty"
+                        [invalidText]="invalidSnmpDestinationError">
+          <input cdsText
+                 type="text"
+                 id="snmp_destination"
+                 formControlName="snmp_destination"
+                 [invalid]="serviceForm.controls.snmp_destination.invalid && serviceForm.controls.snmp_destination.dirty" />
+          <ng-template #invalidSnmpDestinationError>
+            @if (serviceForm.showError('snmp_destination', frm, 'required')) {
+            <span class="invalid-feedback"
+                  i18n>This field is required.</span>
+            }
+            @if (serviceForm.showError('snmp_destination', frm, 'snmpDestinationPattern')) {
+            <span class="invalid-feedback"
+                  i18n>The value does not match the pattern: <strong>hostname:port</strong></span>
+            }
+          </ng-template>
+        </cds-text-label>
+      </div>
+      <!-- Engine id for snmp V3 -->
+      @if (serviceForm.controls.snmp_version.value === 'V3') {
+      <div class="form-item">
+        <cds-text-label cdRequiredField="Engine Id"
+                        helperText="Unique identifier for the device (in hex)."
+                        i18n-helperText
+                        [invalid]="serviceForm.controls.engine_id.invalid && serviceForm.controls.engine_id.dirty"
+                        [invalidText]="invalidEngineIdError">
+          <input cdsText
+                 type="text"
+                 id="engine_id"
+                 formControlName="engine_id"
+                 [invalid]="serviceForm.controls.engine_id.invalid && serviceForm.controls.engine_id.dirty" />
+        </cds-text-label>
+        <ng-template #invalidEngineIdError>
+          @if (serviceForm.showError('engine_id', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+          @if (serviceForm.showError('engine_id', frm, 'snmpEngineIdPattern')) {
+          <span class="invalid-feedback"
+                i18n>The value does not match the pattern: <strong>Must be in hexadecimal and length must be multiple of
+              2
+              with min value = 10 and max value = 64.</strong></span>
+          }
+        </ng-template>
+      </div>
+      <!-- Auth protocol for snmp V3 -->
+      <div class="form-item">
+        <cds-select formControlName="auth_protocol"
+                    id="auth_protocol"
+                    label="Auth protocol"
+                    cdRequiredField="Auth protocol"
+                    [invalid]="serviceForm.controls.auth_protocol.invalid && serviceForm.controls.auth_protocol.dirty"
+                    [invalidText]="requiredFieldAuthProtocol">
+          <option i18n
+                  [ngValue]="null">-- Select auth protocol --</option>
+          @for (authProtocol of ['SHA', 'MD5']; track authProtocol) {
+          <option [value]="authProtocol">
+            {{ authProtocol }}
+          </option>
+          }
+        </cds-select>
+        <ng-template #requiredFieldAuthProtocol>
+          @if (serviceForm.showError('auth_protocol', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
 
-          <!-- monitor_port -->
-          <div class="form-group row">
-            <label class="cd-col-form-label"
-                   [ngClass]="{'required': ['ingress'].includes(serviceForm.controls.service_type.value)}"
-                   for="monitor_port">
-              <span i18n>Monitor Port</span>
-              <cd-helper>
-                <span i18n>The port used by haproxy for load balancer status.</span>
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="monitor_port"
-                     class="form-control"
-                     type="number"
-                     formControlName="monitor_port"
-                     min="1"
-                     max="65535">
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('monitor_port', frm, 'pattern')"
-                    i18n>The entered value needs to be a number.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('monitor_port', frm, 'min')"
-                    i18n>The value must be at least 1.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('monitor_port', frm, 'max')"
-                    i18n>The value cannot exceed 65535.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('monitor_port', frm, 'required')"
-                    i18n>This field is required.</span>
-            </div>
-          </div>
-          <!-- virtual_interface_networks -->
-          <div class="form-group row"
-               *ngIf="!serviceForm.controls.unmanaged.value">
-            <label class="cd-col-form-label"
-                   for="virtual_interface_networks">
-              <span i18n>CIDR Networks</span>
-              <cd-helper>
-                <span i18n>A list of networks to identify which network interface to use for the virtual IP address.</span>
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="virtual_interface_networks"
-                     class="form-control"
-                     type="text"
-                     formControlName="virtual_interface_networks">
-            </div>
-          </div>
-        </ng-container>
+      <!-- Privacy protocol for snmp V3 -->
+      <div class="form-item">
+        <cds-select formControlName="privacy_protocol"
+                    id="privacy_protocol"
+                    label="Privacy protocol">
+          <option i18n
+                  [ngValue]="null">-- Select privacy protocol --</option>
+          @for (privacyProtocol of ['DES', 'AES']; track privacyProtocol) {
+          <option [value]="privacyProtocol">
+            {{ privacyProtocol }}
+          </option>
+          }
+        </cds-select>
+      </div>
+      }
+      <!-- Credentials -->
+      @if (['V2c', 'V3'].includes(serviceForm.controls.snmp_version?.value)) {
+      <fieldset>
+        <legend i18n>Credentials</legend>
+        <!-- snmp v2c snmp_community -->
+        @if (serviceForm.controls.snmp_version.value === 'V2c') {
+        <div class="form-item">
+          <cds-text-label cdRequiredField="SNMP community"
+                          [invalid]="serviceForm.controls.snmp_community.invalid && serviceForm.controls.snmp_community.dirty"
+                          [invalidText]="invalidSnmpCommunityError">
+            <input cdsText
+                   type="text"
+                   id="snmp_community"
+                   formControlName="snmp_community"
+                   [invalid]="serviceForm.controls.snmp_community.invalid && serviceForm.controls.snmp_community.dirty" />
+          </cds-text-label>
+          <ng-template #invalidSnmpCommunityError>
+            @if (serviceForm.showError('snmp_community', frm, 'required')) {
+            <span class="invalid-feedback"
+                  i18n>This field is required.</span>
+            }
+          </ng-template>
+        </div>
+        }
+        <!-- snmp v3 auth username -->
+        @if (serviceForm.controls.snmp_version.value === 'V3') {
+        <div class="form-item">
+          <cds-text-label cdRequiredField="Username"
+                          [invalid]="serviceForm.controls.snmp_v3_auth_username.invalid && serviceForm.controls.snmp_v3_auth_username.dirty"
+                          [invalidText]="invalidSnmpV3AuthUsernameError">
+            <input cdsText
+                   type="text"
+                   id="snmp_v3_auth_username"
+                   formControlName="snmp_v3_auth_username"
+                   [invalid]="serviceForm.controls.snmp_v3_auth_username.invalid && serviceForm.controls.snmp_v3_auth_username.dirty" />
+          </cds-text-label>
+          <ng-template #invalidSnmpV3AuthUsernameError>
+            @if (serviceForm.showError('snmp_v3_auth_username', frm, 'required')) {
+            <span class="invalid-feedback"
+                  i18n>This field is required.</span>
+            }
+          </ng-template>
+        </div>
+        }
+        <!-- snmp v3 auth password -->
+        @if (serviceForm.controls.snmp_version.value === 'V3') {
+        <div class="form-item">
+          <cds-password-label cdRequiredField="Password"
+                              [invalid]="serviceForm.controls.snmp_v3_auth_password.invalid && serviceForm.controls.snmp_v3_auth_password.dirty"
+                              [invalidText]="invalidSnmpV3AuthPasswordError">
+            <input cdsPassword
+                   type="password"
+                   id="snmp_v3_auth_password"
+                   formControlName="snmp_v3_auth_password"
+                   [invalid]="serviceForm.controls.snmp_v3_auth_password.invalid && serviceForm.controls.snmp_v3_auth_password.dirty" />
+          </cds-password-label>
+          <ng-template #invalidSnmpV3AuthPasswordError>
+            @if (serviceForm.showError('snmp_v3_auth_password', frm, 'required')) {
+            <span class="invalid-feedback"
+                  i18n>This field is required.</span>
+            }
+          </ng-template>
+        </div>
+        }
+        <!-- snmp v3 priv password -->
+        @if (serviceForm.controls.snmp_version.value === 'V3' && serviceForm.controls.privacy_protocol.value !== null &&
+        serviceForm.controls.privacy_protocol.value !== undefined) {
+        <div class="form-item">
+          <cds-password-label cdRequiredField="Encryption"
+                              [invalid]="serviceForm.controls.snmp_v3_priv_password.invalid && serviceForm.controls.snmp_v3_priv_password.dirty"
+                              [invalidText]="invalidSnmpV3PrivPasswordError">
+            <input cdsPassword
+                   type="password"
+                   id="snmp_v3_priv_password"
+                   formControlName="snmp_v3_priv_password"
+                   [invalid]="serviceForm.controls.snmp_v3_priv_password.invalid && serviceForm.controls.snmp_v3_priv_password.dirty" />
+          </cds-password-label>
+          <ng-template #invalidSnmpV3PrivPasswordError>
+            @if (serviceForm.showError('snmp_v3_priv_password', frm, 'required')) {
+            <span class="invalid-feedback"
+                  i18n>This field is required.</span>
+            }
+          </ng-template>
+        </div>
+        }
+      </fieldset>
+      }
+      }
 
-        <!-- SNMP-Gateway -->
-        <ng-container *ngIf="serviceForm.controls.service_type.value === 'snmp-gateway'">
-          <!-- snmp-version -->
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="snmp_version"
-                   i18n>Version</label>
-            <div class="cd-col-form-input">
-              <select id="snmp_version"
-                      name="snmp_version"
-                      class="form-select"
-                      formControlName="snmp_version"
-                      (change)="clearValidations()">
-                <option i18n
-                        [ngValue]="null">-- Select SNMP version --</option>
-                <option *ngFor="let snmpVersion of ['V2c', 'V3']"
-                        [value]="snmpVersion">{{ snmpVersion }}</option>
-              </select>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('snmp_version', frm, 'required')"
-                    i18n>This field is required.</span>
-            </div>
-          </div>
-          <!-- Destination -->
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="snmp_destination">
-              <span i18n>Destination</span>
-              <cd-helper>
-                <span i18n>Must be of the format hostname:port.</span>
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="snmp_destination"
-                     class="form-control"
-                     type="text"
-                     formControlName="snmp_destination">
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('snmp_destination', frm, 'required')"
-                    i18n>This field is required.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('snmp_destination', frm, 'snmpDestinationPattern')"
-                    i18n>The value does not match the pattern: <strong>hostname:port</strong></span>
-            </div>
-          </div>
-          <!-- Engine id for snmp V3 -->
-          <div class="form-group row"
-               *ngIf="serviceForm.controls.snmp_version.value === 'V3'">
-            <label class="cd-col-form-label required"
-                   for="engine_id">
-              <span i18n>Engine Id</span>
-              <cd-helper>
-                <span i18n>Unique identifier for the device (in hex).</span>
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="engine_id"
-                     class="form-control"
-                     type="text"
-                     formControlName="engine_id">
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('engine_id', frm, 'required')"
-                    i18n>This field is required.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('engine_id', frm, 'snmpEngineIdPattern')"
-                    i18n>The value does not match the pattern: <strong>Must be in hexadecimal and length must be multiple of 2 with min value = 10 amd max value = 64.</strong></span>
-            </div>
-          </div>
-          <!-- Auth protocol for snmp V3 -->
-          <div class="form-group row"
-               *ngIf="serviceForm.controls.snmp_version.value === 'V3'">
-            <label class="cd-col-form-label required"
-                   for="auth_protocol"
-                   i18n>Auth Protocol</label>
-            <div class="cd-col-form-input">
-              <select id="auth_protocol"
-                      name="auth_protocol"
-                      class="form-select"
-                      formControlName="auth_protocol">
-                <option i18n
-                        [ngValue]="null">-- Select auth protocol --</option>
-                <option *ngFor="let authProtocol of ['SHA', 'MD5']"
-                        [value]="authProtocol">
-                  {{ authProtocol }}
-                </option>
-              </select>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('auth_protocol', frm, 'required')"
-                    i18n>This field is required.</span>
-            </div>
-          </div>
-          <!-- Privacy protocol for snmp V3 -->
-          <div class="form-group row"
-               *ngIf="serviceForm.controls.snmp_version.value === 'V3'">
-            <label class="cd-col-form-label"
-                   for="privacy_protocol"
-                   i18n>Privacy Protocol</label>
-            <div class="cd-col-form-input">
-              <select id="privacy_protocol"
-                      name="privacy_protocol"
-                      class="form-select"
-                      formControlName="privacy_protocol">
-                <option i18n
-                        [ngValue]="null">-- Select privacy protocol --</option>
-                <option *ngFor="let privacyProtocol of ['DES', 'AES']"
-                        [value]="privacyProtocol">
-                  {{ privacyProtocol }}
-                </option>
-              </select>
-            </div>
-          </div>
-          <!-- Credentials -->
-          <fieldset>
-            <legend i18n>Credentials</legend>
-            <!-- snmp v2c snmp_community -->
-            <div class="form-group row"
-                 *ngIf="serviceForm.controls.snmp_version.value === 'V2c'">
-              <label class="cd-col-form-label required"
-                     for="snmp_community">
-                <span i18n>SNMP Community</span>
-              </label>
-              <div class="cd-col-form-input">
-                <input id="snmp_community"
-                       class="form-control"
-                       type="text"
-                       formControlName="snmp_community">
-                <span class="invalid-feedback"
-                      *ngIf="serviceForm.showError('snmp_community', frm, 'required')"
-                      i18n>This field is required.</span>
-              </div>
-            </div>
-            <!-- snmp v3 auth username -->
-            <div class="form-group row"
-                 *ngIf="serviceForm.controls.snmp_version.value === 'V3'">
-              <label class="cd-col-form-label required"
-                     for="snmp_v3_auth_username">
-                <span i18n>Username</span>
-              </label>
-              <div class="cd-col-form-input">
-                <input id="snmp_v3_auth_username"
-                       class="form-control"
-                       type="text"
-                       formControlName="snmp_v3_auth_username">
-                <span class="invalid-feedback"
-                      *ngIf="serviceForm.showError('snmp_v3_auth_username', frm, 'required')"
-                      i18n>This field is required.</span>
-              </div>
-            </div>
-            <!-- snmp v3 auth password -->
-            <div class="form-group row"
-                 *ngIf="serviceForm.controls.snmp_version.value === 'V3'">
-              <label class="cd-col-form-label required"
-                     for="snmp_v3_auth_password">
-                <span i18n>Password</span>
-              </label>
-              <div class="cd-col-form-input">
-                <input id="snmp_v3_auth_password"
-                       class="form-control"
-                       type="password"
-                       formControlName="snmp_v3_auth_password">
-                <span class="invalid-feedback"
-                      *ngIf="serviceForm.showError('snmp_v3_auth_password', frm, 'required')"
-                      i18n>This field is required.</span>
-              </div>
-            </div>
-            <!-- snmp v3 priv password -->
-            <div class="form-group row"
-                 *ngIf="serviceForm.controls.snmp_version.value === 'V3' && serviceForm.controls.privacy_protocol.value !== null && serviceForm.controls.privacy_protocol.value !== undefined">
-              <label class="cd-col-form-label required"
-                     for="snmp_v3_priv_password">
-                <span i18n>Encryption</span>
-              </label>
-              <div class="cd-col-form-input">
-                <input id="snmp_v3_priv_password"
-                       class="form-control"
-                       type="password"
-                       formControlName="snmp_v3_priv_password">
-                <span class="invalid-feedback"
-                      *ngIf="serviceForm.showError('snmp_v3_priv_password', frm, 'required')"
-                      i18n>This field is required.</span>
-              </div>
-            </div>
-          </fieldset>
-        </ng-container>
+      <!-- oauth2-proxy -->
+      @if (serviceForm.controls.service_type.value === 'oauth2-proxy') {
+      <!-- provider_display_name -->
+      <div class="form-item">
+        <cds-text-label cdRequiredField="Provider display name"
+                        placeholder="My OIDC Provider"
+                        i18n-placeholder
+                        [invalid]="serviceForm.controls.provider_display_name.invalid && serviceForm.controls.provider_display_name.dirty"
+                        [invalidText]="invalidProviderDisplayNameError">
+          <input cdsText
+                 type="text"
+                 id="provider_display_name"
+                 formControlName="provider_display_name"
+                 [invalid]="serviceForm.controls.provider_display_name.invalid && serviceForm.controls.provider_display_name.dirty" />
+        </cds-text-label>
+        <ng-template #invalidProviderDisplayNameError>
+          @if (serviceForm.showError('provider_display_name', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+      <!-- client_id -->
+      <div class="form-item">
+        <cds-text-label cdRequiredField="Client ID"
+                        placeholder="oauth2-client"
+                        [invalid]="serviceForm.controls.client_id.invalid && serviceForm.controls.client_id.dirty"
+                        [invalidText]="invalidClientIdError">
+          <input cdsText
+                 type="text"
+                 id="client_id"
+                 formControlName="client_id"
+                 [invalid]="serviceForm.controls.client_id.invalid && serviceForm.controls.client_id.dirty" />
+        </cds-text-label>
+        <ng-template #invalidClientIdError>
+          @if (serviceForm.showError('client_id', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+      <!-- client_secret -->
+      <div class="form-item form-item-append">
+        <cds-password-label cdRequiredField="Client secret"
+                            [invalid]="serviceForm.controls.client_secret.invalid && serviceForm.controls.client_secret.dirty"
+                            [invalidText]="invalidClientSecretError">
+          <input cdsPassword
+                 type="text"
+                 id="client_secret"
+                 formControlName="client_secret"
+                 [invalid]="serviceForm.controls.client_secret.invalid && serviceForm.controls.client_secret.dirty" />
+        </cds-password-label>
+        <cd-copy-2-clipboard-button source="client_secret"
+                                    class="mt-4">
+        </cd-copy-2-clipboard-button>
+        <ng-template #invalidClientSecretError>
+          @if (serviceForm.showError('client_secret', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+      <!-- oidc_issuer_url -->
+      <div class="form-item">
+        <cds-text-label cdRequiredField="OIDC issuer URL"
+                        [invalid]="serviceForm.controls.oidc_issuer_url.invalid && serviceForm.controls.oidc_issuer_url.dirty"
+                        [invalidText]="invalidOidcIssuerUrlError">
+          <input cdsText
+                 type="text"
+                 id="oidc_issuer_url"
+                 formControlName="oidc_issuer_url"
+                 [invalid]="serviceForm.controls.oidc_issuer_url.invalid && serviceForm.controls.oidc_issuer_url.dirty" />
+        </cds-text-label>
+        <ng-template #invalidOidcIssuerUrlError>
+          @if (serviceForm.showError('oidc_issuer_url', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+          @if (serviceForm.showError('oidc_issuer_url', frm, 'validUrl')) {
+          <span class="invalid-feedback"
+                i18n>Invalid url.</span>
+          }
+        </ng-template>
+      </div>
+      <!-- https_address -->
+      <div class="form-item">
+        <cds-text-label cdRequiredField="Https address"
+                        helperText="The address for HTTPS connections as [IP|Hostname]:port."
+                        i18n-helperText
+                        placeholder="0.0.0.0:4180"
+                        [invalid]="serviceForm.controls.https_address.invalid && serviceForm.controls.https_address.dirty"
+                        [invalidText]="invalidHttpsAddressError">
+          <input cdsText
+                 type="text"
+                 id="https_address"
+                 formControlName="https_address"
+                 [invalid]="serviceForm.controls.https_address.invalid && serviceForm.controls.https_address.dirty" />
+        </cds-text-label>
+        <ng-template #invalidHttpsAddressError>
+          @if (serviceForm.showError('https_address', frm, 'invalidAddress')) {
+          <span class="invalid-feedback"
+                i18n>Format must be [IP|Hostname]:port and the port between 0 and 65535</span>
+          }
+        </ng-template>
+      </div>
+      <!-- redirect_url -->
+      <div class="form-item">
+        <cds-text-label cdRequiredField="Redirect URL"
+                        placeholder="https://<IP|Hostname>:4180/oauth2/callback"
+                        helperText="The URL the oauth2-proxy service will redirect to after a successful login."
+                        i18n-helperText
+                        [invalid]="serviceForm.controls.redirect_url.invalid && serviceForm.controls.redirect_url.dirty"
+                        [invalidText]="invalidRedirectUrlError">
+          <input cdsText
+                 type="text"
+                 id="redirect_url"
+                 formControlName="redirect_url"
+                 [invalid]="serviceForm.controls.redirect_url.invalid && serviceForm.controls.redirect_url.dirty" />
+        </cds-text-label>
+        <ng-template #invalidRedirectUrlError>
+          @if (serviceForm.showError('redirect_url', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
+      <!-- Allowlist_domains -->
+      <div class="form-item">
+        <cd-text-label-list formControlName="allowlist_domains"
+                            label="Allowlist domains"
+                            helperText="Domains to be allowed to redirect to, used for login or logout."
+                            placeholder="192.168.100.1:8080">
+        </cd-text-label-list>
+      </div>
+      }
 
-        <!-- oauth2-proxy -->
-        <ng-container *ngIf="serviceForm.controls.service_type.value === 'oauth2-proxy'">
-          <!-- provider_display_name -->
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="provider_display_name">
-              <span i18n>Provider display name</span>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="provider_display_name"
-                     class="form-control"
-                     type="text"
-                     formControlName="provider_display_name"
-                     placeholder="My OIDC Provider"
-                     i18n-placeholder>
-              <cd-help-text i18n>The display name for the identity provider (IdP) in the UI.</cd-help-text>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('provider_display_name', frm, 'required')"
-                    i18n>This field is required.</span>
-            </div>
-          </div>
-          <!-- client_id -->
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="client_id">
-              <span i18n>Client ID</span>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="client_id"
-                     class="form-control"
-                     type="text"
-                     formControlName="client_id"
-                     placeholder="oauth2-client">
-              <cd-help-text i18n>The client ID for authenticating with the IdP.</cd-help-text>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('client_id', frm, 'required')"
-                    i18n>This field is required.</span>
-            </div>
-          </div>
-          <!-- client_secret -->
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="client_secret">
-              <span i18n>Client secret</span>
-            </label>
-            <div class="cd-col-form-input">
-              <div class="input-group">
-                <input id="client_secret"
-                       class="form-control"
-                       type="password"
-                       formControlName="client_secret">
-                <span class="input-group-append">
-                  <button type="button"
-                          class="btn btn-light"
-                          cdPasswordButton="client_secret">
-                  </button>
-                  <cd-copy-2-clipboard-button source="client_secret">
-                  </cd-copy-2-clipboard-button>
-                </span>
-              </div>
-              <cd-help-text i18n>The client secret for authenticating with the IdP.</cd-help-text>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('client_secret', frm, 'required')"
-                    i18n>This field is required.</span>
-            </div>
-          </div>
-          <!-- oidc_issuer_url -->
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="oidc_issuer_url">
-              <span i18n>OIDC Issuer URL</span>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="oidc_issuer_url"
-                     class="form-control"
-                     type="text"
-                     formControlName="oidc_issuer_url"
-                     placeholder="https://<IdPs-domain>/realms/<realm-name>">
-              <cd-help-text i18n>The URL of the OpenID Connect (OIDC) issuer.</cd-help-text>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('oidc_issuer_url', frm, 'required')"
-                    i18n>This field is required.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('oidc_issuer_url', frm, 'validUrl')"
-                    i18n>Invalid url.</span>
-            </div>
-          </div>
-          <!-- https_address -->
-          <div class="form-group row">
-            <label class="cd-col-form-label"
-                   for="https_address">
-              <span i18n>Https address</span>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="https_address"
-                     class="form-control"
-                     type="text"
-                     formControlName="https_address"
-                     placeholder="0.0.0.0:4180">
-              <cd-help-text i18n>The address for HTTPS connections as [IP|Hostname]:port.</cd-help-text>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('https_address', frm, 'invalidAddress')"
-                    i18n>Format must be [IP|Hostname]:port and the port between 0 and 65535</span>
-            </div>
-          </div>
-          <!-- redirect_url -->
-          <div class="form-group row">
-            <label class="cd-col-form-label"
-                   for="redirect_url">
-              <span i18n>Redirect URL</span>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="redirect_url"
-                     class="form-control"
-                     type="text"
-                     formControlName="redirect_url"
-                     placeholder="https://<IP|Hostname>:4180/oauth2/callback">
-              <cd-help-text i18n>The URL the oauth2-proxy service will redirect to after a successful login.</cd-help-text>
-            </div>
-          </div>
-          <!-- Allowlist_domains -->
-          <div class="form-group row">
-            <label class="cd-col-form-label"
-                   for="allowlist_domains">
-              <span i18n>Allowlist domains</span>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="allowlist_domains"
-                     class="form-control"
-                     type="text"
-                     formControlName="allowlist_domains"
-                     placeholder="domain1.com,192.168.100.1:8080">
-              <cd-help-text i18n>Comma separated list of domains to be allowed to redirect to, used for login or logout.</cd-help-text>
-            </div>
-          </div>
-        </ng-container>
+      @if (!serviceForm.controls.unmanaged.value && ['mgmt-gateway'].includes(serviceForm.controls.service_type.value))
+      {
+      <!-- port -->
+      <div class="form-item">
+        <cds-number label="Port"
+                    formControlName="port"
+                    id="port"
+                    min="1"
+                    max="65535"
+                    [invalid]="serviceForm.controls.port.invalid && serviceForm.controls.port.dirty"
+                    [invalidText]="invalidPortError">
+        </cds-number>
+        <ng-template #invalidPortError>
+          @if (serviceForm.showError('port', frm, 'min')) {
+          <span class="invalid-feedback"
+                i18n>The value must be at least 1.</span>
+          }
+          @if (serviceForm.showError('port', frm, 'max')) {
+          <span class="invalid-feedback"
+                i18n>The value cannot exceed 65535.</span>
+          }
+        </ng-template>
+      </div>
+      <!-- enable_auth -->
+      <div class="form-item">
+        <fieldset>
+          <label class="cds--label"
+                 for="pools"
+                 i18n>Authentication</label>
+          <cds-checkbox i18n-label
+                        id="enable_auth"
+                        name="enable_auth"
+                        formControlName="enable_auth">
+            Enable
+            <cd-help-text i18n>
+              Allows to enable authentication through an external Identity Provider (IdP) using Single Sign-On (SSO)
+            </cd-help-text>
+          </cds-checkbox>
+        </fieldset>
+      </div>
+      <!-- ssl_protocols -->
+      <div class="form-item">
+        <cds-combo-box type="multi"
+                       label="SSL protocols"
+                       selectionFeedback="top-after-reopen"
+                       for="ssl_protocols"
+                       name="ssl_protocols"
+                       formControlName="ssl_protocols"
+                       id="ssl_protocols"
+                       placeholder="Select protocols..."
+                       [appendInline]="true"
+                       [items]="sslProtocolsItems"
+                       i18n-placeholder
+                       i18n>
+          <cds-dropdown-list></cds-dropdown-list>
+        </cds-combo-box>
+      </div>
+      <!-- ssl_ciphers -->
+      <div class="form-item">
+        <cds-text-label placeholder="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"
+                        [helperText]="cipherListLink"
+                        i18n-helperText>
+          SSL ciphers
+          <input cdsText
+                 type="text"
+                 id="ssl_ciphers"
+                 formControlName="ssl_ciphers" />
+          @if (serviceForm.showError('ssl_ciphers', frm, 'invalidPattern')) {
+          <span class="invalid-feedback"
+                i18n>Invalid cipher suite. Each cipher must be separated by '-' and each cipher suite must be separated
+            by
+            ':'</span>
+          }
+        </cds-text-label>
+      </div>
+      }
+      <!-- RGW, Ingress, iSCSI, Oauth2-proxy & mgmt-gateway -->
+      @if (!serviceForm.controls.unmanaged.value && ['rgw', 'iscsi', 'ingress', 'oauth2-proxy',
+      'mgmt-gateway'].includes(serviceForm.controls.service_type.value)) {
+      <!-- ssl -->
+      @if (!['mgmt-gateway'].includes(serviceForm.controls.service_type.value)) {
+      <div class="form-item">
+        <cds-checkbox formControlName="ssl"
+                      name="security_label"
+                      id="ssl"
+                      i18n>SSL
+        </cds-checkbox>
+      </div>
+      }
+      <!-- ssl_cert -->
+      @if (serviceForm.controls.ssl.value || ['mgmt-gateway'].includes(serviceForm.controls.service_type.value)) {
+      <div class="form-item">
+        <cds-textarea-label helperText="The SSL certificate in PEM format."
+                            i18n
+                            [invalid]="serviceForm.controls.ssl_cert.invalid && serviceForm.controls.ssl_cert.dirty"
+                            [invalidText]="invalidSslCertError">Certificate
+          <div class="cd-cl-form-input">
+            <textarea cdsTextArea
+                      id="ssl_cert"
+                      formControlName="ssl_cert"
+                      cols="100"
+                      rows="4"
+                      [invalid]="serviceForm.controls.ssl_cert.invalid && serviceForm.controls.ssl_cert.dirty">
+            </textarea>
 
-        <ng-container *ngIf="!serviceForm.controls.unmanaged.value && ['mgmt-gateway'].includes(serviceForm.controls.service_type.value)">
-          <!-- port -->
-          <div class="form-group row">
-            <label i18n
-                   class="cd-col-form-label"
-                   for="port">Port</label>
-            <div class="cd-col-form-input">
-              <input id="port"
-                     class="form-control"
-                     type="number"
-                     formControlName="port"
-                     min="1"
-                     max="65535">
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('port', frm, 'pattern')"
-                    i18n>The entered value needs to be a number.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('port', frm, 'min')"
-                    i18n>The value must be at least 1.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('port', frm, 'max')"
-                    i18n>The value cannot exceed 65535.</span>
-            </div>
+            <cds-file-uploader buttonText="Choose file"
+                               i18n-buttonText
+                               buttonType="secondary"
+                               [multiple]="false"
+                               size="sm"
+                               (filesChange)="fileUpload($event, 'ssl_cert')"
+                               (removeFile)="clearText()"></cds-file-uploader>
           </div>
-          <!-- enable_auth -->
-          <div class="form-item">
-            <fieldset>
-              <label class="cds--label"
-                     for="pools"
-                     i18n>Authentication</label>
-                <cds-checkbox i18n-label
-                              id="enable_auth"
-                              name="enable_auth"
-                              formControlName="enable_auth">
-                Enable
-                <cd-help-text i18n>
-                  Allows to enable authentication through an external Identity Provider (IdP) using Single Sign-On (SSO)
-                </cd-help-text>
-              </cds-checkbox>
-            </fieldset>
-          </div>
-          <!-- ssl_protocols -->
-          <div class="form-item">
-            <cds-combo-box type="multi"
-                           label="SSL protocols"
-                           selectionFeedback="top-after-reopen"
-                           for="ssl_protocols"
-                           name="ssl_protocols"
-                           formControlName="ssl_protocols"
-                           id="ssl_protocols"
-                           placeholder="Select protocols..."
-                           [appendInline]="true"
-                           [items]="sslProtocolsItems"
-                           i18n-placeholder
-                           i18n>
-              <cds-dropdown-list></cds-dropdown-list>
-            </cds-combo-box>
-          </div>
-          <!-- ssl_ciphers -->
-          <div class="form-group row">
-          <label class="cd-col-form-label"
-                 for="ssl_ciphers">
-            <span i18n>SSL ciphers</span>
-          </label>
+        </cds-textarea-label>
+        <ng-template #invalidSslCertError>
+          @if (serviceForm.showError('ssl_cert', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+          @if (serviceForm.showError('ssl_cert', frm, 'pattern')) {
+          <span class="invalid-feedback"
+                i18n>Invalid SSL certificate.</span>
+          }
+        </ng-template>
+      </div>
+      }
+      <!-- ssl_key -->
+      @if ((serviceForm.controls.ssl.value && !(['rgw', 'ingress'].includes(serviceForm.controls.service_type.value)))
+      || ['mgmt-gateway'].includes(serviceForm.controls.service_type.value)) {
+      <div class="form-item">
+        <cds-textarea-label helperText="The SSL certificate in PEM format."
+                            i18n
+                            [invalid]="serviceForm.controls.ssl_key.invalid && serviceForm.controls.ssl_key.dirty"
+                            [invalidText]="invalidSslKeyError">Private key
           <div class="cd-col-form-input">
-            <div class="input-group">
-              <input id="ssl_ciphers"
-                     class="form-control"
-                     type="text"
-                     formControlName="ssl_ciphers"
-                     placeholder="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256">
-            </div>
-            <cd-help-text i18n>Default cipher list used: <a href="https://ssl-config.mozilla.org/#server=nginx"
-                                                            target="_blank">https://ssl-config.mozilla.org/#server=nginx</a></cd-help-text>
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('ssl_ciphers', frm, 'invalidPattern')"
-                  i18n>Invalid cipher suite. Each cipher must be separated by '-' and each cipher suite must be separated by ':'</span>
-          </div>
-        </div>
-        </ng-container>
-        <!-- RGW, Ingress, iSCSI, Oauth2-proxy & mgmt-gateway -->
-        <ng-container *ngIf="!serviceForm.controls.unmanaged.value && ['rgw', 'iscsi', 'ingress', 'oauth2-proxy', 'mgmt-gateway'].includes(serviceForm.controls.service_type.value)">
-          <!-- ssl -->
-          <ng-container *ngIf="!['mgmt-gateway'].includes(serviceForm.controls.service_type.value)">
-            <div class="form-group row">
-              <div class="cd-col-form-offset">
-                <div class="custom-control custom-checkbox">
-                  <input class="custom-control-input"
-                         id="ssl"
-                         type="checkbox"
-                         formControlName="ssl">
-                  <label class="custom-control-label"
-                         for="ssl"
-                         i18n>SSL</label>
-                </div>
-              </div>
-            </div>
-          </ng-container>
-          <!-- ssl_cert -->
-          <div *ngIf="serviceForm.controls.ssl.value || ['mgmt-gateway'].includes(serviceForm.controls.service_type.value)"
-               class="form-group row">
-            <label class="cd-col-form-label"
-                   for="ssl_cert">
-              <span i18n>Certificate</span>
-              <cd-helper i18n>The SSL certificate in PEM format.</cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <textarea id="ssl_cert"
-                        class="form-control resize-vertical text-monospace text-pre"
-                        formControlName="ssl_cert"
-                        rows="5">
-              </textarea>
-              <input type="file"
-                     (change)="fileUpload($event.target.files, 'ssl_cert')">
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('ssl_cert', frm, 'required')"
-                    i18n>This field is required.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('ssl_cert', frm, 'pattern')"
-                    i18n>Invalid SSL certificate.</span>
-            </div>
-          </div>
+            <textarea cdsTextArea
+                      id="ssl_key"
+                      formControlName="ssl_key"
+                      cols="100"
+                      rows="4"
+                      [invalid]="serviceForm.controls.ssl_key.invalid && serviceForm.controls.ssl_key.dirty">
+            </textarea>
 
-          <!-- ssl_key -->
-          <div *ngIf="(serviceForm.controls.ssl.value && !(['rgw', 'ingress'].includes(serviceForm.controls.service_type.value))) || ['mgmt-gateway'].includes(serviceForm.controls.service_type.value)"
-               class="form-group row">
-            <label class="cd-col-form-label"
-                   for="ssl_key">
-              <span i18n>Private key</span>
-              <cd-helper i18n>The SSL private key in PEM format.</cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <textarea id="ssl_key"
-                        class="form-control resize-vertical text-monospace text-pre"
-                        formControlName="ssl_key"
-                        rows="5">
-              </textarea>
-              <input type="file"
-                     (change)="fileUpload($event.target.files,'ssl_key')">
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('ssl_key', frm, 'required')"
-                    i18n>This field is required.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('ssl_key', frm, 'pattern')"
-                    i18n>Invalid SSL private key.</span>
-            </div>
+            <cds-file-uploader buttonText="Choose file"
+                               i18n-buttonText
+                               buttonType="secondary"
+                               [multiple]="false"
+                               size="sm"
+                               (filesChange)="fileUpload($event, 'ssl_key')"
+                               (removeFile)="clearText()"></cds-file-uploader>
           </div>
-        </ng-container>
+        </cds-textarea-label>
+        <ng-template #invalidSslKeyError>
+          @if (serviceForm.showError('ssl_key', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+          @if (serviceForm.showError('ssl_key', frm, 'pattern')) {
+          <span class="invalid-feedback"
+                i18n>Invalid SSL private key.</span>
+          }
+        </ng-template>
+      </div>
+      }
 
       <!-- RGW QAT Compression -->
-        @if(serviceForm.controls.service_type.value === 'rgw') {
+      @if(serviceForm.controls.service_type.value === 'rgw') {
         <div class="form-item"
              formGroupName="qat">
           <cds-select formControlName="compression"
           </cds-select>
         </div>
         }
+      }
+      <!-- Grafana -->
+      @if (serviceForm.controls.service_type.value === 'grafana') {
+      <div class="form-item">
+        <cds-number label="Grafana port"
+                    i18n-label
+                    formControlName="grafana_port"
+                    id="grafana_port"
+                    min="1"
+                    max="65535"
+                    helperText="The default port used by grafana."
+                    i18n-helperText
+                    [invalid]="serviceForm.controls.grafana_port.invalid && serviceForm.controls.grafana_port.dirty"
+                    [invalidText]="invalidGrafanaPortError">
+        </cds-number>
+        <ng-template #invalidGrafanaPortError>
+          @if (serviceForm.showError('grafana_port', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+          @if (serviceForm.showError('grafana_port', frm, 'min')) {
+          <span class="invalid-feedback"
+                i18n>The value must be at least 1.</span>
+          }
+          @if (serviceForm.showError('grafana_port', frm, 'max')) {
+          <span class="invalid-feedback"
+                i18n>The value cannot exceed 65535.</span>
+          }
+        </ng-template>
+      </div>
+      <div class="form-item form-item-append">
+        <cds-password-label for="grafana_admin_password">
+          Grafana password
+          <input cdsPassword
+                 type="password"
+                 id="grafana_admin_password"
+                 formControlName="grafana_admin_password"
+                 [disabled]="editing ? true:null" />
+        </cds-password-label>
+        <cd-copy-2-clipboard-button source="grafana_admin_password"
+                                    class="mt-4">
+        </cd-copy-2-clipboard-button>
+      </div>
+      }
 
-        <!-- Grafana -->
-        <ng-container *ngIf="serviceForm.controls.service_type.value === 'grafana'">
-          <div class="form-group row">
-            <label class="cd-col-form-label"
-                   for="grafana_port">
-              <span i18n>Grafana Port</span>
-              <cd-helper>
-                <span i18n>The default port used by grafana.</span>
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <input id="grafana_port"
-                     class="form-control"
-                     type="number"
-                     formControlName="grafana_port"
-                     min="1"
-                     max="65535">
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('grafana_port', frm, 'pattern')"
-                    i18n>The entered value needs to be a number.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('grafana_port', frm, 'min')"
-                    i18n>The value must be at least 1.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('grafana_port', frm, 'max')"
-                    i18n>The value cannot exceed 65535.</span>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('grafana_port', frm, 'required')"
-                    i18n>This field is required.</span>
-            </div>
-          </div>
-
-          <div class="form-group row">
-            <label i18n
-                   class="cd-col-form-label"
-                   for="grafana_admin_password">
-              <span>Grafana Password</span>
-              <cd-helper>The password of the default Grafana Admin. Set once on first-run.</cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <div class="input-group">
-                <input id="grafana_admin_password"
-                       class="form-control"
-                       type="password"
-                       autocomplete="new-password"
-                       [attr.disabled]="editing ? true:null"
-                       formControlName="grafana_admin_password">
-                <span class="input-group-append">
-                  <button type="button"
-                          class="btn btn-light"
-                          cdPasswordButton="grafana_admin_password">
-                  </button>
-                  <cd-copy-2-clipboard-button source="grafana_admin_password">
-                  </cd-copy-2-clipboard-button>
-                </span>
-              </div>
-            </div>
-          </div>
-        </ng-container>
-
-      <cd-alert-panel *ngIf="serviceForm.controls.service_type.value === 'mgmt-gateway' && showMgmtGatewayMessage"
-                      type="warning"
+      @if (serviceForm.controls.service_type.value === 'mgmt-gateway' && showMgmtGatewayMessage) {
+      <cd-alert-panel type="warning"
                       spacingClass="mb-3"
                       i18n>
         Modifying the default settings could lead to a weaker security configuration
       </cd-alert-panel>
+      }
 
-        <!-- NVMe/TCP -->
-        <!-- mTLS -->
-        <div class="form-group row"
-             *ngIf="serviceForm.controls.service_type.value === 'nvmeof'">
-          <div class="cd-col-form-offset">
-            <div class="custom-control custom-checkbox">
-              <input class="custom-control-input"
-                     id="enable_mtls"
-                     type="checkbox"
-                     formControlName="enable_mtls">
-              <label class="custom-control-label"
-                     for="enable_mtls"
-                     i18n>Encryption</label>
-              <cd-help-text i18n>Enables mutual TLS (mTLS) between the client and the gateway server.</cd-help-text>
-            </div>
-          </div>
-        </div>
+      <!-- NVMe/TCP -->
+      <!-- mTLS -->
+      @if (serviceForm.controls.service_type.value === 'nvmeof') {
+      <div class="form-item">
+        <fieldset class="cds--fieldset">
+          <label class="cds--label"
+                 i18n>Encryption</label>
+          <cds-checkbox i18n-label
+                        id="enable_mtls"
+                        formControlName="enable_mtls">
+            Enable
+            <cd-help-text i18n>
+              Enables mutual TLS (mTLS) between the client and the gateway server.
+            </cd-help-text>
+          </cds-checkbox>
+        </fieldset>
+      </div>
+      @if (serviceForm.controls.enable_mtls.value) {
+      <!-- root_ca_cert -->
+      <div class="form-item">
+        <cds-textarea-label cdRequiredField="Root CA certificate"
+                            [invalid]="serviceForm.controls.root_ca_cert.invalid && serviceForm.controls.root_ca_cert.dirty"
+                            [invalidText]="invalidRootCaCertError">Root CA certificate
+          <textarea cdsTextArea
+                    id="root_ca_cert"
+                    formControlName="root_ca_cert"
+                    cols="100"
+                    rows="4"
+                    [invalid]="serviceForm.controls.root_ca_cert.invalid && serviceForm.controls.root_ca_cert.dirty">
+          </textarea>
 
-        <!-- root_ca_cert -->
-        <div *ngIf="serviceForm.controls.enable_mtls.value"
-             class="form-group row">
-          <label class="cd-col-form-label required"
-                 for="root_ca_cert">
-            <span i18n>Root CA certificate</span>
-          </label>
-          <div class="cd-col-form-input">
-            <textarea id="root_ca_cert"
-                      class="form-control resize-vertical text-monospace text-pre"
-                      formControlName="root_ca_cert"
-                      rows="5"></textarea>
-            <input type="file"
-                   (change)="fileUpload($event.target.files, 'root_ca_cert')">
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('root_ca_cert', frm, 'required')"
-                  i18n>This field is required.</span>
-          </div>
-        </div>
+          <cds-file-uploader buttonText="Choose file"
+                             i18n-buttonText
+                             buttonType="secondary"
+                             [multiple]="false"
+                             size="sm"
+                             (filesChange)="fileUpload($event, 'root_ca_cert')"
+                             (removeFile)="clearText()"></cds-file-uploader>
+        </cds-textarea-label>
+        <ng-template #invalidRootCaCertError>
+          @if (serviceForm.showError('root_ca_cert', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
 
-        <!-- client_cert -->
-        <div *ngIf="serviceForm.controls.enable_mtls.value"
-             class="form-group row">
-          <label class="cd-col-form-label required"
-                 for="client_cert">
-            <span i18n>Client CA certificate</span>
-          </label>
-          <div class="cd-col-form-input">
-            <textarea id="client_cert"
-                      class="form-control resize-vertical text-monospace text-pre"
-                      formControlName="client_cert"
-                      rows="5"></textarea>
-            <input type="file"
-                   (change)="fileUpload($event.target.files, 'client_cert')">
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('client_cert', frm, 'required')"
-                  i18n>This field is required.</span>
-          </div>
-        </div>
+      <!-- client_cert -->
+      <div class="form-item">
+        <cds-textarea-label cdRequiredField="Client CA certificate"
+                            [invalid]="serviceForm.controls.client_cert.invalid && serviceForm.controls.client_cert.dirty"
+                            [invalidText]="invalidClientCertError">Client CA certificate
+          <textarea cdsTextArea
+                    id="client_cert"
+                    formControlName="client_cert"
+                    cols="100"
+                    rows="4"
+                    [invalid]="serviceForm.controls.client_cert.invalid && serviceForm.controls.client_cert.dirty">
+          </textarea>
+          <cds-file-uploader buttonText="Choose file"
+                             i18n-buttonText
+                             buttonType="secondary"
+                             [multiple]="false"
+                             size="sm"
+                             (filesChange)="fileUpload($event, 'client_cert')"
+                             (removeFile)="clearText()"></cds-file-uploader>
+        </cds-textarea-label>
+        <ng-template #invalidClientCertError>
+          @if (serviceForm.showError('client_cert', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
 
-        <!-- client_key -->
-        <div *ngIf="serviceForm.controls.enable_mtls.value"
-             class="form-group row">
-          <label class="cd-col-form-label required"
-                 for="client_key">
-            <span i18n>Client key</span>
-          </label>
-          <div class="cd-col-form-input">
-            <textarea id="client_key"
-                      class="form-control resize-vertical text-monospace text-pre"
-                      formControlName="client_key"
-                      rows="5"></textarea>
-            <input type="file"
-                   (change)="fileUpload($event.target.files, 'client_key')">
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('client_key', frm, 'required')"
-                  i18n>This field is required.</span>
-          </div>
-        </div>
+      <!-- client_key -->
+      <div class="form-item">
+        <cds-textarea-label cdRequiredField="Client key"
+                            [invalid]="serviceForm.controls.client_key.invalid && serviceForm.controls.client_key.dirty"
+                            [invalidText]="invalidClientKeyError">Client key
+          <textarea cdsTextArea
+                    id="client_key"
+                    formControlName="client_key"
+                    cols="100"
+                    rows="4"
+                    [invalid]="serviceForm.controls.client_key.invalid && serviceForm.controls.client_key.dirty">
+          </textarea>
 
-        <!-- server_cert -->
-        <div *ngIf="serviceForm.controls.enable_mtls.value"
-             class="form-group row">
-          <label class="cd-col-form-label required"
-                 for="server_cert">
-            <span i18n>Gateway server certificate</span>
-          </label>
-          <div class="cd-col-form-input">
-            <textarea id="server_cert"
-                      class="form-control resize-vertical text-monospace text-pre"
-                      formControlName="server_cert"
-                      rows="5"></textarea>
-            <input type="file"
-                   (change)="fileUpload($event.target.files, 'server_cert')">
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('server_cert', frm, 'required')"
-                  i18n>This field is required.</span>
-          </div>
-        </div>
+          <cds-file-uploader buttonText="Choose file"
+                             i18n-buttonText
+                             buttonType="secondary"
+                             [multiple]="false"
+                             size="sm"
+                             (filesChange)="fileUpload($event, 'client_key')"
+                             (removeFile)="clearText()"></cds-file-uploader>
+        </cds-textarea-label>
+        <ng-template #invalidClientKeyError>
+          @if (serviceForm.showError('client_key', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
+      </div>
 
-        <!-- server_key -->
-        <div *ngIf="serviceForm.controls.enable_mtls.value"
-             class="form-group row">
-          <label class="cd-col-form-label required"
-                 for="server_key">
-            <span i18n>Gateway server key</span>
-          </label>
-          <div class="cd-col-form-input">
-            <textarea id="server_key"
-                      class="form-control resize-vertical text-monospace text-pre"
-                      formControlName="server_key"
-                      rows="5"></textarea>
-            <input type="file"
-                   (change)="fileUpload($event.target.files, 'server_key')">
-            <span class="invalid-feedback"
-                  *ngIf="serviceForm.showError('server_key', frm, 'required')"
-                  i18n>This field is required.</span>
-          </div>
-        </div>
+      <!-- server_cert -->
+      <div class="form-item">
+        <cds-textarea-label cdRequiredField="Gateway server certificate"
+                            [invalid]="serviceForm.controls.server_cert.invalid && serviceForm.controls.server_cert.dirty"
+                            [invalidText]="invalidServerCertError">Gateway server certificate
+          <textarea cdsTextArea
+                    id="server_cert"
+                    formControlName="server_cert"
+                    cols="100"
+                    rows="4"
+                    [invalid]="serviceForm.controls.server_cert.invalid && serviceForm.controls.server_cert.dirty">
+          </textarea>
 
+          <cds-file-uploader buttonText="Choose file"
+                             i18n-buttonText
+                             buttonType="secondary"
+                             [multiple]="false"
+                             size="sm"
+                             (filesChange)="fileUpload($event, 'server_cert')"
+                             (removeFile)="clearText()"></cds-file-uploader>
+        </cds-textarea-label>
+        <ng-template #invalidServerCertError>
+          @if (serviceForm.showError('server_cert', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
       </div>
 
-      <div class="modal-footer">
-        <div class="text-right">
-          <cd-form-button-panel (submitActionEvent)="onSubmit()"
-                                [form]="serviceForm"
-                                [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
-        </div>
+      <!-- server_key -->
+      <div class="form-item">
+        <cds-textarea-label cdRequiredField="Gateway server key"
+                            [invalid]="serviceForm.controls.server_key.invalid && serviceForm.controls.server_key.dirty"
+                            [invalidText]="invalidServerKeyError">Gateway server key
+          <textarea cdsTextArea
+                    id="server_key"
+                    formControlName="server_key"
+                    cols="100"
+                    rows="4"
+                    [invalid]="serviceForm.controls.server_key.invalid && serviceForm.controls.server_key.dirty">
+          </textarea>
+
+          <cds-file-uploader buttonText="Choose file"
+                             i18n-buttonText
+                             buttonType="secondary"
+                             [multiple]="false"
+                             size="sm"
+                             (filesChange)="fileUpload($event, 'server_key')"
+                             (removeFile)="clearText()"></cds-file-uploader>
+        </cds-textarea-label>
+        <ng-template #invalidServerKeyError>
+          @if (serviceForm.showError('server_key', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+        </ng-template>
       </div>
+      }
+      }
     </form>
-  </ng-container>
-</cd-modal>
+  </section>
+  <cd-form-button-panel (submitActionEvent)="onSubmit()"
+                        [form]="serviceForm"
+                        [submitText]="(action | titlecase) + ' ' + (resource)"
+                        [modalForm]="true"></cd-form-button-panel>
+</cds-modal>
+
+<ng-template #cipherListLink>
+  <span>
+    Default cipher list used: <a href="https://ssl-config.mozilla.org/#server=nginx"
+                                 target="_blank">https://ssl-config.mozilla.org/#server=nginx</a>
+  </span>
+</ng-template>
index 33fdca70b6d1461006da6394ade33f60df3e1d4a..03628af822622f9444c67629dc8e631a8b8f74cc 100644 (file)
@@ -17,7 +17,13 @@ import { configureTestBed, FormHelper, Mocks } from '~/testing/unit-test-helper'
 import { ServiceFormComponent } from './service-form.component';
 import { PoolService } from '~/app/shared/api/pool.service';
 import { USER } from '~/app/shared/constants/app.constants';
-import { SelectModule } from 'carbon-components-angular';
+import {
+  CheckboxModule,
+  InputModule,
+  ModalModule,
+  NumberModule,
+  SelectModule
+} from 'carbon-components-angular';
 
 // for 'nvmeof' service
 const mockPools = [
@@ -47,8 +53,12 @@ describe('ServiceFormComponent', () => {
       ReactiveFormsModule,
       RouterTestingModule,
       SharedModule,
+      ToastrModule.forRoot(),
+      InputModule,
       SelectModule,
-      ToastrModule.forRoot()
+      NumberModule,
+      ModalModule,
+      CheckboxModule
     ]
   });
 
@@ -74,7 +84,11 @@ describe('ServiceFormComponent', () => {
     it('should test placement (host)', () => {
       formHelper.setValue('service_type', 'crash');
       formHelper.setValue('placement', 'hosts');
-      formHelper.setValue('hosts', ['mgr0', 'mon0', 'osd0']);
+      formHelper.setValue('hosts', [
+        { content: 'mgr0', selected: true },
+        { content: 'mon0', selected: true },
+        { content: 'osd0', selected: true }
+      ]);
       formHelper.setValue('count', 2);
       component.onSubmit();
       expect(cephServiceService.create).toHaveBeenCalledWith({
@@ -90,12 +104,12 @@ describe('ServiceFormComponent', () => {
     it('should test placement (label)', () => {
       formHelper.setValue('service_type', 'mgr');
       formHelper.setValue('placement', 'label');
-      formHelper.setValue('label', 'foo');
+      formHelper.setValue('label', [{ content: 'foo', selected: true }]);
       component.onSubmit();
       expect(cephServiceService.create).toHaveBeenCalledWith({
         service_type: 'mgr',
         placement: {
-          label: 'foo'
+          label: ['foo']
         },
         unmanaged: false
       });
@@ -352,7 +366,7 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
 
       it('should submit iscsi with trusted ips', () => {
         formHelper.setValue('ssl', true);
-        formHelper.setValue('trusted_ip_list', ' 172.16.0.5, 192.1.1.10  ');
+        formHelper.setValue('trusted_ip_list', [' 172.16.0.5', '192.1.1.10  ']);
         component.onSubmit();
         expect(cephServiceService.create).toHaveBeenCalledWith({
           service_type: 'iscsi',
@@ -364,7 +378,7 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
           api_secure: true,
           ssl_cert: '',
           ssl_key: '',
-          trusted_ip_list: '172.16.0.5, 192.1.1.10'
+          trusted_ip_list: ['172.16.0.5', '192.1.1.10']
         });
       });
 
@@ -397,13 +411,13 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
 
       it('should submit invalid iscsi port (1)', () => {
         formHelper.setValue('api_port', 0);
-        fixture.detectChanges();
+        component.onSubmit();
         formHelper.expectError('api_port', 'min');
       });
 
       it('should submit invalid iscsi port (2)', () => {
         formHelper.setValue('api_port', 65536);
-        fixture.detectChanges();
+        component.onSubmit();
         formHelper.expectError('api_port', 'max');
       });
 
@@ -459,7 +473,7 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
         expect(component.serviceForm.get('pool')?.value).toBe('rbd');
         const poolInput = fixture.debugElement.query(By.css('#pool')).nativeElement;
         // Simulate input value change
-        poolInput.value = 'pool-2';
+        form.get('pool').setValue('pool-2');
         // Trigger the input event
         poolInput.dispatchEvent(new Event('input'));
         // Trigger the change event
@@ -546,6 +560,7 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
         formHelper.setValue('service_id', 'foo');
         formHelper.setValue('cluster_id', 'cluster_foo');
         formHelper.setValue('config_uri', 'rados://.smb/foo/scc.toml');
+        formHelper.setValue('custom_dns', [' 192.168.76.204', '192.168.76.205 ']);
       });
 
       it('should submit smb', () => {
@@ -557,9 +572,7 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
           service_id: 'foo',
           cluster_id: 'cluster_foo',
           config_uri: 'rados://.smb/foo/scc.toml',
-          custom_dns: null,
-          join_sources: undefined,
-          user_sources: undefined
+          custom_dns: ['192.168.76.204', '192.168.76.205']
         });
       });
     });
@@ -581,7 +594,6 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
           backend_service: 'rgw.foo',
           service_id: 'rgw.foo',
           virtual_ip: '192.168.20.1/24',
-          virtual_interface_networks: null,
           ssl: false
         });
       });
@@ -596,14 +608,14 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
         // min value
         formHelper.setValue('frontend_port', 1);
         formHelper.setValue('monitor_port', 1);
-        fixture.detectChanges();
+        component.onSubmit();
         formHelper.expectValid('frontend_port');
         formHelper.expectValid('monitor_port');
 
         // max value
         formHelper.setValue('frontend_port', 65535);
         formHelper.setValue('monitor_port', 65535);
-        fixture.detectChanges();
+        component.onSubmit();
         formHelper.expectValid('frontend_port');
         formHelper.expectValid('monitor_port');
       });
@@ -612,14 +624,14 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
         // min
         formHelper.setValue('frontend_port', 0);
         formHelper.setValue('monitor_port', 0);
-        fixture.detectChanges();
+        component.onSubmit();
         formHelper.expectError('frontend_port', 'min');
         formHelper.expectError('monitor_port', 'min');
 
         // max
         formHelper.setValue('frontend_port', 65536);
         formHelper.setValue('monitor_port', 65536);
-        fixture.detectChanges();
+        component.onSubmit();
         formHelper.expectError('frontend_port', 'max');
         formHelper.expectError('monitor_port', 'max');
 
@@ -633,7 +645,7 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
 
       it('should not show private key field with ssl enabled', () => {
         formHelper.setValue('ssl', true);
-        fixture.detectChanges();
+        component.onSubmit();
         const ssl_key = fixture.debugElement.query(By.css('#ssl_key'));
         expect(ssl_key).toBeNull();
       });
@@ -651,7 +663,7 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
 -----END RSA PRIVATE KEY-----`;
         formHelper.setValue('ssl', true);
         formHelper.setValue('ssl_cert', pemCert);
-        fixture.detectChanges();
+        component.onSubmit();
         formHelper.expectValid('ssl_cert');
       });
     });
@@ -759,8 +771,8 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
         component.ngOnInit();
         expect(cephServiceSpy).toBeCalledTimes(2);
         expect(component.action).toBe('Edit');
-        const serviceType = fixture.debugElement.query(By.css('#service_type')).nativeElement;
-        const serviceId = fixture.debugElement.query(By.css('#service_id')).nativeElement;
+        const serviceType = fixture.componentInstance.serviceForm.get('service_type');
+        const serviceId = fixture.componentInstance.serviceForm.get('service_id');
         expect(serviceType.disabled).toBeTruthy();
         expect(serviceId.disabled).toBeTruthy();
       });
@@ -770,7 +782,7 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
         formHelper.setValue('service_type', 'nvmeof');
         component.ngOnInit();
         fixture.detectChanges();
-        const poolId = fixture.debugElement.query(By.css('#pool')).nativeElement;
+        const poolId = fixture.componentInstance.serviceForm.get('pool');
         expect(poolId.disabled).toBeTruthy();
       });
 
index ba3d476a432bdd2b614c18fef641e09c4b2c1911..4943ab6651cf0b6f51bb7d4a25dbc87300920afc 100644 (file)
@@ -1,13 +1,14 @@
+import { Location } from '@angular/common';
 import { HttpParams } from '@angular/common/http';
 import { Component, Input, OnInit, ViewChild } from '@angular/core';
 import { AbstractControl, UntypedFormControl, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 
-import { NgbActiveModal, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
+import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
 import { ListItem } from 'carbon-components-angular';
 import _ from 'lodash';
-import { forkJoin, merge, Observable, Subject, Subscription } from 'rxjs';
-import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
+import { forkJoin, Observable, Subject, Subscription } from 'rxjs';
+import { map } from 'rxjs/operators';
 import { Pool } from '~/app/ceph/pool/pool';
 import { CreateRgwServiceEntitiesComponent } from '~/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component';
 import { RgwRealm, RgwZonegroup, RgwZone, RgwEntities } from '~/app/ceph/rgw/models/rgw-multisite';
@@ -20,8 +21,6 @@ import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
 import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
 import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
 import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
-import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
-import { SelectOption } from '~/app/shared/components/select/select-option.model';
 import {
   ActionLabelsI18n,
   TimerServiceInterval,
@@ -73,7 +72,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
   resource: string;
   serviceTypes: string[] = [];
   serviceIds: string[] = [];
-  hosts: any;
+  selectedLabels: string[] = [];
+  selectedHosts: string[] = [];
   labels: string[];
   labelClick = new Subject<string>();
   labelFocus = new Subject<string>();
@@ -112,6 +112,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
     { value: QatOptions.sw, label: 'Software' },
     { value: QatOptions.none, label: 'None' }
   ];
+  open: boolean = false;
+  hostsAndLabels$: Observable<{ hosts: { content: string }[]; labels: { content: string }[] }>;
 
   constructor(
     public actionLabels: ActionLabelsI18n,
@@ -129,18 +131,11 @@ export class ServiceFormComponent extends CdForm implements OnInit {
     public rgwZoneService: RgwZoneService,
     public rgwMultisiteService: RgwMultisiteService,
     private route: ActivatedRoute,
-    public activeModal: NgbActiveModal,
-    public modalService: ModalCdsService
+    public modalService: ModalCdsService,
+    private location: Location
   ) {
     super();
     this.resource = $localize`service`;
-    this.hosts = {
-      options: [],
-      messages: new SelectMessages({
-        empty: $localize`There are no hosts.`,
-        filter: $localize`Filter hosts`
-      })
-    };
     this.createForm();
   }
 
@@ -202,7 +197,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         ]
       ],
       hosts: [[]],
-      count: [null, [CdValidators.number(false)]],
+      count: [null, [CdValidators.number(false), Validators.min(1)]],
       unmanaged: [false],
       // iSCSI
       // NVMe/TCP
@@ -285,7 +280,10 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         ]
       ],
       // RGW
-      rgw_frontend_port: [null, [CdValidators.number(false)]],
+      rgw_frontend_port: [
+        null,
+        [CdValidators.number(false), Validators.min(1), Validators.max(65535)]
+      ],
       realm_name: [null],
       zonegroup_name: [null],
       zone_name: [null],
@@ -294,7 +292,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
       }),
       // iSCSI
       trusted_ip_list: [null],
-      api_port: [null, [CdValidators.number(false)]],
+      api_port: [null, [CdValidators.number(false), Validators.min(1), Validators.max(65535)]],
       api_user: [
         null,
         [
@@ -374,7 +372,9 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           CdValidators.number(false),
           CdValidators.requiredIf({
             service_type: 'ingress'
-          })
+          }),
+          Validators.min(1),
+          Validators.max(65535)
         ]
       ],
       monitor_port: [
@@ -383,7 +383,9 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           CdValidators.number(false),
           CdValidators.requiredIf({
             service_type: 'ingress'
-          })
+          }),
+          Validators.min(1),
+          Validators.max(65535)
         ]
       ],
       virtual_interface_networks: [null],
@@ -507,7 +509,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         null,
         [
           CdValidators.requiredIf({
-            service_type: 'snmp-gateway'
+            snmp_version: 'V3'
           }),
           CdValidators.custom('snmpEngineIdPattern', (value: string) => {
             if (_.isEmpty(value)) {
@@ -521,7 +523,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         'SHA',
         [
           CdValidators.requiredIf({
-            service_type: 'snmp-gateway'
+            snmp_version: 'V3'
           })
         ]
       ],
@@ -538,7 +540,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         null,
         [
           CdValidators.requiredIf({
-            service_type: 'snmp-gateway'
+            snmp_version: 'V3'
           })
         ]
       ],
@@ -546,7 +548,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         null,
         [
           CdValidators.requiredIf({
-            service_type: 'snmp-gateway'
+            snmp_version: 'V3'
           })
         ]
       ],
@@ -625,6 +627,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
   }
 
   ngOnInit(): void {
+    this.open = true;
     this.action = this.actionLabels.CREATE;
     this.resolveRoute();
 
@@ -646,19 +649,15 @@ export class ServiceFormComponent extends CdForm implements OnInit {
 
       this.serviceTypes = _.difference(resp, this.hiddenServices).sort();
     });
-    this.hostService.getAllHosts().subscribe((resp: Host[]) => {
-      const options: SelectOption[] = [];
-      _.forEach(resp, (host: Host) => {
-        if (_.get(host, 'sources.orchestrator', false)) {
-          const option = new SelectOption(false, _.get(host, 'hostname'), '');
-          options.push(option);
-        }
-      });
-      this.hosts.options = [...options];
-    });
-    this.hostService.getLabels().subscribe((resp: string[]) => {
-      this.labels = resp;
-    });
+    this.hostsAndLabels$ = forkJoin({
+      hosts: this.hostService.getAllHosts(),
+      labels: this.hostService.getLabels()
+    }).pipe(
+      map(({ hosts, labels }) => ({
+        hosts: hosts.map((host: Host) => ({ content: host['hostname'] })),
+        labels: labels.map((label: string) => ({ content: label }))
+      }))
+    );
     this.poolService.getList().subscribe((resp: Pool[]) => {
       this.pools = resp;
       this.rbdPools = this.pools.filter(this.rbdService.isRBDPool);
@@ -1123,31 +1122,23 @@ export class ServiceFormComponent extends CdForm implements OnInit {
     }
   }
 
-  searchLabels = (text$: Observable<string>) => {
-    return merge(
-      text$.pipe(debounceTime(200), distinctUntilChanged()),
-      this.labelFocus,
-      this.labelClick.pipe(filter(() => !this.typeahead.isPopupOpen()))
-    ).pipe(
-      map((value) =>
-        this.labels
-          .filter((label: string) => label.toLowerCase().indexOf(value.toLowerCase()) > -1)
-          .slice(0, 10)
-      )
-    );
-  };
+  fileUpload(event: Set<Object>, controlName: string) {
+    const file: File = event?.values()?.next()?.value?.file;
+    const control: AbstractControl = this.serviceForm.get(controlName);
 
-  fileUpload(files: FileList, controlName: string) {
-    const file: File = files[0];
-    const reader = new FileReader();
-    reader.addEventListener('load', (event: ProgressEvent<FileReader>) => {
-      const control: AbstractControl = this.serviceForm.get(controlName);
-      control.setValue(event.target.result);
-      control.markAsDirty();
-      control.markAsTouched();
-      control.updateValueAndValidity();
-    });
-    reader.readAsText(file, 'utf8');
+    if (file) {
+      const reader = new FileReader();
+      reader.addEventListener('load', (event: ProgressEvent<FileReader>) => {
+        control.setValue(event.target.result);
+        control.markAsDirty();
+        control.markAsTouched();
+        control.updateValueAndValidity();
+      });
+      reader.readAsText(file, 'utf8');
+    } else {
+      // Clicking the "X" on the uploaded file emits an empty event
+      control.setValue('');
+    }
   }
 
   prePopulateId() {
@@ -1230,10 +1221,26 @@ export class ServiceFormComponent extends CdForm implements OnInit {
             (serviceSpec['features'] = serviceSpec['features'] || []).push(feature);
           }
         }
-        serviceSpec['custom_dns'] = values['custom_dns'];
-        serviceSpec['join_sources'] = values['join_sources']?.trim();
-        serviceSpec['user_sources'] = values['user_sources']?.trim();
-        serviceSpec['include_ceph_users'] = values['include_ceph_users']?.trim();
+        serviceSpec['custom_dns'] = values['custom_dns']?.map((customDns: string) => {
+          return customDns.trim();
+        });
+        if (values['join_sources']) {
+          serviceSpec['join_sources'] = values['join_sources']?.map((joinSource: string) => {
+            return joinSource.trim();
+          });
+        }
+        if (values['user_sources']) {
+          serviceSpec['user_sources'] = values['user_sources']?.map((userSource: string) => {
+            return userSource.trim();
+          });
+        }
+        if (values['include_ceph_users']) {
+          serviceSpec['include_ceph_users'] = values['include_ceph_users']?.map(
+            (includeCephUser: string) => {
+              return includeCephUser.trim();
+            }
+          );
+        }
         break;
 
       case 'snmp-gateway':
@@ -1259,11 +1266,15 @@ export class ServiceFormComponent extends CdForm implements OnInit {
       switch (values['placement']) {
         case 'hosts':
           if (values['hosts'].length > 0) {
-            serviceSpec['placement']['hosts'] = values['hosts'];
+            serviceSpec['placement']['hosts'] = values['hosts']
+              .filter((host: { content: string; selected: boolean }) => host.selected)
+              .map((host: { content: string }) => host.content);
           }
           break;
         case 'label':
-          serviceSpec['placement']['label'] = values['label'];
+          serviceSpec['placement']['label'] = values['label']
+            .filter((label: { content: string; selected: boolean }) => label.selected)
+            .map((label: { content: string }) => label.content);
           break;
       }
       if (_.isNumber(values['count']) && values['count'] > 0) {
@@ -1280,8 +1291,10 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           }
           break;
         case 'iscsi':
-          if (_.isString(values['trusted_ip_list']) && !_.isEmpty(values['trusted_ip_list'])) {
-            serviceSpec['trusted_ip_list'] = values['trusted_ip_list'].trim();
+          if (values['trusted_ip_list']) {
+            serviceSpec['trusted_ip_list'] = values['trusted_ip_list']?.map((trustedIp: string) => {
+              return trustedIp.trim();
+            });
           }
           if (_.isNumber(values['api_port']) && values['api_port'] > 0) {
             serviceSpec['api_port'] = values['api_port'];
@@ -1300,7 +1313,13 @@ export class ServiceFormComponent extends CdForm implements OnInit {
             serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
             serviceSpec['ssl_key'] = values['ssl_key']?.trim();
           }
-          serviceSpec['virtual_interface_networks'] = values['virtual_interface_networks'];
+          if (values['virtual_interface_networks']) {
+            serviceSpec['virtual_interface_networks'] = values['virtual_interface_networks']
+              ?.split(',')
+              .map((virtualInterfaceNetwork: string) => {
+                return virtualInterfaceNetwork.trim();
+              });
+          }
           break;
         case 'mgmt-gateway':
           serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
@@ -1330,11 +1349,13 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           serviceSpec['oidc_issuer_url'] = values['oidc_issuer_url']?.trim();
           serviceSpec['https_address'] = values['https_address']?.trim();
           serviceSpec['redirect_url'] = values['redirect_url']?.trim();
-          serviceSpec['allowlist_domains'] = values['allowlist_domains']
-            ?.split(',')
-            .map((domain: string) => {
-              return domain.trim();
-            });
+          if (values['allowlist_domains']) {
+            serviceSpec['allowlist_domains'] = values['allowlist_domains']?.map(
+              (allowlistDomain: string) => {
+                return allowlistDomain.trim();
+              }
+            );
+          }
           if (values['ssl']) {
             serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
             serviceSpec['ssl_key'] = values['ssl_key']?.trim();
@@ -1356,9 +1377,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           self.serviceForm.setErrors({ cdSubmitButton: true });
         },
         complete: () => {
-          this.pageURL === 'services'
-            ? this.router.navigate([this.pageURL, { outlets: { modal: null } }])
-            : this.activeModal.close();
+          this.closeModal();
         }
       });
   }
@@ -1387,4 +1406,27 @@ export class ServiceFormComponent extends CdForm implements OnInit {
       this.setRgwFields(item.realm_name, item.zonegroup_name, item.zone_name);
     });
   }
+
+  multiSelector(event: any, field: 'label' | 'hosts') {
+    if (field === 'hosts') this.selectedHosts = event.map((host: any) => host.content);
+    else this.selectedLabels = event.map((label: any) => label.content);
+  }
+
+  get isPrefixedNamedService(): boolean {
+    return (
+      this.serviceForm.controls.service_type?.value &&
+      ['mds', 'rgw', 'nfs', 'iscsi', 'nvmeof', 'smb', 'ingress'].includes(
+        this.serviceForm.controls.service_type?.value
+      )
+    );
+  }
+
+  closeModal(): void {
+    if (this.pageURL === 'services') {
+      this.location.back();
+    } else {
+      this.open = false;
+      this.modalService.dismissAll();
+    }
+  }
 }
index 28476a41485235a84898aa8b74a50d9809bc1b0d..7a5e8ee54b75d0d1cff67e3469e65b9931fa4294 100644 (file)
@@ -1,7 +1,6 @@
 import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
 import { Router } from '@angular/router';
 
-import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
 import { delay } from 'rxjs/operators';
 
 import { CephServiceService } from '~/app/shared/api/ceph-service.service';
@@ -22,7 +21,6 @@ import { Permissions } from '~/app/shared/models/permissions';
 import { CephServiceSpec } from '~/app/shared/models/service.interface';
 import { RelativeDatePipe } from '~/app/shared/pipes/relative-date.pipe';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
-import { ModalService } from '~/app/shared/services/modal.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { URLBuilderService } from '~/app/shared/services/url-builder.service';
 import { PlacementPipe } from './placement.pipe';
@@ -63,7 +61,6 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
   tableActions: CdTableAction[];
   showDocPanel = false;
   count = 0;
-  bsModalRef: NgbModalRef;
 
   orchStatus: OrchestratorStatus;
   actionOrchFeatures = {
@@ -83,7 +80,6 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
   constructor(
     private actionLabels: ActionLabelsI18n,
     private authStorageService: AuthStorageService,
-    private modalService: ModalService,
     private orchService: OrchestratorService,
     private cephServiceService: CephServiceService,
     private relativeDatePipe: RelativeDatePipe,
@@ -149,7 +145,8 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
             hiddenServices: this.hiddenServices,
             editing: edit
           });
-      this.bsModalRef = this.modalService.show(ServiceFormComponent, initialState, { size: 'lg' });
+      let modalRef = this.cdsModalService.show(ServiceFormComponent);
+      Object.assign(modalRef, initialState);
     }
   }
 
index 4cf707434ff31dfd4785106c596381bd8e620866..362e0e260bf899844b098fc834fcd5bd6c39469f 100644 (file)
@@ -669,7 +669,8 @@ export class CdValidators {
   }
 
   static oauthAddressTest(): ValidatorFn {
-    const OAUTH2_HTTPS_ADDRESS_PATTERN = /^((\d{1,3}\.){3}\d{1,3}|([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+)/;
+    // Pattern matches: IPv4 addresses or hostnames (with or without dots, like 'localhost')
+    const OAUTH2_HTTPS_ADDRESS_PATTERN = /^((\d{1,3}\.){3}\d{1,3}|([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+)$/;
     return (control: AbstractControl): Record<string, boolean> | null => {
       if (!control.value) {
         return null;
@@ -681,7 +682,7 @@ export class CdValidators {
       const [address, port] = control.value.split(':');
       const addressTest = OAUTH2_HTTPS_ADDRESS_PATTERN.test(address);
       const portTest = Number(port) >= 0 && Number(port) <= 65535;
-      return { invalidAddress: !(addressTest && portTest) };
+      return addressTest && portTest ? null : { invalidAddress: true };
     };
   }