]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | import { Component, OnInit } from '@angular/core'; |
2 | import { AbstractControl, AsyncValidatorFn, ValidationErrors, Validators } from '@angular/forms'; | |
3 | import { ActivatedRoute, Router } from '@angular/router'; | |
4 | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill'; | |
6 | import * as _ from 'lodash'; | |
7 | ||
8 | import { RgwBucketService } from '../../../shared/api/rgw-bucket.service'; | |
9f95a23c | 9 | import { RgwSiteService } from '../../../shared/api/rgw-site.service'; |
11fdf7f2 TL |
10 | import { RgwUserService } from '../../../shared/api/rgw-user.service'; |
11 | import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants'; | |
9f95a23c | 12 | import { Icons } from '../../../shared/enum/icons.enum'; |
11fdf7f2 TL |
13 | import { NotificationType } from '../../../shared/enum/notification-type.enum'; |
14 | import { CdFormBuilder } from '../../../shared/forms/cd-form-builder'; | |
15 | import { CdFormGroup } from '../../../shared/forms/cd-form-group'; | |
9f95a23c | 16 | import { CdValidators } from '../../../shared/forms/cd-validators'; |
11fdf7f2 | 17 | import { NotificationService } from '../../../shared/services/notification.service'; |
9f95a23c TL |
18 | import { RgwBucketMfaDelete } from '../models/rgw-bucket-mfa-delete'; |
19 | import { RgwBucketVersioning } from '../models/rgw-bucket-versioning'; | |
11fdf7f2 TL |
20 | |
21 | @Component({ | |
22 | selector: 'cd-rgw-bucket-form', | |
23 | templateUrl: './rgw-bucket-form.component.html', | |
24 | styleUrls: ['./rgw-bucket-form.component.scss'] | |
25 | }) | |
26 | export class RgwBucketFormComponent implements OnInit { | |
27 | bucketForm: CdFormGroup; | |
28 | editing = false; | |
29 | error = false; | |
30 | loading = false; | |
9f95a23c | 31 | owners: string[] = null; |
11fdf7f2 TL |
32 | action: string; |
33 | resource: string; | |
9f95a23c TL |
34 | zonegroup: string; |
35 | placementTargets: object[] = []; | |
36 | isVersioningEnabled = false; | |
37 | isVersioningAlreadyEnabled = false; | |
38 | isMfaDeleteEnabled = false; | |
39 | isMfaDeleteAlreadyEnabled = false; | |
40 | icons = Icons; | |
11fdf7f2 TL |
41 | |
42 | constructor( | |
43 | private route: ActivatedRoute, | |
44 | private router: Router, | |
45 | private formBuilder: CdFormBuilder, | |
46 | private rgwBucketService: RgwBucketService, | |
9f95a23c | 47 | private rgwSiteService: RgwSiteService, |
11fdf7f2 TL |
48 | private rgwUserService: RgwUserService, |
49 | private notificationService: NotificationService, | |
50 | private i18n: I18n, | |
51 | public actionLabels: ActionLabelsI18n | |
52 | ) { | |
53 | this.editing = this.router.url.startsWith(`/rgw/bucket/${URLVerbs.EDIT}`); | |
54 | this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE; | |
55 | this.resource = this.i18n('bucket'); | |
56 | this.createForm(); | |
57 | } | |
58 | ||
59 | createForm() { | |
9f95a23c TL |
60 | const self = this; |
61 | const eitherDaysOrYears = CdValidators.custom('eitherDaysOrYears', () => { | |
62 | if (!self.bucketForm || !_.get(self.bucketForm.getRawValue(), 'lock_enabled')) { | |
63 | return false; | |
64 | } | |
65 | const years = self.bucketForm.getValue('lock_retention_period_years'); | |
66 | const days = self.bucketForm.getValue('lock_retention_period_days'); | |
67 | return (days > 0 && years > 0) || (days === 0 && years === 0); | |
68 | }); | |
69 | const lockPeriodDefinition = [0, [CdValidators.number(false), eitherDaysOrYears]]; | |
11fdf7f2 TL |
70 | this.bucketForm = this.formBuilder.group({ |
71 | id: [null], | |
9f95a23c TL |
72 | bid: [null, [Validators.required], this.editing ? [] : [this.bucketNameValidator()]], |
73 | owner: [null, [Validators.required]], | |
74 | 'placement-target': [null, this.editing ? [] : [Validators.required]], | |
75 | versioning: [null], | |
76 | 'mfa-delete': [null], | |
77 | 'mfa-token-serial': [''], | |
78 | 'mfa-token-pin': [''], | |
79 | lock_enabled: [{ value: false, disabled: this.editing }], | |
80 | lock_mode: ['COMPLIANCE'], | |
81 | lock_retention_period_days: lockPeriodDefinition, | |
82 | lock_retention_period_years: lockPeriodDefinition | |
11fdf7f2 TL |
83 | }); |
84 | } | |
85 | ||
86 | ngOnInit() { | |
87 | // Get the list of possible owners. | |
88 | this.rgwUserService.enumerate().subscribe((resp: string[]) => { | |
89 | this.owners = resp.sort(); | |
90 | }); | |
91 | ||
9f95a23c TL |
92 | if (!this.editing) { |
93 | // Get placement targets: | |
e306af50 | 94 | this.rgwSiteService.get('placement-targets').subscribe((placementTargets: any) => { |
9f95a23c TL |
95 | this.zonegroup = placementTargets['zonegroup']; |
96 | _.forEach(placementTargets['placement_targets'], (placementTarget) => { | |
97 | placementTarget['description'] = `${placementTarget['name']} (${this.i18n('pool')}: ${ | |
98 | placementTarget['data_pool'] | |
99 | })`; | |
100 | this.placementTargets.push(placementTarget); | |
11fdf7f2 | 101 | }); |
9f95a23c TL |
102 | |
103 | // If there is only 1 placement target, select it by default: | |
104 | if (this.placementTargets.length === 1) { | |
105 | this.bucketForm.get('placement-target').setValue(this.placementTargets[0]['name']); | |
106 | } | |
107 | }); | |
108 | } | |
109 | ||
110 | // Process route parameters. | |
111 | this.route.params.subscribe((params: { bid: string }) => { | |
112 | if (!params.hasOwnProperty('bid')) { | |
113 | return; | |
11fdf7f2 | 114 | } |
9f95a23c TL |
115 | const bid = decodeURIComponent(params.bid); |
116 | this.loading = true; | |
117 | ||
118 | this.rgwBucketService.get(bid).subscribe((resp: object) => { | |
119 | this.loading = false; | |
120 | // Get the default values (incl. the values from disabled fields). | |
121 | const defaults = _.clone(this.bucketForm.getRawValue()); | |
122 | // Get the values displayed in the form. We need to do that to | |
123 | // extract those key/value pairs from the response data, otherwise | |
124 | // the Angular react framework will throw an error if there is no | |
125 | // field for a given key. | |
126 | let value: object = _.pick(resp, _.keys(defaults)); | |
127 | value['placement-target'] = resp['placement_rule']; | |
128 | // Append default values. | |
129 | value = _.merge(defaults, value); | |
130 | // Update the form. | |
131 | this.bucketForm.setValue(value); | |
132 | if (this.editing) { | |
133 | this.setVersioningStatus(resp['versioning']); | |
134 | this.isVersioningAlreadyEnabled = this.isVersioningEnabled; | |
135 | this.setMfaDeleteStatus(resp['mfa_delete']); | |
136 | this.isMfaDeleteAlreadyEnabled = this.isMfaDeleteEnabled; | |
137 | this.setMfaDeleteValidators(); | |
138 | } | |
139 | }); | |
140 | }); | |
11fdf7f2 TL |
141 | } |
142 | ||
143 | goToListView() { | |
144 | this.router.navigate(['/rgw/bucket']); | |
145 | } | |
146 | ||
147 | submit() { | |
148 | // Exit immediately if the form isn't dirty. | |
149 | if (this.bucketForm.pristine) { | |
150 | this.goToListView(); | |
151 | return; | |
152 | } | |
9f95a23c | 153 | const values = this.bucketForm.value; |
11fdf7f2 TL |
154 | if (this.editing) { |
155 | // Edit | |
9f95a23c TL |
156 | const versioning = this.getVersioningStatus(); |
157 | const mfaDelete = this.getMfaDeleteStatus(); | |
158 | this.rgwBucketService | |
159 | .update( | |
160 | values['bid'], | |
161 | values['id'], | |
162 | values['owner'], | |
163 | versioning, | |
164 | mfaDelete, | |
165 | values['mfa-token-serial'], | |
166 | values['mfa-token-pin'], | |
167 | values['lock_mode'], | |
168 | values['lock_retention_period_days'], | |
169 | values['lock_retention_period_years'] | |
170 | ) | |
171 | .subscribe( | |
172 | () => { | |
173 | this.notificationService.show( | |
174 | NotificationType.success, | |
175 | this.i18n('Updated Object Gateway bucket "{{bid}}".', values) | |
176 | ); | |
177 | this.goToListView(); | |
178 | }, | |
179 | () => { | |
180 | // Reset the 'Submit' button. | |
181 | this.bucketForm.setErrors({ cdSubmitButton: true }); | |
182 | } | |
183 | ); | |
11fdf7f2 TL |
184 | } else { |
185 | // Add | |
9f95a23c TL |
186 | this.rgwBucketService |
187 | .create( | |
188 | values['bid'], | |
189 | values['owner'], | |
190 | this.zonegroup, | |
191 | values['placement-target'], | |
192 | values['lock_enabled'], | |
193 | values['lock_mode'], | |
194 | values['lock_retention_period_days'], | |
195 | values['lock_retention_period_years'] | |
196 | ) | |
197 | .subscribe( | |
198 | () => { | |
199 | this.notificationService.show( | |
200 | NotificationType.success, | |
201 | this.i18n('Created Object Gateway bucket "{{bid}}"', values) | |
202 | ); | |
203 | this.goToListView(); | |
204 | }, | |
205 | () => { | |
206 | // Reset the 'Submit' button. | |
207 | this.bucketForm.setErrors({ cdSubmitButton: true }); | |
208 | } | |
209 | ); | |
11fdf7f2 TL |
210 | } |
211 | } | |
212 | ||
9f95a23c TL |
213 | /** |
214 | * Validate the bucket name. In general, bucket names should follow domain | |
215 | * name constraints: | |
216 | * - Bucket names must be unique. | |
217 | * - Bucket names cannot be formatted as IP address. | |
218 | * - Bucket names can be between 3 and 63 characters long. | |
219 | * - Bucket names must not contain uppercase characters or underscores. | |
220 | * - Bucket names must start with a lowercase letter or number. | |
221 | * - Bucket names must be a series of one or more labels. Adjacent | |
222 | * labels are separated by a single period (.). Bucket names can | |
223 | * contain lowercase letters, numbers, and hyphens. Each label must | |
224 | * start and end with a lowercase letter or a number. | |
225 | */ | |
11fdf7f2 TL |
226 | bucketNameValidator(): AsyncValidatorFn { |
227 | const rgwBucketService = this.rgwBucketService; | |
228 | return (control: AbstractControl): Promise<ValidationErrors | null> => { | |
229 | return new Promise((resolve) => { | |
230 | // Exit immediately if user has not interacted with the control yet | |
231 | // or the control value is empty. | |
232 | if (control.pristine || control.value === '') { | |
233 | resolve(null); | |
234 | return; | |
235 | } | |
9f95a23c TL |
236 | const constraints = []; |
237 | // - Bucket names cannot be formatted as IP address. | |
238 | constraints.push((name: AbstractControl) => { | |
239 | const validatorFn = CdValidators.ip(); | |
240 | return !validatorFn(name); | |
241 | }); | |
242 | // - Bucket names can be between 3 and 63 characters long. | |
243 | constraints.push((name: string) => _.inRange(name.length, 3, 64)); | |
244 | // - Bucket names must not contain uppercase characters or underscores. | |
245 | // - Bucket names must start with a lowercase letter or number. | |
246 | // - Bucket names must be a series of one or more labels. Adjacent | |
247 | // labels are separated by a single period (.). Bucket names can | |
248 | // contain lowercase letters, numbers, and hyphens. Each label must | |
249 | // start and end with a lowercase letter or a number. | |
250 | constraints.push((name: string) => { | |
251 | const labels = _.split(name, '.'); | |
252 | return _.every(labels, (label) => { | |
253 | // Bucket names must not contain uppercase characters or underscores. | |
254 | if (label !== _.toLower(label) || label.includes('_')) { | |
255 | return false; | |
256 | } | |
257 | // Bucket names can contain lowercase letters, numbers, and hyphens. | |
258 | if (!/[0-9a-z-]/.test(label)) { | |
259 | return false; | |
260 | } | |
261 | // Each label must start and end with a lowercase letter or a number. | |
262 | return _.every([0, label.length], (index) => { | |
263 | return /[a-z]/.test(label[index]) || _.isInteger(_.parseInt(label[index])); | |
264 | }); | |
265 | }); | |
266 | }); | |
267 | if (!_.every(constraints, (func: Function) => func(control.value))) { | |
11fdf7f2 TL |
268 | resolve({ bucketNameInvalid: true }); |
269 | return; | |
270 | } | |
9f95a23c | 271 | // - Bucket names must be unique. |
11fdf7f2 TL |
272 | rgwBucketService.exists(control.value).subscribe((resp: boolean) => { |
273 | if (!resp) { | |
274 | resolve(null); | |
275 | } else { | |
276 | resolve({ bucketNameExists: true }); | |
277 | } | |
278 | }); | |
279 | }); | |
280 | }; | |
281 | } | |
9f95a23c TL |
282 | |
283 | areMfaCredentialsRequired() { | |
284 | return ( | |
285 | this.isMfaDeleteEnabled !== this.isMfaDeleteAlreadyEnabled || | |
286 | (this.isMfaDeleteAlreadyEnabled && | |
287 | this.isVersioningEnabled !== this.isVersioningAlreadyEnabled) | |
288 | ); | |
289 | } | |
290 | ||
291 | setMfaDeleteValidators() { | |
292 | const mfaTokenSerialControl = this.bucketForm.get('mfa-token-serial'); | |
293 | const mfaTokenPinControl = this.bucketForm.get('mfa-token-pin'); | |
294 | ||
295 | if (this.areMfaCredentialsRequired()) { | |
296 | mfaTokenSerialControl.setValidators(Validators.required); | |
297 | mfaTokenPinControl.setValidators(Validators.required); | |
298 | } else { | |
299 | mfaTokenSerialControl.setValidators(null); | |
300 | mfaTokenPinControl.setValidators(null); | |
301 | } | |
302 | ||
303 | mfaTokenSerialControl.updateValueAndValidity(); | |
304 | mfaTokenPinControl.updateValueAndValidity(); | |
305 | } | |
306 | ||
307 | getVersioningStatus() { | |
308 | return this.isVersioningEnabled ? RgwBucketVersioning.ENABLED : RgwBucketVersioning.SUSPENDED; | |
309 | } | |
310 | ||
311 | setVersioningStatus(status: RgwBucketVersioning) { | |
312 | this.isVersioningEnabled = status === RgwBucketVersioning.ENABLED; | |
313 | } | |
314 | ||
315 | updateVersioning() { | |
316 | this.isVersioningEnabled = !this.isVersioningEnabled; | |
317 | this.setMfaDeleteValidators(); | |
318 | } | |
319 | ||
320 | getMfaDeleteStatus() { | |
321 | return this.isMfaDeleteEnabled ? RgwBucketMfaDelete.ENABLED : RgwBucketMfaDelete.DISABLED; | |
322 | } | |
323 | ||
324 | setMfaDeleteStatus(status: RgwBucketMfaDelete) { | |
325 | this.isMfaDeleteEnabled = status === RgwBucketMfaDelete.ENABLED; | |
326 | } | |
327 | ||
328 | updateMfaDelete() { | |
329 | this.isMfaDeleteEnabled = !this.isMfaDeleteEnabled; | |
330 | this.setMfaDeleteValidators(); | |
331 | } | |
11fdf7f2 | 332 | } |