read_through_restore_days?: number;
restore_storage_class?: string;
retain_head_object?: boolean;
+ acls?: ACL[];
+ acl_mappings?: ACL[];
}
export interface ZoneGroup {
placement_targets?: Target[];
}
+export interface ACL {
+ key: string;
+ val: ACLVal;
+}
+
+export interface ACLVal extends AclMapping {
+ type: string;
+}
+
+export interface AclMapping {
+ source_id: string;
+ dest_id: string;
+}
+
+export interface GroupedACLs {
+ [type: string]: AclMapping[];
+}
+
export interface S3Details {
endpoint: string;
access_key: string;
host_style: boolean;
retain_head_object?: boolean;
allow_read_through?: boolean;
+ acl_mappings?: ACL[];
}
export interface S3Glacier {
glacier_restore_days: number;
placement_id: string;
tags?: string[];
tier_type?: TIER_TYPE;
+ tier_config_rm: TierConfigRm;
tier_config?: {
endpoint: string;
access_key: string;
restore_storage_class?: string;
read_through_restore_days?: number;
target_storage_class?: string;
+ acls?: ACL[];
};
storage_class?: string;
name?: string;
tier_targets?: TierTarget[];
}
-export interface StorageClassOption {
+export interface TierConfigRm {
+ [key: string]: string;
+}
+
+export interface TypeOption {
value: string;
label: string;
}
GLACIER: 'Cloud S3 Glacier'
};
-export const GLACIER_TARGET_STORAGE_CLASS = 'GLACIER';
+export const GLACIER_TARGET_STORAGE_CLASS = $localize`GLACIER`;
-export const ALLOW_READ_THROUGH_TEXT =
- 'Enables fetching objects from remote cloud S3 if not found locally.';
+export const ALLOW_READ_THROUGH_TEXT = $localize`Enables fetching objects from remote cloud S3 if not found locally.`;
-export const MULTIPART_MIN_PART_TEXT =
- 'It specifies that objects this size or larger are transitioned to the cloud using multipart upload.';
+export const MULTIPART_MIN_PART_TEXT = $localize`It specifies that objects this size or larger are transitioned to the cloud using multipart upload.`;
-export const MULTIPART_SYNC_THRESHOLD_TEXT =
- 'It specifies the minimum part size to use when transitioning objects using multipart upload.';
+export const MULTIPART_SYNC_THRESHOLD_TEXT = $localize`It specifies the minimum part size to use when transitioning objects using multipart upload.`;
-export const TARGET_PATH_TEXT =
- 'Target Path refers to the storage location (e.g., bucket or container) in the cloud where data will be stored.';
+export const TARGET_PATH_TEXT = $localize`Target Path refers to the storage location (e.g., bucket or container) in the cloud where data will be stored.`;
-export const TARGET_REGION_TEXT =
- 'The region of the remote cloud service where storage is located.';
+export const TARGET_REGION_TEXT = $localize`The region of the remote cloud service where storage is located.`;
-export const TARGET_ENDPOINT_TEXT =
- 'The URL endpoint of the remote cloud service for accessing storage.';
+export const TARGET_ENDPOINT_TEXT = $localize`The URL endpoint of the remote cloud service for accessing storage.`;
-export const TARGET_ACCESS_KEY_TEXT =
- "To view or copy your access key, go to your cloud service's user management or credentials section, find your user profile, and locate the access key. You can view and copy the key by following the instructions provided.";
+export const TARGET_ACCESS_KEY_TEXT = $localize`To view or copy your access key, go to your cloud service's user management or credentials section, find your user profile, and locate the access key. You can view and copy the key by following the instructions provided.`;
-export const TARGET_SECRET_KEY_TEXT =
- "To view or copy your secret key, go to your cloud service's user management or credentials section, find your user profile, and locate the secret key. You can view and copy the key by following the instructions provided.";
+export const TARGET_SECRET_KEY_TEXT = $localize`To view or copy your secret key, go to your cloud service's user management or credentials section, find your user profile, and locate the secret key. You can view and copy the key by following the instructions provided.`;
-export const RETAIN_HEAD_OBJECT_TEXT = 'Retain object metadata after transition to the cloud.';
+export const RETAIN_HEAD_OBJECT_TEXT = $localize`Retain object metadata after transition to the cloud.`;
-export const HOST_STYLE = `The URL format for accessing the remote S3 endpoint:
+export const HOST_STYLE = $localize`The URL format for accessing the remote S3 endpoint:
- 'Path': Use for a path-based URL
- 'Virtual': Use for a domain-based URL`;
export const ZONEGROUP_TEXT = $localize`A Zone Group is a logical grouping of one or more zones that share the same data
and metadata, allowing for multi-site replication and geographic distribution of
data.`;
+
+export type AclType = 'id' | 'email' | 'uri';
+
+export interface AclLabelAndHelper {
+ source: string;
+ destination: string;
+}
+
+export interface AclMaps {
+ [key: string]: AclLabelAndHelper & {
+ [field: string]: string;
+ };
+}
+
+export enum AclLabel {
+ source = 'Source',
+ destination = 'Destination'
+}
+
+export enum AclFieldType {
+ Source = 'source',
+ Destination = 'destination'
+}
+
+export const AclTypeOptions = [
+ { value: 'id', label: 'ID' },
+ { value: 'email', label: 'Email' },
+ { value: 'uri', label: 'URI' }
+] as const;
+
+export const AclTypeConst = {
+ ID: 'id',
+ EMAIL: 'email',
+ URI: 'uri'
+} as const;
+
+export const AclTypeLabel: AclMaps = {
+ id: {
+ source: $localize`Source User`,
+ destination: $localize`Destination User`
+ },
+ email: {
+ source: $localize`Source Email`,
+ destination: $localize`Destination Email`
+ },
+ uri: {
+ source: $localize`Source URI`,
+ destination: $localize`Destination URI`
+ }
+};
+
+export const AclHelperText: AclMaps = {
+ id: {
+ source: $localize`The unique user ID in the source system.`,
+ destination: $localize`The unique user ID in the destination system.`
+ },
+ email: {
+ source: $localize`The email address of the source user.`,
+ destination: $localize`The email address of the destination user.`
+ },
+ uri: {
+ source: $localize`The URI identifying the source group or user.`,
+ destination: $localize`The URI identifying the destination group or user.`
+ }
+};
data-testid="rgw-storage-details"
>
<tbody>
- @if( isTierMatch(
- TIER_TYPE_DISPLAY.LOCAL
- )){
+ @if( isTierMatch( TIER_TYPE_DISPLAY.LOCAL )){
<tr>
<td class="bold"
i18n>
</td>
<td>{{ selection?.zonegroup_name }}</td>
</tr>
- }
- @if(isTierMatch(
- TIER_TYPE_DISPLAY.LOCAL
- )){
+ } @if(isTierMatch( TIER_TYPE_DISPLAY.LOCAL )){
<tr>
<td class="bold"
i18n>
</td>
<td>{{ selection?.placement_target }}</td>
</tr>
- }
- @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
+ } @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
<tr>
<td class="bold"
i18n>
</td>
<td>{{ selection?.target_path }}</td>
</tr>
- }
- @if(isTierMatch(TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
<tr>
<td class="bold"
i18n>
</div>
</td>
</tr>
- }
- @if(isTierMatch(TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
<tr>
<td class="bold"
i18n>
</div>
</td>
</tr>
- }
- @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
<tr>
<td class="bold"
i18n>
</td>
<td>{{ selection?.host_style }}</td>
</tr>
- }
- @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
<tr>
<td class="bold"
i18n>
</td>
<td>{{ selection?.retain_head_object ? 'Enabled' : 'Disabled' }}</td>
</tr>
- }
- @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
<tr>
<td class="bold"
i18n>
</td>
<td>{{ selection?.allow_read_through ? 'Enabled' : 'Disabled' }}</td>
</tr>
- }
- @if(isTierMatch(TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER) && (selection?.allow_read_through)) {
- <tr *ngIf="isTierMatch(TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER) && (selection?.allow_read_through)">
+ } @if(isTierMatch(TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER) &&
+ (selection?.allow_read_through)) {
+ <tr>
<td class="bold"
i18n>
Read through Restore Days
</span>
</cd-helper>
</td>
- <td>{{ selection?.read_through_restore_days }}</td>
+ <td>
+ {{ selection?.read_through_restore_days }}
+ {{ selection?.read_through_restore_days === 1 ? 'Day' : 'Days' }}
+ </td>
</tr>
- }
- @if(isTierMatch( TIER_TYPE_DISPLAY.GLACIER)){
+ } @if(isTierMatch( TIER_TYPE_DISPLAY.GLACIER)){
<tr>
<td class="bold"
i18n>
</span>
</cd-helper>
</td>
- <td>{{ selection?.glacier_restore_days }}</td>
+ <td> {{ selection?.glacier_restore_days }}
+ {{ selection?.glacier_restore_days === 1 ? 'Day' : 'Days' }}</td>
</tr>
- }
- @if(isTierMatch( TIER_TYPE_DISPLAY.GLACIER)) {
<tr>
<td class="bold"
i18n>
</td>
<td>{{ selection?.glacier_restore_tier_type }}</td>
</tr>
- }
- @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
+ } @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
<tr>
<td class="bold"
i18n>
</td>
<td>{{ selection?.restore_storage_class }}</td>
</tr>
- }
- @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
<tr>
<td class="bold"
i18n>
</td>
<td>{{ selection?.multipart_min_part_size | dimlessBinary }}</td>
</tr>
- }
- @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
<tr>
<td class="bold"
i18n>
Multipart Sync Threshold
<cd-helper class="text-pre-wrap">
<span>
- {{ multipartSyncThreholdText }}
+ {{ multipartSyncThreholdText }}
</span>
</cd-helper>
</td>
<td>{{ selection?.multipart_sync_threshold | dimlessBinary }}</td>
</tr>
- }
+ } @if(selection?.acl_mappings.length > 0) {
+ <tr>
+ <td class="bold w-25"
+ i18n>ACLs</td>
+ <td>
+ <ng-container *ngFor="let type of groupedACLs | keyvalue">
+ <div class="mb-2">
+ <div class="cds--label">{{ type.key }}:</div>
+ <div *ngFor="let item of type.value"
+ [cdsStack]="'horizontal'"
+ class="mt-1">
+ <cds-tag size="sm"
+ class="tags-background-gray">
+ {{ item.source_id }} : {{ item.dest_id }}
+ </cds-tag>
+ </div>
+ </div>
+ </ng-container>
+ </td>
+ </tr>
+ }
</tbody>
</table>
</cds-tab>
import { ComponentFixture, TestBed } from '@angular/core/testing';
-
import { RgwStorageClassDetailsComponent } from './rgw-storage-class-details.component';
import { StorageClassDetails } from '../models/rgw-storage-class.model';
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 = {
+ access_key: 'TestAccessKey',
+ secret: 'TestSecret',
+ target_path: '/test/path',
+ multipart_min_part_size: 100,
+ multipart_sync_threshold: 200,
+ host_style: 'path',
+ retain_head_object: true,
+ allow_read_through: true,
+ tier_type: 'local',
+ acl_mappings: []
+ };
+
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
fixture = TestBed.createComponent(RgwStorageClassDetailsComponent);
component = fixture.componentInstance;
+ component.selection = mockSelection;
+
fixture.detectChanges();
});
});
it('should update storageDetails when selection input changes', () => {
- const mockSelection: StorageClassDetails = {
- access_key: 'TestAccessKey',
- secret: 'TestSecret',
- target_path: '/test/path',
- multipart_min_part_size: 100,
- multipart_sync_threshold: 200,
- host_style: 'path',
- retain_head_object: true,
- allow_read_through: true,
- tier_type: 'local'
+ 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 = mockSelection;
- component.ngOnChanges();
- expect(component.storageDetails).toEqual(mockSelection);
+
+ component.selection = newSelection;
+ component.ngOnChanges({
+ selection: new SimpleChange(null, newSelection, false)
+ });
+ expect(component.storageDetails).toEqual(newSelection);
});
});
-import { Component, Input, OnChanges } from '@angular/core';
+import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { CdTableColumn } from '~/app/shared/models/cd-table-column';
import {
ALLOW_READ_THROUGH_TEXT,
RESTORE_DAYS_TEXT,
READTHROUGH_RESTORE_DAYS_TEXT,
RESTORE_STORAGE_CLASS_TEXT,
- ZONEGROUP_TEXT
+ ZONEGROUP_TEXT,
+ ACL,
+ GroupedACLs
} from '../models/rgw-storage-class.model';
@Component({
selector: 'cd-rgw-storage-class-details',
templateUrl: './rgw-storage-class-details.component.html',
styleUrls: ['./rgw-storage-class-details.component.scss']
})
-export class RgwStorageClassDetailsComponent implements OnChanges {
+export class RgwStorageClassDetailsComponent implements OnChanges, OnInit {
@Input()
selection: StorageClassDetails;
columns: CdTableColumn[] = [];
readthroughrestoreDaysText = READTHROUGH_RESTORE_DAYS_TEXT;
restoreStorageClassText = RESTORE_STORAGE_CLASS_TEXT;
zoneGroupText = ZONEGROUP_TEXT;
+ groupedACLs: GroupedACLs = {};
- ngOnChanges() {
- if (this.selection) {
+ 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
+ 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
};
}
}
+ ngOnInit() {
+ this.groupedACLs = this.groupByType(this.selection.acl_mappings);
+ }
+
isTierMatch(...types: string[]): boolean {
const tier_type = this.selection.tier_type?.toLowerCase();
return types.some((type) => type.toLowerCase() === tier_type);
}
+
+ groupByType(acls: ACL[]): GroupedACLs {
+ return acls?.reduce((groupAcls: GroupedACLs, item: ACL) => {
+ const type = item.val?.type?.toUpperCase();
+ groupAcls[type] = groupAcls[type] ?? [];
+ groupAcls[type].push({
+ source_id: item.val?.source_id,
+ dest_id: item.val?.dest_id
+ });
+ return groupAcls;
+ }, {});
+ }
}
[invalid]="storageClassForm.showError('placement_target', formDir, 'required')"
[invalidText]="placementError"
>
- <option [value]=""
+ <option [value]="''"
i18n>--Select--</option>
<option
*ngFor="let placementTarget of placementTargets"
>Name
<input
cdsText
- type="type"
+ type="text"
id="storage_class"
formControlName="storage_class"
[invalid]="storageClassForm.showError('storage_class', formDir, 'required')"
<div cdsCol>
<!-- Target Endpoint -->
<cds-text-label
- labelInputID="target_endpoint"
- i18n
- [invalid]="storageClassForm.showError('target_endpoint', formDir, 'invalidURL') || storageClassForm.showError('target_endpoint', formDir, 'required')"
- [invalidText]="endpointError"
- [helperText]="helpTextLabels.targetEndpointText"
- >Target Endpoint
- <input
- cdsText
- type="text"
- placeholder="e.g, http://ceph-node-00.com:80"
- i18n-placeholder
- id="target_endpoint"
- formControlName="target_endpoint"
- [invalid]="storageClassForm.showError('target_endpoint', formDir, 'invalidURL') || storageClassForm.showError('target_endpoint', formDir, 'required')"
- [invalidText]="endpointError"
- />
- </cds-text-label>
- <ng-template #endpointError>
- <span
- class="invalid-feedback"
- *ngIf="storageClassForm.showError('target_endpoint', formDir, 'required')"
- i18n
- >This field is required.</span
- >
- <span class="invalid-feedback"
- *ngIf="storageClassForm.showError('target_endpoint', formDir, 'invalidURL')"
- i18n>Please enter a valid URL.</span>
- </ng-template>
+ i18n
+ [invalid]="storageClassForm.showError('target_endpoint', formDir, 'required') || storageClassForm.showError('target_endpoint', formDir, 'invalidUrl')"
+
+ [invalidText]="
+ storageClassForm.controls['target_endpoint'].errors?.['required'] ? 'This field is required.' :
+ storageClassForm.controls['target_endpoint'].errors?.['invalidUrl'] ? 'Please enter a valid URL.' : ''
+ "
+ i18n-invalidText
+ >Target Endpoint
+ <input
+ cdsText
+ formControlName="target_endpoint"
+ placeholder="e.g. 192.168.0.10, 192.168.1.0/8"
+ [invalid]="storageClassForm.showError('target_endpoint', formDir, 'required') || storageClassForm.showError('target_endpoint', formDir, 'invalidUrl')"
+ />
+ </cds-text-label>
</div>
</div>
<!-- Access Key -->
[helperText]="helpTextLabels.restoreStorageClassText"
i18n-label
>
- <option value=""
+ <option [value]="''"
i18n>-- Select the glacier restore storage class --</option>
<option [ngValue]="standard"
i18n>Standard</option>
[helperText]="helpTextLabels.tiertypeText"
i18n-label
>
- <option value=""
+ <option value="''"
i18n>-- Select the glacier restore tier type --</option>
<option [ngValue]="standard"
i18n>Standard</option>
</cds-text-label>
</div>
</div>
+ <!-- ACLs -->
+ <div>
+ <legend class="cd-header"
+ i18n>ACLs Mapping</legend>
+ <ng-container formArrayName="acls">
+ @for (acl of acls.controls; let i = $index; track acl) {
+ <ng-container [formGroupName]="i">
+ <div class="form-item form-item-append"
+ cdsRow>
+ <div cdsCol
+ [columnNumbers]="{ lg: 7 }">
+ <cds-select id="type"
+ formControlName="type"
+ i18n-label
+ label="Type">
+ @for (type of typeOptions; track type.value) {
+ <option [value]="type.value"
+ i18n>
+ {{ type.label }}
+ </option>
+ }
+ </cds-select>
+ </div>
+ </div>
+ <div cdsRow
+ class="form-item form-item-append">
+ <div cdsCol>
+ <cds-text-label
+ labelInputID="source_id"
+ i18n
+ [helperText]="getAclHelperText(acl.get('type')?.value, 'source')"
+ [invalid]="
+ acl.get('source_id')?.invalid && acl.get('source_id')?.touched
+ "
+ [invalidText]="
+ acl.get('source_id')?.errors?.['required'] ? 'This field is required.' :
+ acl.get('source_id')?.errors?.['email'] ? 'Please enter a valid email address.' :
+ acl.get('source_id')?.errors?.['invalidUrl'] ? 'Please enter a valid URL.' : ''
+ "
+ >
+ {{ getAclLabel('source', acl.get('type')?.value) }}
+ <input
+ cdsText
+ type="text"
+ formControlName="source_id"
+ [invalidText]="
+ acl.get('source_id')?.errors?.['required'] ? 'This field is required.' :
+ acl.get('source_id')?.errors?.['email'] ? 'Please enter a valid email address.' :
+ acl.get('source_id')?.errors?.['invalidUrl'] ? 'Please enter a valid URL.' : ''
+ "
+ />
+ </cds-text-label>
+ </div>
+ <div cdsCol>
+ <cds-text-label
+ labelInputID="dest_id"
+ i18n
+ [helperText]="getAclHelperText(acl.get('type')?.value, 'destination')"
+ [invalid]="acl.get('dest_id')?.invalid && acl.get('dest_id')?.touched"
+ [invalidText]="
+ acl.get('dest_id')?.errors?.['required'] ? 'This field is required.' :
+ acl.get('dest_id')?.errors?.['email'] ? 'Please enter a valid email address.' :
+ acl.get('dest_id')?.errors?.['invalidUrl'] ? 'Please enter a valid URL.' : ''
+ "
+ >
+ {{ getAclLabel('destination', acl.get('type')?.value) }}
+ <input
+ cdsText
+ type="text"
+ formControlName="dest_id"
+ [invalidText]="
+ acl.get('dest_id')?.errors?.['required'] ? 'This field is required.' :
+ acl.get('dest_id')?.errors?.['email'] ? 'Please enter a valid email address.' :
+ acl.get('dest_id')?.errors?.['invalidUrl'] ? 'Please enter a valid URL.' : ''
+ "
+ />
+ </cds-text-label>
+ </div>
+
+ <div
+ cdsCol
+ [columnNumbers]="{ lg: 1, md: 1 }"
+ class="item-action-btn spacing"
+ >
+ <cds-icon-button kind="primary"
+ size="sm"
+ (click)="addAcls(acls, i)">
+ <svg cdsIcon="add"
+ size="32"
+ class="cds--btn__icon"></svg>
+ </cds-icon-button>
+ </div>
+ <div cdsCol
+ [columnNumbers]="{ lg: 1, md: 1 }"
+ class="item-action-btn">
+ <cds-icon-button kind="danger"
+ size="sm"
+ (click)="removeAcl(i)">
+ <svg cdsIcon="trash-can"
+ size="32"
+ class="cds--btn__icon"></svg>
+ </cds-icon-button>
+ </div>
+ </div>
+ </ng-container>
+ }
+ </ng-container>
+ </div>
</cds-accordion-item>
</cds-accordion>
</fieldset>
-import { Component, OnInit } from '@angular/core';
-import { AbstractControl, FormControl, Validators } from '@angular/forms';
+import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import {
+ AbstractControl,
+ FormArray,
+ FormControl,
+ ValidationErrors,
+ Validators
+} from '@angular/forms';
import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
import { CdForm } from '~/app/shared/forms/cd-form';
import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
import { ActivatedRoute, Router } from '@angular/router';
import { RgwStorageClassService } from '~/app/shared/api/rgw-storage-class.service';
import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+
import {
ALLOW_READ_THROUGH_TEXT,
DEFAULT_PLACEMENT,
RESTORE_STORAGE_CLASS_TEXT,
TIER_TYPE_DISPLAY,
S3Glacier,
- StorageClassOption,
+ TypeOption,
STORAGE_CLASS_CONSTANTS,
STANDARD_TIER_TYPE_TEXT,
EXPEDITED_TIER_TYPE_TEXT,
TextLabels,
CLOUD_TIER_REQUIRED_FIELDS,
GLACIER_REQUIRED_FIELDS,
- GLACIER_TARGET_STORAGE_CLASS
+ GLACIER_TARGET_STORAGE_CLASS,
+ AclHelperText,
+ AclTypeLabel,
+ AclFieldType,
+ TierConfigRm,
+ ACL,
+ AclTypeOptions,
+ AclTypeConst,
+ ACLVal,
+ AclLabel,
+ AclType
} 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';
@Component({
selector: 'cd-rgw-storage-class-form',
allowReadThrough: boolean = false;
TIER_TYPE = TIER_TYPE;
TIER_TYPE_DISPLAY = TIER_TYPE_DISPLAY;
- storageClassOptions: StorageClassOption[];
+ storageClassOptions: TypeOption[];
helpTextLabels: TextLabels;
-
+ typeOptions: TypeOption[];
+ aclTypeLabel = AclTypeLabel;
+ aclHelperText = AclHelperText;
+ aclList: ACL[] = [];
+ removedAclSourceIds: string[] = [];
+ urlValidator = (control: AbstractControl): ValidationErrors | null => {
+ const value = control.value;
+ return !value || validator.isURL(value) ? null : { invalidUrl: true };
+ };
constructor(
public actionLabels: ActionLabelsI18n,
private formBuilder: CdFormBuilder,
private rgwZoneGroupService: RgwZonegroupService,
private router: Router,
private route: ActivatedRoute,
- public formatter: FormatterService
+ public formatter: FormatterService,
+ private cdRef: ChangeDetectorRef
) {
super();
this.resource = $localize`Tiering Storage Class`;
{ value: TIER_TYPE.CLOUD_TIER, label: TIER_TYPE_DISPLAY.CLOUD_TIER },
{ value: TIER_TYPE.GLACIER, label: TIER_TYPE_DISPLAY.GLACIER }
];
+ this.typeOptions = [...AclTypeOptions];
this.createForm();
this.storageClassTypeText();
this.updateTierTypeHelpText();
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();
) {
this.storageClassForm.get('storageClassType').disable();
}
+ this.aclList = this.tierTargetInfo?.val?.s3?.acl_mappings || [];
this.storageClassForm.patchValue({
zonegroup: this.storageClassInfo?.zonegroup_name,
region: response?.region,
multipart_min_part_size: 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
+ read_through_restore_days: this.tierTargetInfo?.val?.read_through_restore_days,
+ acl_mappings: this.tierTargetInfo?.val?.s3?.acl_mappings || []
});
+ 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();
+ }
if (this.tierTargetInfo?.val?.tier_type == TIER_TYPE.GLACIER) {
let glacierResponse = this.tierTargetInfo?.val['s3-glacier'];
this.storageClassForm.patchValue({
});
}
- private updateValidatorsBasedOnStorageClass(value: string) {
- GLACIER_REQUIRED_FIELDS.forEach((field) => {
- const control = this.storageClassForm.get(field);
-
- if (
- (value === TIER_TYPE.CLOUD_TIER && CLOUD_TIER_REQUIRED_FIELDS.includes(field)) ||
- (value === TIER_TYPE.GLACIER && GLACIER_REQUIRED_FIELDS.includes(field))
- ) {
- control.setValidators([Validators.required]);
- } else {
- control.clearValidators();
- }
- control.updateValueAndValidity();
- });
-
- if (this.editing) {
- const defaultValues = {
- 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]);
- });
- }
- }
-
- storageClassTypeText() {
- this.storageClassForm?.get('storageClassType')?.valueChanges.subscribe((value) => {
- if (value === TIER_TYPE.LOCAL) {
- this.helpTextLabels.storageClassText = LOCAL_STORAGE_CLASS_TEXT;
- } else if (value === TIER_TYPE.CLOUD_TIER) {
- this.helpTextLabels.storageClassText = CLOUDS3_STORAGE_CLASS_TEXT;
- } else if (value === TIER_TYPE.GLACIER) {
- this.helpTextLabels.storageClassText = GLACIER_STORAGE_CLASS_TEXT;
- }
- });
- }
-
- updateTierTypeHelpText() {
- this.storageClassForm?.get('glacier_restore_tier_type')?.valueChanges.subscribe((value) => {
- if (value === STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS) {
- this.helpTextLabels.tiertypeText = STANDARD_TIER_TYPE_TEXT;
- } else {
- this.helpTextLabels.tiertypeText = EXPEDITED_TIER_TYPE_TEXT;
- }
- });
- }
-
createForm() {
const self = this;
placement_target: new FormControl('', {
validators: [Validators.required]
}),
- target_endpoint: new FormControl(null, {
- validators: [CdValidators.url, Validators.required]
- }),
access_key: new FormControl(null, [
CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
]),
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),
STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_MIN_PART_SIZE
),
allow_read_through: new FormControl(false),
- storageClassType: new FormControl(TIER_TYPE.LOCAL, Validators.required)
+ storageClassType: new FormControl(TIER_TYPE.LOCAL, Validators.required),
+ acls: new FormArray([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;
+ }
+
+ private updateValidatorsBasedOnStorageClass(value: string) {
+ GLACIER_REQUIRED_FIELDS.forEach((field) => {
+ const control = this.storageClassForm.get(field);
+
+ if (
+ (value === TIER_TYPE.CLOUD_TIER && CLOUD_TIER_REQUIRED_FIELDS.includes(field)) ||
+ (value === TIER_TYPE.GLACIER && GLACIER_REQUIRED_FIELDS.includes(field))
+ ) {
+ control.setValidators([Validators.required]);
+ } else {
+ control.clearValidators();
+ }
+ control.updateValueAndValidity();
+ });
+
+ if (this.editing) {
+ const defaultValues = {
+ 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]);
+ });
+ }
+ }
+
+ addAcls() {
+ this.acls.push(this.createAcls());
+ }
+
+ removeAcl(index: number) {
+ if (this.acls.length > 1) {
+ this.acls.removeAt(index);
+ } else {
+ const removedAcl = this.acls.at(0).value;
+
+ if (removedAcl?.source_id) {
+ this.removedAclSourceIds.push(removedAcl.source_id);
+ }
+ const newGroup = this.createAcls();
+ this.acls.setControl(0, newGroup);
+ }
+
+ this.cdRef.detectChanges();
+ }
+
+ getAclLabel(field: AclFieldType, type?: string): string {
+ if (!type) {
+ return field === AclFieldType.Source ? AclLabel.source : AclLabel.destination;
+ }
+ return (
+ this.aclTypeLabel[type]?.[field] ||
+ (field === AclFieldType.Source ? AclLabel.source : AclLabel.destination)
+ );
+ }
+
+ getAclHelperText(type: string, field: AclFieldType): string {
+ return this.aclHelperText[type]?.[field] || '';
+ }
+
+ storageClassTypeText() {
+ this.storageClassForm?.get('storageClassType')?.valueChanges.subscribe((value) => {
+ if (value === TIER_TYPE.LOCAL) {
+ this.helpTextLabels.storageClassText = LOCAL_STORAGE_CLASS_TEXT;
+ } else if (value === TIER_TYPE.CLOUD_TIER) {
+ this.helpTextLabels.storageClassText = CLOUDS3_STORAGE_CLASS_TEXT;
+ } else if (value === TIER_TYPE.GLACIER) {
+ this.helpTextLabels.storageClassText = GLACIER_STORAGE_CLASS_TEXT;
+ }
+ });
+ }
+
+ updateTierTypeHelpText() {
+ this.storageClassForm?.get('glacier_restore_tier_type')?.valueChanges.subscribe((value) => {
+ if (value === STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS) {
+ this.helpTextLabels.tiertypeText = STANDARD_TIER_TYPE_TEXT;
+ } else {
+ this.helpTextLabels.tiertypeText = EXPEDITED_TIER_TYPE_TEXT;
+ }
});
}
const multipart_sync_threshold = this.formatter.toBytes(
this.storageClassForm.get('multipart_sync_threshold').value
);
+
+ const removeAclList: ACLVal[] = rawFormValue.acls || [];
+ const tier_config_rm: TierConfigRm = {};
+ this.removedAclSourceIds.forEach((sourceId: string, index: number) => {
+ tier_config_rm[`acls[${index}].source_id`] = sourceId;
+ });
+ if (this.aclList.length > rawFormValue.acls.length) {
+ this.aclList.forEach((acl: ACL, index: number) => {
+ const sourceId = acl?.val?.source_id;
+ const ifExist = removeAclList.find((acl: ACLVal) => acl?.source_id === sourceId);
+
+ if (!ifExist) {
+ tier_config_rm[`acls[${index}].source_id`] = sourceId;
+ }
+ });
+ }
+
return this.buildPlacementTargets(
storageClassType,
zoneGroup,
retain_head_object,
rawFormValue,
multipart_sync_threshold,
- multipart_min_part_size
+ multipart_min_part_size,
+ tier_config_rm
);
}
retain_head_object: boolean,
rawFormValue: any,
multipart_sync_threshold: number,
- multipart_min_part_size: number
+ multipart_min_part_size: number,
+ tier_config_rm: TierConfigRm
): RequestModel {
const baseTarget = {
placement_id: placementId,
- storage_class: storageClass
+ storage_class: storageClass,
+ tier_config_rm: tier_config_rm
};
if (storageClassType === TIER_TYPE.LOCAL) {
};
}
+ const aclConfig: { [key: string]: string } = {};
+
+ rawFormValue.acls.forEach((acl: ACLVal, index: number) => {
+ const sourceId = acl?.source_id?.trim();
+ if (!sourceId) return;
+
+ const destId = acl?.dest_id?.trim() || '';
+ const type = acl?.type?.trim() || AclTypeConst.ID;
+
+ aclConfig[`acls[${index}].source_id`] = sourceId;
+ aclConfig[`acls[${index}].dest_id`] = destId;
+ aclConfig[`acls[${index}].type`] = type as AclType;
+ });
const tierConfig = {
endpoint: rawFormValue.target_endpoint,
access_key: rawFormValue.access_key,
retain_head_object,
allow_read_through: rawFormValue.allow_read_through,
region: rawFormValue.region,
- multipart_sync_threshold: multipart_sync_threshold,
- multipart_min_part_size: multipart_min_part_size,
+ multipart_sync_threshold,
+ multipart_min_part_size,
restore_storage_class: rawFormValue.restore_storage_class,
...(rawFormValue.allow_read_through
? { read_through_restore_days: rawFormValue.read_through_restore_days }
- : {})
+ : {}),
+ ...aclConfig
};
if (storageClassType === TIER_TYPE.CLOUD_TIER) {
{
...baseTarget,
tier_type: TIER_TYPE.CLOUD_TIER,
+ tier_config_rm: tier_config_rm,
tier_config: {
...tierConfig
}
{
...baseTarget,
tier_type: TIER_TYPE.GLACIER,
+ tier_config_rm: tier_config_rm,
tier_config: {
...tierConfig,
glacier_restore_days: rawFormValue.glacier_restore_days,
]
};
}
+
+ this.removedAclSourceIds = [];
return {
zone_group: zoneGroup,
placement_targets: [baseTarget]
allow_read_through: val.allow_read_through,
restore_storage_class: val.restore_storage_class,
read_through_restore_days: val.read_through_restore_days,
+ acls: val.s3.acl_mappings,
...val.s3
};
placement_id: 'default-placement',
storage_class: 'test1',
tier_type: 'cloud-s3',
+ tier_config_rm: { 'acls.source_id': 'test1' },
tier_config: {
endpoint: 'http://198.162.100.100:80',
access_key: 'test56',
'--tier-type', tier_type, '--tier-config', tier_config_str
]
+ tier_config_rm = placement_target.get('tier_config_rm', {})
+ if tier_config_rm:
+ tier_config_rm_str = ','.join(
+ f"{key}={value}" for key, value in tier_config_rm.items()
+ )
+ cmd_add_placement_options += ['--tier-config-rm', tier_config_rm_str]
+
if placement_target.get('tags') and storage_class_name != STANDARD_STORAGE_CLASS:
cmd_add_placement_options += ['--tags', placement_target['tags']]