]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Local storage class creation via dashboard doesn't handle creation...
authorDnyaneshwari <dnyaneshwari@li-9c9fbecc-2d5c-11b2-a85c-e2a7cc8a424f.ibm.com>
Fri, 5 Sep 2025 10:17:11 +0000 (15:47 +0530)
committerDnyaneshwari <dnyaneshwari@li-9c9fbecc-2d5c-11b2-a85c-e2a7cc8a424f.ibm.com>
Thu, 25 Sep 2025 11:11:42 +0000 (16:41 +0530)
Fixes: https://tracker.ceph.com/issues/72569
Resolves: rhbz#2398058

Signed-off-by: Dnyaneshwari <dtalweka@redhat.com>
mgr/dashboard: handle creation of new pool

Commit includes:
1) Provide link to create a new pool
2) Refactored validation on ACL mapping, removed required validator as default
3) fixed runtime error on console due to ACL length due to which the details section was not opening
4) Used rxjs operators to make API calls and making form ready once all data is available, fixing the form patch issues
5) Refactored some part of code to improve the performance
6) Added zone and pool information in details section for local storage class

Fixes: https://tracker.ceph.com/issues/72569
Signed-off-by: Naman Munet <naman.munet@ibm.com>
(cherry picked from commit 2d0e71c845643a26d4425ddac8ee0ff30153eff2)

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/services/rgw_client.py

(cherry picked from commit a6431bfe8de5c5e32cb62ff5b072aef00239c6e9)

14 files changed:
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-storage-class.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/utils/rgw-bucket-tiering.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-storage-class.service.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_client.py

index f520ed2a52e7224b795f2f78da22455a7c257dcf..facf4bbcb9cbc4f0b38ef281fe3df27b57c86a70 100755 (executable)
@@ -1562,6 +1562,57 @@ class RgwZone(RESTController):
         result = multisite_instance.get_user_list(zoneName, realmName)
         return result
 
+    @Endpoint('POST', path='storage-class')
+    @CreatePermission
+    def create_storage_class(self, zone_name: str, placement_target: str, storage_class: str,
+                             data_pool: str, compression=''):
+        return self.handle_storage_class(zone_name, placement_target, storage_class, data_pool,
+                                         operation='create', compression=compression)
+
+    @Endpoint('PUT', path='storage-class')
+    @CreatePermission
+    def edit_storage_class(self, zone_name: str, placement_target: str, storage_class: str,
+                           data_pool: str, compression=''):
+        return self.handle_storage_class(zone_name, placement_target, storage_class, data_pool,
+                                         operation='edit', compression=compression)
+
+    def handle_storage_class(self, zone_name: str, placement_target: str, storage_class: str,
+                             data_pool: str, operation: str, compression=''):
+        if not (placement_target and storage_class and data_pool):
+            raise DashboardException(
+                msg='Failed to get placement target',
+                http_status_code=404,
+                component='rgw'
+            )
+        multisite_instance = RgwMultisite()
+
+        try:
+            if operation == 'create':
+                multisite_instance.add_storage_class_zone(
+                    zone_name=zone_name,
+                    placement_target=placement_target,
+                    storage_class=storage_class,
+                    data_pool=data_pool,
+                    compression=compression
+                )
+            elif operation == 'edit':
+                multisite_instance.edit_storage_class_zone(
+                    zone_name=zone_name,
+                    placement_target=placement_target,
+                    storage_class=storage_class,
+                    data_pool=data_pool,
+                    compression=compression
+                )
+        except DashboardException as e:
+            raise DashboardException(e, http_status_code=404, component='rgw')
+
+        return {
+            'placement_target': placement_target,
+            'storage_class': storage_class,
+            'data_pool': data_pool,
+            'status': 'success'
+        }
+
 
 @APIRouter('/rgw/topic', Scope.RGW)
 @APIDoc("RGW Topic Management API", "RGW Topic Management")
index 7a34f9450653dc6fdc3aa6ce8557b60b6137fbb8..370fa78d0b640edec8a928b0e6efe4eb55ac7815 100644 (file)
@@ -10,6 +10,8 @@ export interface StorageClass {
   endpoint?: string;
   region?: string;
   zonegroup_name?: string;
+  zone_name?: string;
+  data_pool?: string;
 }
 
 export interface TierTarget {
@@ -41,8 +43,9 @@ export interface StorageClassDetails {
   multipart_sync_threshold: number;
   host_style: string;
   allow_read_through: boolean;
+  storage_class: string;
   zonegroup_name?: string;
-  placement_targets?: string;
+  placement_target?: string;
   glacier_restore_days?: number;
   glacier_restore_tier_type?: string;
   read_through_restore_days?: number;
@@ -50,12 +53,43 @@ export interface StorageClassDetails {
   retain_head_object?: boolean;
   acls?: ACL[];
   acl_mappings?: ACL[];
+  zone_name?: string;
+  data_pool?: string;
 }
 
 export interface ZoneGroup {
   name: string;
   id: string;
   placement_targets?: Target[];
+  zones?: string[];
+}
+
+export interface ZoneRequest {
+  zone_name: string;
+  placement_target: string;
+  storage_class: string;
+  data_pool: string;
+}
+export interface StorageClassPool {
+  data_pool: string;
+}
+
+export interface PlacementPool {
+  key: string;
+  val: {
+    storage_classes: {
+      [storage_class: string]: StorageClassPool;
+    };
+  };
+}
+
+export interface Zone {
+  name: string;
+  placement_pools: PlacementPool[];
+}
+
+export interface AllZonesResponse {
+  zones: Zone[];
 }
 
 export interface ACL {
@@ -102,10 +136,10 @@ export interface RequestModel {
 }
 
 export interface PlacementTarget {
-  placement_id: string;
+  placement_id?: string;
   tags?: string[];
   tier_type?: TIER_TYPE;
-  tier_config_rm: TierConfigRm;
+  tier_config_rm?: TierConfigRm;
   tier_config?: {
     endpoint: string;
     access_key: string;
@@ -126,6 +160,8 @@ export interface PlacementTarget {
   storage_class?: string;
   name?: string;
   tier_targets?: TierTarget[];
+  data_pool?: string;
+  placement_target?: string;
 }
 
 export interface TierConfigRm {
@@ -310,3 +346,7 @@ export const AclHelperText: AclMaps = {
     destination: $localize`The URI identifying the destination group or user.`
   }
 };
+
+export const POOL = {
+  PATH: '/pool/create'
+};
index 1a988d3407b1fff96314090d57c5f19bb66ff9df..8f794c69222b34253ee6630e035d70ed6d932a00 100644 (file)
               </ng-container>
             </td>
           </tr>
+         }
+         @if (isTierMatch(TIER_TYPE.LOCAL)) {
+          <tr>
+            <td class="bold">
+              Zone
+              <cd-helper class="text-pre-wrap">
+                <span i18n>
+                  A zone defines a logical group that consists of one or more Ceph Object Gateway instances.
+                </span>
+              </cd-helper>
+            </td>
+            <td>
+@if (loading) {
+              <cds-skeleton-text
+                [lines]="1"
+                [minLineWidth]="100"
+                [maxLineWidth]="200">
+              </cds-skeleton-text>
+            } @else {
+              <span>{{ localStorageClassDetails?.zone_name }}</span>
+            }
+            </td>
+          </tr>
+          <tr>
+            <td class="bold">
+              Data Pool
+              <cd-helper class="text-pre-wrap">
+                <span i18n>
+                    The data pool contains the objects associated with this storage class.
+                </span>
+              </cd-helper>
+            </td>
+            <td>
+              @if (loading) {
+              <cds-skeleton-text
+                [lines]="1"
+                [minLineWidth]="100"
+                [maxLineWidth]="200">
+              </cds-skeleton-text>
+            } @else {
+              <span>{{ localStorageClassDetails?.data_pool }}</span>
+            }
+            </td>
+          </tr>
          }
           <tr>
             <td class="bold">
               Placement Target
               <cd-helper class="text-pre-wrap">
                 <span i18n>
-                  Placement Target defines the destination and rules for moving objects between
-                  storage tiers.
+                  Placement targets control which Pools are associated with a particular bucket.
                 </span>
               </cd-helper>
             </td>
index 1321a72c035a75d2a933d32e7f63a3bed6401730..2d3e5338491b4f8e0eb7ef3e025ad6344e5c8895 100644 (file)
@@ -5,13 +5,13 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { SharedModule } from '~/app/shared/shared.module';
 import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { RouterTestingModule } from '@angular/router/testing';
-import { SimpleChange } from '@angular/core';
 
 describe('RgwStorageClassDetailsComponent', () => {
   let component: RgwStorageClassDetailsComponent;
   let fixture: ComponentFixture<RgwStorageClassDetailsComponent>;
 
   const mockSelection: StorageClassDetails = {
+    storage_class: 'TestStorageClass',
     access_key: 'TestAccessKey',
     secret: 'TestSecret',
     target_path: '/test/path',
@@ -45,30 +45,4 @@ describe('RgwStorageClassDetailsComponent', () => {
   it('should create', () => {
     expect(component).toBeTruthy();
   });
-
-  it('should update storageDetails when selection input changes', () => {
-    const newSelection: StorageClassDetails = {
-      access_key: 'NewAccessKey',
-      secret: 'NewSecret',
-      target_path: '/new/path',
-      multipart_min_part_size: 500,
-      multipart_sync_threshold: 1000,
-      host_style: 'virtual',
-      retain_head_object: false,
-      allow_read_through: false,
-      tier_type: 'archive',
-      glacier_restore_days: 1,
-      glacier_restore_tier_type: 'standard',
-      placement_targets: '',
-      read_through_restore_days: 7,
-      restore_storage_class: 'restored',
-      zonegroup_name: 'zone1'
-    };
-
-    component.selection = newSelection;
-    component.ngOnChanges({
-      selection: new SimpleChange(null, newSelection, false)
-    });
-    expect(component.storageDetails).toEqual(newSelection);
-  });
 });
index 69c8f9dbfe88a5a37bf825ab8abefeedb0b12df3..5c2e5fddecbb727f30faaed0c1d73e5005e455cf 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
+import { Component, inject, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
 import {
   ALLOW_READ_THROUGH_TEXT,
@@ -19,8 +19,11 @@ import {
   RESTORE_STORAGE_CLASS_TEXT,
   ZONEGROUP_TEXT,
   ACL,
-  GroupedACLs
+  GroupedACLs,
+  AllZonesResponse
 } from '../models/rgw-storage-class.model';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { BucketTieringUtils } from '../utils/rgw-bucket-tiering';
 @Component({
   selector: 'cd-rgw-storage-class-details',
   templateUrl: './rgw-storage-class-details.component.html',
@@ -30,7 +33,6 @@ export class RgwStorageClassDetailsComponent implements OnChanges, OnInit {
   @Input()
   selection: StorageClassDetails;
   columns: CdTableColumn[] = [];
-  storageDetails: StorageClassDetails;
   allowReadThroughText = ALLOW_READ_THROUGH_TEXT;
   retainHeadObjectText = RETAIN_HEAD_OBJECT_TEXT;
   multipartMinPartText = MULTIPART_MIN_PART_TEXT;
@@ -48,31 +50,28 @@ export class RgwStorageClassDetailsComponent implements OnChanges, OnInit {
   restoreStorageClassText = RESTORE_STORAGE_CLASS_TEXT;
   zoneGroupText = ZONEGROUP_TEXT;
   groupedACLs: GroupedACLs = {};
+  localStorageClassDetails = { zone_name: '', data_pool: '' };
+  loading = false;
+
+  private rgwZoneService = inject(RgwZoneService);
 
   ngOnChanges(changes: SimpleChanges): void {
-    if (changes['selection']) {
-      this.storageDetails = {
-        zonegroup_name: this.selection?.zonegroup_name,
-        placement_targets: this.selection?.placement_targets,
-        access_key: this.selection?.access_key,
-        secret: this.selection?.secret,
-        target_path: this.selection?.target_path,
-        tier_type: this.selection?.tier_type,
-        multipart_min_part_size: this.selection?.multipart_min_part_size,
-        multipart_sync_threshold: this.selection?.multipart_sync_threshold,
-        host_style: this.selection?.host_style,
-        retain_head_object: this.selection?.retain_head_object,
-        allow_read_through: this.selection?.allow_read_through,
-        glacier_restore_days: this.selection?.glacier_restore_days,
-        glacier_restore_tier_type: this.selection?.glacier_restore_tier_type,
-        restore_storage_class: this.selection?.restore_storage_class,
-        read_through_restore_days: this.selection?.read_through_restore_days
-      };
+    if (
+      changes['selection'] &&
+      changes['selection'].currentValue?.tier_type?.toLowerCase() === TIER_TYPE.LOCAL &&
+      changes['selection'].firstChange
+    ) {
+      // The idea here is to not call the API if we already have the zone_name and data_pool
+      // When the details view is expanded and table refreshes data then this API should not be called again
+      const { zone_name, data_pool } = this.localStorageClassDetails;
+      if (!zone_name || !data_pool) {
+        this.getZoneInfo();
+      }
     }
   }
 
   ngOnInit() {
-    this.groupedACLs = this.groupByType(this.selection.acl_mappings);
+    this.groupedACLs = this.groupByType(this.selection?.acl_mappings);
   }
 
   isTierMatch(...types: string[]): boolean {
@@ -80,6 +79,22 @@ export class RgwStorageClassDetailsComponent implements OnChanges, OnInit {
     return types.some((type) => type.toLowerCase() === tier_type);
   }
 
+  getZoneInfo() {
+    this.loading = true;
+    this.rgwZoneService.getAllZonesInfo().subscribe({
+      next: (data: AllZonesResponse) => {
+        this.localStorageClassDetails = BucketTieringUtils.getZoneInfoHelper(
+          data.zones,
+          this.selection
+        );
+        this.loading = false;
+      },
+      error: () => {
+        this.loading = false;
+      }
+    });
+  }
+
   groupByType(acls: ACL[]): GroupedACLs {
     return acls?.reduce((groupAcls: GroupedACLs, item: ACL) => {
       const type = item.val?.type?.toUpperCase();
index aa65fc210d060bed3683f3df5d7195a0aedf18f9..821c529a52afbb5087c660944b33f52b5ab5dae3 100644 (file)
@@ -22,8 +22,6 @@
           [invalid]="storageClassForm.showError('storageClassType', formDir, 'required')"
           [invalidText]="storageError"
         >
-          <option value=""
-                  i18n>-- Select Storage Class --</option>
           <option *ngFor="let opt of storageClassOptions"
                   [value]="opt.value"
                   i18n>
       </div>
       <div class="form-item form-item-append"
            cdsRow>
-        <!-- Storage Class Name -->
-        <div cdsCol>
-          <cds-text-label
-            labelInputID="storage_class"
-            i18n
-            [disabled]="editing"
-            [invalid]="
-              storageClassForm.controls.storage_class.invalid &&
-              storageClassForm.controls.storage_class.dirty
-            "
-            [invalidText]="storageClassError"
-          >
-            Storage Class name
-            <input
-              cdsText
-              type="text"
-              formControlName="storage_class"
-              [invalid]="storageClassForm.showError('storage_class', formDir, 'required')"
-            />
-          </cds-text-label>
-          <ng-template #storageClassError>
-            @if (storageClassForm.showError('storage_class', formDir, 'required')) {
-            <span class="invalid-feedback"
-                  i18n>This field is required.</span>
-            }
-          </ng-template>
-        </div>
-        <!-- Zone Group / Region -->
+        <!-- Zonegroup -->
         <div cdsCol>
           <cds-select
-            label="Zone Group / Region"
+            label="Zone Group Name"
             i18n-label
             formControlName="zonegroup"
             id="zonegroup"
             }
           </ng-template>
         </div>
+        @if( isTierMatch(TIER_TYPE.CLOUD_TIER, TIER_TYPE.GLACIER )){
+          <div cdsCol>
+            <ng-container *ngTemplateOutlet="storageClassField"></ng-container>
+          </div>
+        }
+        @if( isTierMatch(TIER_TYPE.LOCAL )){
+        <div cdsCol>
+          <cds-select
+            label="Zone"
+            i18n-label
+            formControlName="zone"
+            id="zone"
+            [invalid]="storageClassForm.showError('zone', formDir, 'required')"
+            [invalidText]="zoneError"
+          >
+            <option [value]="''"
+                    i18n>--Select--</option>
+            <option *ngFor="let zone of zones"
+                    [value]="zone?.name"
+                    [selected]="zone.name === storageClassForm.getValue('zone')"
+                    i18n>
+              {{ zone.name }}
+            </option>
+          </cds-select>
+          <ng-template #zoneError>
+            <span
+              class="invalid-feedback"
+              *ngIf="storageClassForm.showError('zone', formDir, 'required')"
+              i18n
+              >This field is required.</span
+            >
+          </ng-template>
+        </div>
+        }
       </div>
-      @if( isTierMatch( TIER_TYPE.CLOUD_TIER, TIER_TYPE.GLACIER )){
+      @if( isTierMatch(TIER_TYPE.LOCAL )){
+        <ng-container *ngTemplateOutlet="storageClassField"></ng-container>
+        <div class="form-item">
+        <cd-alert-panel type="info"
+                        [showTitle]="false"
+                        spacingClass="mb-2"
+                        (action)="navigateCreatePool()">
+          <span i18n>To create a new pool click <a [routerLink]="[POOL.PATH]">here</a></span>
+        </cd-alert-panel>
+        <cds-select
+          label="Pool"
+          i18n-label
+          formControlName="pool"
+          id="pool"
+          [invalid]="storageClassForm.showError('pool', formDir, 'required')"
+          [invalidText]="poolError"
+        >
+          <option [value]="''"
+                  i18n>--Select--</option>
+          <option *ngFor="let pool of rgwPools"
+                  [value]="pool.pool_name"
+                  i18n>
+            {{ pool.pool_name }}
+          </option>
+        </cds-select>
+        <ng-template #poolError>
+          <span
+            class="invalid-feedback"
+            *ngIf="storageClassForm.showError('pool', formDir, 'required')"
+            i18n
+            >This field is required.</span
+          >
+        </ng-template>
+      </div>
+      } @if( isTierMatch( TIER_TYPE.CLOUD_TIER, TIER_TYPE.GLACIER )){
       <div>
         <div class="form-item form-item-append"
              cdsRow>
                       "
                       [invalidText]="placementError"
                     >
-                      <option [value]="''"
-                              i18n>--Select--</option>
                       @for (placementTarget of placementTargets; track placementTarget) {
                       <option
                         [value]="placementTarget"
     </form>
   </ng-container>
 </div>
+
+<ng-template #storageClassField
+             [formGroup]="storageClassForm">
+  <cds-text-label labelInputID="storage_class"
+                  i18n
+                  [disabled]="editing"
+                  [invalid]="storageClassForm.controls.storage_class.invalid && storageClassForm.controls.storage_class.dirty"
+                  [invalidText]="storageClassError"
+                  >Name
+    <input cdsText
+           type="text"
+           id="storage_class"
+           formControlName="storage_class"
+           [invalid]="storageClassForm.showError('storage_class', formDir, 'required')"/>
+  </cds-text-label>
+  <ng-template #storageClassError>
+    <span class="invalid-feedback"
+          *ngIf="storageClassForm.showError('storage_class', formDir, 'required')"
+          i18n>This field is required.</span>
+  </ng-template>
+</ng-template>
index 15a9bb7b4c2c4b4b4770d7d8f1a5d7d129c19c04..5288e7d3d971bcdd97fa459e6500db1cdbcdb285 100644 (file)
@@ -61,13 +61,24 @@ import {
   AclTypeConst,
   ACLVal,
   AclLabel,
-  AclType
+  AclType,
+  ZoneRequest,
+  AllZonesResponse,
+  POOL
 } from '../models/rgw-storage-class.model';
 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
 import { NotificationService } from '~/app/shared/services/notification.service';
 import { CdValidators } from '~/app/shared/forms/cd-validators';
 import { FormatterService } from '~/app/shared/services/formatter.service';
 import validator from 'validator';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { Pool } from '../../pool/pool';
+import { catchError, map, switchMap } from 'rxjs/operators';
+import { RGW } from '../utils/constants';
+import { forkJoin, of } from 'rxjs';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { BucketTieringUtils } from '../utils/rgw-bucket-tiering';
 
 @Component({
   selector: 'cd-rgw-storage-class-form',
@@ -84,6 +95,7 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
   zonegroupNames: ZoneGroup[];
   placementTargets: string[] = [];
   selectedZoneGroup: string;
+  selectedZone: string;
   defaultZonegroup: ZoneGroup;
   zoneGroupDetails: ZoneGroupDetails;
   storageClassInfo: StorageClass;
@@ -103,6 +115,10 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
     const value = control.value;
     return !value || validator.isURL(value) ? null : { invalidUrl: true };
   };
+  rgwPools: Pool[];
+  zones: any[];
+  POOL = POOL;
+
   constructor(
     public actionLabels: ActionLabelsI18n,
     private formBuilder: CdFormBuilder,
@@ -112,7 +128,10 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
     private router: Router,
     private route: ActivatedRoute,
     public formatter: FormatterService,
-    private cdRef: ChangeDetectorRef
+    private cdRef: ChangeDetectorRef,
+    private poolService: PoolService,
+    private dimlessBinary: DimlessBinaryPipe,
+    private rgwZoneService: RgwZoneService
   ) {
     super();
     this.resource = $localize`Tiering Storage Class`;
@@ -148,7 +167,6 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
     this.createForm();
     this.storageClassTypeText();
     this.updateTierTypeHelpText();
-    this.loadingReady();
     this.loadZoneGroup();
     if (this.editing) {
       this.route.params.subscribe((params: StorageClass) => {
@@ -156,66 +174,117 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
       });
       this.rgwStorageService
         .getPlacement_target(this.storageClassInfo.placement_target)
-        .subscribe((placementTargetInfo: PlacementTarget) => {
-          this.tierTargetInfo = this.getTierTargetByStorageClass(
-            placementTargetInfo,
-            this.storageClassInfo.storage_class
-          );
-          let response = this.tierTargetInfo?.val?.s3;
-          const aclMappings = this.tierTargetInfo?.val?.s3?.acl_mappings || [];
-          this.storageClassForm.get('zonegroup').disable();
-          this.storageClassForm.get('placement_target').disable();
-          this.storageClassForm.get('storage_class').disable();
-          if (
-            this.tierTargetInfo?.val?.tier_type === TIER_TYPE.CLOUD_TIER ||
-            this.tierTargetInfo?.val?.tier_type === TIER_TYPE.GLACIER
-          ) {
-            this.storageClassForm.get('storageClassType').disable();
-          }
-          this.aclList = this.tierTargetInfo?.val?.s3?.acl_mappings || [];
-          this.storageClassForm.patchValue({
-            zonegroup: this.storageClassInfo?.zonegroup_name,
-            region: response?.region,
-            placement_target: this.storageClassInfo?.placement_target,
-            storageClassType: this.tierTargetInfo?.val?.tier_type ?? TIER_TYPE.LOCAL,
-            target_endpoint: response?.endpoint,
-            storage_class: this.storageClassInfo?.storage_class,
-            access_key: response?.access_key,
-            secret_key: response?.secret,
-            target_path: response?.target_path,
-            retain_head_object: this.tierTargetInfo?.val?.retain_head_object || false,
-            allow_read_through: this.tierTargetInfo?.val?.allow_read_through || false,
-            restore_storage_class: this.tierTargetInfo?.val?.restore_storage_class,
-            read_through_restore_days: this.tierTargetInfo?.val?.read_through_restore_days,
-            acl_mappings: this.tierTargetInfo?.val?.s3?.acl_mappings || []
-          });
-          if (
-            this.storageClassForm.get('storageClassType')?.value === TIER_TYPE.CLOUD_TIER ||
-            this.storageClassForm.get('storageClassType')?.value === TIER_TYPE.GLACIER
-          ) {
-            this.acls?.clear();
-            if (aclMappings.length > 0) {
-              aclMappings.forEach((acl) => {
-                this.acls?.push(
-                  this.formBuilder.group({
-                    source_id: [acl.val?.source_id || ''],
-                    dest_id: [acl.val?.dest_id || ''],
-                    type: [acl.val?.type || AclTypeConst.ID, Validators.required]
-                  })
-                );
-              });
-            } else {
-              this.addAcls();
+        .pipe(
+          switchMap((placementTargetInfo: PlacementTarget) => {
+            // Set the tierTargetInfo based on the placementTargetInfo and storageClassInfo
+            this.tierTargetInfo = this.getTierTargetByStorageClass(
+              placementTargetInfo,
+              this.storageClassInfo.storage_class
+            );
+            const tierType = this.tierTargetInfo?.val?.tier_type ?? TIER_TYPE.LOCAL;
+
+            // If tierType is LOCAL, make the second API calls
+            if (tierType === TIER_TYPE.LOCAL) {
+              return forkJoin([
+                this.poolService.getList(),
+                this.rgwZoneService.getAllZonesInfo()
+              ]).pipe(map(([pools, zones]) => ({ placementTargetInfo, pools, zones })));
             }
-          }
-          if (this.tierTargetInfo?.val?.tier_type == TIER_TYPE.GLACIER) {
-            let glacierResponse = this.tierTargetInfo?.val['s3-glacier'];
+
+            // If tierType is not LOCAL, just return placementTargetInfo with null pools and zones
+            return of({ placementTargetInfo, pools: null, zones: null });
+          }),
+          map(({ placementTargetInfo, pools, zones }) => {
+            return { placementTargetInfo, pools, zones };
+          }),
+          catchError(() => {
+            return of({
+              placementTargetInfo: null,
+              pools: null,
+              zones: null
+            });
+          })
+        )
+        .subscribe(
+          (data: {
+            placementTargetInfo: PlacementTarget;
+            pools: Pool[] | null;
+            zones: AllZonesResponse | null;
+          }) => {
+            let response = this.tierTargetInfo?.val?.s3;
+            this.aclList = response?.acl_mappings || [];
             this.storageClassForm.patchValue({
-              glacier_restore_tier_type: glacierResponse.glacier_restore_tier_type,
-              glacier_restore_days: glacierResponse.glacier_restore_days
+              zonegroup: this.storageClassInfo?.zonegroup_name,
+              region: response?.region,
+              placement_target: this.storageClassInfo?.placement_target,
+              storageClassType: this.tierTargetInfo?.val?.tier_type ?? TIER_TYPE.LOCAL,
+              target_endpoint: response?.endpoint,
+              storage_class: this.storageClassInfo?.storage_class,
+              access_key: response?.access_key,
+              secret_key: response?.secret,
+              target_path: response?.target_path,
+              retain_head_object: this.tierTargetInfo?.val?.retain_head_object || false,
+              multipart_sync_threshold:
+                this.dimlessBinary.transform(response?.multipart_sync_threshold) || '',
+              multipart_min_part_size:
+                this.dimlessBinary.transform(response?.multipart_min_part_size) || '',
+              allow_read_through: this.tierTargetInfo?.val?.allow_read_through || false,
+              restore_storage_class: this.tierTargetInfo?.val?.restore_storage_class,
+              read_through_restore_days: this.tierTargetInfo?.val?.read_through_restore_days,
+              acl_mappings: response?.acl_mappings || []
+            });
+            if (
+              this.storageClassForm.get('storageClassType')?.value === TIER_TYPE.CLOUD_TIER ||
+              this.storageClassForm.get('storageClassType')?.value === TIER_TYPE.GLACIER
+            ) {
+              this.acls?.clear();
+              if (this.aclList.length > 0) {
+                this.aclList.forEach((acl) => {
+                  this.acls?.push(
+                    this.formBuilder.group({
+                      source_id: [acl.val?.source_id || ''],
+                      dest_id: [acl.val?.dest_id || ''],
+                      type: [acl.val?.type || AclTypeConst.ID, Validators.required]
+                    })
+                  );
+                });
+              } else {
+                this.addAcls();
+              }
+            }
+            if (this.tierTargetInfo?.val?.tier_type == TIER_TYPE.GLACIER) {
+              let glacierResponse = this.tierTargetInfo?.val['s3-glacier'];
+              this.storageClassForm.patchValue({
+                glacier_restore_tier_type: glacierResponse.glacier_restore_tier_type,
+                glacier_restore_days: glacierResponse.glacier_restore_days
+              });
+            }
+            const zoneInfo = BucketTieringUtils.getZoneInfoHelper(data.zones?.zones, {
+              placement_target: this.storageClassInfo?.placement_target,
+              storage_class: this.storageClassInfo?.storage_class
             });
+            if (data.pools) {
+              this.rgwPools = data.pools.filter((pool: Pool) =>
+                pool.application_metadata?.includes(RGW)
+              );
+              this.storageClassForm.get('pool').setValue(zoneInfo.data_pool);
+              this.storageClassForm.get('zone').setValue(zoneInfo.zone_name);
+            }
+            this.loadingReady();
           }
-        });
+        );
+      this.storageClassForm.get('zonegroup').disable();
+      this.storageClassForm.get('placement_target').disable();
+      this.storageClassForm.get('storage_class').disable();
+      this.storageClassForm.get('zone').disable();
+      this.storageClassForm.get('storageClassType').disable();
+    } else {
+      this.addAcls();
+      this.poolService.getList().subscribe((resp: Pool[]) => {
+        // Filter only pools with "rgw" in application_metadata
+        this.rgwPools = resp.filter((pool: Pool) => pool.application_metadata?.includes(RGW));
+        this.loadingReady();
+      });
     }
     this.storageClassForm.get('storageClassType').valueChanges.subscribe((value) => {
       this.updateValidatorsBasedOnStorageClass(value);
@@ -224,128 +293,42 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
       this.onAllowReadThroughChange(value);
     });
   }
-  createForm() {
-    const self = this;
 
-    const lockDaysValidator = CdValidators.custom('lockDays', () => {
-      if (!self.storageClassForm || !self.storageClassForm.getRawValue()) {
-        return false;
-      }
-
-      const lockDays = Number(self.storageClassForm.getValue('read_through_restore_days'));
-      return !Number.isInteger(lockDays) || lockDays === 0;
-    });
-    this.storageClassForm = this.formBuilder.group({
-      storage_class: new FormControl('', {
-        validators: [Validators.required]
-      }),
-      zonegroup: new FormControl(this.selectedZoneGroup, {
-        validators: [Validators.required]
-      }),
-      region: new FormControl('', [
-        CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
-      ]),
-      placement_target: new FormControl('', {
-        validators: [Validators.required]
-      }),
-      access_key: new FormControl(null, [
-        CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
-      ]),
-      secret_key: new FormControl(null, [
-        CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
-      ]),
-      target_path: new FormControl('', [
-        CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
-      ]),
-      retain_head_object: new FormControl(true),
-      glacier_restore_tier_type: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS, [
-        CdValidators.composeIf({ storageClassType: TIER_TYPE.GLACIER }, [Validators.required])
-      ]),
-      target_endpoint: new FormControl('', [Validators.required, this.urlValidator]),
-      glacier_restore_days: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_GLACIER_RESTORE_DAYS, [
-        CdValidators.composeIf({ storageClassType: TIER_TYPE.GLACIER || TIER_TYPE.CLOUD_TIER }, [
-          CdValidators.number(false),
-          lockDaysValidator
-        ])
+  public createAcls(): CdFormGroup {
+    const group = this.formBuilder.group({
+      type: new FormControl(AclTypeConst.ID, Validators.required),
+      source_id: new FormControl('', [
+        CdValidators.composeIf(
+          {
+            type: AclTypeConst.EMAIL
+          },
+          [Validators.email]
+        ),
+        CdValidators.composeIf(
+          {
+            type: AclTypeConst.URI
+          },
+          [this.urlValidator]
+        )
       ]),
-      restore_storage_class: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS),
-      read_through_restore_days: new FormControl(
-        {
-          value: STORAGE_CLASS_CONSTANTS.DEFAULT_READTHROUGH_RESTORE_DAYS,
-          disabled: true
-        },
+      dest_id: new FormControl('', [
         CdValidators.composeIf(
-          (form: AbstractControl) => {
-            const type = form.get('storageClassType')?.value;
-            return type === TIER_TYPE.GLACIER || type === TIER_TYPE.CLOUD_TIER;
+          {
+            type: AclTypeConst.EMAIL
           },
-          [CdValidators.number(false), lockDaysValidator]
+          [Validators.email]
+        ),
+        CdValidators.composeIf(
+          {
+            type: AclTypeConst.URI
+          },
+          [this.urlValidator]
         )
-      ),
-      multipart_sync_threshold: new FormControl(
-        STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_SYNC_THRESHOLD
-      ),
-      multipart_min_part_size: new FormControl(
-        STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_MIN_PART_SIZE
-      ),
-      allow_read_through: new FormControl(false),
-      storageClassType: new FormControl(TIER_TYPE.LOCAL, Validators.required),
-      acls: new FormArray([])
-    });
-    this.storageClassForm.get('storageClassType')?.valueChanges.subscribe((type: string) => {
-      if (type === TIER_TYPE.CLOUD_TIER) {
-        const aclsArray = this.storageClassForm.get('acls') as FormArray;
-        aclsArray.push(this.createAcls());
-      }
-    });
-  }
-
-  public createAcls(): CdFormGroup {
-    const group = this.formBuilder.group({
-      type: new FormControl(AclTypeConst.ID, Validators.required),
-      source_id: new FormControl(''),
-      dest_id: new FormControl('')
+      ])
     });
-
-    const sourceId = group.get('source_id');
-    const destId = group.get('dest_id');
-
-    const validators = this.getValidatorsType(AclTypeConst.ID);
-
-    sourceId.setValidators(validators);
-    destId.setValidators(validators);
-
-    sourceId.updateValueAndValidity();
-    destId.updateValueAndValidity();
-
-    group.get('type')?.valueChanges.subscribe((newType: AclType) => {
-      const sourceId = group.get('source_id');
-      const destId = group.get('dest_id');
-
-      const validators = this.getValidatorsType(newType);
-
-      sourceId.setValidators(validators);
-      destId.setValidators(validators);
-
-      sourceId.updateValueAndValidity();
-      destId.updateValueAndValidity();
-    });
-
     return group;
   }
 
-  private getValidatorsType(type: AclType) {
-    switch (type) {
-      case AclTypeConst.EMAIL:
-        return [Validators.email];
-      case AclTypeConst.URI:
-        return [this.urlValidator];
-      case AclTypeConst.ID:
-      default:
-        return [Validators.required];
-    }
-  }
-
   get acls(): FormArray {
     return this.storageClassForm.get('acls') as FormArray;
   }
@@ -366,15 +349,21 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
     });
 
     if (this.editing) {
-      const defaultValues = {
+      const defaultValues: {
+        allow_read_through: boolean;
+        read_through_restore_days: number;
+        restore_storage_class: string;
+        multipart_min_part_size: number;
+        multipart_sync_threshold: number;
+      } = {
         allow_read_through: false,
         read_through_restore_days: STORAGE_CLASS_CONSTANTS.DEFAULT_READTHROUGH_RESTORE_DAYS,
         restore_storage_class: STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS,
         multipart_min_part_size: STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_MIN_PART_SIZE,
         multipart_sync_threshold: STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_SYNC_THRESHOLD
       };
-      Object.keys(defaultValues).forEach((key) => {
-        this.storageClassForm.get(key).setValue(defaultValues[key]);
+      (Object.keys(defaultValues) as Array<keyof typeof defaultValues>).forEach((key) => {
+        this.storageClassForm.get(key)?.setValue(defaultValues[key]);
       });
     }
   }
@@ -435,66 +424,169 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
     });
   }
 
+  createForm() {
+    const self = this;
+
+    const lockDaysValidator = CdValidators.custom('lockDays', () => {
+      if (!self.storageClassForm || !self.storageClassForm.getRawValue()) {
+        return false;
+      }
+
+      const lockDays = Number(self.storageClassForm.getValue('read_through_restore_days'));
+      return !Number.isInteger(lockDays) || lockDays === 0;
+    });
+    this.storageClassForm = this.formBuilder.group({
+      storage_class: new FormControl('', {
+        validators: [Validators.required]
+      }),
+      zonegroup: new FormControl(this.selectedZoneGroup, {
+        validators: [Validators.required]
+      }),
+      region: new FormControl('', [
+        CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
+      ]),
+      placement_target: new FormControl('', {
+        validators: [Validators.required]
+      }),
+      target_endpoint: new FormControl('', [Validators.required, this.urlValidator]),
+      access_key: new FormControl(null, [
+        CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
+      ]),
+      secret_key: new FormControl(null, [
+        CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
+      ]),
+      target_path: new FormControl('', [
+        CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
+      ]),
+      retain_head_object: new FormControl(true),
+      glacier_restore_tier_type: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS, [
+        CdValidators.composeIf({ storageClassType: TIER_TYPE.GLACIER }, [Validators.required])
+      ]),
+      glacier_restore_days: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_GLACIER_RESTORE_DAYS, [
+        CdValidators.composeIf({ storageClassType: TIER_TYPE.GLACIER || TIER_TYPE.CLOUD_TIER }, [
+          CdValidators.number(false),
+          lockDaysValidator
+        ])
+      ]),
+      restore_storage_class: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS),
+      read_through_restore_days: new FormControl(
+        {
+          value: STORAGE_CLASS_CONSTANTS.DEFAULT_READTHROUGH_RESTORE_DAYS,
+          disabled: true
+        },
+        CdValidators.composeIf(
+          (form: AbstractControl) => {
+            const type = form.get('storageClassType')?.value;
+            return type === TIER_TYPE.GLACIER || type === TIER_TYPE.CLOUD_TIER;
+          },
+          [CdValidators.number(false), lockDaysValidator]
+        )
+      ),
+      multipart_sync_threshold: new FormControl(
+        STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_SYNC_THRESHOLD
+      ),
+      multipart_min_part_size: new FormControl(
+        STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_MIN_PART_SIZE
+      ),
+      allow_read_through: new FormControl(false),
+      storageClassType: new FormControl(TIER_TYPE.LOCAL, Validators.required),
+      pool: new FormControl('', [
+        CdValidators.composeIf({ storageClassType: TIER_TYPE.LOCAL }, [Validators.required])
+      ]),
+      zone: new FormControl(null, [
+        CdValidators.composeIf({ storageClassType: TIER_TYPE.LOCAL }, [Validators.required])
+      ]),
+      acls: new FormArray([])
+    });
+    this.storageClassForm.get('storageClassType')?.valueChanges.subscribe((type: string) => {
+      if (type === TIER_TYPE.CLOUD_TIER) {
+        const aclsArray = this.storageClassForm.get('acls') as FormArray;
+        aclsArray.clear();
+        aclsArray.push(this.createAcls());
+      }
+    });
+  }
+
   loadZoneGroup(): Promise<void> {
     return new Promise((resolve, reject) => {
       this.rgwZoneGroupService.getAllZonegroupsInfo().subscribe(
         (data: ZoneGroupDetails) => {
           this.zoneGroupDetails = data;
           this.zonegroupNames = [];
-          this.placementTargets = [];
+          this.zones = [];
           if (data.zonegroups && data.zonegroups.length > 0) {
-            this.zonegroupNames = data.zonegroups.map((zoneGroup: ZoneGroup) => {
-              return {
-                id: zoneGroup.id,
-                name: zoneGroup.name
-              };
-            });
+            this.zonegroupNames = data.zonegroups.map((zoneGroup: ZoneGroup) => ({
+              id: zoneGroup.id,
+              name: zoneGroup.name,
+              zones: zoneGroup.zones
+            }));
           }
           this.defaultZonegroup = this.zonegroupNames.find(
-            (zonegroups: ZoneGroup) => zonegroups.id === data.default_zonegroup
+            (zonegroup) => zonegroup.id === data?.default_zonegroup
           );
-          this.storageClassForm.get('zonegroup').setValue(this.defaultZonegroup.name);
+          this.storageClassForm.get('zonegroup').setValue(this.defaultZonegroup?.name);
           this.onZonegroupChange();
+
           resolve();
         },
-        (error) => reject(error)
+        (error) => {
+          reject(error);
+        }
       );
     });
   }
 
-  onZonegroupChange() {
+  onZonegroupChange(): void {
     const zoneGroupControl = this.storageClassForm.get('zonegroup').value;
-    const selectedZoneGroup = this.zoneGroupDetails.zonegroups.find(
-      (zonegroup) => zonegroup.name === zoneGroupControl
+    const selectedZoneGroup = this.zoneGroupDetails?.zonegroups?.find(
+      (zonegroup) => zonegroup?.name === zoneGroupControl
     );
-    const defaultPlacementTarget = selectedZoneGroup.placement_targets.find(
+    const defaultPlacementTarget = selectedZoneGroup?.placement_targets?.find(
       (target: Target) => target.name === DEFAULT_PLACEMENT
     );
-    if (selectedZoneGroup) {
-      const placementTargetNames = selectedZoneGroup.placement_targets.map(
+
+    if (selectedZoneGroup?.placement_targets) {
+      this.placementTargets = selectedZoneGroup.placement_targets.map(
         (target: Target) => target.name
       );
-      this.placementTargets = placementTargetNames;
     }
     if (defaultPlacementTarget && !this.editing) {
       this.storageClassForm.get('placement_target').setValue(defaultPlacementTarget.name);
     } else {
       this.storageClassForm
         .get('placement_target')
-        .setValue(this.storageClassInfo.placement_target);
+        .setValue(this.storageClassInfo?.placement_target || null);
     }
+    this.zones = selectedZoneGroup?.zones;
   }
 
   submitAction() {
     const component = this;
     const requestModel = this.buildRequest();
+    const rawFormValue = _.cloneDeep(this.storageClassForm.getRawValue());
+    const zoneRequest: ZoneRequest = {
+      zone_name: this.storageClassForm.get('zone').value,
+      placement_target: this.storageClassForm.get('placement_target').value,
+      storage_class: this.storageClassForm.get('storage_class').value,
+      data_pool: this.storageClassForm.get('pool')?.value || ''
+    };
+
     const storageclassName = this.storageClassForm.get('storage_class').value;
     if (this.editing) {
-      this.rgwStorageService.editStorageClass(requestModel).subscribe(
+      const editStorageClass$ = this.rgwStorageService.editStorageClass(requestModel);
+
+      const editZone$ =
+        rawFormValue.storageClassType === TIER_TYPE.LOCAL
+          ? editStorageClass$.pipe(
+              switchMap(() => this.rgwStorageService.editStorageClassZone(zoneRequest))
+            )
+          : editStorageClass$;
+
+      editZone$.subscribe(
         () => {
           this.notificationService.show(
             NotificationType.success,
-            $localize`Updated Storage Class '${storageclassName}'`
+            $localize`Edited Storage Class '${storageclassName}'`
           );
           this.goToListView();
         },
@@ -503,7 +595,16 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
         }
       );
     } else {
-      this.rgwStorageService.createStorageClass(requestModel).subscribe(
+      const createStorageClass$ = this.rgwStorageService.createStorageClass(requestModel);
+
+      const createZone$ =
+        rawFormValue.storageClassType === TIER_TYPE.LOCAL
+          ? createStorageClass$.pipe(
+              switchMap(() => this.rgwStorageService.createStorageClassZone(zoneRequest))
+            )
+          : createStorageClass$;
+
+      createZone$.subscribe(
         () => {
           this.notificationService.show(
             NotificationType.success,
@@ -517,7 +618,6 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
       );
     }
   }
-
   goToListView() {
     this.router.navigate([`rgw/tiering`]);
   }
index 696be083a660ceb1d5462062032113cc26b88357..069c5525cba22935ab8611a6c2768670b3d422ea 100644 (file)
@@ -14,7 +14,7 @@
   [hasDetails]="true"
   (setExpandedRow)="setExpandedRow($event)"
   (updateSelection)="updateSelection($event)"
->
+  [autoReload]="false">
   <div class="table-actions">
     <cd-table-actions class="btn-group"
                       [permission]="permission"
index 9cd8abf13aaedc3fe2f7c0f8a323fa747653c8fa..991c82c42967686461c1156158de6cf925679c61 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, NgZone, OnInit } from '@angular/core';
 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';
@@ -46,7 +46,8 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI
     private authStorageService: AuthStorageService,
     private rgwStorageClassService: RgwStorageClassService,
     private router: Router,
-    private urlBuilder: URLBuilderService
+    private urlBuilder: URLBuilderService,
+    protected ngZone: NgZone
   ) {
     super();
     this.permission = this.authStorageService.getPermissions().rgw;
@@ -60,7 +61,7 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI
         isHidden: true
       },
       {
-        name: $localize`Storage Class name`,
+        name: $localize`Name`,
         prop: 'storage_class',
         flexGrow: 2
       },
@@ -85,11 +86,16 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI
         flexGrow: 2
       }
     ];
-    const getStorageUri = () =>
-      this.selection.first() &&
-      `${encodeURI(this.selection.first().zonegroup_name)}/${encodeURI(
-        this.selection.first().placement_target
-      )}/${encodeURI(this.selection.first().storage_class)}`;
+    const getStorageUri = () => {
+      const selection = this.selection.first();
+      if (!selection) return '';
+
+      let url = `${encodeURIComponent(selection.zonegroup_name)}/${encodeURIComponent(
+        selection.placement_target
+      )}/${encodeURIComponent(selection.storage_class)}`;
+
+      return url;
+    };
     this.tableActions = [
       {
         name: this.actionLabels.CREATE,
@@ -111,9 +117,11 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI
         click: () => this.removeStorageClassModal()
       }
     ];
+    this.setTableRefreshTimeout();
   }
 
   loadStorageClass(): Promise<void> {
+    this.setTableRefreshTimeout();
     return new Promise((resolve, reject) => {
       this.rgwZonegroupService.getAllZonegroupsInfo().subscribe(
         (data: ZoneGroupDetails) => {
@@ -179,8 +187,4 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI
   updateSelection(selection: CdTableSelection) {
     this.selection = selection;
   }
-
-  setExpandedRow(expandedRow: any) {
-    super.setExpandedRow(expandedRow);
-  }
 }
index 099c0650b7901be90dc52d2d9a73e1f6696e2657..62920a1d3d33c127de3f31ca61e0ea3a7d557978 100644 (file)
@@ -83,8 +83,9 @@ import {
   TooltipModule,
   ComboBoxModule,
   ToggletipModule,
+  IconService,
   LayoutModule,
-  IconService
+  SkeletonModule
 } from 'carbon-components-angular';
 import EditIcon from '@carbon/icons/es/edit/16';
 import ScalesIcon from '@carbon/icons/es/scales/20';
@@ -153,7 +154,8 @@ import { NfsClusterFormComponent } from '../nfs/nfs-cluster-form/nfs-cluster-for
     ToggletipModule,
     RadioModule,
     SelectModule,
-    LayoutModule
+    LayoutModule,
+    SkeletonModule
   ],
   exports: [
     RgwDaemonDetailsComponent,
index c6896d62fa04cee6fcadfa7403acbc7193413216..4a619b39cae729054529850687938b9a85b90e1d 100644 (file)
@@ -3,7 +3,9 @@ import {
   TierTarget,
   TIER_TYPE,
   ZoneGroup,
-  ZoneGroupDetails
+  ZoneGroupDetails,
+  StorageClassDetails,
+  Zone
 } from '../models/rgw-storage-class.model';
 
 export class BucketTieringUtils {
@@ -61,4 +63,30 @@ export class BucketTieringUtils {
     }
     return cloudProps;
   }
+
+  static getZoneInfoHelper(zones: Zone[], selectedStorageClass: Partial<StorageClassDetails>) {
+    if (zones && zones.length > 0 && selectedStorageClass) {
+      const zoneFound = zones.find((zone) =>
+        zone.placement_pools.some(
+          (placement) =>
+            placement.key === selectedStorageClass?.placement_target &&
+            placement.val.storage_classes[selectedStorageClass?.storage_class]
+        )
+      );
+
+      if (zoneFound) {
+        const placement = zoneFound.placement_pools.find(
+          (p) =>
+            p.key === selectedStorageClass?.placement_target &&
+            p.val.storage_classes[selectedStorageClass?.storage_class]
+        );
+        const storageClassEntry =
+          placement?.val.storage_classes[selectedStorageClass?.storage_class];
+        if (storageClassEntry) {
+          return { zone_name: zoneFound.name, data_pool: storageClassEntry.data_pool };
+        }
+      }
+    }
+    return { zone_name: '', data_pool: '' };
+  }
 }
index b6b3cbc3867c7c1d62e24ddde9b459c44f73c787..cff162286f97a9f93b46608cf213c443410a816c 100644 (file)
@@ -1,12 +1,13 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
-import { RequestModel } from '~/app/ceph/rgw/models/rgw-storage-class.model';
+import { RequestModel, ZoneRequest } from '~/app/ceph/rgw/models/rgw-storage-class.model';
 
 @Injectable({
   providedIn: 'root'
 })
 export class RgwStorageClassService {
   private baseUrl = 'api/rgw/zonegroup';
+  private zoneUrl = 'api/rgw/zone';
   private url = `${this.baseUrl}/storage-class`;
 
   constructor(private http: HttpClient) {}
@@ -28,4 +29,12 @@ export class RgwStorageClassService {
   getPlacement_target(placement_id: string) {
     return this.http.get(`${this.baseUrl}/get_placement_target_by_placement_id/${placement_id}`);
   }
+
+  createStorageClassZone(zoneRequest: ZoneRequest) {
+    return this.http.post(`${this.zoneUrl}/storage-class`, zoneRequest);
+  }
+
+  editStorageClassZone(zoneRequest: ZoneRequest) {
+    return this.http.put(`${this.zoneUrl}/storage-class`, zoneRequest);
+  }
 }
index 756a85b3c2f28f7739dac0cf3e56d4ab80fa4536..4de24e5f70521350c1743a863add4b6f1ca05f0b 100755 (executable)
@@ -15562,6 +15562,103 @@ paths:
       - jwt: []
       tags:
       - RgwZone
+  /api/rgw/zone/storage-class:
+    post:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                compression:
+                  default: ''
+                  type: string
+                data_pool:
+                  type: string
+                placement_target:
+                  type: string
+                storage_class:
+                  type: string
+                zone_name:
+                  type: string
+              required:
+              - zone_name
+              - placement_target
+              - storage_class
+              - data_pool
+              type: object
+      responses:
+        '201':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource created.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - RgwZone
+    put:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                compression:
+                  default: ''
+                  type: string
+                data_pool:
+                  type: string
+                placement_target:
+                  type: string
+                storage_class:
+                  type: string
+                zone_name:
+                  type: string
+              required:
+              - zone_name
+              - placement_target
+              - storage_class
+              - data_pool
+              type: object
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource updated.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - RgwZone
   /api/rgw/zone/{zone_name}:
     delete:
       parameters:
index a90fc6e96138ff69eb9a9969e9b5f6bd098fcf5a..69126321c05b1d155e9d65bb49c1546bae84cfc2 100755 (executable)
@@ -2441,7 +2441,23 @@ class RgwMultisite:
                                          http_status_code=500, component='rgw')
         except SubprocessError as error:
             raise DashboardException(error, http_status_code=500, component='rgw')
-        self.update_period()
+
+    def edit_storage_class_zone(self, zone_name: str, placement_target: str, storage_class: str,
+                                data_pool: str, compression: str):
+        edit_placement_target_cmd = ['zone', 'placement', 'modify', '--rgw-zone', zone_name,
+                                     '--placement-id', placement_target,
+                                     '--storage-class', storage_class,
+                                     '--data-pool', data_pool]
+        if compression:
+            edit_placement_target_cmd.extend(['--compression', compression])
+        try:
+            exit_code, _, err = mgr.send_rgwadmin_command(edit_placement_target_cmd)
+            if exit_code > 0:
+                raise DashboardException(e=err, msg=f'Unable to modify storage class \
+                                         {storage_class} to zone {zone_name}',
+                                         http_status_code=500, component='rgw')
+        except SubprocessError as error:
+            raise DashboardException(error, http_status_code=500, component='rgw')
 
     def edit_zone(self, zone_name: str, new_zone_name: str, zonegroup_name: str, default: str = '',
                   master: str = '', endpoints: str = '', access_key: str = '', secret_key: str = '',