]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Carbonize multisite sync policy forms 66302/head
authorNaman Munet <naman.munet@ibm.com>
Fri, 31 Oct 2025 17:38:49 +0000 (23:08 +0530)
committerNaman Munet <naman.munet@ibm.com>
Tue, 18 Nov 2025 03:26:38 +0000 (08:56 +0530)
Fixes: https://tracker.ceph.com/issues/73164
Signed-off-by: Naman Munet <naman.munet@ibm.com>
(cherry picked from commit 495f9b74965ea255776b9f4605f7a2df0016bcfd)

 Conflicts:
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.spec.ts

12 files changed:
src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/multisite.po.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.ts

index 5240c8364244a3be407abe2528d20598906b752d..1eefd77f7c7242d07b5c16d7aa0a7cfe2a6b47fe 100644 (file)
@@ -38,14 +38,15 @@ export class MultisitePageHelper extends PageHelper {
   @PageHelper.restrictTo(pages.create.url)
   create(group_id: string, status: string, bucket_name: string) {
     // Enter in group_id
-    cy.get('#group_id').type(group_id);
+    cy.get('#group_id').type(group_id, { force: true });
     // Show Status
     this.selectOption('status', status);
     cy.get('#status').should('have.class', 'ng-valid');
     // Enter the bucket_name
     cy.get('#bucket_name').type(bucket_name);
+    cy.get('#bucket_name').should('have.class', 'ng-valid');
     // Click the create button and wait for policy to be made
-    cy.contains('button', 'Create Sync Policy Group').wait(WAIT_TIMER).click();
+    cy.contains('cd-submit-button button', 'Create').click();
     this.getFirstTableCell(group_id).should('exist');
   }
 
@@ -55,7 +56,7 @@ export class MultisitePageHelper extends PageHelper {
 
     // Change the status field
     this.selectOption('status', status);
-    cy.contains('button', 'Edit Sync Policy Group').click();
+    cy.contains('cd-submit-button button', 'Edit').click();
 
     this.searchTable(group_id);
     cy.get(`[cdstabledata]:nth-child(${this.columnIndex.status})`)
index 5f13a0aedb1f9a1d7936fb4f9336e2d2398acd05..4a18e14b5f8d90399724ad04d9b80b01a516e620 100644 (file)
-<cd-modal [modalRef]="activeModal">
-  <ng-container i18n="form title"
-                class="modal-title">{{ action | titlecase }} {{ groupType | upperFirst }} Flow</ng-container>
-
-  <ng-container class="modal-content">
-    <form name="flowForm"
-          #frm="ngForm"
-          [formGroup]="currentFormGroupContext"
-          novalidate>
-      <div class="modal-body">
-        <div class="form-group row">
-          <label class="cd-col-form-label required"
-                 for="flow_id"
-                 i18n>Name</label>
-          <div class="cd-col-form-input">
-            <input class="form-control"
-                   type="text"
-                   placeholder="Flow Name..."
-                   id="flow_id"
-                   name="flow_id"
-                   formControlName="flow_id"/>
-          </div>
-        </div>
-        <div class="form-group row">
-          <label class="cd-col-form-label"
-                 for="bucket"
-                 i18n>Bucket Name</label>
-          <div class="cd-col-form-input">
-            <input id="bucket"
-                   name="bucket"
-                   class="form-control"
-                   type="text"
-                   i18n-placeholder
-                   placeholder="Bucket Name..."
-                   formControlName="bucket_name"/>
-            <span class="invalid-feedback"
-                  *ngIf="currentFormGroupContext.showError('bucket_name', frm, 'bucketNameNotAllowed')"
-                  i18n>The bucket with chosen name does not exist.</span>
-          </div>
-        </div>
-        <ng-container *ngIf="groupType == flowType.symmetrical; else directionalFlow">
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="zones">
-              <ng-container i18n>Zones</ng-container>
-              <cd-helper>
-                <span i18n>Flow need to be associated with atleast one zone</span>
-              </cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <ng-container *ngTemplateOutlet="zoneMultiSelect;context: { name: 'zones', zone: zones }"></ng-container>
-            </div>
-          </div>
-        </ng-container>
-        <ng-template #directionalFlow>
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="source_zone"
-                   i18n>Source Zone
-            </label>
-            <div class="cd-col-form-input">
-              <ng-container *ngTemplateOutlet="sourceAndDestZone;context: { name: 'source_zone', zones: zones }"></ng-container>
-            </div>
-          </div>
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="destination_zone"
-                   i18n>Destination Zone</label>
-            <div class="cd-col-form-input">
-              <ng-container *ngTemplateOutlet="sourceAndDestZone;context: { name: 'destination_zone', zones: zones }"></ng-container>
-            </div>
-          </div>
+<cds-modal size="sm"
+           [open]="open">
+  <cds-modal-header (closeSelect)="closeModal()">
+    <h3 cdsModalHeaderHeading
+        i18n>{{ action | titlecase }} {{ groupType }} flow</h3>
+    <cd-help-text [formAllFieldsRequired]="true"></cd-help-text>
+  </cds-modal-header>
+  <form name="flowForm"
+        #frm="ngForm"
+        [formGroup]="currentFormGroupContext"
+        novalidate>
+    <div cdsModalContent>
+      <!-- Flow Name-->
+      <div class="form-item">
+        <cds-text-label for="flow_id"
+                        [invalid]="!currentFormGroupContext.controls.flow_id.valid && currentFormGroupContext.controls.flow_id.dirty"
+                        [invalidText]="flowIdError">
+          <ng-container i18n>Name</ng-container>
+          <input cdsText
+                 type="text"
+                 id="flow_id"
+                 formControlName="flow_id"
+                 autofocus
+                 [invalid]="!currentFormGroupContext.controls.flow_id.valid && currentFormGroupContext.controls.flow_id.dirty">
+        </cds-text-label>
+        <ng-template #flowIdError>
+        @if (currentFormGroupContext.showError('flow_id', formDir, 'required')) {
+          <span class="invalid-feedback">
+            <ng-container i18n> This field is required. </ng-container>
+          </span>
+        }
         </ng-template>
       </div>
-      <div class="modal-footer">
-        <cd-form-button-panel (submitActionEvent)="submit()"
-                              [form]="currentFormGroupContext"
-                              [submitText]="(action | titlecase) + ' ' + (groupType | upperFirst) + ' ' + 'Flow'"></cd-form-button-panel>
+      <!-- Bucket Name-->
+      @if (currentFormGroupContext.controls.bucket_name.value) {
+      <div class="form-item">
+        <cds-text-label for="bucket_name"
+                        cdOptionalField="Bucket name">
+          <ng-container i18n>Bucket name</ng-container>
+          <input cdsText
+                 type="text"
+                 id="bucket_name"
+                 formControlName="bucket_name"
+                 [readOnly]="true">
+        </cds-text-label>
       </div>
-    </form>
-  </ng-container>
-</cd-modal>
-
-<ng-template #zoneMultiSelect
-             let-name="name"
-             let-zone="zone">
-  <cd-select-badges [id]="name"
-                    [name]="name"
-                    [customBadges]="zone.customBadges"
-                    [customBadgeValidators]="zone.data.validators"
-                    [messages]="zone.data.messages"
-                    [data]="zone.data.selected"
-                    [options]="zone.data.available"
-                    (selection)="zoneSelection()">
-  </cd-select-badges>
-  <i *ngIf="zone.data.selected.length <= 0"
-     i18n-title
-     title="Flow should be associated with {{name?.split('_').join(' ')}}"
-     class="{{ icons.warning }} icon-warning-color">
-  </i>
-  <span class="invalid-feedback"
-        *ngIf="currentFormGroupContext.showError(name, frm, 'required')"
-        i18n>{{name?.split('_').join(' ')}} selection is required!
-  </span>
-</ng-template>
+      }
+      <!-- Symmetrical flow zones -->
+      @if (groupType == flowType.symmetrical) {
+        <div class="form-item">
+          <cds-combo-box label="Zones"
+                         type="multi"
+                         selectionFeedback="top-after-reopen"
+                         for="zones"
+                         formControlName="zones"
+                         [helperText]="'Flow need to be associated with atleast one zone'"
+                         i18n-helperText
+                         [appendInline]="true"
+                         [items]="zones"
+                         itemValueKey="content"
+                         id="zones"
+                         cdDynamicInputCombobox
+                         [invalid]="currentFormGroupContext.controls?.zones?.invalid && currentFormGroupContext.controls?.zones?.dirty"
+                         [invalidText]="'Zone selection is required!'"
+                         i18n>
+            <cds-dropdown-list></cds-dropdown-list>
+          </cds-combo-box>
+        </div>
+      } @else {
+        <div class="form-item">
+          <ng-container *ngTemplateOutlet="sourceAndDestZone;context: { formControl: 'source_zone', zones: zones, name: 'Source zone' }"></ng-container>
+        </div>
+        <div class="form-item">
+          <ng-container *ngTemplateOutlet="sourceAndDestZone;context: { formControl: 'destination_zone', zones: zones, name: 'Destination zone' }"></ng-container>
+        </div>
+      }
+      <!-- Directional flow zones -->
+    </div>
+    <cd-form-button-panel (submitActionEvent)="submit()"
+                          [form]="currentFormGroupContext"
+                          [modalForm]="true"
+                          [submitText]="(action | titlecase)"></cd-form-button-panel>
+  </form>
+</cds-modal>
 
 <ng-template #sourceAndDestZone
              let-name="name"
-             let-zones="zones">
-  <select [id]="name"
-          [name]="name"
-          class="form-select"
-          (change)="onChangeZoneDropdown(name, $event)"
-          [autofocus]="editing">
-  <option i18n
-          *ngIf="zones.data.available.length == 0"
-          [ngValue]="null">Loading...</option>
-  <option i18n
-          *ngIf="zones.data.available.length > 0"
-          [ngValue]="null">-- Select {{name.split('_').join(' ')}} --</option>
-  <option *ngFor="let destinationZone of zones.data.available"
-          [value]="destinationZone.name">{{ destinationZone.name }}</option>
-  </select>
-  <span class="invalid-feedback"
-        *ngIf="currentFormGroupContext.showError(name, frm, 'required')"
-        i18n>This field is required.</span>
+             let-zones="zones"
+             let-formControl="formControl"
+             [formGroup]="currentFormGroupContext">
+  <cds-select [label]="name"
+              [formControlName]="formControl"
+              [id]="formControl"
+              [invalid]="currentFormGroupContext.controls[formControl].invalid && (currentFormGroupContext.controls[formControl].dirty)"
+              [invalidText]="zoneError"
+              i18n>
+    @if (zones.length == 0) {
+    <option [ngValue]="null">Loading...</option>
+    }
+    @if (zones.length > 0) {
+    <option [ngValue]="null">-- Select {{name}} --</option>
+    }
+    @for (zone of zones; track zone) {
+    <option [value]="zone.name">{{ zone.name }}</option>
+    }
+  </cds-select>
+  <ng-template #zoneError>
+  @if (currentFormGroupContext.showError(formControl, frm, 'required')) {
+    <span class="invalid-feedback"
+          i18n>This field is required.</span>
+  }
+  </ng-template>
 </ng-template>
index 1aa21f437eaca1abecbcd8d6e38d9d931502920d..82f42cc11217213c91f524a55bf11a8adc177205 100644 (file)
@@ -1,6 +1,5 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { RgwMultisiteSyncFlowModalComponent } from './rgw-multisite-sync-flow-modal.component';
-import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 import { ToastrModule } from 'ngx-toastr';
 import { PipesModule } from '~/app/shared/pipes/pipes.module';
 import { HttpClientTestingModule } from '@angular/common/http/testing';
@@ -9,6 +8,7 @@ import { CommonModule } from '@angular/common';
 import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
 import { of } from 'rxjs';
 import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComboBoxModule, ModalModule, SelectModule } from 'carbon-components-angular';
 
 enum FlowType {
   symmetrical = 'symmetrical',
@@ -32,16 +32,31 @@ describe('RgwMultisiteSyncFlowModalComponent', () => {
         ToastrModule.forRoot(),
         PipesModule,
         ReactiveFormsModule,
-        CommonModule
+        CommonModule,
+        SelectModule,
+        ModalModule,
+        ComboBoxModule
       ],
       schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA],
-      providers: [NgbActiveModal, { provide: RgwMultisiteService, useClass: MultisiteServiceMock }]
+      providers: [
+        { provide: RgwMultisiteService, useClass: MultisiteServiceMock },
+        { provide: 'groupType', useValue: FlowType.symmetrical },
+        { provide: 'groupExpandedRow', useValue: { groupName: 'new', bucket: 'bucket1' } },
+        {
+          provide: 'flowSelectedRow',
+          useValue: { id: 'symmetrical', zones: ['zone1-zg1-realm1'] }
+        },
+        { provide: 'action', useValue: 'create' }
+      ]
     }).compileComponents();
 
     fixture = TestBed.createComponent(RgwMultisiteSyncFlowModalComponent);
     multisiteServiceMock = (TestBed.inject(RgwMultisiteService) as unknown) as MultisiteServiceMock;
     component = fixture.componentInstance;
     component.groupType = FlowType.symmetrical;
+    component.groupExpandedRow = { groupName: 'new', bucket: 'bucket1' };
+    component.flowSelectedRow = { id: 'symmetrical', zones: ['zone1-zg1-realm1'] };
+    component.action = 'create';
     fixture.detectChanges();
   });
 
@@ -61,12 +76,12 @@ describe('RgwMultisiteSyncFlowModalComponent', () => {
 
   it('should call createEditSyncFlow for creating/editing symmetrical sync flow', () => {
     component.editing = false;
+    component.ngOnInit();
     component.currentFormGroupContext.patchValue({
       flow_id: 'symmetrical',
       group_id: 'new',
-      zones: { added: ['zone1-zg1-realm1'], removed: [] }
+      zones: ['zone1-zg1-realm1']
     });
-    component.zones.data.selected = ['zone1-zg1-realm1'];
     const spy = jest.spyOn(component, 'submit');
     const putDataSpy = jest
       .spyOn(multisiteServiceMock, 'createEditSyncFlow')
@@ -74,19 +89,21 @@ describe('RgwMultisiteSyncFlowModalComponent', () => {
     component.submit();
     expect(spy).toHaveBeenCalled();
     expect(putDataSpy).toHaveBeenCalled();
-    expect(putDataSpy).toHaveBeenCalledWith(component.currentFormGroupContext.getRawValue());
+    expect(putDataSpy).toHaveBeenCalledWith({
+      ...component.currentFormGroupContext.getRawValue(),
+      zones: { added: ['zone1-zg1-realm1'], removed: [] }
+    });
   });
 
   it('should call createEditSyncFlow for creating/editing directional sync flow', () => {
     component.editing = false;
     component.groupType = FlowType.directional;
     component.ngOnInit();
-    fixture.detectChanges();
     component.currentFormGroupContext.patchValue({
       flow_id: 'directional',
       group_id: 'new',
-      source_zone: { added: ['zone1-zg1-realm1'], removed: [] },
-      destination_zone: { added: ['zone2-zg1-realm1'], removed: [] }
+      source_zone: ['zone1-zg1-realm1'],
+      destination_zone: ['zone2-zg1-realm1']
     });
     const spy = jest.spyOn(component, 'submit');
     const putDataSpy = jest
index 1ab0705f53b2849bfcf43cfa707ab038441e4e6d..f2b136e3e453fae4054b6a07fcc342027176457f 100755 (executable)
@@ -1,6 +1,5 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, Inject, OnInit } from '@angular/core';
 import { UntypedFormControl, Validators } from '@angular/forms';
-import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
 import { ActionLabelsI18n, SucceededActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
@@ -15,36 +14,38 @@ import _ from 'lodash';
 import { Icons } from '~/app/shared/enum/icons.enum';
 import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
-import { ZoneData } from '../models/rgw-multisite-zone-selector';
+import { BaseModal } from 'carbon-components-angular';
+import { ComboBoxItem } from '~/app/shared/models/combo-box.model';
 
 @Component({
   selector: 'cd-rgw-multisite-sync-flow-modal',
   templateUrl: './rgw-multisite-sync-flow-modal.component.html',
   styleUrls: ['./rgw-multisite-sync-flow-modal.component.scss']
 })
-export class RgwMultisiteSyncFlowModalComponent implements OnInit {
-  action: string;
+export class RgwMultisiteSyncFlowModalComponent extends BaseModal implements OnInit {
   editing: boolean = false;
-  groupType: FlowType;
-  groupExpandedRow: any;
-  flowSelectedRow: any;
   syncPolicyDirectionalFlowForm: CdFormGroup;
   syncPolicySymmetricalFlowForm: CdFormGroup;
   syncPolicyPipeForm: CdFormGroup;
   currentFormGroupContext: CdFormGroup;
   flowType = FlowType;
   icons = Icons;
-  zones = new ZoneData(false, 'Filter Zones');
+  zones: ComboBoxItem[] = [];
 
   constructor(
-    public activeModal: NgbActiveModal,
+    @Inject('groupType') public groupType: FlowType,
+    @Inject('groupExpandedRow') public groupExpandedRow: { groupName: string; bucket: string },
+    @Inject('flowSelectedRow') public flowSelectedRow: { id: string; zones: string[] },
+    @Inject('action') public action: string,
     public actionLabels: ActionLabelsI18n,
     public notificationService: NotificationService,
     private rgwDaemonService: RgwDaemonService,
     private rgwZonegroupService: RgwZonegroupService,
     private rgwMultisiteService: RgwMultisiteService,
     private succeededLabels: SucceededActionLabelsI18n
-  ) {}
+  ) {
+    super();
+  }
 
   ngOnInit(): void {
     if (this.action === 'edit') {
@@ -57,7 +58,6 @@ export class RgwMultisiteSyncFlowModalComponent implements OnInit {
       this.createDirectionalFlowForm();
       this.currentFormGroupContext = _.cloneDeep(this.syncPolicyDirectionalFlowForm);
     }
-    this.currentFormGroupContext.get('bucket_name').disable();
     if (this.editing) {
       this.currentFormGroupContext.patchValue({
         flow_id: this.flowSelectedRow.id,
@@ -82,18 +82,27 @@ export class RgwMultisiteSyncFlowModalComponent implements OnInit {
           }
         })
       )
-      .subscribe((zonegroupData: any) => {
+      .subscribe((zonegroupData: { zones: { name: string }[] }) => {
         if (zonegroupData && zonegroupData?.zones?.length > 0) {
-          const zones: any = [];
-          zonegroupData.zones.forEach((zone: any) => {
+          const zones: SelectOption[] = [];
+          zonegroupData.zones.forEach((zone: { name: string }) => {
             zones.push(new SelectOption(false, zone.name, ''));
           });
-          this.zones.data.available = [...zones];
+          this.zones = [...zones].map((zone: { name: string }) => {
+            return { name: zone.name, content: zone.name };
+          });
           if (this.editing) {
+            // @TODO: Editing/deletion of directional flow not supported yet.
+            // Integrate it once the backend supports it.
             if (this.groupType === FlowType.symmetrical) {
-              this.zones.data.selected = [...this.flowSelectedRow.zones];
+              this.zones = [...zones].map((zone: { name: string }) => {
+                if (this.flowSelectedRow.zones.includes(zone.name)) {
+                  return { name: zone.name, content: zone.name, selected: true };
+                }
+                return { name: zone.name, content: zone.name };
+              });
+              this.currentFormGroupContext.patchValue({ zones: this.zones });
             }
-            this.zoneSelection();
           }
         }
       });
@@ -120,11 +129,6 @@ export class RgwMultisiteSyncFlowModalComponent implements OnInit {
     });
   }
 
-  onChangeZoneDropdown(zoneType: string, event: Event) {
-    const selectedVal = (event.target as HTMLSelectElement).value;
-    this.currentFormGroupContext.get(zoneType).setValue(selectedVal);
-  }
-
   commonFormControls(flowType: FlowType) {
     return {
       bucket_name: new UntypedFormControl(this.groupExpandedRow?.bucket),
@@ -140,14 +144,6 @@ export class RgwMultisiteSyncFlowModalComponent implements OnInit {
     };
   }
 
-  zoneSelection() {
-    if (this.groupType === FlowType.symmetrical) {
-      this.currentFormGroupContext.patchValue({
-        zones: this.zones.data.selected
-      });
-    }
-  }
-
   getZoneData(zoneDataToFilter: string[], zoneDataForCondition: string[]) {
     return zoneDataToFilter.filter((zone: string) => !zoneDataForCondition.includes(zone));
   }
@@ -169,28 +165,30 @@ export class RgwMultisiteSyncFlowModalComponent implements OnInit {
     }
 
     if (this.groupType == FlowType.symmetrical) {
+      const selectedZones = this.currentFormGroupContext.get('zones').value;
       if (this.editing) {
-        zones.removed = this.getZoneData(this.flowSelectedRow.zones, this.zones.data.selected);
-        zones.added = this.getZoneData(this.zones.data.selected, this.flowSelectedRow.zones);
+        zones.removed = this.getZoneData(this.flowSelectedRow.zones, selectedZones);
+        zones.added = this.getZoneData(selectedZones, this.flowSelectedRow.zones);
       }
-      zones.added = this.assignZoneValue(zones.added, this.zones.data.selected);
+      zones.added = this.assignZoneValue(zones.added, selectedZones);
     }
     this.rgwMultisiteService
       .createEditSyncFlow({ ...this.currentFormGroupContext.getRawValue(), zones: zones })
-      .subscribe(
-        () => {
+      .subscribe({
+        next: () => {
           const action = this.editing ? this.succeededLabels.EDITED : this.succeededLabels.CREATED;
           this.notificationService.show(
             NotificationType.success,
             $localize`${action} Sync Flow '${this.currentFormGroupContext.getValue('flow_id')}'`
           );
-          this.activeModal.close(NotificationType.success);
         },
-        () => {
+        error: () => {
           // Reset the 'Submit' button.
           this.currentFormGroupContext.setErrors({ cdSubmitButton: true });
-          this.activeModal.dismiss();
+        },
+        complete: () => {
+          this.closeModal();
         }
-      );
+      });
   }
 }
index 767305958d4c8fe7dbab9e541deb1b61e807c9db..95640b60ea3ac1b3a5c37440041649b8e0c2e148 100644 (file)
-<cd-modal [modalRef]="activeModal">
-  <ng-container i18n="form title"
-                class="modal-title">{{ action | titlecase }} Pipe</ng-container>
-
-  <ng-container class="modal-content">
-    <form name="pipeForm"
-          #frm="ngForm"
-          [formGroup]="pipeForm"
-          novalidate>
-      <div class="modal-body">
-        <div class="form-group row">
-          <label class="cd-col-form-label required"
-                 for="pipe_id"
-                 i18n>Name</label>
-          <div class="cd-col-form-input">
-            <input class="form-control"
-                   type="text"
-                   placeholder="Pipe Name..."
-                   id="pipe_id"
-                   name="pipe_id"
-                   formControlName="pipe_id"
-                   [readonly]="editing"/>
-          </div>
-        </div>
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="source_zone"
-                   i18n>Source Zone </label>
-            <div class="cd-col-form-input">
-              <ng-container *ngTemplateOutlet="zoneMultiSelect;context: { name: 'source_zones', zone: sourceZones }"></ng-container>
-            </div>
-          </div>
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="destination_zone"
-                   i18n>Destination Zone</label>
-            <div class="cd-col-form-input">
-              <ng-container *ngTemplateOutlet="zoneMultiSelect;context: { name: 'destination_zones', zone: destZones }"></ng-container>
-            </div>
-          </div>
-        <div class="form-group row">
-          <label class="cd-col-form-label"
-                 for="bucket"
-                 i18n>Bucket Name</label>
-          <div class="cd-col-form-input">
-            <input id="bucket"
-                   name="bucket"
-                   class="form-control"
-                   type="text"
-                   i18n-placeholder
-                   placeholder="Bucket Name..."
-                   formControlName="bucket_name"/>
-          </div>
-        </div>
-        <div class="form-group row">
-          <label class="cd-col-form-label"
-                 for="source_bucket"
-                 i18n>Source Bucket</label>
-          <div class="cd-col-form-input">
-            <input id="source_bucket"
-                   name="source_bucket"
-                   class="form-control"
-                   type="text"
-                   i18n-placeholder
-                   placeholder="Source Bucket Name..."
-                   formControlName="source_bucket"/>
-            <cd-help-text>
-              <span i18n>{{ allBucketSelectedHelpText }}</span>
-            </cd-help-text>
-          </div>
-          </div>
-        <div class="form-group row">
-          <label class="cd-col-form-label"
-                 for="dest_bucket"
-                 i18n>Destination Bucket</label>
-          <div class="cd-col-form-input">
-            <input id="dest_bucket"
-                   name="dest_bucket"
-                   class="form-control"
-                   type="text"
-                   i18n-placeholder
-                   placeholder="Destination Bucket Name..."
-                   formControlName="destination_bucket"/>
-            <cd-help-text>
-              <span i18n>{{ allBucketSelectedHelpText }}</span>
-            </cd-help-text>
-          </div>
-        </div>
+<cds-modal [open]="open"
+           size="md">
+  <cds-modal-header (closeSelect)="closeModal()">
+    <h3 cdsModalHeaderHeading
+        i18n>{{ action | titlecase }} pipe</h3>
+    <cd-help-text [formAllFieldsRequired]="true"></cd-help-text>
+  </cds-modal-header>
+  <form name="pipeForm"
+        #frm="ngForm"
+        [formGroup]="pipeForm"
+        novalidate>
+    <div cdsModalContent>
+      <!-- Pipe Name -->
+      <div class="form-item">
+        <cds-text-label for="pipe_id"
+                        [invalid]="!pipeForm.controls.pipe_id.valid && pipeForm.controls.pipe_id.dirty"
+                        [invalidText]="pipeIdError">
+          <ng-container i18n>Name</ng-container>
+          <input cdsText
+                 type="text"
+                 id="pipe_id"
+                 formControlName="pipe_id"
+                 autofocus
+                 [invalid]="!pipeForm.controls.pipe_id.valid && pipeForm.controls.pipe_id.dirty"
+                 [readonly]="editing">
+        </cds-text-label>
+        <ng-template #pipeIdError>
+          @if (pipeForm.showError('pipe_id', formDir, 'required')) {
+          <span class="invalid-feedback">
+            <ng-container i18n> This field is required. </ng-container>
+          </span>
+          }
+        </ng-template>
+      </div>
+      <!-- Source and Destination Zones -->
+      <div class="form-item">
+        <ng-container *ngTemplateOutlet="zoneMultiSelect;context: { formControl: 'source_zones', zone: sourceZones, name: 'Source zone' }"></ng-container>
+      </div>
+      <div class="form-item">
+        <ng-container *ngTemplateOutlet="zoneMultiSelect;context: { formControl: 'destination_zones', zone: destZones, name: 'Destination zone' }"></ng-container>
+      </div>
+      <!-- Bucket Name -->
+      @if (pipeForm.controls.bucket_name.value) {
+      <div class="form-item">
+        <cds-text-label for="bucket_name"
+                        cdOptionalField="Bucket name">
+          <ng-container i18n>Bucket name</ng-container>
+          <input cdsText
+                 type="text"
+                 id="bucket_name"
+                 formControlName="bucket_name"
+                 autofocus
+                 [readonly]="true">
+        </cds-text-label>
+      </div>
+      }
+      <!-- Source Bucket -->
+      <div class="form-item">
+        <cds-text-label for="source_bucket"
+                        [helperText]="allBucketSelectedHelpText"
+                        i18n-helperText
+                        cdOptionalField="Source bucket">
+          <ng-container i18n>Source bucket</ng-container>
+          <input cdsText
+                 type="text"
+                 id="source_bucket"
+                 formControlName="source_bucket">
+        </cds-text-label>
       </div>
-      <div class="modal-footer">
-        <cd-form-button-panel (submitActionEvent)="submit()"
-                              [form]="pipeForm"
-                              [submitText]="(action | titlecase) + ' ' + 'Pipe'">
-        </cd-form-button-panel>
+      <!-- Destination Bucket -->
+      <div class="form-item">
+        <cds-text-label for="destination_bucket"
+                        [helperText]="allBucketSelectedHelpText"
+                        i18n-helperText
+                        cdOptionalField="Destination bucket">
+          <ng-container i18n>Destination bucket</ng-container>
+          <input cdsText
+                 type="text"
+                 id="destination_bucket"
+                 formControlName="destination_bucket">
+        </cds-text-label>
       </div>
-    </form>
-  </ng-container>
-</cd-modal>
+    </div>
+    <cd-form-button-panel (submitActionEvent)="submit()"
+                          [form]="pipeForm"
+                          [modalForm]="true"
+                          [submitText]="(action | titlecase) + ' ' + 'Pipe'">
+    </cd-form-button-panel>
+  </form>
+</cds-modal>
 
 <ng-template #zoneMultiSelect
              let-name="name"
-             let-zone="zone">
-  <cd-select-badges id="{{ name }}"
-                    name="{{ name }}"
-                    [customBadges]="zone.customBadges"
-                    [customBadgeValidators]="zone.data.validators"
-                    [messages]="zone.data.messages"
-                    [data]="zone.data.selected"
-                    [options]="zone.data.available"
-                    (selection)="onZoneSelection(name)">
-  </cd-select-badges>
-  <i *ngIf="zone.data.selected.length <= 0"
-     i18n-title
-     title="Pipe should be associated with {{ name?.split('_').join(' ') }}"
-     class="{{ icons.warning }} icon-warning-color">
-  </i>
-  <span class="invalid-feedback"
-        *ngIf="pipeForm.showError(name, frm, 'required')"
-        i18n>{{ name?.split('_').join(' ') }} selection is required!
-  </span>
+             let-zone="zone"
+             let-formControl="formControl"
+             [formGroup]="pipeForm">
+  <cds-combo-box [label]="name"
+                 type="multi"
+                 selectionFeedback="top-after-reopen"
+                 [for]="formControl"
+                 [formControlName]="formControl"
+                 [helperText]="'Pipe need to be associated with atleast one zone'"
+                 i18n-helperText
+                 [items]="zone"
+                 itemValueKey="content"
+                 [id]="formControl"
+                 [invalid]="pipeForm.controls[formControl].invalid && pipeForm.controls[formControl].dirty"
+                 [invalidText]="'Zone selection is required!'"
+                 i18n>
+    <cds-dropdown-list></cds-dropdown-list>
+  </cds-combo-box>
 </ng-template>
index faf382b78028e34529b72a8fb2fd2597dd3d9beb..1e476ce91744e53da98045cde5bd116e6f753be9 100644 (file)
@@ -10,6 +10,9 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 import { of } from 'rxjs';
 import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
 import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
+import { USER } from '~/app/shared/constants/app.constants';
+import { FlowType } from '../models/rgw-multisite';
+import { ComboBoxModule } from 'carbon-components-angular';
 
 class MultisiteServiceMock {
   createEditSyncPipe = jest.fn().mockReturnValue(of(null));
@@ -28,10 +31,26 @@ describe('RgwMultisiteSyncPipeModalComponent', () => {
         ToastrModule.forRoot(),
         PipesModule,
         ReactiveFormsModule,
-        CommonModule
+        CommonModule,
+        ComboBoxModule
       ],
       schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA],
-      providers: [NgbActiveModal, { provide: RgwMultisiteService, useClass: MultisiteServiceMock }]
+      providers: [
+        NgbActiveModal,
+        { provide: RgwMultisiteService, useClass: MultisiteServiceMock },
+        { provide: 'groupType', useValue: FlowType.symmetrical },
+        { provide: 'groupExpandedRow', useValue: { groupName: 'new', bucket: 'bucket1' } },
+        {
+          provide: 'pipeSelectedRow',
+          useValue: {
+            source: { zones: ['zone1-zg1-realm1'], bucket: 'bucket1' },
+            dest: { zones: ['zone2-zg1-realm1'], bucket: 'bucket1' },
+            id: 'pipe1',
+            params: { user: 'dashboard', mode: USER }
+          }
+        },
+        { provide: 'action', useValue: 'create' }
+      ]
     }).compileComponents();
 
     fixture = TestBed.createComponent(RgwMultisiteSyncPipeModalComponent);
@@ -76,16 +95,21 @@ describe('RgwMultisiteSyncPipeModalComponent', () => {
 
   it('should call createEditSyncPipe for creating/editing sync pipe', () => {
     component.editing = false;
+    component.ngOnInit();
     component.pipeForm.patchValue({
       pipe_id: 'pipe1',
       group_id: 'new',
       source_bucket: '',
-      source_zones: { added: ['zone1-zg1-realm1'], removed: [] },
+      source_zones: ['zone1-zg1-realm1'],
       destination_bucket: '',
-      destination_zones: { added: ['zone2-zg1-realm1'], removed: [] }
+      destination_zones: ['zone2-zg1-realm1']
     });
-    component.sourceZones.data.selected = ['zone1-zg1-realm1'];
-    component.destZones.data.selected = ['zone2-zg1-realm1'];
+    component.sourceZones = [
+      { name: 'zone1-zg1-realm1', content: 'zone1-zg1-realm1', selected: true }
+    ];
+    component.destZones = [
+      { name: 'zone2-zg1-realm1', content: 'zone2-zg1-realm1', selected: true }
+    ];
     const spy = jest.spyOn(component, 'submit');
     const putDataSpy = jest.spyOn(multisiteServiceMock, 'createEditSyncPipe');
     component.submit();
@@ -93,6 +117,8 @@ describe('RgwMultisiteSyncPipeModalComponent', () => {
     expect(putDataSpy).toHaveBeenCalled();
     expect(putDataSpy).toHaveBeenCalledWith({
       ...component.pipeForm.getRawValue(),
+      source_zones: { added: ['zone1-zg1-realm1'], removed: [] },
+      destination_zones: { added: ['zone2-zg1-realm1'], removed: [] },
       mode: '',
       user: ''
     });
@@ -104,25 +130,26 @@ describe('RgwMultisiteSyncPipeModalComponent', () => {
       pipe_id: 'pipe1',
       group_id: 's3-bucket-replication:enabled',
       source_bucket: '',
-      source_zones: { added: ['zone1-zg1-realm1'], removed: [] },
+      source_zones: ['zone1-zg1-realm1'],
       destination_bucket: '',
-      destination_zones: { added: ['zone2-zg1-realm1'], removed: [] }
+      destination_zones: ['zone2-zg1-realm1']
     });
     component.pipeSelectedRow = {
       dest: { bucket: '*', zones: ['zone2-zg1-realm1'] },
       id: 'pipi1',
       params: {
-        dest: {},
-        mode: 'user',
-        priority: 0,
-        source: { filter: { tags: [] } },
+        mode: USER,
         user: 'dashboard'
       },
       source: { bucket: '*', zones: ['zone1-zg1-realm1'] }
     };
 
-    component.sourceZones.data.selected = ['zone1-zg1-realm1'];
-    component.destZones.data.selected = ['zone2-zg1-realm1'];
+    component.sourceZones = [
+      { name: 'zone1-zg1-realm1', content: 'zone1-zg1-realm1', selected: true }
+    ];
+    component.destZones = [
+      { name: 'zone2-zg1-realm1', content: 'zone2-zg1-realm1', selected: true }
+    ];
     const spy = jest.spyOn(component, 'submit');
     const putDataSpy = jest.spyOn(multisiteServiceMock, 'createEditSyncPipe');
     component.submit();
@@ -130,7 +157,9 @@ describe('RgwMultisiteSyncPipeModalComponent', () => {
     expect(putDataSpy).toHaveBeenCalled();
     expect(putDataSpy).toHaveBeenCalledWith({
       ...component.pipeForm.getRawValue(),
-      mode: 'user',
+      source_zones: { added: ['zone1-zg1-realm1'], removed: [] },
+      destination_zones: { added: ['zone2-zg1-realm1'], removed: [] },
+      mode: USER,
       user: 'dashboard'
     });
   });
index 43742ef60b839110a347eda027f343818f729a47..aeaf66b3d3e13509b1609f7d94e24f452183bf31 100755 (executable)
@@ -1,4 +1,4 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, Inject, OnInit } from '@angular/core';
 import { UntypedFormControl, Validators } from '@angular/forms';
 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
 import { RgwZonegroup, Zone } from '../models/rgw-multisite';
@@ -8,43 +8,56 @@ import { of } from 'rxjs';
 import { RgwDaemon } from '../models/rgw-daemon';
 import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
 import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
-import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 import _ from 'lodash';
 import { Icons } from '~/app/shared/enum/icons.enum';
 import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
 import { NotificationService } from '~/app/shared/services/notification.service';
-import { ZoneData } from '../models/rgw-multisite-zone-selector';
 import { SucceededActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { BaseModal } from 'carbon-components-angular';
+import { ComboBoxItem } from '~/app/shared/models/combo-box.model';
 
 const ALL_ZONES = $localize`All zones (*)`;
 const ALL_BUCKET_SELECTED_HELP_TEXT =
   'If no value is provided, all the buckets in the zone group will be selected.';
 
+interface PipeParams {
+  source: { zones: string[]; bucket: string };
+  dest: { zones: string[]; bucket: string };
+  id: string;
+  params: { user: string; mode: string };
+}
+
+interface GroupParams {
+  groupName: string;
+  bucket: string;
+}
+
 @Component({
   selector: 'cd-rgw-multisite-sync-pipe-modal',
   templateUrl: './rgw-multisite-sync-pipe-modal.component.html',
   styleUrls: ['./rgw-multisite-sync-pipe-modal.component.scss']
 })
-export class RgwMultisiteSyncPipeModalComponent implements OnInit {
-  groupExpandedRow: any;
-  pipeSelectedRow: any;
+export class RgwMultisiteSyncPipeModalComponent extends BaseModal implements OnInit {
   pipeForm: CdFormGroup;
-  action: string;
   editing: boolean;
-  sourceZones = new ZoneData(false, 'Filter Zones');
-  destZones = new ZoneData(false, 'Filter Zones');
+  sourceZones: ComboBoxItem[] = [];
+  destZones: ComboBoxItem[] = [];
   icons = Icons;
   allBucketSelectedHelpText = ALL_BUCKET_SELECTED_HELP_TEXT;
 
   constructor(
-    public activeModal: NgbActiveModal,
+    @Inject('groupExpandedRow') public groupExpandedRow: GroupParams,
+    @Inject('pipeSelectedRow') public pipeSelectedRow: PipeParams,
+    @Inject('action') public action: string,
     private rgwDaemonService: RgwDaemonService,
     private rgwZonegroupService: RgwZonegroupService,
     private rgwMultisiteService: RgwMultisiteService,
     private notificationService: NotificationService,
     private succeededLabels: SucceededActionLabelsI18n
-  ) {}
+  ) {
+    super();
+  }
 
   ngOnInit(): void {
     if (this.pipeSelectedRow) {
@@ -73,7 +86,6 @@ export class RgwMultisiteSyncPipeModalComponent implements OnInit {
         validators: [Validators.required]
       })
     });
-    this.pipeForm.get('bucket_name').disable();
     this.rgwDaemonService.selectedDaemon$
       .pipe(
         switchMap((daemon: RgwDaemon) => {
@@ -90,19 +102,33 @@ export class RgwMultisiteSyncPipeModalComponent implements OnInit {
           }
         })
       )
-      .subscribe((zonegroupData: any) => {
+      .subscribe((zonegroupData: { zones: { name: string }[] }) => {
         if (zonegroupData && zonegroupData?.zones?.length > 0) {
-          let zones: any[] = [];
+          let zones: SelectOption[] = [];
           zones.push(new SelectOption(false, ALL_ZONES, ''));
-          zonegroupData.zones.forEach((zone: any) => {
+          zonegroupData.zones.forEach((zone: { name: string }) => {
             zones.push(new SelectOption(false, zone.name, ''));
           });
-          this.sourceZones.data.available = JSON.parse(JSON.stringify(zones));
-          this.destZones.data.available = JSON.parse(JSON.stringify(zones));
+          this.sourceZones = JSON.parse(JSON.stringify(zones)).map((zone: { name: string }) => {
+            return { name: zone.name, content: zone.name };
+          });
+          this.destZones = JSON.parse(JSON.stringify(zones)).map((zone: { name: string }) => {
+            return { name: zone.name, content: zone.name };
+          });
           if (this.editing) {
             this.pipeForm.get('pipe_id').disable();
-            this.sourceZones.data.selected = [...this.pipeSelectedRow.source.zones];
-            this.destZones.data.selected = [...this.pipeSelectedRow.dest.zones];
+            this.sourceZones = [...this.sourceZones].map((zone: { name: string }) => {
+              if (this.pipeSelectedRow.source.zones.includes(zone.name)) {
+                return { name: zone.name, content: zone.name, selected: true };
+              }
+              return { name: zone.name, content: zone.name };
+            });
+            this.destZones = [...this.destZones].map((zone: { name: string }) => {
+              if (this.pipeSelectedRow.dest.zones.includes(zone.name)) {
+                return { name: zone.name, content: zone.name, selected: true };
+              }
+              return { name: zone.name, content: zone.name };
+            });
             const availableDestZone: SelectOption[] = [];
             this.pipeSelectedRow.dest.zones.forEach((zone: string) => {
               availableDestZone.push(new SelectOption(true, zone, ''));
@@ -127,18 +153,6 @@ export class RgwMultisiteSyncPipeModalComponent implements OnInit {
     return zones.map((str) => str.replace('*', ALL_ZONES));
   }
 
-  onZoneSelection(zoneType: string) {
-    if (zoneType === 'source_zones') {
-      this.pipeForm.patchValue({
-        source_zones: this.sourceZones.data.selected
-      });
-    } else {
-      this.pipeForm.patchValue({
-        destination_zones: this.destZones.data.selected
-      });
-    }
-  }
-
   getZoneData(zoneDataToFilter: string[], zoneDataForCondition: string[]) {
     return zoneDataToFilter.filter((zone: string) => !zoneDataForCondition.includes(zone));
   }
@@ -162,26 +176,19 @@ export class RgwMultisiteSyncPipeModalComponent implements OnInit {
       return;
     }
 
+    const selectedSourceZones = this.pipeForm.getValue('source_zones');
+    const selectedDestZones = this.pipeForm.getValue('destination_zones');
     if (this.editing) {
-      destZones.removed = this.getZoneData(
-        this.pipeSelectedRow.dest.zones,
-        this.destZones.data.selected
-      );
-      destZones.added = this.getZoneData(
-        this.destZones.data.selected,
-        this.pipeSelectedRow.dest.zones
-      );
+      destZones.removed = this.getZoneData(this.pipeSelectedRow.dest.zones, selectedDestZones);
+      destZones.added = this.getZoneData(selectedDestZones, this.pipeSelectedRow.dest.zones);
       sourceZones.removed = this.getZoneData(
         this.pipeSelectedRow.source.zones,
-        this.sourceZones.data.selected
-      );
-      sourceZones.added = this.getZoneData(
-        this.sourceZones.data.selected,
-        this.pipeSelectedRow.source.zones
+        selectedSourceZones
       );
+      sourceZones.added = this.getZoneData(selectedSourceZones, this.pipeSelectedRow.source.zones);
     }
-    sourceZones.added = this.assignZoneValue(sourceZones.added, this.sourceZones.data.selected);
-    destZones.added = this.assignZoneValue(destZones.added, this.destZones.data.selected);
+    sourceZones.added = this.assignZoneValue(sourceZones.added, selectedSourceZones);
+    destZones.added = this.assignZoneValue(destZones.added, selectedDestZones);
 
     sourceZones.removed = this.replaceWithAsterisk(sourceZones.removed);
     destZones.removed = this.replaceWithAsterisk(destZones.removed);
@@ -194,20 +201,21 @@ export class RgwMultisiteSyncPipeModalComponent implements OnInit {
         user: this.editing ? this.pipeSelectedRow?.params?.user : '',
         mode: this.editing ? this.pipeSelectedRow?.params?.mode : ''
       })
-      .subscribe(
-        () => {
+      .subscribe({
+        next: () => {
           const action = this.editing ? this.succeededLabels.EDITED : this.succeededLabels.CREATED;
           this.notificationService.show(
             NotificationType.success,
             $localize`${action} Sync Pipe '${this.pipeForm.getValue('pipe_id')}'`
           );
-          this.activeModal.close(NotificationType.success);
         },
-        () => {
+        error: () => {
           // Reset the 'Submit' button.
           this.pipeForm.setErrors({ cdSubmitButton: true });
-          this.activeModal.dismiss();
+        },
+        complete: () => {
+          this.closeModal();
         }
-      );
+      });
   }
 }
index fb3ad72369154cbbbaf32462cc8927225ae2e3cd..e7cd302431b2fcf7cab4fce2c6e07c74055e8872 100644 (file)
@@ -1,4 +1,4 @@
-<ng-container *ngIf="expandedRow">
+@if (expandedRow) {
   <nav
     ngbNav
     #nav="ngbNav"
@@ -7,7 +7,7 @@
     <ng-container ngbNavItem="flow">
       <a
         ngbNavLink
-        i18n>Flow</a>
+      i18n>Flow</a>
       <ng-template ngbNavContent>
         <legend>
           Symmetrical
           </cd-help-text>
         </legend>
         <cd-table
-        #table
-        [data]="pipeData"
-        [columns]="pipeCols"
-        selectionType="multiClick"
-        [searchableObjects]="true"
-        [hasDetails]="false"
-        [serverSide]="false"
-        [toolHeader]="true"
-        (updateSelection)="pipeSelection = $event"
-        (fetchData)="loadData($event)">
-        <div class="table-actions">
-          <cd-table-actions
-            [permission]="permission"
-            [selection]="pipeSelection"
-            class="btn-group"
-            [tableActions]="pipeTableActions">
-          </cd-table-actions>
-        </div>
+          #table
+          [data]="pipeData"
+          [columns]="pipeCols"
+          selectionType="multiClick"
+          [searchableObjects]="true"
+          [hasDetails]="false"
+          [serverSide]="false"
+          [toolHeader]="true"
+          (updateSelection)="pipeSelection = $event"
+          (fetchData)="loadData($event)">
+          <div class="table-actions">
+            <cd-table-actions
+              [permission]="permission"
+              [selection]="pipeSelection"
+              class="btn-group"
+              [tableActions]="pipeTableActions">
+            </cd-table-actions>
+          </div>
         </cd-table>
       </ng-template>
     </ng-container>
   </nav>
   <div [ngbNavOutlet]="nav"></div>
-</ng-container>
+}
 
 <ng-template #deleteTpl>
   <cd-alert-panel type="danger"
index 0f2175b0bcfc542e1306d6dc6c4269c026870559..86503c01711fd612ba6b723d7158757bd384104f 100755 (executable)
@@ -6,7 +6,6 @@ import { CdTableAction } from '~/app/shared/models/cd-table-action';
 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
 import { Permission } from '~/app/shared/models/permissions';
-import { ModalService } from '~/app/shared/services/modal.service';
 import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
 import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
 import { FinishedTask } from '~/app/shared/models/finished-task';
@@ -58,7 +57,7 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges {
 
   constructor(
     private actionLabels: ActionLabelsI18n,
-    private modalService: ModalService,
+    private modalService: ModalCdsService,
     private rgwMultisiteService: RgwMultisiteService,
     private taskWrapper: TaskWrapperService,
     private cdsModalService: ModalCdsService
@@ -225,9 +224,7 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges {
       action: action
     };
 
-    this.modalRef = this.modalService.show(RgwMultisiteSyncFlowModalComponent, initialState, {
-      size: 'lg'
-    });
+    this.modalRef = this.modalService.show(RgwMultisiteSyncFlowModalComponent, initialState);
 
     try {
       const res = await this.modalRef.result;
@@ -294,9 +291,7 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges {
       action: action
     };
 
-    this.modalRef = this.modalService.show(RgwMultisiteSyncPipeModalComponent, initialState, {
-      size: 'lg'
-    });
+    this.modalRef = this.modalService.show(RgwMultisiteSyncPipeModalComponent, initialState);
 
     try {
       const res = await this.modalRef.result;
index 511eaaa652645e3f9759163e49e82255d0c93c0c..106715a4b08c386824cb2c9d1cd9911bd2130fd4 100644 (file)
@@ -1,95 +1,85 @@
-<cd-modal [pageURL]="pageURL">
-  <span class="modal-title"
-        i18n>{{ action | titlecase }} {{ resource | upperFirst }}</span>
-  <ng-container class="modal-content">
-    <form
-      #frm="ngForm"
-      [formGroup]="syncPolicyForm"
-      *cdFormLoading="loading"
-      novalidate>
-      <div class="modal-body">
+<cds-modal size="sm"
+           [open]="open">
+  <cds-modal-header (closeSelect)="closeModal()">
+    <h3 cdsModalHeaderHeading
+        i18n>{{ action | titlecase }} {{ resource }}</h3>
+    <cd-help-text [formAllFieldsRequired]="true"></cd-help-text>
+  </cds-modal-header>
+  <ng-container *cdFormLoading="loading">
+    <form #frm="ngForm"
+          [formGroup]="syncPolicyForm"
+          *cdFormLoading="loading"
+          novalidate>
+      <div cdsModalContent>
         <!-- Group Id -->
-        <div class="form-group row">
-          <label
-            class="cd-col-form-label required"
-            for="group_id"
-            i18n>Group Name</label>
-          <div class="cd-col-form-input">
-            <input
-              id="group_id"
-              name="group_id"
-              class="form-control"
-              type="text"
-              i18n-placeholder
-              placeholder="Group Name..."
-              formControlName="group_id"
-              [readonly]="editing"/>
-            <span
-              class="invalid-feedback"
-              *ngIf="syncPolicyForm.showError('group_id', frm, 'required')"
-              i18n>This field is required.</span>
-          </div>
+        <div class="form-item">
+          <cds-text-label for="group_id"
+                          [invalid]="!syncPolicyForm.controls.group_id.valid && syncPolicyForm.controls.group_id.dirty"
+                          [invalidText]="groupIdError">
+            <ng-container i18n>Group name</ng-container>
+            <input cdsText
+                   type="text"
+                   id="group_id"
+                   formControlName="group_id"
+                   autofocus
+                   [invalid]="!syncPolicyForm.controls.group_id.valid && syncPolicyForm.controls.group_id.dirty"
+                   [readonly]="editing"/>
+          </cds-text-label>
+          <ng-template #groupIdError>
+            @if (syncPolicyForm.showError('group_id', frm, 'required')) {
+            <span class="invalid-feedback">
+              <ng-container i18n> This field is required. </ng-container>
+            </span>
+            }
+          </ng-template>
         </div>
         <!-- Status -->
-        <div class="form-group row">
-          <label
-            class="cd-col-form-label required"
-            for="status"
-            i18n>Status</label>
-          <div class="cd-col-form-input">
-            <select
-                id="status"
-                name="status"
-                class="form-select"
-                formControlName="status">
-              <option
-                i18n
-                value="{{syncPolicyStatus.ENABLED}}">{{syncPolicyStatus.ENABLED | upperFirst }}</option>
-              <option
-                i18n
-                value="{{syncPolicyStatus.ALLOWED}}">{{syncPolicyStatus.ALLOWED | upperFirst }}</option>
-              <option
-                i18n
-                value="{{syncPolicyStatus.FORBIDDEN}}">{{syncPolicyStatus.FORBIDDEN | upperFirst }}</option>
-            </select>
-            <span
-              class="invalid-feedback"
-              *ngIf="syncPolicyForm.showError('status', frm, 'required')"
-              i18n>This field is required.</span>
-          </div>
+        <div class="form-item">
+          <cds-select label="Status"
+                      for="status"
+                      formControlName="status"
+                      id="status"
+                      [invalid]="syncPolicyForm.controls['status'].invalid && (syncPolicyForm.controls['status'].dirty)"
+                      [invalidText]="statusError"
+                      i18n>
+            <ng-container>Status</ng-container>
+            <option value="{{syncPolicyStatus.ENABLED}}">{{syncPolicyStatus.ENABLED | upperFirst }}</option>
+            <option value="{{syncPolicyStatus.ALLOWED}}">{{syncPolicyStatus.ALLOWED | upperFirst }}</option>
+            <option value="{{syncPolicyStatus.FORBIDDEN}}">{{syncPolicyStatus.FORBIDDEN | upperFirst }}</option>
+          </cds-select>
+          <ng-template #statusError>
+            @if (syncPolicyForm.showError('status', frm, 'required')) {
+            <span class="invalid-feedback"
+                  i18n>This field is required.</span>
+            }
+          </ng-template>
         </div>
         <!-- Bucket Name -->
-        <div class="form-group row">
-          <label
-            class="cd-col-form-label"
-            for="bucket_name"
-            i18n>Bucket Name</label>
-          <div class="cd-col-form-input">
-            <input
-              id="bucket_name"
-              name="bucket_name"
-              class="form-control"
-              type="text"
-              i18n-placeholder
-              placeholder="Bucket Name..."
-              formControlName="bucket_name"
-              [ngbTypeahead]="bucketDataSource"/>
-            <span
-              class="invalid-feedback"
-              *ngIf="syncPolicyForm.showError('bucket_name', frm, 'bucketNameNotAllowed')"
-              i18n>The bucket with chosen name does not exist.</span>
-          </div>
-        </div>
-      </div>
-
-      <div class="modal-footer">
-        <div class="text-right">
-          <cd-form-button-panel (submitActionEvent)="submit()"
-                                [form]="syncPolicyForm"
-                                [disabled]="syncPolicyForm.pending || syncPolicyForm.pristine || syncPolicyForm.invalid"
-                                [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+        <div class="form-item">
+          <cds-text-label for="bucket_name"
+                          [invalid]="!syncPolicyForm.controls.bucket_name.valid && syncPolicyForm.controls.bucket_name.dirty"
+                          [invalidText]="bucketNameError"
+                          cdOptionalField="Bucket name">
+            <ng-container i18n>Bucket name</ng-container>
+            <input cdsText
+                   type="text"
+                   id="bucket_name"
+                   formControlName="bucket_name"
+                   [invalid]="!syncPolicyForm.controls.bucket_name.valid && syncPolicyForm.controls.bucket_name.dirty"
+                   [readonly]="editing"/>
+          </cds-text-label>
+          <ng-template #bucketNameError>
+            @if (syncPolicyForm.showError('bucket_name', frm, 'bucketNameNotAllowed')) {
+            <span class="invalid-feedback"
+                  i18n>The bucket with chosen name does not exist.</span>
+            }
+          </ng-template>
         </div>
       </div>
+      <cd-form-button-panel (submitActionEvent)="submit()"
+                            [form]="syncPolicyForm"
+                            [modalForm]="true"
+                            [submitText]="(action | titlecase)"></cd-form-button-panel>
     </form>
   </ng-container>
-</cd-modal>
+</cds-modal>
index d3c2a3227e131b7d0829f06487d7f6a102d09e3e..64ad562ef53ac301ba5e8c5c9c12345f349e4697 100644 (file)
@@ -8,6 +8,7 @@ import { ComponentsModule } from '~/app/shared/components/components.module';
 import { RouterTestingModule } from '@angular/router/testing';
 import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
 import { SharedModule } from '~/app/shared/shared.module';
+import { ModalModule, SelectModule } from 'carbon-components-angular';
 
 describe('RgwMultisiteSyncPolicyFormComponent', () => {
   let component: RgwMultisiteSyncPolicyFormComponent;
@@ -23,7 +24,9 @@ describe('RgwMultisiteSyncPolicyFormComponent', () => {
         PipesModule,
         ComponentsModule,
         SharedModule,
-        RouterTestingModule
+        RouterTestingModule,
+        ModalModule,
+        SelectModule
       ],
       schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA],
       providers: []
index dda1671e531f70091f9cedcec0770b76d0d65ff3..da9508cd0e8b89ec3824cc0ecd9a232451cadb5d 100644 (file)
@@ -33,6 +33,7 @@ export class RgwMultisiteSyncPolicyFormComponent extends CdForm implements OnIni
   resource: string;
   syncPolicyStatus = RgwMultisiteSyncPolicyStatus;
   pageURL: string;
+  open: boolean = false;
   bucketDataSource = (text$: Observable<string>) => {
     return text$.pipe(
       debounceTime(200),
@@ -53,13 +54,14 @@ export class RgwMultisiteSyncPolicyFormComponent extends CdForm implements OnIni
     super();
     this.editing = this.router.url.includes('(modal:edit');
     this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
-    this.resource = $localize`Sync Policy Group`;
+    this.resource = $localize`sync policy group`;
     this.createForm();
     this.loadingReady();
     this.pageURL = 'rgw/multisite/sync-policy';
   }
 
   ngOnInit(): void {
+    this.open = this.route.outlet === 'modal';
     if (this.editing) {
       this.route.paramMap.subscribe((params: any) => {
         const groupName = params.get('groupName');
@@ -71,7 +73,6 @@ export class RgwMultisiteSyncPolicyFormComponent extends CdForm implements OnIni
             .subscribe((syncPolicy: any) => {
               this.loadingReady();
               if (syncPolicy) {
-                this.syncPolicyForm.get('bucket_name').disable();
                 this.syncPolicyForm.patchValue({
                   group_id: syncPolicy.id,
                   status: syncPolicy.status,
@@ -145,7 +146,7 @@ export class RgwMultisiteSyncPolicyFormComponent extends CdForm implements OnIni
 
   bucketExistence(requiredExistenceResult: boolean): AsyncValidatorFn {
     return (control: AbstractControl): Observable<ValidationErrors | null> => {
-      if (control.dirty) {
+      if (control.dirty && control.value) {
         return observableTimer(500).pipe(
           switchMapTo(this.rgwBucketService.exists(control.value)),
           map((existenceResult: boolean) =>
@@ -171,4 +172,8 @@ export class RgwMultisiteSyncPolicyFormComponent extends CdForm implements OnIni
       return of([]);
     }
   }
+
+  closeModal(): void {
+    this.router.navigate([this.pageURL, { outlets: { modal: null } }]);
+  }
 }