]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: RGW multisite sync flow 58544/head
authorNaman Munet <namanmunet@Namans-MacBook-Pro.local>
Thu, 11 Jul 2024 18:40:51 +0000 (00:10 +0530)
committerNaman Munet <nmunet@redhat.com>
Tue, 16 Jul 2024 06:47:46 +0000 (12:17 +0530)
Fixes: https://tracker.ceph.com/issues/66915
Signed-off-by: Naman Munet <nmunet@redhat.com>
22 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/models/rgw-multisite-zone-selector.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-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 [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.scss [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.spec.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.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy/rgw-multisite-sync-policy.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy/rgw-multisite-sync-policy.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 c479214dce365414ac2192be52e0bce57e03c2d1..014d9eb953b797e33dfb060fb8351ef093960a38 100644 (file)
@@ -200,7 +200,9 @@ class RgwMultisiteController(RESTController):
     @EndpointDoc("Create or update the sync flow")
     @CreatePermission
     def create_sync_flow(self, flow_id: str, flow_type: str, group_id: str,
-                         source_zone='', destination_zone='', zones: Optional[List[str]] = None,
+                         source_zone: Optional[List[str]] = None,
+                         destination_zone: Optional[List[str]] = None,
+                         zones: Optional[List[str]] = None,
                          bucket_name=''):
         multisite_instance = RgwMultisite()
         return multisite_instance.create_sync_flow(group_id, flow_id, flow_type, zones,
index 49144b25fbfc9347e961ff9f657e2637bf0a147f..6a7eb2a802d7f67e10456f6d49d2004d3c282e24 100644 (file)
@@ -306,4 +306,33 @@ export abstract class PageHelper {
     // Waits for item to be removed from table
     getRow(name).should('not.exist');
   }
+
+  getNestedTableCell(
+    tableSelector: string,
+    columnIndex: number,
+    exactContent: string,
+    partialMatch = false
+  ) {
+    this.waitDataTableToLoad();
+    this.clearTableSearchInput();
+    this.searchNestedTable(tableSelector, exactContent);
+    if (partialMatch) {
+      return cy
+        .get(`${tableSelector} datatable-body-row datatable-body-cell:nth-child(${columnIndex})`)
+        .should('contain', exactContent);
+    }
+    return cy
+      .get(`${tableSelector}`)
+      .contains(
+        `datatable-body-row datatable-body-cell:nth-child(${columnIndex})`,
+        new RegExp(`^${exactContent}$`)
+      );
+  }
+
+  searchNestedTable(tableSelector: string, text: string) {
+    this.waitDataTableToLoad();
+
+    this.setPageSize('10');
+    cy.get(`${tableSelector} [aria-label=search]`).first().clear({ force: true }).type(text);
+  }
 }
index 5633bb2f5b4b8e748481db588d5ac7495b458ed1..e884ec624e34d86adce7c513e76bd1a172034959 100644 (file)
@@ -20,11 +20,6 @@ describe('Multisite page', () => {
     it('should show sync policy tab as a second tab', () => {
       multisite.getTabText(1).should('eq', 'Sync Policy');
     });
-
-    it('should show empty table in Sync Policy page', () => {
-      multisite.getTab('Sync Policy').click();
-      multisite.getDataTables().should('exist');
-    });
   });
 
   describe('create, edit & delete sync group policy', () => {
@@ -43,4 +38,45 @@ describe('Multisite page', () => {
       multisite.delete('test');
     });
   });
+
+  describe('create, edit & delete symmetrical sync Flow', () => {
+    it('Preparing...(creating sync group policy)', () => {
+      multisite.navigateTo('create');
+      multisite.create('test', 'Enabled');
+      multisite.getFirstTableCell('test').should('exist');
+    });
+    describe('symmetrical Flow creation started', () => {
+      beforeEach(() => {
+        multisite.getTab('Sync Policy').click();
+        multisite.getExpandCollapseElement().click();
+      });
+
+      it('should create flow', () => {
+        multisite.createSymmetricalFlow('new-sym-flow', ['zone1-zg1-realm1']);
+      });
+
+      it('should modify flow zones', () => {
+        multisite.editSymFlow('new-sym-flow', 'zone2-zg1-realm1');
+      });
+
+      it('should delete flow', () => {
+        multisite.deleteSymFlow('new-sym-flow');
+      });
+    });
+  });
+
+  describe('create, edit & delete directional sync Flow', () => {
+    beforeEach(() => {
+      multisite.getTab('Sync Policy').click();
+      multisite.getExpandCollapseElement().click();
+    });
+
+    it('should create flow', () => {
+      multisite.createDirectionalFlow(
+        'new-dir-flow',
+        ['zone1-zg1-realm1', 'zone2-zg1-realm1'],
+        ['new-zone']
+      );
+    });
+  });
 });
index bbeda74e9cfac63c458f83b6a3622406ea456f2a..d912f6b1ca02484cb00169cd5551f226912d2a68 100644 (file)
@@ -10,7 +10,7 @@ export class MultisitePageHelper extends PageHelper {
   pages = pages;
 
   columnIndex = {
-    status: 3
+    status: 4
   };
 
   @PageHelper.restrictTo(pages.create.url)
@@ -39,4 +39,150 @@ export class MultisitePageHelper extends PageHelper {
       .find('.badge-warning')
       .should('contain', status);
   }
+
+  @PageHelper.restrictTo(pages.index.url)
+  createSymmetricalFlow(flow_id: string, zones: string[]) {
+    cy.get('cd-rgw-multisite-sync-policy-details').should('exist');
+    this.getTab('Flow').should('exist');
+    this.getTab('Flow').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-flow-modal').should('exist');
+
+    // Enter in flow_id
+    cy.get('#flow_id').type(flow_id);
+    // Select zone
+    cy.get('a[data-testid=select-menu-edit]').click();
+    for (const zone of zones) {
+      cy.get('.popover-body div.select-menu-item-content').contains(zone).click();
+    }
+
+    cy.get('button.tc_submitButton').click();
+
+    cy.get('cd-rgw-multisite-sync-policy-details .datatable-body-cell-label').should(
+      'contain',
+      flow_id
+    );
+
+    cy.get('cd-rgw-multisite-sync-policy-details')
+      .first()
+      .find('[aria-label=search]')
+      .first()
+      .clear({ force: true })
+      .type(flow_id);
+  }
+
+  @PageHelper.restrictTo(pages.index.url)
+  editSymFlow(flow_id: string, zoneToAdd: string) {
+    cy.get('cd-rgw-multisite-sync-policy-details').should('exist');
+    this.getTab('Flow').should('exist');
+    this.getTab('Flow').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', flow_id);
+      cy.get('[aria-label=search]').first().clear({ force: true }).type(flow_id);
+      cy.get('input.cd-datatable-checkbox').first().check();
+      cy.get('.table-actions button').first().click();
+    });
+    cy.get('cd-rgw-multisite-sync-flow-modal').should('exist');
+
+    // Enter in flow_id
+    cy.get('#flow_id').wait(100).should('contain.value', flow_id);
+    // Select zone
+    cy.get('a[data-testid=select-menu-edit]').click();
+
+    cy.get('.popover-body div.select-menu-item-content').contains(zoneToAdd).click();
+
+    cy.get('button.tc_submitButton').click();
+
+    this.getNestedTableCell('cd-rgw-multisite-sync-policy-details', 3, zoneToAdd, true);
+  }
+
+  getTableCellWithContent(nestedClass: string, content: string) {
+    return cy.contains(`${nestedClass} .datatable-body-cell-label`, content);
+  }
+
+  @PageHelper.restrictTo(pages.index.url)
+  deleteSymFlow(flow_id: string) {
+    cy.get('cd-rgw-multisite-sync-policy-details').should('exist');
+    this.getTab('Flow').should('exist');
+    this.getTab('Flow').click();
+    cy.get('cd-rgw-multisite-sync-policy-details').within(() => {
+      cy.get('.datatable-body-cell-label').should('contain', flow_id);
+      cy.get('[aria-label=search]').first().clear({ force: true }).type(flow_id);
+    });
+
+    const getRow = this.getTableCellWithContent.bind(this);
+    getRow('cd-rgw-multisite-sync-policy-details', flow_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 Flow"]').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(flow_id);
+      });
+    // Waits for item to be removed from table
+    getRow(flow_id).should('not.exist');
+  }
+
+  createDirectionalFlow(flow_id: string, source_zones: string[], dest_zones: string[]) {
+    cy.get('cd-rgw-multisite-sync-policy-details').should('exist');
+    this.getTab('Flow').should('exist');
+    this.getTab('Flow').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 cd-table')
+      .eq(1)
+      .find('.table-actions button')
+      .first()
+      .click();
+    cy.get('cd-rgw-multisite-sync-flow-modal').should('exist');
+    cy.wait(WAIT_TIMER);
+    // Enter in flow_id
+    cy.get('#flow_id').type(flow_id);
+    // Select source zone
+    cy.get('a[data-testid=select-menu-edit]').first().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-flow-modal').click();
+
+    // Select destination zone
+    cy.get('a[data-testid=select-menu-edit]').eq(1).click();
+    for (const dest_zone of dest_zones) {
+      cy.get('.popover-body').find('input[type="text"]').type(`${dest_zone}{enter}`);
+    }
+    cy.get('button.tc_submitButton').click();
+
+    cy.get('cd-rgw-multisite-sync-policy-details cd-table')
+      .eq(1)
+      .find('[aria-label=search]')
+      .first()
+      .clear({ force: true })
+      .type(dest_zones[0]);
+    cy.get('cd-rgw-multisite-sync-policy-details cd-table')
+      .eq(1)
+      .find('.datatable-body-cell-label')
+      .should('contain', dest_zones[0]);
+  }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-selector.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-selector.ts
new file mode 100644 (file)
index 0000000..011aa06
--- /dev/null
@@ -0,0 +1,33 @@
+import { Validators } from '@angular/forms';
+import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+
+interface Zone {
+  selected: string[];
+  available: SelectOption[];
+  validators: any[];
+  messages: SelectMessages;
+}
+
+export class ZoneData {
+  data: Zone;
+  customBadges: boolean;
+
+  constructor(customBadges: boolean = false, filterMsg: string) {
+    this.customBadges = customBadges;
+    this.data = {
+      selected: [],
+      available: [],
+      validators: [Validators.pattern('[A-Za-z0-9_-]+|\\*'), Validators.maxLength(50)],
+      messages: new SelectMessages({
+        empty: $localize`No zones added`,
+        customValidations: {
+          pattern: $localize`Allowed characters '-_a-zA-Z0-9|*'`,
+          maxlength: $localize`Maximum length is 50 characters`
+        },
+        filter: $localize`${filterMsg}`,
+        add: $localize`Add zone`
+      })
+    };
+  }
+}
index f2fc381e806f148a84a2239b7d137a70b51f7895..bc2f8eafe5b45808c87a77a184363d33499643f1 100644 (file)
@@ -56,3 +56,8 @@ export enum RgwMultisiteSyncPolicyStatus {
   FORBIDDEN = 'forbidden',
   ALLOWED = 'allowed'
 }
+
+export enum FlowType {
+  directional = 'directional',
+  symmetrical = 'symmetrical'
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.html
new file mode 100644 (file)
index 0000000..b6f8e3e
--- /dev/null
@@ -0,0 +1,121 @@
+<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"
+                [readonly]="editing"/>
+            </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"
+                [readonly]="true"/>
+              <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="zoneMultiSelect;context: { name: 'source_zone', 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_zone', zone: destinationZones }"></ng-container>
+              </div>
+            </div>
+          </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>
+        </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('_')}}"
+    class="{{ icons.warning }} icon-warning-color">
+  </i>
+  <span
+    class="invalid-feedback"
+    *ngIf="currentFormGroupContext.showError(name, frm, 'required')"
+    i18n>{{name?.split('_')}} selection is required!
+  </span>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-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-flow-modal/rgw-multisite-sync-flow-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.spec.ts
new file mode 100644 (file)
index 0000000..fb864ba
--- /dev/null
@@ -0,0 +1,40 @@
+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';
+import { ReactiveFormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+
+enum FlowType {
+  symmetrical = 'symmetrical',
+  directional = 'directional'
+}
+describe('RgwMultisiteSyncFlowModalComponent', () => {
+  let component: RgwMultisiteSyncFlowModalComponent;
+  let fixture: ComponentFixture<RgwMultisiteSyncFlowModalComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [RgwMultisiteSyncFlowModalComponent],
+      imports: [
+        HttpClientTestingModule,
+        ToastrModule.forRoot(),
+        PipesModule,
+        ReactiveFormsModule,
+        CommonModule
+      ],
+      providers: [NgbActiveModal]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(RgwMultisiteSyncFlowModalComponent);
+    component = fixture.componentInstance;
+    component.groupType = FlowType.symmetrical;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.ts
new file mode 100644 (file)
index 0000000..95db659
--- /dev/null
@@ -0,0 +1,179 @@
+import { Component, 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 } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { catchError, switchMap } from 'rxjs/operators';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { RgwDaemon } from '../models/rgw-daemon';
+import { FlowType, RgwZonegroup } from '../models/rgw-multisite';
+import { of } from 'rxjs';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+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';
+
+@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;
+  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');
+  sourceZones = new ZoneData(false, 'Filter Zones');
+  destinationZones = new ZoneData(true, 'Filter or Add Zones');
+
+  constructor(
+    public activeModal: NgbActiveModal,
+    public actionLabels: ActionLabelsI18n,
+    public notificationService: NotificationService,
+    private rgwDaemonService: RgwDaemonService,
+    private rgwZonegroupService: RgwZonegroupService,
+    private rgwMultisiteService: RgwMultisiteService
+  ) {}
+
+  ngOnInit(): void {
+    if (this.action === 'edit') {
+      this.editing = true;
+    }
+    if (this.groupType === FlowType.symmetrical) {
+      this.createSymmetricalFlowForm();
+      this.currentFormGroupContext = _.cloneDeep(this.syncPolicySymmetricalFlowForm);
+    } else if (this.groupType === FlowType.directional) {
+      this.createDirectionalFlowForm();
+      this.currentFormGroupContext = _.cloneDeep(this.syncPolicyDirectionalFlowForm);
+    }
+
+    if (this.editing) {
+      this.currentFormGroupContext.patchValue({
+        flow_id: this.flowSelectedRow.id,
+        bucket_name: this.groupExpandedRow.bucket || ''
+      });
+    }
+
+    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) {
+          const zones: any = [];
+          zonegroupData.zones.forEach((zone: any) => {
+            zones.push(new SelectOption(false, zone.name, ''));
+          });
+          this.zones.data.available = [...zones];
+          this.sourceZones.data.available = [...zones];
+          if (this.editing) {
+            if (this.groupType === FlowType.symmetrical) {
+              this.zones.data.selected = this.flowSelectedRow.zones;
+            } else {
+              this.destinationZones.data.selected = [this.flowSelectedRow.dest_zone];
+              this.sourceZones.data.selected = [this.flowSelectedRow.source_zone];
+            }
+            this.zoneSelection();
+          }
+        }
+      });
+  }
+
+  createSymmetricalFlowForm() {
+    this.syncPolicySymmetricalFlowForm = new CdFormGroup({
+      ...this.commonFormControls(FlowType.symmetrical),
+      zones: new UntypedFormControl([], {
+        validators: [Validators.required]
+      })
+    });
+  }
+
+  createDirectionalFlowForm() {
+    this.syncPolicyDirectionalFlowForm = new CdFormGroup({
+      ...this.commonFormControls(FlowType.directional),
+      source_zone: new UntypedFormControl('', {
+        validators: [Validators.required]
+      }),
+      destination_zone: new UntypedFormControl('', {
+        validators: [Validators.required]
+      })
+    });
+  }
+
+  commonFormControls(flowType: FlowType) {
+    return {
+      bucket_name: new UntypedFormControl(this.groupExpandedRow?.bucket),
+      group_id: new UntypedFormControl(this.groupExpandedRow?.groupName, {
+        validators: [Validators.required]
+      }),
+      flow_id: new UntypedFormControl('', {
+        validators: [Validators.required]
+      }),
+      flow_type: new UntypedFormControl(flowType, {
+        validators: [Validators.required]
+      })
+    };
+  }
+
+  zoneSelection() {
+    if (this.groupType === FlowType.symmetrical) {
+      this.currentFormGroupContext.patchValue({
+        zones: this.zones.data.selected
+      });
+    } else {
+      this.currentFormGroupContext.patchValue({
+        source_zone: this.sourceZones.data.selected,
+        destination_zone: this.destinationZones.data.selected
+      });
+    }
+  }
+
+  submit() {
+    if (this.currentFormGroupContext.invalid) {
+      return;
+    }
+    // Ensure that no validation is pending
+    if (this.currentFormGroupContext.pending) {
+      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();
+      }
+    );
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.html
new file mode 100644 (file)
index 0000000..5fabf9d
--- /dev/null
@@ -0,0 +1,89 @@
+<ng-container *ngIf="expandedRow">
+  <nav
+    ngbNav
+    #nav="ngbNav"
+    class="nav-tabs"
+    cdStatefulTab="sync-policy-details">
+    <ng-container ngbNavItem="flow">
+      <a
+        ngbNavLink
+        i18n>Flow</a>
+      <ng-template ngbNavContent>
+        <legend>
+          Symmetrical
+          <cd-help-text>
+            It can define symmetrical data flow, in which multiple zones sync data from each other.
+          </cd-help-text>
+        </legend>
+        <cd-table
+          #table
+          [autoReload]="false"
+          [data]="symmetricalFlowData"
+          [columns]="symmetricalFlowCols"
+          columnMode="flex"
+          selectionType="multiClick"
+          [searchableObjects]="true"
+          [hasDetails]="false"
+          [serverSide]="false"
+          [count]="0"
+          [maxLimit]="25"
+          [toolHeader]="true"
+          (updateSelection)="updateSelection($event, flowType.symmetrical)"
+          (fetchData)="loadFlowData($event)">
+          <div class="table-actions btn-toolbar">
+            <cd-table-actions
+              [permission]="permission"
+              [selection]="symFlowSelection"
+              class="btn-group"
+              [tableActions]="symFlowTableActions" >
+            </cd-table-actions>
+          </div>
+        </cd-table>
+        <legend>
+          Directional
+          <cd-help-text>
+            It can define directional data flow, in which the data moves in one way, from one zone to another.
+          </cd-help-text>
+        </legend>
+        <cd-table
+          #table
+          [autoReload]="false"
+          [data]="directionalFlowData"
+          [columns]="directionalFlowCols"
+          columnMode="flex"
+          selectionType="multiClick"
+          [searchableObjects]="true"
+          [hasDetails]="false"
+          [serverSide]="false"
+          [count]="0"
+          [maxLimit]="25"
+          [toolHeader]="true"
+          (updateSelection)="updateSelection($event, flowType.directional)"
+          (fetchData)="loadFlowData($event)">
+          <div class="table-actions btn-toolbar">
+            <cd-table-actions
+              [permission]="permission"
+              [selection]="dirFlowSelection"
+              class="btn-group"
+              [tableActions]="dirFlowTableActions">
+            </cd-table-actions>
+          </div>
+        </cd-table>
+        <cd-alert-panel
+          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.
+        </cd-alert-panel>
+      </ng-template>
+    </ng-container>
+  </nav>
+
+  <div [ngbNavOutlet]="nav"></div>
+</ng-container>
+
+<ng-template #deleteTpl>
+  <cd-alert-panel type="danger"
+                  i18n>
+    Are you sure you want to delete these Flow?
+  </cd-alert-panel>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.scss
new file mode 100644 (file)
index 0000000..e1fec97
--- /dev/null
@@ -0,0 +1,3 @@
+::ng-deep datatable-scroller {
+  width: 100% !important;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.spec.ts
new file mode 100644 (file)
index 0000000..ae3ab13
--- /dev/null
@@ -0,0 +1,26 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwMultisiteSyncPolicyDetailsComponent } from './rgw-multisite-sync-policy-details.component';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ToastrModule } from 'ngx-toastr';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+
+describe('RgwMultisiteSyncPolicyDetailsComponent', () => {
+  let component: RgwMultisiteSyncPolicyDetailsComponent;
+  let fixture: ComponentFixture<RgwMultisiteSyncPolicyDetailsComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [RgwMultisiteSyncPolicyDetailsComponent],
+      imports: [HttpClientTestingModule, ToastrModule.forRoot(), PipesModule]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(RgwMultisiteSyncPolicyDetailsComponent);
+    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-policy-details/rgw-multisite-sync-policy-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.ts
new file mode 100644 (file)
index 0000000..14cc7a7
--- /dev/null
@@ -0,0 +1,228 @@
+import { Component, Input, OnChanges, SimpleChanges, TemplateRef, ViewChild } from '@angular/core';
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+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 { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Observable, Subscriber, forkJoin as observableForkJoin } from 'rxjs';
+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';
+
+@Component({
+  selector: 'cd-rgw-multisite-sync-policy-details',
+  templateUrl: './rgw-multisite-sync-policy-details.component.html',
+  styleUrls: ['./rgw-multisite-sync-policy-details.component.scss']
+})
+export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges {
+  @Input()
+  expandedRow: any;
+  @Input()
+  permission: Permission;
+
+  @ViewChild(TableComponent)
+  table: TableComponent;
+  @ViewChild('deleteTpl', { static: true })
+  deleteTpl: TemplateRef<any>;
+
+  flowType = FlowType;
+  modalRef: NgbModalRef;
+  symmetricalFlowData: any = [];
+  directionalFlowData: any = [];
+  symmetricalFlowCols: CdTableColumn[];
+  directionalFlowCols: CdTableColumn[];
+  symFlowTableActions: CdTableAction[];
+  dirFlowTableActions: CdTableAction[];
+  symFlowSelection = new CdTableSelection();
+  dirFlowSelection = new CdTableSelection();
+
+  constructor(
+    private actionLabels: ActionLabelsI18n,
+    private modalService: ModalService,
+    private rgwMultisiteService: RgwMultisiteService,
+    private taskWrapper: TaskWrapperService
+  ) {
+    this.symmetricalFlowCols = [
+      {
+        name: 'Name',
+        prop: 'id',
+        flexGrow: 1
+      },
+      {
+        name: 'Zones',
+        prop: 'zones',
+        flexGrow: 1
+      }
+    ];
+    this.directionalFlowCols = [
+      {
+        name: 'Source Zone',
+        prop: 'source_zone',
+        flexGrow: 1
+      },
+      {
+        name: 'Destination Zone',
+        prop: 'dest_zone',
+        flexGrow: 1
+      }
+    ];
+    const symAddAction: CdTableAction = {
+      permission: 'create',
+      icon: Icons.add,
+      name: this.actionLabels.CREATE,
+      click: () => this.openModal(FlowType.symmetrical),
+      canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+    };
+    const symEditAction: CdTableAction = {
+      permission: 'update',
+      icon: Icons.edit,
+      name: this.actionLabels.EDIT,
+      click: () => this.openModal(FlowType.symmetrical, true)
+    };
+    const symDeleteAction: CdTableAction = {
+      permission: 'delete',
+      icon: Icons.destroy,
+      disable: () => !this.symFlowSelection.hasSelection,
+      name: this.actionLabels.DELETE,
+      click: () => this.deleteFlow(FlowType.symmetrical),
+      canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
+    };
+    this.symFlowTableActions = [symAddAction, symEditAction, symDeleteAction];
+    const dirAddAction: CdTableAction = {
+      permission: 'create',
+      icon: Icons.add,
+      name: this.actionLabels.CREATE,
+      click: () => this.openModal(FlowType.directional),
+      canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+    };
+    const dirEditAction: CdTableAction = {
+      permission: 'update',
+      icon: Icons.edit,
+      name: this.actionLabels.EDIT,
+      click: () => this.openModal(FlowType.directional, true),
+      disable: () => true // TODO: disabling 'edit' as we are not getting flow ID from backend which is needed for edit
+    };
+    const dirDeleteAction: CdTableAction = {
+      permission: 'delete',
+      icon: Icons.destroy,
+      disable: () => true, // TODO: disabling 'delete' as we are not getting flow ID from backend which is needed for deletion
+      name: this.actionLabels.DELETE,
+      click: () => this.deleteFlow(FlowType.directional),
+      canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
+    };
+    this.dirFlowTableActions = [dirAddAction, dirEditAction, dirDeleteAction];
+  }
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.expandedRow.currentValue && changes.expandedRow.currentValue.groupName) {
+      this.symmetricalFlowData = [];
+      this.directionalFlowData = [];
+      this.loadFlowData();
+    }
+  }
+
+  loadFlowData(context?: any) {
+    if (this.expandedRow) {
+      this.rgwMultisiteService
+        .getSyncPolicyGroup(this.expandedRow.groupName, this.expandedRow.bucket)
+        .subscribe(
+          (policy: any) => {
+            this.symmetricalFlowData = policy.data_flow[FlowType.symmetrical] || [];
+            this.directionalFlowData = policy.data_flow[FlowType.directional] || [];
+          },
+          () => {
+            if (context) {
+              context.error();
+            }
+          }
+        );
+    }
+  }
+
+  updateSelection(selection: any, type: FlowType) {
+    if (type === FlowType.directional) {
+      this.dirFlowSelection = selection;
+    } else {
+      this.symFlowSelection = selection;
+    }
+  }
+
+  async openModal(flowType: FlowType, edit = false) {
+    const action = edit ? 'edit' : 'create';
+    const initialState = {
+      groupType: flowType,
+      groupExpandedRow: this.expandedRow,
+      flowSelectedRow:
+        flowType === FlowType.symmetrical
+          ? this.symFlowSelection.first()
+          : this.dirFlowSelection.first(),
+      action: action
+    };
+
+    this.modalRef = this.modalService.show(RgwMultisiteSyncFlowModalComponent, initialState, {
+      size: 'lg'
+    });
+
+    try {
+      const res = await this.modalRef.result;
+      if (res === 'success') {
+        this.loadFlowData();
+      }
+    } catch (err) {}
+  }
+
+  deleteFlow(flowType: FlowType) {
+    let selection = this.symFlowSelection;
+    if (flowType === FlowType.directional) {
+      selection = this.dirFlowSelection;
+    }
+    const flowIds = selection.selected.map((flow: any) => flow.id);
+    this.modalService.show(CriticalConfirmationModalComponent, {
+      itemDescription: selection.hasSingleSelection ? $localize`Flow` : $localize`Flows`,
+      itemNames: flowIds,
+      bodyTemplate: this.deleteTpl,
+      submitActionObservable: () => {
+        return new Observable((observer: Subscriber<any>) => {
+          this.taskWrapper
+            .wrapTaskAroundCall({
+              task: new FinishedTask('rgw/multisite/sync-flow/delete', {
+                flow_ids: flowIds
+              }),
+              call: observableForkJoin(
+                selection.selected.map((flow: any) => {
+                  return this.rgwMultisiteService.removeSyncFlow(
+                    flow.id,
+                    flowType,
+                    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 8909f44c32b394d721c5e19ed30ad2667c426705..2dd4e239cf57ff6589e3d596941580a8396dd5ef 100644 (file)
@@ -2,7 +2,8 @@
   Multisite Sync Policy
   <cd-help-text>
     Multisite bucket-granularity sync policy provides fine grained control of data movement between
-    buckets in different zones.
+    buckets in different zones. Leveraging the bucket-granularity sync policy is possible for buckets to diverge,
+     and a bucket can pull data from other buckets (ones that don’t share its name or its ID) in different zone.
   </cd-help-text>
 </legend>
 <cd-table
   [columns]="columns"
   identifier="uniqueId"
   [forceIdentifier]="true"
-  columnMode="flex"
   selectionType="multiClick"
   [searchableObjects]="true"
-  [hasDetails]="false"
-  [serverSide]="false"
-  [count]="0"
-  [maxLimit]="25"
+  [hasDetails]="true"
   [toolHeader]="true"
+  (setExpandedRow)="setExpandedRow($event)"
   (fetchData)="getPolicyList($event)"
   (updateSelection)="updateSelection($event)">
   <div class="table-actions btn-toolbar">
       [tableActions]="tableActions">
     </cd-table-actions>
   </div>
+  <cd-rgw-multisite-sync-policy-details
+    cdTableDetail
+    [expandedRow]="expandedRow"
+    [permission]="permission">
+  </cd-rgw-multisite-sync-policy-details>
 </cd-table>
 
 <ng-template #deleteTpl>
index 2ed1ec0f7e2c26e583ca22ac8502b08bb42ae4fb..422e796671b38bfb7fc9987352d1f7fb6af923fb 100644 (file)
@@ -2,6 +2,7 @@ import { TitleCasePipe } from '@angular/common';
 import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
 import { forkJoin as observableForkJoin, Observable, Subscriber } from 'rxjs';
 import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { TableComponent } from '~/app/shared/datatable/table/table.component';
@@ -26,7 +27,7 @@ const BASE_URL = 'rgw/multisite/sync-policy';
   styleUrls: ['./rgw-multisite-sync-policy.component.scss'],
   providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
 })
-export class RgwMultisiteSyncPolicyComponent implements OnInit {
+export class RgwMultisiteSyncPolicyComponent extends ListWithDetails implements OnInit {
   @ViewChild(TableComponent, { static: true })
   table: TableComponent;
   @ViewChild('deleteTpl', { static: true })
@@ -46,7 +47,9 @@ export class RgwMultisiteSyncPolicyComponent implements OnInit {
     private authStorageService: AuthStorageService,
     private modalService: ModalService,
     private taskWrapper: TaskWrapperService
-  ) {}
+  ) {
+    super();
+  }
 
   ngOnInit(): void {
     this.permission = this.authStorageService.getPermissions().rgw;
index 9767230ba1b402bdd2a4be936e1aad75d83f948c..66a7c5d2db4f897bd7e2f58047a354fce0295fc5 100644 (file)
@@ -59,6 +59,8 @@ import { RgwMultisiteSyncPolicyFormComponent } from './rgw-multisite-sync-policy
 import { RgwConfigurationPageComponent } from './rgw-configuration-page/rgw-configuration-page.component';
 import { RgwConfigDetailsComponent } from './rgw-config-details/rgw-config-details.component';
 import { RgwMultisiteWizardComponent } from './rgw-multisite-wizard/rgw-multisite-wizard.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';
 
 @NgModule({
   imports: [
@@ -122,7 +124,9 @@ import { RgwMultisiteWizardComponent } from './rgw-multisite-wizard/rgw-multisit
     RgwMultisiteSyncPolicyFormComponent,
     RgwConfigDetailsComponent,
     RgwConfigurationPageComponent,
-    RgwMultisiteWizardComponent
+    RgwMultisiteWizardComponent,
+    RgwMultisiteSyncPolicyDetailsComponent,
+    RgwMultisiteSyncFlowModalComponent
   ],
   providers: [TitleCasePipe]
 })
index 424eb21e41b2a38e38f5969f805d4d4afb3f8009..cdc85b25b9fb54e0e3f7ac295d4aa3946509b412 100644 (file)
@@ -89,4 +89,84 @@ describe('RgwMultisiteService', () => {
     expect(req.request.method).toBe('GET');
     req.flush(mockSyncPolicyData[1]);
   });
+
+  it('should create Symmetrical Sync flow', () => {
+    const payload = {
+      group_id: 'test',
+      bucket_name: 'test',
+      flow_type: 'symmetrical',
+      flow_id: 'new-flow',
+      zones: ['zone1-zg1-realm1']
+    };
+    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 create Directional Sync flow', () => {
+    const payload = {
+      group_id: 'test',
+      bucket_name: 'test',
+      flow_type: 'directional',
+      flow_id: 'new-flow',
+      source_zone: ['zone1-zg1-realm1'],
+      destination_zone: ['zone1-zg2-realm2']
+    };
+    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 edit Symmetrical Sync flow', () => {
+    const payload = {
+      group_id: 'test',
+      bucket_name: 'test',
+      flow_type: 'symmetrical',
+      flow_id: 'new-flow',
+      zones: ['zone1-zg1-realm1', 'zone2-zg1-realm1']
+    };
+    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 edit Directional Sync flow', () => {
+    const payload = {
+      group_id: 'test',
+      bucket_name: 'test',
+      flow_type: 'directional',
+      flow_id: 'new-flow',
+      source_zone: ['zone1-zg1-realm1'],
+      destination_zone: ['zone1-zg2-realm2', 'zone2-zg2-realm2']
+    };
+    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 Symmetrical Sync flow', () => {
+    service.removeSyncFlow('test', 'symmetrical', 'test', 'new-bucket').subscribe();
+    const req = httpTesting.expectOne(
+      `api/rgw/multisite/sync-flow/test/symmetrical/test?bucket_name=new-bucket`
+    );
+    expect(req.request.method).toBe('DELETE');
+    req.flush(null);
+  });
+
+  it('should remove Directional Sync flow', () => {
+    service.removeSyncFlow('test', 'directional', 'test', 'new-bucket').subscribe();
+    const req = httpTesting.expectOne(
+      `api/rgw/multisite/sync-flow/test/directional/test?bucket_name=new-bucket`
+    );
+    expect(req.request.method).toBe('DELETE');
+    req.flush(null);
+  });
 });
index e77e6afab1f926350eeb2ac7b16c39573ee15301..e9d4f398707e6b229311a3056512b6efc89e84c5 100644 (file)
@@ -95,4 +95,21 @@ export class RgwMultisiteService {
 
     return this.http.post(`${this.uiUrl}/multisite-replications`, null, { params: params });
   }
+
+  createEditSyncFlow(payload: any) {
+    return this.http.put(`${this.url}/sync-flow`, payload);
+  }
+
+  removeSyncFlow(flow_id: string, flow_type: 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-flow/${encodeURIComponent(flow_id)}/${flow_type}/${encodeURIComponent(
+        group_id
+      )}`,
+      { params }
+    );
+  }
 }
index 71621072783c19b655529c80c344c1f477d54ea7..0e966a9474b7bca2d9ad827851818d2a3c421de7 100644 (file)
@@ -334,6 +334,16 @@ export class TaskMessageService {
         }`;
       }
     ),
+    'rgw/multisite/sync-flow/delete': this.newTaskMessage(
+      this.commonOperations.delete,
+      (metadata) => {
+        return $localize`${
+          metadata.flow_ids.length > 1
+            ? 'selected Flow Names'
+            : `Flow Name '${metadata.flow_ids[0]}'`
+        }`;
+      }
+    ),
     // iSCSI target tasks
     'iscsi/target/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
       this.iscsiTarget(metadata)
index ecf305d77acee2b8ec6b0816ec04ca1917a98cee..98c6c562f098586c7f6a4f29902f5a616a394a5e 100644 (file)
@@ -11168,7 +11168,6 @@ paths:
                   default: ''
                   type: string
                 destination_zone:
-                  default: ''
                   type: string
                 flow_id:
                   type: string
@@ -11177,7 +11176,6 @@ paths:
                 group_id:
                   type: string
                 source_zone:
-                  default: ''
                   type: string
                 zones:
                   type: string
index 2fb30b67f43c1b73c5c0ea7429c126a042dd9416..4a9b9b66a8bf6646b12ab47b11da3a5f78d2f7cf 100644 (file)
@@ -2072,12 +2072,16 @@ class RgwMultisite:
 
     def create_sync_flow(self, group_id: str, flow_id: str, flow_type: str,
                          zones: Optional[List[str]] = None, bucket_name: str = '',
-                         source_zone: str = '', destination_zone: str = ''):
+                         source_zone: Optional[List[str]] = None,
+                         destination_zone: Optional[List[str]] = None):
         rgw_sync_policy_cmd = ['sync', 'group', 'flow', 'create', '--group-id', group_id,
                                '--flow-id', flow_id, '--flow-type', SyncFlowTypes[flow_type].value]
 
         if SyncFlowTypes[flow_type].value == 'directional':
-            rgw_sync_policy_cmd += ['--source-zone', source_zone, '--dest-zone', destination_zone]
+            if source_zone is not None:
+                rgw_sync_policy_cmd += ['--source-zone', ','.join(source_zone)]
+            if destination_zone is not None:
+                rgw_sync_policy_cmd += ['--dest-zone', ','.join(destination_zone)]
         else:
             if zones:
                 rgw_sync_policy_cmd += ['--zones', ','.join(zones)]