]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: RGW multisite sync pipe
authorNaman Munet <namanmunet@Namans-MacBook-Pro.local>
Thu, 11 Jul 2024 18:40:51 +0000 (00:10 +0530)
committerNaman Munet <nmunet@redhat.com>
Thu, 18 Jul 2024 11:13:46 +0000 (16:43 +0530)
Fixes: https://tracker.ceph.com/issues/66926
Signed-off-by: Naman Munet <nmunet@redhat.com>
(cherry picked from commit d451b4d1795e1429b0a530940558cf646579cdb9)

18 files changed:
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/multisite.e2e.spec.ts
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.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.ts [new file with mode: 0644]
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.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_client.py

index e265d2e6bae33662ecdd3a0e0872b8886dac02da..03d8cdc8cf415c29425ddfc8469c47e464560604 100644 (file)
@@ -200,14 +200,15 @@ class RgwMultisiteController(RESTController):
     @EndpointDoc("Create or update the sync pipe")
     @CreatePermission
     def create_sync_pipe(self, group_id: str, pipe_id: str,
+                         source_bucket: str = '',
                          source_zones: Optional[List[str]] = None,
                          destination_zones: Optional[List[str]] = None,
-                         destination_buckets: Optional[List[str]] = None,
+                         destination_bucket: str = '',
                          bucket_name: str = ''):
         multisite_instance = RgwMultisite()
         return multisite_instance.create_sync_pipe(group_id, pipe_id, source_zones,
-                                                   destination_zones, destination_buckets,
-                                                   bucket_name)
+                                                   destination_zones, source_bucket,
+                                                   destination_bucket, bucket_name)
 
     @Endpoint(method='DELETE', path='/sync-pipe')
     @EndpointDoc("Remove the sync pipe")
@@ -215,11 +216,11 @@ class RgwMultisiteController(RESTController):
     def remove_sync_pipe(self, group_id: str, pipe_id: str,
                          source_zones: Optional[List[str]] = None,
                          destination_zones: Optional[List[str]] = None,
-                         destination_buckets: Optional[List[str]] = None,
+                         destination_bucket: str = '',
                          bucket_name: str = ''):
         multisite_instance = RgwMultisite()
         return multisite_instance.remove_sync_pipe(group_id, pipe_id, source_zones,
-                                                   destination_zones, destination_buckets,
+                                                   destination_zones, destination_bucket,
                                                    bucket_name)
 
 
index 6a7eb2a802d7f67e10456f6d49d2004d3c282e24..35f34a34b4b698f3d48c670d84db7f96b56d710f 100644 (file)
@@ -308,31 +308,31 @@ export abstract class PageHelper {
   }
 
   getNestedTableCell(
-    tableSelector: string,
+    selector: string,
     columnIndex: number,
     exactContent: string,
     partialMatch = false
   ) {
     this.waitDataTableToLoad();
     this.clearTableSearchInput();
-    this.searchNestedTable(tableSelector, exactContent);
+    this.searchNestedTable(selector, exactContent);
     if (partialMatch) {
       return cy
-        .get(`${tableSelector} datatable-body-row datatable-body-cell:nth-child(${columnIndex})`)
+        .get(`${selector} datatable-body-row datatable-body-cell:nth-child(${columnIndex})`)
         .should('contain', exactContent);
     }
     return cy
-      .get(`${tableSelector}`)
+      .get(`${selector}`)
       .contains(
         `datatable-body-row datatable-body-cell:nth-child(${columnIndex})`,
         new RegExp(`^${exactContent}$`)
       );
   }
 
-  searchNestedTable(tableSelector: string, text: string) {
+  searchNestedTable(selector: string, text: string) {
     this.waitDataTableToLoad();
 
     this.setPageSize('10');
-    cy.get(`${tableSelector} [aria-label=search]`).first().clear({ force: true }).type(text);
+    cy.get(`${selector} [aria-label=search]`).first().clear({ force: true }).type(text);
   }
 }
index e884ec624e34d86adce7c513e76bd1a172034959..a0306f126b321197c5a21ac6585dfcffdb63c874 100644 (file)
@@ -79,4 +79,23 @@ describe('Multisite page', () => {
       );
     });
   });
+
+  describe('create, edit, delete pipe', () => {
+    beforeEach(() => {
+      multisite.getTab('Sync Policy').click();
+      multisite.getExpandCollapseElement().click();
+    });
+
+    it('should create pipe', () => {
+      multisite.createPipe('new-pipe', ['zone1-zg1-realm1'], ['zone3-zg2-realm1']);
+    });
+
+    it('should modify pipe zones', () => {
+      multisite.editPipe('new-pipe', 'zone2-zg1-realm1');
+    });
+
+    it('should delete pipe', () => {
+      multisite.deletePipe('new-pipe');
+    });
+  });
 });
index d912f6b1ca02484cb00169cd5551f226912d2a68..11a43a7b8b30bf37c67d794115d026596d0d2b41 100644 (file)
@@ -185,4 +185,108 @@ export class MultisitePageHelper extends PageHelper {
       .find('.datatable-body-cell-label')
       .should('contain', dest_zones[0]);
   }
+
+  @PageHelper.restrictTo(pages.index.url)
+  createPipe(pipe_id: string, source_zones: string[], dest_zones: string[]) {
+    cy.get('cd-rgw-multisite-sync-policy-details').should('exist');
+    this.getTab('Pipe').should('exist');
+    this.getTab('Pipe').click();
+    cy.request({
+      method: 'GET',
+      url: '/api/rgw/daemon',
+      headers: { Accept: 'application/vnd.ceph.api.v1.0+json' }
+    });
+    cy.get('cd-rgw-multisite-sync-policy-details .table-actions button').first().click();
+    cy.get('cd-rgw-multisite-sync-pipe-modal').should('exist');
+
+    // Enter in pipe_id
+    cy.get('#pipe_id').type(pipe_id);
+    cy.wait(WAIT_TIMER);
+    // Select zone
+    cy.get('a[data-testid=select-menu-edit]').eq(0).click();
+    for (const zone of source_zones) {
+      cy.get('.popover-body div.select-menu-item-content').contains(zone).click();
+    }
+    cy.get('cd-rgw-multisite-sync-pipe-modal').click();
+    cy.get('a[data-testid=select-menu-edit]').eq(1).click();
+    for (const zone of dest_zones) {
+      cy.get('.popover-body input').type(`${zone}{enter}`);
+    }
+    cy.get('button.tc_submitButton').click();
+
+    cy.get('cd-rgw-multisite-sync-policy-details .datatable-body-cell-label').should(
+      'contain',
+      pipe_id
+    );
+
+    cy.get('cd-rgw-multisite-sync-policy-details')
+      .first()
+      .find('[aria-label=search]')
+      .first()
+      .clear({ force: true })
+      .type(pipe_id);
+  }
+
+  @PageHelper.restrictTo(pages.index.url)
+  editPipe(pipe_id: string, zoneToAdd: string) {
+    cy.get('cd-rgw-multisite-sync-policy-details').should('exist');
+    this.getTab('Pipe').should('exist');
+    this.getTab('Pipe').click();
+    cy.request({
+      method: 'GET',
+      url: '/api/rgw/daemon',
+      headers: { Accept: 'application/vnd.ceph.api.v1.0+json' }
+    });
+
+    cy.get('cd-rgw-multisite-sync-policy-details').within(() => {
+      cy.get('.datatable-body-cell-label').should('contain', pipe_id);
+      cy.get('[aria-label=search]').first().clear({ force: true }).type(pipe_id);
+      cy.get('input.cd-datatable-checkbox').first().check();
+      cy.get('.table-actions button').first().click();
+    });
+    cy.get('cd-rgw-multisite-sync-pipe-modal').should('exist');
+
+    cy.wait(WAIT_TIMER);
+    // Enter in pipe_id
+    cy.get('#pipe_id').should('contain.value', pipe_id);
+    // Select zone
+    cy.get('a[data-testid=select-menu-edit]').eq(1).click();
+
+    cy.get('.popover-body input').type(`${zoneToAdd}{enter}`);
+
+    cy.get('button.tc_submitButton').click();
+
+    this.getNestedTableCell('cd-rgw-multisite-sync-policy-details', 4, zoneToAdd, true);
+  }
+
+  @PageHelper.restrictTo(pages.index.url)
+  deletePipe(pipe_id: string) {
+    cy.get('cd-rgw-multisite-sync-policy-details').should('exist');
+    this.getTab('Pipe').should('exist');
+    this.getTab('Pipe').click();
+    cy.get('cd-rgw-multisite-sync-policy-details').within(() => {
+      cy.get('.datatable-body-cell-label').should('contain', pipe_id);
+      cy.get('[aria-label=search]').first().clear({ force: true }).type(pipe_id);
+    });
+
+    const getRow = this.getTableCellWithContent.bind(this);
+    getRow('cd-rgw-multisite-sync-policy-details', pipe_id).click();
+
+    cy.get('cd-rgw-multisite-sync-policy-details').within(() => {
+      cy.get('.table-actions button.dropdown-toggle').first().click(); // open submenu
+      cy.get(`button.delete`).first().click();
+    });
+
+    cy.get('cd-modal .custom-control-label').click();
+    cy.get('[aria-label="Delete Pipe"]').click();
+    cy.get('cd-modal').should('not.exist');
+
+    cy.get('cd-rgw-multisite-sync-policy-details')
+      .first()
+      .within(() => {
+        cy.get('[aria-label=search]').first().clear({ force: true }).type(pipe_id);
+      });
+    // Waits for item to be removed from table
+    getRow(pipe_id).should('not.exist');
+  }
 }
index b6f8e3ec4e5af170c65b088dd9f98ef3809f1f6f..974474c593878b7bd98dba00604dc4541fa0e467 100644 (file)
@@ -38,8 +38,7 @@
                 type="text"
                 i18n-placeholder
                 placeholder="Bucket Name..."
-                formControlName="bucket_name"
-                [readonly]="true"/>
+                formControlName="bucket_name"/>
               <span
                 class="invalid-feedback"
                 *ngIf="currentFormGroupContext.showError('bucket_name', frm, 'bucketNameNotAllowed')"
index 95db659cbe363e83437647ad50719c50290cd086..445ef4867ff8c5d181fd5e80edc69cf585bfec42 100644 (file)
@@ -58,7 +58,7 @@ 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,
@@ -161,19 +161,22 @@ export class RgwMultisiteSyncFlowModalComponent implements OnInit {
       this.currentFormGroupContext.setErrors({ cdSubmitButton: true });
       return;
     }
-    this.rgwMultisiteService.createEditSyncFlow(this.currentFormGroupContext.value).subscribe(
-      () => {
-        this.notificationService.show(
-          NotificationType.success,
-          $localize`Created Sync Flow '${this.currentFormGroupContext.getValue('flow_id')}'`
-        );
-        this.activeModal.close('success');
-      },
-      () => {
-        // Reset the 'Submit' button.
-        this.currentFormGroupContext.setErrors({ cdSubmitButton: true });
-        this.activeModal.dismiss();
-      }
-    );
+    this.rgwMultisiteService
+      .createEditSyncFlow(this.currentFormGroupContext.getRawValue())
+      .subscribe(
+        () => {
+          const action = this.editing ? 'Modified' : 'Created';
+          this.notificationService.show(
+            NotificationType.success,
+            $localize`${action} Sync Flow '${this.currentFormGroupContext.getValue('flow_id')}'`
+          );
+          this.activeModal.close('success');
+        },
+        () => {
+          // Reset the 'Submit' button.
+          this.currentFormGroupContext.setErrors({ cdSubmitButton: true });
+          this.activeModal.dismiss();
+        }
+      );
   }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.html
new file mode 100644 (file)
index 0000000..98f3e10
--- /dev/null
@@ -0,0 +1,132 @@
+<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"/>
+          </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"/>
+          </div>
+        </div>
+      </div>
+      <div class="modal-footer">
+        <cd-form-button-panel
+          (submitActionEvent)="submit()"
+          [form]="pipeForm"
+          [submitText]="(action | titlecase) + ' ' + 'Pipe'">
+        </cd-form-button-panel>
+      </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)="onZoneSelection(name)">
+  </cd-select-badges>
+  <i
+    *ngIf="zone.data.selected.length <= 0"
+    i18n-title
+    title="Pipe should be associated with {{ name }}"
+    class="{{ icons.warning }} icon-warning-color">
+  </i>
+  <span
+    class="invalid-feedback"
+    *ngIf="pipeForm.showError(name, frm, 'required')"
+    i18n>{{ name }} selection is required!
+  </span>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.spec.ts
new file mode 100644 (file)
index 0000000..30fd3e4
--- /dev/null
@@ -0,0 +1,36 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwMultisiteSyncPipeModalComponent } from './rgw-multisite-sync-pipe-modal.component';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ToastrModule } from 'ngx-toastr';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { ReactiveFormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+describe('RgwMultisiteSyncPipeModalComponent', () => {
+  let component: RgwMultisiteSyncPipeModalComponent;
+  let fixture: ComponentFixture<RgwMultisiteSyncPipeModalComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [RgwMultisiteSyncPipeModalComponent],
+      imports: [
+        HttpClientTestingModule,
+        ToastrModule.forRoot(),
+        PipesModule,
+        ReactiveFormsModule,
+        CommonModule
+      ],
+      providers: [NgbActiveModal]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(RgwMultisiteSyncPipeModalComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.ts
new file mode 100644 (file)
index 0000000..29d32ea
--- /dev/null
@@ -0,0 +1,137 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { RgwZonegroup } from '../models/rgw-multisite';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+import { catchError, switchMap } from 'rxjs/operators';
+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';
+
+@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;
+  pipeForm: CdFormGroup;
+  action: string;
+  editing: boolean;
+  sourceZones = new ZoneData(false, 'Filter Zones');
+  destZones = new ZoneData(true, 'Filter or Add Zones');
+  icons = Icons;
+
+  constructor(
+    public activeModal: NgbActiveModal,
+    private rgwDaemonService: RgwDaemonService,
+    private rgwZonegroupService: RgwZonegroupService,
+    private rgwMultisiteService: RgwMultisiteService,
+    private notificationService: NotificationService
+  ) {}
+
+  ngOnInit(): void {
+    this.editing = this.action === 'create' ? false : true;
+    this.pipeForm = new CdFormGroup({
+      pipe_id: new UntypedFormControl('', {
+        validators: [Validators.required]
+      }),
+      group_id: new UntypedFormControl(this.groupExpandedRow?.groupName || '', {
+        validators: [Validators.required]
+      }),
+      bucket_name: new UntypedFormControl(this.groupExpandedRow?.bucket || ''),
+      source_bucket: new UntypedFormControl(''),
+      source_zones: new UntypedFormControl('', {
+        validators: [Validators.required]
+      }),
+      destination_bucket: new UntypedFormControl(''),
+      destination_zones: new UntypedFormControl('', {
+        validators: [Validators.required]
+      })
+    });
+    this.pipeForm.get('bucket_name').disable();
+    this.rgwDaemonService.selectedDaemon$
+      .pipe(
+        switchMap((daemon: RgwDaemon) => {
+          if (daemon) {
+            const zonegroupObj = new RgwZonegroup();
+            zonegroupObj.name = daemon.zonegroup_name;
+            return this.rgwZonegroupService.get(zonegroupObj).pipe(
+              catchError(() => {
+                return of([]);
+              })
+            );
+          } else {
+            return of([]);
+          }
+        })
+      )
+      .subscribe((zonegroupData: any) => {
+        if (zonegroupData && zonegroupData?.zones?.length > 0) {
+          let zones: any[] = [];
+          zonegroupData.zones.forEach((zone: any) => {
+            zones.push(new SelectOption(false, zone.name, ''));
+          });
+          this.sourceZones.data.available = [...zones];
+          if (this.editing) {
+            this.sourceZones.data.selected = this.pipeSelectedRow.source.zones;
+            this.destZones.data.selected = this.pipeSelectedRow.dest.zones;
+            this.pipeForm.patchValue({
+              pipe_id: this.pipeSelectedRow.id,
+              source_zones: this.pipeSelectedRow.source.zones,
+              destination_zones: this.pipeSelectedRow.dest.zones,
+              source_bucket: this.pipeSelectedRow.source.bucket,
+              destination_bucket: this.pipeSelectedRow.dest.bucket
+            });
+          }
+        }
+      });
+  }
+
+  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
+      });
+    }
+  }
+
+  submit() {
+    if (this.pipeForm.invalid) {
+      return;
+    }
+    // Ensure that no validation is pending
+    if (this.pipeForm.pending) {
+      this.pipeForm.setErrors({ cdSubmitButton: true });
+      return;
+    }
+    this.rgwMultisiteService.createEditSyncPipe(this.pipeForm.getRawValue()).subscribe(
+      () => {
+        const action = this.editing ? 'Modified' : 'Created';
+        this.notificationService.show(
+          NotificationType.success,
+          $localize`${action} Sync Pipe '${this.pipeForm.getValue('pipe_id')}'`
+        );
+        this.activeModal.close('success');
+      },
+      () => {
+        // Reset the 'Submit' button.
+        this.pipeForm.setErrors({ cdSubmitButton: true });
+        this.activeModal.dismiss();
+      }
+    );
+  }
+}
index 5fabf9d6bcb48e65dd705a587c75cdbe9110c4b7..b9f16ab4d3c7b223621bbda44f36bfdb5580bcbf 100644 (file)
@@ -29,7 +29,7 @@
           [maxLimit]="25"
           [toolHeader]="true"
           (updateSelection)="updateSelection($event, flowType.symmetrical)"
-          (fetchData)="loadFlowData($event)">
+          (fetchData)="loadData($event)">
           <div class="table-actions btn-toolbar">
             <cd-table-actions
               [permission]="permission"
@@ -59,7 +59,7 @@
           [maxLimit]="25"
           [toolHeader]="true"
           (updateSelection)="updateSelection($event, flowType.directional)"
-          (fetchData)="loadFlowData($event)">
+          (fetchData)="loadData($event)">
           <div class="table-actions btn-toolbar">
             <cd-table-actions
               [permission]="permission"
           </div>
         </cd-table>
         <cd-alert-panel
+          *ngIf="dirFlowSelection.hasSelection"
           type="info"
-          *ngIf="dirFlowSelection.hasSelection">
-          'Edit' and 'Delete' functionalities for Directional flow are disabled for now due to some internal dependency. They will be enabled once the issue is resolved.
+          title="Directional Flow 'edit' & 'delete' actions disabled"
+          i18n-title
+          i18n>
+          Due to some internal dependencies these actions are disabled, it will get enabled once the issue gets resolved.
         </cd-alert-panel>
       </ng-template>
     </ng-container>
+    <ng-container ngbNavItem="pipe">
+      <a ngbNavLink
+         i18n>Pipe</a>
+      <ng-template ngbNavContent>
+        <legend i18n>
+          Pipe
+          <cd-help-text>
+            A pipe defines the actual buckets that can use these data flows, and the properties that are associated with it.
+          </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 btn-toolbar">
+          <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>
 
index 14cc7a7f02fae087fd0a01885e44fcf774eaf0a9..b93c4ae788ec446964db799fb24229273a8cbd9a 100644 (file)
@@ -15,6 +15,7 @@ import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { TableComponent } from '~/app/shared/datatable/table/table.component';
 import { RgwMultisiteSyncFlowModalComponent } from '../rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component';
 import { FlowType } from '../models/rgw-multisite';
+import { RgwMultisiteSyncPipeModalComponent } from '../rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component';
 
 @Component({
   selector: 'cd-rgw-multisite-sync-policy-details',
@@ -36,12 +37,16 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges {
   modalRef: NgbModalRef;
   symmetricalFlowData: any = [];
   directionalFlowData: any = [];
+  pipeData: any = [];
   symmetricalFlowCols: CdTableColumn[];
   directionalFlowCols: CdTableColumn[];
+  pipeCols: CdTableColumn[];
   symFlowTableActions: CdTableAction[];
   dirFlowTableActions: CdTableAction[];
+  pipeTableActions: CdTableAction[];
   symFlowSelection = new CdTableSelection();
   dirFlowSelection = new CdTableSelection();
+  pipeSelection = new CdTableSelection();
 
   constructor(
     private actionLabels: ActionLabelsI18n,
@@ -73,6 +78,33 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges {
         flexGrow: 1
       }
     ];
+    this.pipeCols = [
+      {
+        name: 'Name',
+        prop: 'id',
+        flexGrow: 1
+      },
+      {
+        name: 'Source Zone',
+        prop: 'source.zones',
+        flexGrow: 1
+      },
+      {
+        name: 'Destination Zone',
+        prop: 'dest.zones',
+        flexGrow: 1
+      },
+      {
+        name: 'Source Bucket',
+        prop: 'source.bucket',
+        flexGrow: 1
+      },
+      {
+        name: 'Destination Bucket',
+        prop: 'dest.bucket',
+        flexGrow: 1
+      }
+    ];
     const symAddAction: CdTableAction = {
       permission: 'create',
       icon: Icons.add,
@@ -118,17 +150,39 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges {
       canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
     };
     this.dirFlowTableActions = [dirAddAction, dirEditAction, dirDeleteAction];
+    const pipeAddAction: CdTableAction = {
+      permission: 'create',
+      icon: Icons.add,
+      name: this.actionLabels.CREATE,
+      click: () => this.openPipeModal(),
+      canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+    };
+    const pipeEditAction: CdTableAction = {
+      permission: 'update',
+      icon: Icons.edit,
+      name: this.actionLabels.EDIT,
+      click: () => this.openPipeModal(true)
+    };
+    const pipeDeleteAction: CdTableAction = {
+      permission: 'delete',
+      icon: Icons.destroy,
+      disable: () => !this.pipeSelection.hasSelection,
+      name: this.actionLabels.DELETE,
+      click: () => this.deletePipe(),
+      canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
+    };
+    this.pipeTableActions = [pipeAddAction, pipeEditAction, pipeDeleteAction];
   }
 
   ngOnChanges(changes: SimpleChanges): void {
     if (changes.expandedRow.currentValue && changes.expandedRow.currentValue.groupName) {
       this.symmetricalFlowData = [];
       this.directionalFlowData = [];
-      this.loadFlowData();
+      this.loadData();
     }
   }
 
-  loadFlowData(context?: any) {
+  loadData(context?: any) {
     if (this.expandedRow) {
       this.rgwMultisiteService
         .getSyncPolicyGroup(this.expandedRow.groupName, this.expandedRow.bucket)
@@ -136,6 +190,7 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges {
           (policy: any) => {
             this.symmetricalFlowData = policy.data_flow[FlowType.symmetrical] || [];
             this.directionalFlowData = policy.data_flow[FlowType.directional] || [];
+            this.pipeData = policy.pipes || [];
           },
           () => {
             if (context) {
@@ -173,7 +228,7 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges {
     try {
       const res = await this.modalRef.result;
       if (res === 'success') {
-        this.loadFlowData();
+        this.loadData();
       }
     } catch (err) {}
   }
@@ -225,4 +280,67 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges {
       }
     });
   }
+
+  async openPipeModal(edit = false) {
+    const action = edit ? 'edit' : 'create';
+    const initialState = {
+      groupExpandedRow: this.expandedRow,
+      pipeSelectedRow: this.pipeSelection.first(),
+      action: action
+    };
+
+    this.modalRef = this.modalService.show(RgwMultisiteSyncPipeModalComponent, initialState, {
+      size: 'lg'
+    });
+
+    try {
+      const res = await this.modalRef.result;
+      if (res === 'success') {
+        this.loadData();
+      }
+    } catch (err) {}
+  }
+
+  deletePipe() {
+    const pipeIds = this.pipeSelection.selected.map((pipe: any) => pipe.id);
+    this.modalService.show(CriticalConfirmationModalComponent, {
+      itemDescription: this.pipeSelection.hasSingleSelection ? $localize`Pipe` : $localize`Pipes`,
+      itemNames: pipeIds,
+      bodyTemplate: this.deleteTpl,
+      submitActionObservable: () => {
+        return new Observable((observer: Subscriber<any>) => {
+          this.taskWrapper
+            .wrapTaskAroundCall({
+              task: new FinishedTask('rgw/multisite/sync-pipe/delete', {
+                pipe_ids: pipeIds
+              }),
+              call: observableForkJoin(
+                this.pipeSelection.selected.map((pipe: any) => {
+                  return this.rgwMultisiteService.removeSyncPipe(
+                    pipe.id,
+                    this.expandedRow.groupName,
+                    this.expandedRow.bucket
+                  );
+                })
+              )
+            })
+            .subscribe({
+              error: (error: any) => {
+                // Forward the error to the observer.
+                observer.error(error);
+                // Reload the data table content because some deletions might
+                // have been executed successfully in the meanwhile.
+                this.table.refreshBtn();
+              },
+              complete: () => {
+                // Notify the observer that we are done.
+                observer.complete();
+                // Reload the data table content.
+                this.table.refreshBtn();
+              }
+            });
+        });
+      }
+    });
+  }
 }
index 9c5cb55d2bd809e31e15c0029ffc921490b7743d..accd83fc7387b1e9b2fc83e5550dc9a3e6987f5f 100644 (file)
@@ -58,6 +58,7 @@ import { RgwMultisiteSyncPolicyComponent } from './rgw-multisite-sync-policy/rgw
 import { RgwMultisiteSyncPolicyFormComponent } from './rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component';
 import { RgwMultisiteSyncPolicyDetailsComponent } from './rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component';
 import { RgwMultisiteSyncFlowModalComponent } from './rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component';
+import { RgwMultisiteSyncPipeModalComponent } from './rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component';
 
 @NgModule({
   imports: [
@@ -120,7 +121,8 @@ import { RgwMultisiteSyncFlowModalComponent } from './rgw-multisite-sync-flow-mo
     RgwMultisiteSyncPolicyComponent,
     RgwMultisiteSyncPolicyFormComponent,
     RgwMultisiteSyncPolicyDetailsComponent,
-    RgwMultisiteSyncFlowModalComponent
+    RgwMultisiteSyncFlowModalComponent,
+    RgwMultisiteSyncPipeModalComponent
   ],
   providers: [TitleCasePipe]
 })
index cdc85b25b9fb54e0e3f7ac295d4aa3946509b412..01e4ccb9945a9dd9dbaa892dd88efe9f2b0af10a 100644 (file)
@@ -169,4 +169,43 @@ describe('RgwMultisiteService', () => {
     expect(req.request.method).toBe('DELETE');
     req.flush(null);
   });
+
+  it('should create Sync Pipe', () => {
+    const payload = {
+      pipe_id: 'test',
+      bucket_name: 'test',
+      source_zones: ['zone1-zg1-realm1'],
+      destination_zones: ['zone1-zg2-realm2'],
+      group_id: 'sync-grp'
+    };
+    service.createEditSyncPipe(payload).subscribe();
+    const req = httpTesting.expectOne('api/rgw/multisite/sync-pipe');
+    expect(req.request.method).toBe('PUT');
+    expect(req.request.body).toEqual(payload);
+    req.flush(null);
+  });
+
+  it('should edit Symmetrical Sync flow', () => {
+    const payload = {
+      pipe_id: 'test',
+      bucket_name: 'test',
+      source_zones: ['zone1-zg1-realm1'],
+      destination_zones: ['zone1-zg2-realm2', 'zone2-zg1-realm1'],
+      group_id: 'sync-grp'
+    };
+    service.createEditSyncFlow(payload).subscribe();
+    const req = httpTesting.expectOne('api/rgw/multisite/sync-flow');
+    expect(req.request.method).toBe('PUT');
+    expect(req.request.body).toEqual(payload);
+    req.flush(null);
+  });
+
+  it('should remove Sync Pipe', () => {
+    service.removeSyncPipe('test', 'sync-grp', 'new-bucket').subscribe();
+    const req = httpTesting.expectOne(
+      `api/rgw/multisite/sync-pipe/sync-grp/test?bucket_name=new-bucket`
+    );
+    expect(req.request.method).toBe('DELETE');
+    req.flush(null);
+  });
 });
index 41110333531d4bdf66b5df5ab58bae8276496bb8..0048abe3810dc5108408c2f0f0db005a899815e8 100644 (file)
@@ -88,4 +88,19 @@ export class RgwMultisiteService {
       { params }
     );
   }
+
+  createEditSyncPipe(payload: any) {
+    return this.http.put(`${this.url}/sync-pipe`, payload);
+  }
+
+  removeSyncPipe(pipe_id: string, group_id: string, bucket_name?: string) {
+    let params = new HttpParams();
+    if (bucket_name) {
+      params = params.append('bucket_name', encodeURIComponent(bucket_name));
+    }
+    return this.http.delete(
+      `${this.url}/sync-pipe/${encodeURIComponent(group_id)}/${encodeURIComponent(pipe_id)}`,
+      { params }
+    );
+  }
 }
index 399003b1cb64ae86316427a1afb799e82237b139..a3e973da6bd50d570e7c674225e25622906f19db 100644 (file)
@@ -332,9 +332,15 @@ export class TaskMessageService {
       this.commonOperations.delete,
       (metadata) => {
         return $localize`${
-          metadata.flow_ids.length > 1
-            ? 'selected Flow Names'
-            : `Flow Name '${metadata.flow_ids[0]}'`
+          metadata.flow_ids.length > 1 ? 'selected Flow' : `Flow '${metadata.flow_ids[0]}'`
+        }`;
+      }
+    ),
+    'rgw/multisite/sync-pipe/delete': this.newTaskMessage(
+      this.commonOperations.delete,
+      (metadata) => {
+        return $localize`${
+          metadata.pipe_ids.length > 1 ? 'selected pipe' : `Pipe '${metadata.pipe_ids[0]}'`
         }`;
       }
     ),
index 9f9167ef4c435a41ca81449a523fc6eef4417c84..c3292e8c7e54350795d2452dc3656e0d74ace37a 100644 (file)
@@ -10816,7 +10816,8 @@ paths:
                 bucket_name:
                   default: ''
                   type: string
-                destination_buckets:
+                destination_bucket:
+                  default: ''
                   type: string
                 destination_zones:
                   type: string
@@ -10824,6 +10825,9 @@ paths:
                   type: string
                 pipe_id:
                   type: string
+                source_bucket:
+                  default: ''
+                  type: string
                 source_zones:
                   type: string
               required:
@@ -10878,9 +10882,9 @@ paths:
         name: destination_zones
         schema:
           type: string
-      - allowEmptyValue: true
+      - default: ''
         in: query
-        name: destination_buckets
+        name: destination_bucket
         schema:
           type: string
       - default: ''
index cd177e8065bdce3824a7f4fbd0752e6b0264d3df..dfac109edc739e3fb0a99b3d18f77f6097d3711b 100644 (file)
@@ -1948,7 +1948,9 @@ class RgwMultisite:
     def create_sync_pipe(self, group_id: str, pipe_id: str,
                          source_zones: Optional[List[str]] = None,
                          destination_zones: Optional[List[str]] = None,
-                         destination_buckets: Optional[List[str]] = None, bucket_name: str = ''):
+                         source_bucket: str = '',
+                         destination_bucket: str = '',
+                         bucket_name: str = ''):
         rgw_sync_policy_cmd = ['sync', 'group', 'pipe', 'create',
                                '--group-id', group_id, '--pipe-id', pipe_id]
 
@@ -1961,8 +1963,11 @@ class RgwMultisite:
         if destination_zones:
             rgw_sync_policy_cmd += ['--dest-zones', ','.join(destination_zones)]
 
-        if destination_buckets:
-            rgw_sync_policy_cmd += ['--dest-bucket', ','.join(destination_buckets)]
+        if source_bucket:
+            rgw_sync_policy_cmd += ['--source-bucket', source_bucket]
+
+        if destination_bucket:
+            rgw_sync_policy_cmd += ['--dest-bucket', destination_bucket]
 
         try:
             exit_code, _, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd)
@@ -1975,7 +1980,7 @@ class RgwMultisite:
     def remove_sync_pipe(self, group_id: str, pipe_id: str,
                          source_zones: Optional[List[str]] = None,
                          destination_zones: Optional[List[str]] = None,
-                         destination_buckets: Optional[List[str]] = None, bucket_name: str = ''):
+                         destination_bucket: str = '', bucket_name: str = ''):
         rgw_sync_policy_cmd = ['sync', 'group', 'pipe', 'remove',
                                '--group-id', group_id, '--pipe-id', pipe_id]
 
@@ -1988,8 +1993,8 @@ class RgwMultisite:
         if destination_zones:
             rgw_sync_policy_cmd += ['--dest-zones', ','.join(destination_zones)]
 
-        if destination_buckets:
-            rgw_sync_policy_cmd += ['--dest-bucket', ','.join(destination_buckets)]
+        if destination_bucket:
+            rgw_sync_policy_cmd += ['--dest-bucket', destination_bucket]
 
         try:
             exit_code, _, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd)
@@ -2014,7 +2019,7 @@ class RgwMultisite:
                               zones=zone_names)
         # create a sync pipe with source and destination zones
         self.create_sync_pipe(_SYNC_GROUP_ID, _SYNC_PIPE_ID, source_zones=['*'],
-                              destination_zones=['*'], destination_buckets=['*'])
+                              destination_zones=['*'], source_bucket='*', destination_bucket='*')
         # period update --commit
         self.update_period()