1 import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
8 } from '@angular/forms';
9 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
10 import { CdForm } from '~/app/shared/forms/cd-form';
11 import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
12 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
13 import _ from 'lodash';
14 import { ActivatedRoute, Router } from '@angular/router';
15 import { RgwStorageClassService } from '~/app/shared/api/rgw-storage-class.service';
16 import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
19 ALLOW_READ_THROUGH_TEXT,
21 MULTIPART_MIN_PART_TEXT,
22 MULTIPART_SYNC_THRESHOLD_TEXT,
25 RETAIN_HEAD_OBJECT_TEXT,
28 TARGET_ACCESS_KEY_TEXT,
32 TARGET_SECRET_KEY_TEXT,
37 CLOUDS3_STORAGE_CLASS_TEXT,
38 LOCAL_STORAGE_CLASS_TEXT,
39 GLACIER_STORAGE_CLASS_TEXT,
40 GLACIER_RESTORE_DAY_TEXT,
41 GLACIER_RESTORE_TIER_TYPE_TEXT,
43 READTHROUGH_RESTORE_DAYS_TEXT,
44 RESTORE_STORAGE_CLASS_TEXT,
48 STORAGE_CLASS_CONSTANTS,
49 STANDARD_TIER_TYPE_TEXT,
50 EXPEDITED_TIER_TYPE_TEXT,
52 CLOUD_TIER_REQUIRED_FIELDS,
53 GLACIER_REQUIRED_FIELDS,
54 GLACIER_TARGET_STORAGE_CLASS,
65 } from '../models/rgw-storage-class.model';
66 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
67 import { NotificationService } from '~/app/shared/services/notification.service';
68 import { CdValidators } from '~/app/shared/forms/cd-validators';
69 import { FormatterService } from '~/app/shared/services/formatter.service';
70 import validator from 'validator';
71 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
74 selector: 'cd-rgw-storage-class-form',
75 templateUrl: './rgw-storage-class-form.component.html',
76 styleUrls: ['./rgw-storage-class-form.component.scss']
78 export class RgwStorageClassFormComponent extends CdForm implements OnInit {
79 storageClassForm: CdFormGroup;
83 showAdvanced: boolean = false;
84 defaultZoneGroup: string;
85 zonegroupNames: ZoneGroup[];
86 placementTargets: string[] = [];
87 selectedZoneGroup: string;
88 defaultZonegroup: ZoneGroup;
89 zoneGroupDetails: ZoneGroupDetails;
90 storageClassInfo: StorageClass;
91 tierTargetInfo: TierTarget;
92 glacierStorageClassDetails: S3Glacier;
93 allowReadThrough: boolean = false;
94 TIER_TYPE = TIER_TYPE;
95 TIER_TYPE_DISPLAY = TIER_TYPE_DISPLAY;
96 storageClassOptions: TypeOption[];
97 helpTextLabels: TextLabels;
98 typeOptions: TypeOption[];
99 aclTypeLabel = AclTypeLabel;
100 aclHelperText = AclHelperText;
102 removedAclSourceIds: string[] = [];
103 urlValidator = (control: AbstractControl): ValidationErrors | null => {
104 const value = control.value;
105 return !value || validator.isURL(value) ? null : { invalidUrl: true };
108 public actionLabels: ActionLabelsI18n,
109 private formBuilder: CdFormBuilder,
110 private notificationService: NotificationService,
111 private rgwStorageService: RgwStorageClassService,
112 private rgwZoneGroupService: RgwZonegroupService,
113 private router: Router,
114 private route: ActivatedRoute,
115 public formatter: FormatterService,
116 private cdRef: ChangeDetectorRef,
117 private dimlessBinary: DimlessBinaryPipe
120 this.resource = $localize`Tiering Storage Class`;
121 this.editing = this.router.url.startsWith(`/rgw/tiering/${URLVerbs.EDIT}`);
122 this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
126 this.helpTextLabels = {
127 targetPathText: TARGET_PATH_TEXT,
128 targetEndpointText: TARGET_ENDPOINT_TEXT,
129 targetRegionText: TARGET_REGION_TEXT,
130 targetAccessKeyText: TARGET_ACCESS_KEY_TEXT,
131 targetSecretKeyText: TARGET_SECRET_KEY_TEXT,
132 retainHeadObjectText: RETAIN_HEAD_OBJECT_TEXT,
133 allowReadThroughText: ALLOW_READ_THROUGH_TEXT,
134 storageClassText: LOCAL_STORAGE_CLASS_TEXT,
135 multipartMinPartText: MULTIPART_MIN_PART_TEXT,
136 multipartSyncThresholdText: MULTIPART_SYNC_THRESHOLD_TEXT,
137 tiertypeText: STANDARD_TIER_TYPE_TEXT,
138 glacierRestoreDayText: GLACIER_RESTORE_DAY_TEXT,
139 glacierRestoreTiertypeText: GLACIER_RESTORE_TIER_TYPE_TEXT,
140 restoreDaysText: RESTORE_DAYS_TEXT,
141 readthroughrestoreDaysText: READTHROUGH_RESTORE_DAYS_TEXT,
142 restoreStorageClassText: RESTORE_STORAGE_CLASS_TEXT
144 this.storageClassOptions = [
145 { value: TIER_TYPE.LOCAL, label: TIER_TYPE_DISPLAY.LOCAL },
146 { value: TIER_TYPE.CLOUD_TIER, label: TIER_TYPE_DISPLAY.CLOUD_TIER },
147 { value: TIER_TYPE.GLACIER, label: TIER_TYPE_DISPLAY.GLACIER }
149 this.typeOptions = [...AclTypeOptions];
151 this.storageClassTypeText();
152 this.updateTierTypeHelpText();
154 this.loadZoneGroup();
156 this.route.params.subscribe((params: StorageClass) => {
157 this.storageClassInfo = params;
159 this.rgwStorageService
160 .getPlacement_target(this.storageClassInfo.placement_target)
161 .subscribe((placementTargetInfo: PlacementTarget) => {
162 this.tierTargetInfo = this.getTierTargetByStorageClass(
164 this.storageClassInfo.storage_class
166 let response = this.tierTargetInfo?.val?.s3;
167 const aclMappings = this.tierTargetInfo?.val?.s3?.acl_mappings || [];
168 this.storageClassForm.get('zonegroup').disable();
169 this.storageClassForm.get('placement_target').disable();
170 this.storageClassForm.get('storage_class').disable();
172 this.tierTargetInfo?.val?.tier_type === TIER_TYPE.CLOUD_TIER ||
173 this.tierTargetInfo?.val?.tier_type === TIER_TYPE.GLACIER
175 this.storageClassForm.get('storageClassType').disable();
177 this.aclList = this.tierTargetInfo?.val?.s3?.acl_mappings || [];
178 this.storageClassForm.patchValue({
179 zonegroup: this.storageClassInfo?.zonegroup_name,
180 region: response?.region,
181 placement_target: this.storageClassInfo?.placement_target,
182 storageClassType: this.tierTargetInfo?.val?.tier_type ?? TIER_TYPE.LOCAL,
183 target_endpoint: response?.endpoint,
184 storage_class: this.storageClassInfo?.storage_class,
185 access_key: response?.access_key,
186 secret_key: response?.secret,
187 target_path: response?.target_path,
188 retain_head_object: this.tierTargetInfo?.val?.retain_head_object || false,
189 multipart_sync_threshold:
190 this.dimlessBinary.transform(response?.multipart_sync_threshold) || '',
191 multipart_min_part_size:
192 this.dimlessBinary.transform(response?.multipart_min_part_size) || '',
193 allow_read_through: this.tierTargetInfo?.val?.allow_read_through || false,
194 restore_storage_class: this.tierTargetInfo?.val?.restore_storage_class,
195 read_through_restore_days: this.tierTargetInfo?.val?.read_through_restore_days,
196 acl_mappings: this.tierTargetInfo?.val?.s3?.acl_mappings || []
199 this.storageClassForm.get('storageClassType')?.value === TIER_TYPE.CLOUD_TIER ||
200 this.storageClassForm.get('storageClassType')?.value === TIER_TYPE.GLACIER
203 if (aclMappings.length > 0) {
204 aclMappings.forEach((acl) => {
206 this.formBuilder.group({
207 source_id: [acl.val?.source_id || ''],
208 dest_id: [acl.val?.dest_id || ''],
209 type: [acl.val?.type || AclTypeConst.ID, Validators.required]
217 if (this.tierTargetInfo?.val?.tier_type == TIER_TYPE.GLACIER) {
218 let glacierResponse = this.tierTargetInfo?.val['s3-glacier'];
219 this.storageClassForm.patchValue({
220 glacier_restore_tier_type: glacierResponse.glacier_restore_tier_type,
221 glacier_restore_days: glacierResponse.glacier_restore_days
226 this.storageClassForm.get('storageClassType').valueChanges.subscribe((value) => {
227 this.updateValidatorsBasedOnStorageClass(value);
229 this.storageClassForm.get('allow_read_through').valueChanges.subscribe((value) => {
230 this.onAllowReadThroughChange(value);
236 const lockDaysValidator = CdValidators.custom('lockDays', () => {
237 if (!self.storageClassForm || !self.storageClassForm.getRawValue()) {
241 const lockDays = Number(self.storageClassForm.getValue('read_through_restore_days'));
242 return !Number.isInteger(lockDays) || lockDays === 0;
244 this.storageClassForm = this.formBuilder.group({
245 storage_class: new FormControl('', {
246 validators: [Validators.required]
248 zonegroup: new FormControl(this.selectedZoneGroup, {
249 validators: [Validators.required]
251 region: new FormControl('', [
252 CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
254 placement_target: new FormControl('', {
255 validators: [Validators.required]
257 access_key: new FormControl(null, [
258 CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
260 secret_key: new FormControl(null, [
261 CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
263 target_path: new FormControl('', [
264 CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
266 retain_head_object: new FormControl(true),
267 glacier_restore_tier_type: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS, [
268 CdValidators.composeIf({ storageClassType: TIER_TYPE.GLACIER }, [Validators.required])
270 target_endpoint: new FormControl('', [Validators.required, this.urlValidator]),
271 glacier_restore_days: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_GLACIER_RESTORE_DAYS, [
272 CdValidators.composeIf({ storageClassType: TIER_TYPE.GLACIER || TIER_TYPE.CLOUD_TIER }, [
273 CdValidators.number(false),
277 restore_storage_class: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS),
278 read_through_restore_days: new FormControl(
280 value: STORAGE_CLASS_CONSTANTS.DEFAULT_READTHROUGH_RESTORE_DAYS,
283 CdValidators.composeIf(
284 (form: AbstractControl) => {
285 const type = form.get('storageClassType')?.value;
286 return type === TIER_TYPE.GLACIER || type === TIER_TYPE.CLOUD_TIER;
288 [CdValidators.number(false), lockDaysValidator]
291 multipart_sync_threshold: new FormControl(
292 STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_SYNC_THRESHOLD
294 multipart_min_part_size: new FormControl(
295 STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_MIN_PART_SIZE
297 allow_read_through: new FormControl(false),
298 storageClassType: new FormControl(TIER_TYPE.LOCAL, Validators.required),
299 acls: new FormArray([])
301 this.storageClassForm.get('storageClassType')?.valueChanges.subscribe((type: string) => {
302 if (type === TIER_TYPE.CLOUD_TIER) {
303 const aclsArray = this.storageClassForm.get('acls') as FormArray;
304 aclsArray.push(this.createAcls());
309 public createAcls(): CdFormGroup {
310 const group = this.formBuilder.group({
311 type: new FormControl(AclTypeConst.ID, Validators.required),
312 source_id: new FormControl(''),
313 dest_id: new FormControl('')
316 const sourceId = group.get('source_id');
317 const destId = group.get('dest_id');
319 const validators = this.getValidatorsType(AclTypeConst.ID);
321 sourceId.setValidators(validators);
322 destId.setValidators(validators);
324 sourceId.updateValueAndValidity();
325 destId.updateValueAndValidity();
327 group.get('type')?.valueChanges.subscribe((newType: AclType) => {
328 const sourceId = group.get('source_id');
329 const destId = group.get('dest_id');
331 const validators = this.getValidatorsType(newType);
333 sourceId.setValidators(validators);
334 destId.setValidators(validators);
336 sourceId.updateValueAndValidity();
337 destId.updateValueAndValidity();
343 private getValidatorsType(type: AclType) {
345 case AclTypeConst.EMAIL:
346 return [Validators.email];
347 case AclTypeConst.URI:
348 return [this.urlValidator];
349 case AclTypeConst.ID:
351 return [Validators.required];
355 get acls(): FormArray {
356 return this.storageClassForm.get('acls') as FormArray;
359 private updateValidatorsBasedOnStorageClass(value: string) {
360 GLACIER_REQUIRED_FIELDS.forEach((field) => {
361 const control = this.storageClassForm.get(field);
364 (value === TIER_TYPE.CLOUD_TIER && CLOUD_TIER_REQUIRED_FIELDS.includes(field)) ||
365 (value === TIER_TYPE.GLACIER && GLACIER_REQUIRED_FIELDS.includes(field))
367 control.setValidators([Validators.required]);
369 control.clearValidators();
371 control.updateValueAndValidity();
375 const defaultValues = {
376 allow_read_through: false,
377 read_through_restore_days: STORAGE_CLASS_CONSTANTS.DEFAULT_READTHROUGH_RESTORE_DAYS,
378 restore_storage_class: STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS,
379 multipart_min_part_size: STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_MIN_PART_SIZE,
380 multipart_sync_threshold: STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_SYNC_THRESHOLD
382 Object.keys(defaultValues).forEach((key) => {
383 this.storageClassForm.get(key).setValue(defaultValues[key]);
389 this.acls.push(this.createAcls());
392 removeAcl(index: number) {
393 if (this.acls.length > 1) {
394 this.acls.removeAt(index);
396 const removedAcl = this.acls.at(0).value;
398 if (removedAcl?.source_id) {
399 this.removedAclSourceIds.push(removedAcl.source_id);
401 const newGroup = this.createAcls();
402 this.acls.setControl(0, newGroup);
405 this.cdRef.detectChanges();
408 getAclLabel(field: AclFieldType, type?: string): string {
410 return field === AclFieldType.Source ? AclLabel.source : AclLabel.destination;
413 this.aclTypeLabel[type]?.[field] ||
414 (field === AclFieldType.Source ? AclLabel.source : AclLabel.destination)
418 getAclHelperText(type: string, field: AclFieldType): string {
419 return this.aclHelperText[type]?.[field] || '';
422 storageClassTypeText() {
423 this.storageClassForm?.get('storageClassType')?.valueChanges.subscribe((value) => {
424 if (value === TIER_TYPE.LOCAL) {
425 this.helpTextLabels.storageClassText = LOCAL_STORAGE_CLASS_TEXT;
426 } else if (value === TIER_TYPE.CLOUD_TIER) {
427 this.helpTextLabels.storageClassText = CLOUDS3_STORAGE_CLASS_TEXT;
428 } else if (value === TIER_TYPE.GLACIER) {
429 this.helpTextLabels.storageClassText = GLACIER_STORAGE_CLASS_TEXT;
434 updateTierTypeHelpText() {
435 this.storageClassForm?.get('glacier_restore_tier_type')?.valueChanges.subscribe((value) => {
436 if (value === STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS) {
437 this.helpTextLabels.tiertypeText = STANDARD_TIER_TYPE_TEXT;
439 this.helpTextLabels.tiertypeText = EXPEDITED_TIER_TYPE_TEXT;
444 loadZoneGroup(): Promise<void> {
445 return new Promise((resolve, reject) => {
446 this.rgwZoneGroupService.getAllZonegroupsInfo().subscribe(
447 (data: ZoneGroupDetails) => {
448 this.zoneGroupDetails = data;
449 this.zonegroupNames = [];
450 this.placementTargets = [];
451 if (data.zonegroups && data.zonegroups.length > 0) {
452 this.zonegroupNames = data.zonegroups.map((zoneGroup: ZoneGroup) => {
459 this.defaultZonegroup = this.zonegroupNames.find(
460 (zonegroups: ZoneGroup) => zonegroups.id === data.default_zonegroup
462 this.storageClassForm.get('zonegroup').setValue(this.defaultZonegroup.name);
463 this.onZonegroupChange();
466 (error) => reject(error)
471 onZonegroupChange() {
472 const zoneGroupControl = this.storageClassForm.get('zonegroup').value;
473 const selectedZoneGroup = this.zoneGroupDetails.zonegroups.find(
474 (zonegroup) => zonegroup.name === zoneGroupControl
476 const defaultPlacementTarget = selectedZoneGroup.placement_targets.find(
477 (target: Target) => target.name === DEFAULT_PLACEMENT
479 if (selectedZoneGroup) {
480 const placementTargetNames = selectedZoneGroup.placement_targets.map(
481 (target: Target) => target.name
483 this.placementTargets = placementTargetNames;
485 if (defaultPlacementTarget && !this.editing) {
486 this.storageClassForm.get('placement_target').setValue(defaultPlacementTarget.name);
488 this.storageClassForm
489 .get('placement_target')
490 .setValue(this.storageClassInfo.placement_target);
495 const component = this;
496 const requestModel = this.buildRequest();
497 const storageclassName = this.storageClassForm.get('storage_class').value;
499 this.rgwStorageService.editStorageClass(requestModel).subscribe(
501 this.notificationService.show(
502 NotificationType.success,
503 $localize`Updated Storage Class '${storageclassName}'`
508 component.storageClassForm.setErrors({ cdSubmitButton: true });
512 this.rgwStorageService.createStorageClass(requestModel).subscribe(
514 this.notificationService.show(
515 NotificationType.success,
516 $localize`Created Storage Class '${storageclassName}'`
521 component.storageClassForm.setErrors({ cdSubmitButton: true });
528 this.router.navigate([`rgw/tiering`]);
531 getTierTargetByStorageClass(placementTargetInfo: PlacementTarget, storageClass: string) {
532 const tierTarget = placementTargetInfo?.tier_targets?.find(
533 (target: TierTarget) => target.val.storage_class === storageClass
538 onAllowReadThroughChange(checked: boolean): void {
539 this.allowReadThrough = checked;
540 const readThroughDaysControl = this.storageClassForm.get('read_through_restore_days');
541 if (this.allowReadThrough) {
542 this.storageClassForm.get('retain_head_object')?.setValue(true);
543 this.storageClassForm.get('retain_head_object')?.disable();
544 readThroughDaysControl?.enable();
546 this.storageClassForm.get('retain_head_object')?.enable();
547 readThroughDaysControl?.disable();
551 isTierMatch(...types: string[]): boolean {
552 const tierType = this.storageClassForm.getValue('storageClassType');
553 return types.includes(tierType);
557 if (this.storageClassForm.errors) return null;
558 const rawFormValue = _.cloneDeep(this.storageClassForm.value);
559 const zoneGroup = this.storageClassForm.get('zonegroup').value;
560 const storageClass = this.storageClassForm.get('storage_class').value;
561 const placementId = this.storageClassForm.get('placement_target').value;
562 const storageClassType = this.storageClassForm.get('storageClassType').value;
563 const retain_head_object = this.storageClassForm.get('retain_head_object').value;
564 const multipart_min_part_size = this.formatter.toBytes(
565 this.storageClassForm.get('multipart_min_part_size').value
567 const multipart_sync_threshold = this.formatter.toBytes(
568 this.storageClassForm.get('multipart_sync_threshold').value
571 const removeAclList: ACLVal[] = rawFormValue.acls || [];
572 const tier_config_rm: TierConfigRm = {};
573 this.removedAclSourceIds.forEach((sourceId: string, index: number) => {
574 tier_config_rm[`acls[${index}].source_id`] = sourceId;
576 if (this.aclList?.length > rawFormValue.acls?.length) {
577 this.aclList?.forEach((acl: ACL, index: number) => {
578 const sourceId = acl?.val?.source_id;
579 const ifExist = removeAclList.find((acl: ACLVal) => acl?.source_id === sourceId);
582 tier_config_rm[`acls[${index}].source_id`] = sourceId;
587 return this.buildPlacementTargets(
594 multipart_sync_threshold,
595 multipart_min_part_size,
600 private buildPlacementTargets(
601 storageClassType: string,
604 storageClass: string,
605 retain_head_object: boolean,
607 multipart_sync_threshold: number,
608 multipart_min_part_size: number,
609 tier_config_rm: TierConfigRm
612 placement_id: placementId,
613 storage_class: storageClass,
614 tier_config_rm: tier_config_rm
617 if (storageClassType === TIER_TYPE.LOCAL) {
619 zone_group: zoneGroup,
620 placement_targets: [baseTarget]
624 const aclConfig: { [key: string]: string } = {};
626 rawFormValue.acls.forEach((acl: ACLVal, index: number) => {
627 const sourceId = acl?.source_id?.trim();
628 if (!sourceId) return;
630 const destId = acl?.dest_id?.trim() || '';
631 const type = acl?.type?.trim() || AclTypeConst.ID;
633 aclConfig[`acls[${index}].source_id`] = sourceId;
634 aclConfig[`acls[${index}].dest_id`] = destId;
635 aclConfig[`acls[${index}].type`] = type as AclType;
638 endpoint: rawFormValue.target_endpoint,
639 access_key: rawFormValue.access_key,
640 secret: rawFormValue.secret_key,
641 target_path: rawFormValue.target_path,
643 allow_read_through: rawFormValue.allow_read_through,
644 region: rawFormValue.region,
645 multipart_sync_threshold,
646 multipart_min_part_size,
647 restore_storage_class: rawFormValue.restore_storage_class,
648 ...(rawFormValue.allow_read_through
649 ? { read_through_restore_days: rawFormValue.read_through_restore_days }
654 if (storageClassType === TIER_TYPE.CLOUD_TIER) {
656 zone_group: zoneGroup,
660 tier_type: TIER_TYPE.CLOUD_TIER,
661 tier_config_rm: tier_config_rm,
670 if (storageClassType === TIER_TYPE.GLACIER) {
672 zone_group: zoneGroup,
676 tier_type: TIER_TYPE.GLACIER,
677 tier_config_rm: tier_config_rm,
680 glacier_restore_days: rawFormValue.glacier_restore_days,
681 glacier_restore_tier_type: rawFormValue.glacier_restore_tier_type,
682 target_storage_class: GLACIER_TARGET_STORAGE_CLASS
689 this.removedAclSourceIds = [];
691 zone_group: zoneGroup,
692 placement_targets: [baseTarget]