]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Local storage class creation via dashboard doesn't handle creation... 65680/head
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>
Tue, 30 Sep 2025 04:38:01 +0000 (10:08 +0530)
Fixes: https://tracker.ceph.com/issues/72569
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

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 bbbf5b971adbae0318f445f47c3db2e0d1420f83..c9eb6badd30e405877a02c7e9e126b865c282e6b 100755 (executable)
@@ -1569,6 +1569,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 68c9d393375d0464791f557a79c46791e1993c2f..5288e7d3d971bcdd97fa459e6500db1cdbcdb285 100644 (file)
@@ -61,7 +61,10 @@ 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';
@@ -69,6 +72,13 @@ 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',
@@ -85,6 +95,7 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
   zonegroupNames: ZoneGroup[];
   placementTargets: string[] = [];
   selectedZoneGroup: string;
+  selectedZone: string;
   defaultZonegroup: ZoneGroup;
   zoneGroupDetails: ZoneGroupDetails;
   storageClassInfo: StorageClass;
@@ -104,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,
@@ -114,7 +129,9 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
     private route: ActivatedRoute,
     public formatter: FormatterService,
     private cdRef: ChangeDetectorRef,
-    private dimlessBinary: DimlessBinaryPipe
+    private poolService: PoolService,
+    private dimlessBinary: DimlessBinaryPipe,
+    private rgwZoneService: RgwZoneService
   ) {
     super();
     this.resource = $localize`Tiering Storage Class`;
@@ -150,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) => {
@@ -158,70 +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,
-            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: 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);
@@ -230,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;
   }
@@ -372,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]);
       });
     }
   }
@@ -441,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();
         },
@@ -509,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,
@@ -523,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 440f751ec953bc8e431231783b7b8a3d484ba337..837aaa5ba6ca1f8b841edf191af36089701b7a6a 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';
@@ -152,7 +153,8 @@ import { RgwNotificationFormComponent } from './rgw-notification-form/rgw-notifi
     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 f68a86ff003354446849162b5bab3f0047777d71..efce8173ae68673c11757c9e6f84381a77e780fd 100755 (executable)
@@ -15137,6 +15137,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 0afce9e2b0af9d65763cb4fd0e5bc1e76c33ec9a..35fd913622264fd46fd82672571b1bff98c01f5f 100755 (executable)
@@ -2440,11 +2440,44 @@ class RgwMultisite:
             raise DashboardException(error, http_status_code=500, component='rgw')
         self.update_period()
 
-    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 = '', placement_target: str = '',
-                  data_pool: str = '', index_pool: str = '', data_extra_pool: str = '',
-                  storage_class: str = '', data_pool_class: str = '', compression: str = ''):
+    def add_storage_class_zone(self, zone_name: str, placement_target: str, storage_class: str,
+                               data_pool: str, compression: str):
+        rgw_zone_add_storage_class_cmd = ['zone', 'placement', 'add', '--rgw-zone', zone_name,
+                                          '--placement-id', placement_target,
+                                          '--storage-class', storage_class,
+                                          '--data-pool', data_pool]
+        if compression:
+            rgw_zone_add_storage_class_cmd.extend(['--compression', compression])
+        try:
+            exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_add_storage_class_cmd)
+            if exit_code > 0:
+                raise DashboardException(e=err, msg='Unable to add storage class {} to zone {}'.format(storage_class, zone_name),  # noqa E501 #pylint: disable=line-too-long
+                                         http_status_code=500, component='rgw')
+        except SubprocessError as error:
+            raise DashboardException(error, http_status_code=500, component='rgw')
+
+    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 = '',
+                  placement_target: str = '', data_pool: str = '', index_pool: str = '',
+                  data_extra_pool: str = '', storage_class: str = '', data_pool_class: str = '',
+                  compression: str = ''):
         if new_zone_name != zone_name:
             rgw_zone_rename_cmd = ['zone', 'rename', '--rgw-zone',
                                    zone_name, '--zone-new-name', new_zone_name]