1 import { Component, OnInit } from '@angular/core';
2 import { AbstractControl, AsyncValidatorFn, ValidationErrors, Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
5 import _ from 'lodash';
6 import { forkJoin } from 'rxjs';
8 import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
9 import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
10 import { RgwUserService } from '~/app/shared/api/rgw-user.service';
11 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
12 import { Icons } from '~/app/shared/enum/icons.enum';
13 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
14 import { CdForm } from '~/app/shared/forms/cd-form';
15 import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
16 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
17 import { CdValidators } from '~/app/shared/forms/cd-validators';
18 import { NotificationService } from '~/app/shared/services/notification.service';
19 import { RgwBucketMfaDelete } from '../models/rgw-bucket-mfa-delete';
20 import { RgwBucketVersioning } from '../models/rgw-bucket-versioning';
23 selector: 'cd-rgw-bucket-form',
24 templateUrl: './rgw-bucket-form.component.html',
25 styleUrls: ['./rgw-bucket-form.component.scss']
27 export class RgwBucketFormComponent extends CdForm implements OnInit {
28 bucketForm: CdFormGroup;
30 owners: string[] = null;
34 placementTargets: object[] = [];
35 isVersioningAlreadyEnabled = false;
36 isMfaDeleteAlreadyEnabled = false;
39 get isVersioningEnabled(): boolean {
40 return this.bucketForm.getValue('versioning');
42 get isMfaDeleteEnabled(): boolean {
43 return this.bucketForm.getValue('mfa-delete');
47 private route: ActivatedRoute,
48 private router: Router,
49 private formBuilder: CdFormBuilder,
50 private rgwBucketService: RgwBucketService,
51 private rgwSiteService: RgwSiteService,
52 private rgwUserService: RgwUserService,
53 private notificationService: NotificationService,
54 public actionLabels: ActionLabelsI18n
57 this.editing = this.router.url.startsWith(`/rgw/bucket/${URLVerbs.EDIT}`);
58 this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
59 this.resource = $localize`bucket`;
65 const eitherDaysOrYears = CdValidators.custom('eitherDaysOrYears', () => {
66 if (!self.bucketForm || !_.get(self.bucketForm.getRawValue(), 'lock_enabled')) {
69 const years = self.bucketForm.getValue('lock_retention_period_years');
70 const days = self.bucketForm.getValue('lock_retention_period_days');
71 return (days > 0 && years > 0) || (days === 0 && years === 0);
73 const lockPeriodDefinition = [0, [CdValidators.number(false), eitherDaysOrYears]];
74 this.bucketForm = this.formBuilder.group({
76 bid: [null, [Validators.required], this.editing ? [] : [this.bucketNameValidator()]],
77 owner: [null, [Validators.required]],
78 'placement-target': [null, this.editing ? [] : [Validators.required]],
81 'mfa-token-serial': [''],
82 'mfa-token-pin': [''],
83 lock_enabled: [{ value: false, disabled: this.editing }],
84 lock_mode: ['COMPLIANCE'],
85 lock_retention_period_days: lockPeriodDefinition,
86 lock_retention_period_years: lockPeriodDefinition
92 owners: this.rgwUserService.enumerate()
96 promises['getPlacementTargets'] = this.rgwSiteService.get('placement-targets');
99 // Process route parameters.
100 this.route.params.subscribe((params: { bid: string }) => {
101 if (params.hasOwnProperty('bid')) {
102 const bid = decodeURIComponent(params.bid);
103 promises['getBid'] = this.rgwBucketService.get(bid);
106 forkJoin(promises).subscribe((data: any) => {
107 // Get the list of possible owners.
108 this.owners = (<string[]>data.owners).sort();
110 // Get placement targets:
111 if (data['getPlacementTargets']) {
112 const placementTargets = data['getPlacementTargets'];
113 this.zonegroup = placementTargets['zonegroup'];
114 _.forEach(placementTargets['placement_targets'], (placementTarget) => {
115 placementTarget['description'] = `${placementTarget['name']} (${$localize`pool`}: ${
116 placementTarget['data_pool']
118 this.placementTargets.push(placementTarget);
121 // If there is only 1 placement target, select it by default:
122 if (this.placementTargets.length === 1) {
123 this.bucketForm.get('placement-target').setValue(this.placementTargets[0]['name']);
127 if (data['getBid']) {
128 const bidResp = data['getBid'];
129 // Get the default values (incl. the values from disabled fields).
130 const defaults = _.clone(this.bucketForm.getRawValue());
132 // Get the values displayed in the form. We need to do that to
133 // extract those key/value pairs from the response data, otherwise
134 // the Angular react framework will throw an error if there is no
135 // field for a given key.
136 let value: object = _.pick(bidResp, _.keys(defaults));
137 value['placement-target'] = bidResp['placement_rule'];
138 value['versioning'] = bidResp['versioning'] === RgwBucketVersioning.ENABLED;
139 value['mfa-delete'] = bidResp['mfa_delete'] === RgwBucketMfaDelete.ENABLED;
141 // Append default values.
142 value = _.merge(defaults, value);
145 this.bucketForm.setValue(value);
147 this.isVersioningAlreadyEnabled = this.isVersioningEnabled;
148 this.isMfaDeleteAlreadyEnabled = this.isMfaDeleteEnabled;
149 this.setMfaDeleteValidators();
159 this.router.navigate(['/rgw/bucket']);
163 // Exit immediately if the form isn't dirty.
164 if (this.bucketForm.pristine) {
168 const values = this.bucketForm.value;
171 const versioning = this.getVersioningStatus();
172 const mfaDelete = this.getMfaDeleteStatus();
173 this.rgwBucketService
180 values['mfa-token-serial'],
181 values['mfa-token-pin'],
183 values['lock_retention_period_days'],
184 values['lock_retention_period_years']
188 this.notificationService.show(
189 NotificationType.success,
190 $localize`Updated Object Gateway bucket '${values.bid}'.`
195 // Reset the 'Submit' button.
196 this.bucketForm.setErrors({ cdSubmitButton: true });
201 this.rgwBucketService
206 values['placement-target'],
207 values['lock_enabled'],
209 values['lock_retention_period_days'],
210 values['lock_retention_period_years']
214 this.notificationService.show(
215 NotificationType.success,
216 $localize`Created Object Gateway bucket '${values.bid}'`
221 // Reset the 'Submit' button.
222 this.bucketForm.setErrors({ cdSubmitButton: true });
229 * Validate the bucket name. In general, bucket names should follow domain
231 * - Bucket names must be unique.
232 * - Bucket names cannot be formatted as IP address.
233 * - Bucket names can be between 3 and 63 characters long.
234 * - Bucket names must not contain uppercase characters or underscores.
235 * - Bucket names must start with a lowercase letter or number.
236 * - Bucket names must be a series of one or more labels. Adjacent
237 * labels are separated by a single period (.). Bucket names can
238 * contain lowercase letters, numbers, and hyphens. Each label must
239 * start and end with a lowercase letter or a number.
241 bucketNameValidator(): AsyncValidatorFn {
242 const rgwBucketService = this.rgwBucketService;
243 return (control: AbstractControl): Promise<ValidationErrors | null> => {
244 return new Promise((resolve) => {
245 // Exit immediately if user has not interacted with the control yet
246 // or the control value is empty.
247 if (control.pristine || control.value === '') {
251 const constraints = [];
252 // - Bucket names cannot be formatted as IP address.
253 constraints.push((name: AbstractControl) => {
254 const validatorFn = CdValidators.ip();
255 return !validatorFn(name);
257 // - Bucket names can be between 3 and 63 characters long.
258 constraints.push((name: string) => _.inRange(name.length, 3, 64));
259 // - Bucket names must not contain uppercase characters or underscores.
260 // - Bucket names must start with a lowercase letter or number.
261 // - Bucket names must be a series of one or more labels. Adjacent
262 // labels are separated by a single period (.). Bucket names can
263 // contain lowercase letters, numbers, and hyphens. Each label must
264 // start and end with a lowercase letter or a number.
265 constraints.push((name: string) => {
266 const labels = _.split(name, '.');
267 return _.every(labels, (label) => {
268 // Bucket names must not contain uppercase characters or underscores.
269 if (label !== _.toLower(label) || label.includes('_')) {
272 // Bucket names can contain lowercase letters, numbers, and hyphens.
273 if (!/[0-9a-z-]/.test(label)) {
276 // Each label must start and end with a lowercase letter or a number.
277 return _.every([0, label.length], (index) => {
278 return /[a-z]/.test(label[index]) || _.isInteger(_.parseInt(label[index]));
282 if (!_.every(constraints, (func: Function) => func(control.value))) {
283 resolve({ bucketNameInvalid: true });
286 // - Bucket names must be unique.
287 rgwBucketService.exists(control.value).subscribe((resp: boolean) => {
291 resolve({ bucketNameExists: true });
298 areMfaCredentialsRequired() {
300 this.isMfaDeleteEnabled !== this.isMfaDeleteAlreadyEnabled ||
301 (this.isMfaDeleteAlreadyEnabled &&
302 this.isVersioningEnabled !== this.isVersioningAlreadyEnabled)
306 setMfaDeleteValidators() {
307 const mfaTokenSerialControl = this.bucketForm.get('mfa-token-serial');
308 const mfaTokenPinControl = this.bucketForm.get('mfa-token-pin');
310 if (this.areMfaCredentialsRequired()) {
311 mfaTokenSerialControl.setValidators(Validators.required);
312 mfaTokenPinControl.setValidators(Validators.required);
314 mfaTokenSerialControl.setValidators(null);
315 mfaTokenPinControl.setValidators(null);
318 mfaTokenSerialControl.updateValueAndValidity();
319 mfaTokenPinControl.updateValueAndValidity();
322 getVersioningStatus() {
323 return this.isVersioningEnabled ? RgwBucketVersioning.ENABLED : RgwBucketVersioning.SUSPENDED;
326 getMfaDeleteStatus() {
327 return this.isMfaDeleteEnabled ? RgwBucketMfaDelete.ENABLED : RgwBucketMfaDelete.DISABLED;