]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: RGW sync policy crud operations
authorNaman Munet <namanmunet@Namans-MacBook-Pro.local>
Tue, 2 Jul 2024 06:36:59 +0000 (12:06 +0530)
committerNaman Munet <namanmunet@Namans-MacBook-Pro.local>
Thu, 4 Jul 2024 07:11:26 +0000 (12:41 +0530)
sync group crud ops added

Fixes: https://tracker.ceph.com/issues/66798
Signed-off-by: Naman Munet <nmunet@redhat.com>
16 files changed:
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.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.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.spec.ts
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

index 13f893035e9b83b577467b713ae45901457581d2..5633bb2f5b4b8e748481db588d5ac7495b458ed1 100644 (file)
@@ -24,7 +24,23 @@ describe('Multisite page', () => {
     it('should show empty table in Sync Policy page', () => {
       multisite.getTab('Sync Policy').click();
       multisite.getDataTables().should('exist');
-      multisite.getTableCount('total').should('eq', 0);
+    });
+  });
+
+  describe('create, edit & delete sync group policy', () => {
+    it('should create policy', () => {
+      multisite.navigateTo('create');
+      multisite.create('test', 'Enabled');
+      multisite.getFirstTableCell('test').should('exist');
+    });
+
+    it('should edit policy status', () => {
+      multisite.edit('test', 'Forbidden');
+    });
+
+    it('should delete policy', () => {
+      multisite.getTab('Sync Policy').click();
+      multisite.delete('test');
     });
   });
 });
index b48b58e8ee38e04f25d88cecf4f52b4088592957..bbeda74e9cfac63c458f83b6a3622406ea456f2a 100644 (file)
@@ -1,8 +1,42 @@
 import { PageHelper } from '../page-helper.po';
 
+const WAIT_TIMER = 1000;
 const pages = {
-  index: { url: '#/rgw/multisite', id: 'cd-rgw-multisite-details' }
+  index: { url: '#/rgw/multisite', id: 'cd-rgw-multisite-details' },
+  create: { url: '#/rgw/multisite/sync-policy/create', id: 'cd-rgw-multisite-sync-policy-form' },
+  edit: { url: '#/rgw/multisite/sync-policy/edit', id: 'cd-rgw-multisite-sync-policy-form' }
 };
 export class MultisitePageHelper extends PageHelper {
   pages = pages;
+
+  columnIndex = {
+    status: 3
+  };
+
+  @PageHelper.restrictTo(pages.create.url)
+  create(group_id: string, status: string) {
+    // Enter in group_id
+    cy.get('#group_id').type(group_id);
+    // Show Status
+    this.selectOption('status', status);
+    cy.get('#status').should('have.class', 'ng-valid');
+
+    // Click the create button and wait for policy to be made
+    cy.contains('button', 'Create Sync Policy Group').wait(WAIT_TIMER).click();
+    this.getFirstTableCell(group_id).should('exist');
+  }
+
+  @PageHelper.restrictTo(pages.index.url)
+  edit(group_id: string, status: string) {
+    cy.visit(`${pages.edit.url}/${group_id}`);
+
+    // Change the status field
+    this.selectOption('status', status);
+    cy.contains('button', 'Edit Sync Policy Group').click();
+
+    this.searchTable(group_id);
+    cy.get(`datatable-body-cell:nth-child(${this.columnIndex.status})`)
+      .find('.badge-warning')
+      .should('contain', status);
+  }
 }
index 1729f6418b2d82f885893207cdee22044bbf3ac5..f2fc381e806f148a84a2239b7d137a70b51f7895 100644 (file)
@@ -50,3 +50,9 @@ export class SystemKey {
   access_key: string;
   secret_key: string;
 }
+
+export enum RgwMultisiteSyncPolicyStatus {
+  ENABLED = 'enabled',
+  FORBIDDEN = 'forbidden',
+  ALLOWED = 'allowed'
+}
index f6b9bbae8f33e950d65b6b9a16b547ec752ad651..921a2dfe3eb7d1e803228fd09466b9010c76b0a3 100644 (file)
@@ -1,6 +1,7 @@
 <nav ngbNav
      #nav="ngbNav"
      class="nav-tabs"
+     [(activeId)]="activeId"
      (navChange)="onNavChange($event)">
   <ng-container ngbNavItem="configuration">
     <a ngbNavLink
index d5e0fd063f8d52c0f0eed7420b844ec3fd3b481a..4b65b7e37bd4573cb38112ffaf05585269949e3e 100644 (file)
@@ -99,6 +99,7 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit {
   rgwModuleStatus: boolean;
   restartGatewayMessage = false;
   rgwModuleData: string | any[] = [];
+  activeId: string;
 
   constructor(
     private modalService: ModalService,
@@ -115,6 +116,10 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit {
     private notificationService: NotificationService
   ) {
     this.permission = this.authStorageService.getPermissions().rgw;
+    const activeId = this.router.getCurrentNavigation()?.extras?.state?.activeId;
+    if (activeId) {
+      this.activeId = activeId;
+    }
   }
 
   openModal(entity: any, edit = false) {
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.html
new file mode 100644 (file)
index 0000000..f1c2af0
--- /dev/null
@@ -0,0 +1,102 @@
+<div class="cd-col-form">
+  <form
+    name="bucketForm"
+    #frm="ngForm"
+    [formGroup]="syncPolicyForm"
+    *cdFormLoading="loading"
+    novalidate>
+    <div class="card">
+      <div
+        i18n="form title"
+        class="card-header">
+        {{ action | titlecase }} {{ resource | upperFirst }}
+      </div>
+
+      <div class="card-body">
+        <!-- Group Id -->
+        <div class="form-group row">
+          <label
+            class="cd-col-form-label required"
+            for="group_id"
+            i18n>Group Id</label>
+          <div class="cd-col-form-input">
+            <input
+              id="group_id"
+              name="group_id"
+              class="form-control"
+              type="text"
+              i18n-placeholder
+              placeholder="Group Id..."
+              formControlName="group_id"
+              [readonly]="editing"/>
+            <span
+              class="invalid-feedback"
+              *ngIf="syncPolicyForm.showError('group_id', frm, 'required')"
+              i18n>This field is required.</span>
+          </div>
+        </div>
+
+        <!-- Status -->
+        <div class="form-group row">
+          <label
+            class="cd-col-form-label required"
+            for="status"
+            i18n>Status</label>
+          <div class="cd-col-form-input">
+            <select
+                id="status"
+                name="status"
+                class="form-select"
+                formControlName="status">
+              <option
+                i18n
+                value="{{syncPolicyStatus.ENABLED}}">{{syncPolicyStatus.ENABLED | upperFirst }}</option>
+              <option
+                i18n
+                value="{{syncPolicyStatus.ALLOWED}}">{{syncPolicyStatus.ALLOWED | upperFirst }}</option>
+              <option
+                i18n
+                value="{{syncPolicyStatus.FORBIDDEN}}">{{syncPolicyStatus.FORBIDDEN | upperFirst }}</option>
+            </select>
+            <span
+              class="invalid-feedback"
+              *ngIf="syncPolicyForm.showError('status', frm, 'required')"
+              i18n>This field is required.</span>
+          </div>
+        </div>
+
+        <!-- Bucket Name -->
+        <div class="form-group row">
+          <label
+            class="cd-col-form-label"
+            for="bucket_name"
+            i18n>Bucket Name</label>
+          <div class="cd-col-form-input">
+            <input
+              id="bucket_name"
+              name="bucket_name"
+              class="form-control"
+              type="text"
+              i18n-placeholder
+              placeholder="Bucket Name..."
+              formControlName="bucket_name"
+              [readonly]="editing"
+              [ngbTypeahead]="bucketDataSource"/>
+            <span
+              class="invalid-feedback"
+              *ngIf="syncPolicyForm.showError('bucket_name', frm, 'bucketNameNotAllowed')"
+              i18n>The bucket with chosen name does not exist.</span>
+          </div>
+        </div>
+      </div>
+
+      <div class="card-footer">
+        <cd-form-button-panel
+          (submitActionEvent)="submit()"
+          [form]="syncPolicyForm"
+          [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+          wrappingClass="text-right"></cd-form-button-panel>
+      </div>
+    </div>
+  </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.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-policy-form/rgw-multisite-sync-policy-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.spec.ts
new file mode 100644 (file)
index 0000000..b886ad1
--- /dev/null
@@ -0,0 +1,36 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RgwMultisiteSyncPolicyFormComponent } from './rgw-multisite-sync-policy-form.component';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ToastrModule } from 'ngx-toastr';
+import { ReactiveFormsModule } from '@angular/forms';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { RouterTestingModule } from '@angular/router/testing';
+
+describe('RgwMultisiteSyncPolicyFormComponent', () => {
+  let component: RgwMultisiteSyncPolicyFormComponent;
+  let fixture: ComponentFixture<RgwMultisiteSyncPolicyFormComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [RgwMultisiteSyncPolicyFormComponent],
+      imports: [
+        HttpClientTestingModule,
+        ReactiveFormsModule,
+        ToastrModule.forRoot(),
+        PipesModule,
+        ComponentsModule,
+        RouterTestingModule
+      ],
+      providers: []
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(RgwMultisiteSyncPolicyFormComponent);
+    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-form/rgw-multisite-sync-policy-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.ts
new file mode 100644 (file)
index 0000000..300f617
--- /dev/null
@@ -0,0 +1,172 @@
+import { Component, OnInit } from '@angular/core';
+import { AbstractControl, AsyncValidatorFn, ValidationErrors, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { Observable, timer as observableTimer, of } from 'rxjs';
+import {
+  catchError,
+  debounceTime,
+  distinctUntilChanged,
+  map,
+  mergeMap,
+  switchMapTo
+} from 'rxjs/operators';
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwMultisiteSyncPolicyStatus } from '../models/rgw-multisite';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import _ from 'lodash';
+
+@Component({
+  selector: 'cd-rgw-multisite-sync-policy-form',
+  templateUrl: './rgw-multisite-sync-policy-form.component.html',
+  styleUrls: ['./rgw-multisite-sync-policy-form.component.scss']
+})
+export class RgwMultisiteSyncPolicyFormComponent extends CdForm implements OnInit {
+  syncPolicyForm: CdFormGroup;
+  editing = false;
+  action: string;
+  resource: string;
+  syncPolicyStatus = RgwMultisiteSyncPolicyStatus;
+
+  bucketDataSource = (text$: Observable<string>) => {
+    return text$.pipe(
+      debounceTime(200),
+      distinctUntilChanged(),
+      mergeMap((token: string) => this.getBucketTypeahead(token))
+    );
+  };
+
+  constructor(
+    private router: Router,
+    private route: ActivatedRoute,
+    public actionLabels: ActionLabelsI18n,
+    private fb: CdFormBuilder,
+    private rgwMultisiteService: RgwMultisiteService,
+    private notificationService: NotificationService,
+    private rgwBucketService: RgwBucketService
+  ) {
+    super();
+    this.editing = this.router.url.startsWith(`/rgw/multisite/sync-policy/${URLVerbs.EDIT}`);
+    this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
+    this.resource = $localize`Sync Policy Group`;
+    this.createForm();
+    this.loadingReady();
+  }
+
+  ngOnInit(): void {
+    if (this.editing) {
+      this.route.paramMap.subscribe((params: any) => {
+        const groupName = params.get('groupName');
+        if (groupName) {
+          const bucketName = params.get('bucketName');
+          this.loadingStart();
+          this.rgwMultisiteService
+            .getSyncPolicyGroup(groupName, bucketName)
+            .subscribe((syncPolicy: any) => {
+              this.loadingReady();
+              if (syncPolicy) {
+                this.syncPolicyForm.patchValue({
+                  group_id: syncPolicy.id,
+                  status: syncPolicy.status,
+                  bucket_name: bucketName
+                });
+              } else {
+                this.goToListView();
+              }
+            });
+        }
+      });
+    }
+  }
+
+  createForm() {
+    this.syncPolicyForm = this.fb.group({
+      group_id: ['', Validators.required],
+      status: [`${this.syncPolicyStatus.ENABLED}`, Validators.required],
+      bucket_name: ['', , this.bucketExistence(true)]
+    });
+  }
+
+  goToListView() {
+    // passing state in order to return to same tab on details page
+    this.router.navigate(['/rgw/multisite'], { state: { activeId: 'syncPolicy' } });
+  }
+
+  submit() {
+    if (this.syncPolicyForm.pristine) {
+      this.goToListView();
+      return;
+    }
+
+    // Ensure that no validation is pending
+    if (this.syncPolicyForm.pending) {
+      this.syncPolicyForm.setErrors({ cdSubmitButton: true });
+      return;
+    }
+
+    if (!this.editing) {
+      // Add
+      this.rgwMultisiteService.createSyncPolicyGroup(this.syncPolicyForm.value).subscribe(
+        () => {
+          this.notificationService.show(
+            NotificationType.success,
+            $localize`Created Sync Policy Group '${this.syncPolicyForm.getValue('group_id')}'`
+          );
+          this.goToListView();
+        },
+        () => {
+          // Reset the 'Submit' button.
+          this.syncPolicyForm.setErrors({ cdSubmitButton: true });
+        }
+      );
+    } else {
+      this.rgwMultisiteService.modifySyncPolicyGroup(this.syncPolicyForm.value).subscribe(
+        () => {
+          this.notificationService.show(
+            NotificationType.success,
+            $localize`Modified Sync Policy Group '${this.syncPolicyForm.getValue('group_id')}'`
+          );
+          this.goToListView();
+        },
+        () => {
+          // Reset the 'Submit' button.
+          this.syncPolicyForm.setErrors({ cdSubmitButton: true });
+        }
+      );
+    }
+  }
+
+  bucketExistence(requiredExistenceResult: boolean): AsyncValidatorFn {
+    return (control: AbstractControl): Observable<ValidationErrors | null> => {
+      if (control.dirty) {
+        return observableTimer(500).pipe(
+          switchMapTo(this.rgwBucketService.exists(control.value)),
+          map((existenceResult: boolean) =>
+            existenceResult === requiredExistenceResult ? null : { bucketNameNotAllowed: true }
+          )
+        );
+      }
+      return of(null);
+    };
+  }
+
+  private getBucketTypeahead(path: string): Observable<any> {
+    if (_.isString(path) && path !== '/' && path !== '') {
+      return this.rgwBucketService.list().pipe(
+        map((bucketList: any) =>
+          bucketList
+            .filter((bucketName: string) => bucketName.toLowerCase().includes(path))
+            .slice(0, 15)
+        ),
+        catchError(() => of([$localize`Error while retrieving bucket names.`]))
+      );
+    } else {
+      return of([]);
+    }
+  }
+}
index 7de60b56e6834cfec934afb8423050f44e486bbc..8909f44c32b394d721c5e19ed30ad2667c426705 100644 (file)
@@ -1,18 +1,40 @@
-  <legend i18n>
-    Multisite Sync Policy
-    <cd-help-text>
-      Multisite bucket-granularity sync policy provides fine grained control of data movement between buckets in different zones.
-    </cd-help-text>
-  </legend>
-  <cd-table #table
-            [data]="syncPolicyData"
-            [columns]="columns"
-            columnMode="flex"
-            selectionType="single"
-            [searchableObjects]="true"
-            [hasDetails]="false"
-            [serverSide]="false"
-            [count]="0"
-            [maxLimit]="25"
-            [toolHeader]="true">
-  </cd-table>
+<legend i18n>
+  Multisite Sync Policy
+  <cd-help-text>
+    Multisite bucket-granularity sync policy provides fine grained control of data movement between
+    buckets in different zones.
+  </cd-help-text>
+</legend>
+<cd-table
+  #table
+  [autoReload]="false"
+  [data]="syncPolicyData"
+  [columns]="columns"
+  identifier="uniqueId"
+  [forceIdentifier]="true"
+  columnMode="flex"
+  selectionType="multiClick"
+  [searchableObjects]="true"
+  [hasDetails]="false"
+  [serverSide]="false"
+  [count]="0"
+  [maxLimit]="25"
+  [toolHeader]="true"
+  (fetchData)="getPolicyList($event)"
+  (updateSelection)="updateSelection($event)">
+  <div class="table-actions btn-toolbar">
+    <cd-table-actions
+      [permission]="permission"
+      [selection]="selection"
+      class="btn-group"
+      [tableActions]="tableActions">
+    </cd-table-actions>
+  </div>
+</cd-table>
+
+<ng-template #deleteTpl>
+  <cd-alert-panel type="danger"
+                  i18n>
+    Are you sure you want to delete these policy groups?
+  </cd-alert-panel>
+</ng-template>
index 6811f867e23be63972d8d744def41b8c8f265a1b..f555af7e765226cac0c6887a801843fe5f04b206 100644 (file)
@@ -3,6 +3,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { RgwMultisiteSyncPolicyComponent } from './rgw-multisite-sync-policy.component';
 import { HttpClientModule } from '@angular/common/http';
 import { TitleCasePipe } from '@angular/common';
+import { ToastrModule } from 'ngx-toastr';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
 
 describe('RgwMultisiteSyncPolicyComponent', () => {
   let component: RgwMultisiteSyncPolicyComponent;
@@ -11,7 +13,7 @@ describe('RgwMultisiteSyncPolicyComponent', () => {
   beforeEach(async () => {
     await TestBed.configureTestingModule({
       declarations: [RgwMultisiteSyncPolicyComponent],
-      imports: [HttpClientModule],
+      imports: [HttpClientModule, ToastrModule.forRoot(), PipesModule],
       providers: [TitleCasePipe]
     }).compileComponents();
 
index 77b49a9ba1806d43fedc44ab7550bd94ba69c5e7..2ed1ec0f7e2c26e583ca22ac8502b08bb42ae4fb 100644 (file)
@@ -1,25 +1,60 @@
 import { TitleCasePipe } from '@angular/common';
-import { Component, OnInit } from '@angular/core';
+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 { 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';
 import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+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 { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+
+const BASE_URL = 'rgw/multisite/sync-policy';
 
 @Component({
   selector: 'cd-rgw-multisite-sync-policy',
   templateUrl: './rgw-multisite-sync-policy.component.html',
-  styleUrls: ['./rgw-multisite-sync-policy.component.scss']
+  styleUrls: ['./rgw-multisite-sync-policy.component.scss'],
+  providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
 })
 export class RgwMultisiteSyncPolicyComponent implements OnInit {
+  @ViewChild(TableComponent, { static: true })
+  table: TableComponent;
+  @ViewChild('deleteTpl', { static: true })
+  deleteTpl: TemplateRef<any>;
+
   columns: Array<CdTableColumn> = [];
   syncPolicyData: any = [];
+  tableActions: CdTableAction[];
+  selection = new CdTableSelection();
+  permission: Permission;
 
   constructor(
     private rgwMultisiteService: RgwMultisiteService,
-    private titleCasePipe: TitleCasePipe
+    private titleCasePipe: TitleCasePipe,
+    private actionLabels: ActionLabelsI18n,
+    private urlBuilder: URLBuilderService,
+    private authStorageService: AuthStorageService,
+    private modalService: ModalService,
+    private taskWrapper: TaskWrapperService
   ) {}
 
   ngOnInit(): void {
+    this.permission = this.authStorageService.getPermissions().rgw;
     this.columns = [
+      {
+        prop: 'uniqueId',
+        isHidden: true
+      },
       {
         name: $localize`Group Name`,
         prop: 'groupName',
@@ -54,21 +89,113 @@ export class RgwMultisiteSyncPolicyComponent implements OnInit {
         flexGrow: 1
       }
     ];
-
-    this.rgwMultisiteService
-      .getSyncPolicy('', '', true)
-      .subscribe((allSyncPolicyData: Array<Object>) => {
-        if (allSyncPolicyData && allSyncPolicyData.length > 0) {
-          allSyncPolicyData.forEach((policy) => {
-            this.syncPolicyData.push({
-              groupName: policy['id'],
-              status: policy['status'],
-              bucket: policy['bucketName'],
-              zonegroup: ''
-            });
-          });
-          this.syncPolicyData = [...this.syncPolicyData];
+    const getSyncGroupName = () => {
+      if (this.selection.first() && this.selection.first().groupName) {
+        if (this.selection.first().bucket) {
+          return `${encodeURIComponent(this.selection.first().groupName)}/${encodeURIComponent(
+            this.selection.first().bucket
+          )}`;
         }
+        return `${encodeURIComponent(this.selection.first().groupName)}`;
+      }
+      return '';
+    };
+    const addAction: CdTableAction = {
+      permission: 'create',
+      icon: Icons.add,
+      routerLink: () => this.urlBuilder.getCreate(),
+      name: this.actionLabels.CREATE,
+      canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+    };
+    const editAction: CdTableAction = {
+      permission: 'update',
+      icon: Icons.edit,
+      routerLink: () => this.urlBuilder.getEdit(getSyncGroupName()),
+      name: this.actionLabels.EDIT
+    };
+    const deleteAction: CdTableAction = {
+      permission: 'delete',
+      icon: Icons.destroy,
+      click: () => this.deleteAction(),
+      disable: () => !this.selection.hasSelection,
+      name: this.actionLabels.DELETE,
+      canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
+    };
+    this.tableActions = [addAction, editAction, deleteAction];
+  }
+
+  transformSyncPolicyData(allSyncPolicyData: any) {
+    if (allSyncPolicyData && allSyncPolicyData.length > 0) {
+      allSyncPolicyData.forEach((policy: any) => {
+        this.syncPolicyData.push({
+          uniqueId: policy['id'] + (policy['bucketName'] ? policy['bucketName'] : ''),
+          groupName: policy['id'],
+          status: policy['status'],
+          bucket: policy['bucketName'],
+          zonegroup: ''
+        });
       });
+      this.syncPolicyData = [...this.syncPolicyData];
+    }
+  }
+
+  updateSelection(selection: CdTableSelection) {
+    this.selection = selection;
+  }
+
+  getPolicyList(context: CdTableFetchDataContext) {
+    this.rgwMultisiteService.getSyncPolicy('', '', true).subscribe(
+      (resp: object[]) => {
+        this.syncPolicyData = [];
+        this.transformSyncPolicyData(resp);
+      },
+      () => {
+        context.error();
+      }
+    );
+  }
+
+  deleteAction() {
+    const groupNames = this.selection.selected.map((policy: any) => policy.groupName);
+    this.modalService.show(CriticalConfirmationModalComponent, {
+      itemDescription: this.selection.hasSingleSelection
+        ? $localize`Policy Group`
+        : $localize`Policy Groups`,
+      itemNames: groupNames,
+      bodyTemplate: this.deleteTpl,
+      submitActionObservable: () => {
+        return new Observable((observer: Subscriber<any>) => {
+          this.taskWrapper
+            .wrapTaskAroundCall({
+              task: new FinishedTask('rgw/multisite/sync-policy/delete', {
+                group_names: groupNames
+              }),
+              call: observableForkJoin(
+                this.selection.selected.map((policy: any) => {
+                  return this.rgwMultisiteService.removeSyncPolicyGroup(
+                    policy.groupName,
+                    policy.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 d07d8f1bf49016269d5056ab010e3f392a78a072..803e3c5bdf83df5f972bf450865dc05a53a54a32 100644 (file)
@@ -3,7 +3,12 @@ import { NgModule } from '@angular/core';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { RouterModule, Routes } from '@angular/router';
 
-import { NgbNavModule, NgbPopoverModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import {
+  NgbNavModule,
+  NgbPopoverModule,
+  NgbTooltipModule,
+  NgbTypeaheadModule
+} from '@ng-bootstrap/ng-bootstrap';
 import { NgxPipeFunctionModule } from 'ngx-pipe-function';
 
 import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants';
@@ -50,6 +55,7 @@ import { BucketTagModalComponent } from './bucket-tag-modal/bucket-tag-modal.com
 import { NfsListComponent } from '../nfs/nfs-list/nfs-list.component';
 import { NfsFormComponent } from '../nfs/nfs-form/nfs-form.component';
 import { RgwMultisiteSyncPolicyComponent } from './rgw-multisite-sync-policy/rgw-multisite-sync-policy.component';
+import { RgwMultisiteSyncPolicyFormComponent } from './rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component';
 
 @NgModule({
   imports: [
@@ -65,7 +71,8 @@ import { RgwMultisiteSyncPolicyComponent } from './rgw-multisite-sync-policy/rgw
     NgxPipeFunctionModule,
     TreeModule,
     DataTableModule,
-    DashboardV3Module
+    DashboardV3Module,
+    NgbTypeaheadModule
   ],
   exports: [
     RgwDaemonListComponent,
@@ -108,7 +115,8 @@ import { RgwMultisiteSyncPolicyComponent } from './rgw-multisite-sync-policy/rgw
     RgwSyncMetadataInfoComponent,
     RgwSyncDataInfoComponent,
     BucketTagModalComponent,
-    RgwMultisiteSyncPolicyComponent
+    RgwMultisiteSyncPolicyComponent,
+    RgwMultisiteSyncPolicyFormComponent
   ],
   providers: [TitleCasePipe]
 })
@@ -200,7 +208,24 @@ const routes: Routes = [
   {
     path: 'multisite',
     data: { breadcrumbs: 'Multi-site' },
-    children: [{ path: '', component: RgwMultisiteDetailsComponent }]
+    children: [
+      { path: '', component: RgwMultisiteDetailsComponent },
+      {
+        path: `sync-policy/${URLVerbs.CREATE}`,
+        component: RgwMultisiteSyncPolicyFormComponent,
+        data: { breadcrumbs: `${ActionLabels.CREATE} Sync Policy` }
+      },
+      {
+        path: `sync-policy/${URLVerbs.EDIT}/:groupName`,
+        component: RgwMultisiteSyncPolicyFormComponent,
+        data: { breadcrumbs: `${ActionLabels.EDIT} Sync Policy` }
+      },
+      {
+        path: `sync-policy/${URLVerbs.EDIT}/:groupName/:bucketName`,
+        component: RgwMultisiteSyncPolicyFormComponent,
+        data: { breadcrumbs: `${ActionLabels.EDIT} Sync Policy` }
+      }
+    ]
   },
   {
     path: 'nfs',
index 3f5b3d3b57284534448da1c82f15af2b6a11a1d0..424eb21e41b2a38e38f5969f805d4d4afb3f8009 100644 (file)
@@ -47,4 +47,46 @@ describe('RgwMultisiteService', () => {
     expect(req.request.method).toBe('GET');
     req.flush(mockSyncPolicyData);
   });
+
+  it('should create Sync Policy Group w/o bucket_name', () => {
+    const postData = { group_id: 'test', status: 'enabled' };
+    service.createSyncPolicyGroup(postData).subscribe();
+    const req = httpTesting.expectOne('api/rgw/multisite/sync-policy-group');
+    expect(req.request.method).toBe('POST');
+    expect(req.request.body).toEqual(postData);
+    req.flush(null);
+  });
+
+  it('should create Sync Policy Group with bucket_name', () => {
+    const postData = { group_id: 'test', status: 'enabled', bucket_name: 'test' };
+    service.createSyncPolicyGroup(postData).subscribe();
+    const req = httpTesting.expectOne('api/rgw/multisite/sync-policy-group');
+    expect(req.request.method).toBe('POST');
+    expect(req.request.body).toEqual(postData);
+    req.flush(null);
+  });
+
+  it('should modify Sync Policy Group', () => {
+    const postData = { group_id: 'test', status: 'enabled', bucket_name: 'test' };
+    service.modifySyncPolicyGroup(postData).subscribe();
+    const req = httpTesting.expectOne('api/rgw/multisite/sync-policy-group');
+    expect(req.request.method).toBe('PUT');
+    expect(req.request.body).toEqual(postData);
+    req.flush(null);
+  });
+
+  it('should remove Sync Policy Group', () => {
+    const group_id = 'test';
+    service.removeSyncPolicyGroup(group_id).subscribe();
+    const req = httpTesting.expectOne('api/rgw/multisite/sync-policy-group/' + group_id);
+    expect(req.request.method).toBe('DELETE');
+    req.flush(null);
+  });
+
+  it('should fetch the sync policy group with given group_id and bucket_name', () => {
+    service.getSyncPolicyGroup('test', 'test').subscribe();
+    const req = httpTesting.expectOne('api/rgw/multisite/sync-policy-group/test?bucket_name=test');
+    expect(req.request.method).toBe('GET');
+    req.flush(mockSyncPolicyData[1]);
+  });
 });
index 4f69d6ab2a4c1ec194060cb657bee709820506fb..cc03042815e7bfa6c5d06636177cb2bed5fb325e 100644 (file)
@@ -47,4 +47,28 @@ export class RgwMultisiteService {
     params = params.append('all_policy', fetchAllPolicy);
     return this.http.get(`${this.url}/sync-policy`, { params });
   }
+
+  getSyncPolicyGroup(group_id: string, bucket_name?: string) {
+    let params = new HttpParams();
+    if (bucket_name) {
+      params = params.append('bucket_name', bucket_name);
+    }
+    return this.http.get(`${this.url}/sync-policy-group/${group_id}`, { params });
+  }
+
+  createSyncPolicyGroup(payload: { group_id: string; status: string; bucket_name?: string }) {
+    return this.http.post(`${this.url}/sync-policy-group`, payload);
+  }
+
+  modifySyncPolicyGroup(payload: { group_id: string; status: string; bucket_name?: string }) {
+    return this.http.put(`${this.url}/sync-policy-group`, payload);
+  }
+
+  removeSyncPolicyGroup(group_id: string, bucket_name?: string) {
+    let params = new HttpParams();
+    if (bucket_name) {
+      params = params.append('bucket_name', bucket_name);
+    }
+    return this.http.delete(`${this.url}/sync-policy-group/${group_id}`, { params });
+  }
 }
index a7e2cf8f421a30cd27c1e2909349b118ddfae1e7..4197b4790e62189fb3c8ce6c9895518834df831a 100644 (file)
@@ -324,6 +324,16 @@ export class TaskMessageService {
         metadata.bucket_names.length > 1 ? 'selected buckets' : metadata.bucket_names[0]
       }`;
     }),
+    'rgw/multisite/sync-policy/delete': this.newTaskMessage(
+      this.commonOperations.delete,
+      (metadata) => {
+        return $localize`${
+          metadata.group_names.length > 1
+            ? 'selected policy groups'
+            : `policy group '${metadata.group_names[0]}'`
+        }`;
+      }
+    ),
     // iSCSI target tasks
     'iscsi/target/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
       this.iscsiTarget(metadata)