]> git.apps.os.sepia.ceph.com Git - ceph.git/blob
7459a7472c5f26dbc80f82ca94f0e8e5eedefb1e
[ceph.git] /
1 import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
2 import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
3 import { NgbActiveModal, NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap';
4 import { uniq } from 'lodash';
5 import { Observable, OperatorFunction, of, timer } from 'rxjs';
6 import { catchError, debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
7 import { CephfsSnapshotScheduleService } from '~/app/shared/api/cephfs-snapshot-schedule.service';
8 import { DirectoryStoreService } from '~/app/shared/api/directory-store.service';
9 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
10 import { Icons } from '~/app/shared/enum/icons.enum';
11 import { RepeatFrequency } from '~/app/shared/enum/repeat-frequency.enum';
12 import { RetentionFrequency } from '~/app/shared/enum/retention-frequency.enum';
13 import { CdForm } from '~/app/shared/forms/cd-form';
14 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
15 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
16 import { FinishedTask } from '~/app/shared/models/finished-task';
17 import {
18   RetentionPolicy,
19   SnapshotSchedule,
20   SnapshotScheduleFormValue
21 } from '~/app/shared/models/snapshot-schedule';
22 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
23
24 const VALIDATON_TIMER = 300;
25 const DEBOUNCE_TIMER = 300;
26
27 @Component({
28   selector: 'cd-cephfs-snapshotschedule-form',
29   templateUrl: './cephfs-snapshotschedule-form.component.html',
30   styleUrls: ['./cephfs-snapshotschedule-form.component.scss']
31 })
32 export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnInit {
33   fsName!: string;
34   path!: string;
35   schedule!: string;
36   retention!: string;
37   start!: string;
38   status!: string;
39   id!: number;
40   isEdit = false;
41   icons = Icons;
42   repeatFrequencies = Object.entries(RepeatFrequency);
43   retentionFrequencies = Object.entries(RetentionFrequency);
44   retentionPoliciesToRemove: RetentionPolicy[] = [];
45
46   currentTime!: NgbTimeStruct;
47   minDate!: NgbDateStruct;
48
49   snapScheduleForm!: CdFormGroup;
50
51   action!: string;
52   resource!: string;
53
54   columns!: CdTableColumn[];
55
56   constructor(
57     public activeModal: NgbActiveModal,
58     private actionLabels: ActionLabelsI18n,
59     private snapScheduleService: CephfsSnapshotScheduleService,
60     private taskWrapper: TaskWrapperService,
61     private cd: ChangeDetectorRef,
62     public directoryStore: DirectoryStoreService
63   ) {
64     super();
65     this.resource = $localize`Snapshot schedule`;
66
67     const currentDatetime = new Date();
68     this.minDate = {
69       year: currentDatetime.getUTCFullYear(),
70       month: currentDatetime.getUTCMonth() + 1,
71       day: currentDatetime.getUTCDate()
72     };
73     this.currentTime = {
74       hour: currentDatetime.getUTCHours(),
75       minute: currentDatetime.getUTCMinutes(),
76       second: currentDatetime.getUTCSeconds()
77     };
78   }
79
80   ngOnInit(): void {
81     this.action = this.actionLabels.CREATE;
82     this.directoryStore.loadDirectories(this.id, '/', 3);
83     this.createForm();
84     this.isEdit ? this.populateForm() : this.loadingReady();
85   }
86
87   get retentionPolicies() {
88     return this.snapScheduleForm.get('retentionPolicies') as FormArray;
89   }
90
91   search: OperatorFunction<string, readonly string[]> = (input: Observable<string>) =>
92     input.pipe(
93       debounceTime(DEBOUNCE_TIMER),
94       distinctUntilChanged(),
95       switchMap((term) =>
96         this.directoryStore.search(term, this.id).pipe(
97           catchError(() => {
98             return of([]);
99           })
100         )
101       )
102     );
103
104   populateForm() {
105     this.action = this.actionLabels.EDIT;
106     this.snapScheduleService.getSnapshotSchedule(this.path, this.fsName, false).subscribe({
107       next: (response: SnapshotSchedule[]) => {
108         const first = response.find((x) => x.path === this.path);
109         this.snapScheduleForm.get('directory').disable();
110         this.snapScheduleForm.get('directory').setValue(first.path);
111         this.snapScheduleForm.get('startDate').disable();
112         this.snapScheduleForm.get('startDate').setValue({
113           year: new Date(first.start).getUTCFullYear(),
114           month: new Date(first.start).getUTCMonth() + 1,
115           day: new Date(first.start).getUTCDate()
116         });
117         this.snapScheduleForm.get('startTime').disable();
118         this.snapScheduleForm.get('startTime').setValue({
119           hour: new Date(first.start).getUTCHours(),
120           minute: new Date(first.start).getUTCMinutes(),
121           second: new Date(first.start).getUTCSeconds()
122         });
123         this.snapScheduleForm.get('repeatInterval').disable();
124         this.snapScheduleForm.get('repeatInterval').setValue(first.schedule.split('')?.[0]);
125         this.snapScheduleForm.get('repeatFrequency').disable();
126         this.snapScheduleForm.get('repeatFrequency').setValue(first.schedule.split('')?.[1]);
127
128         // retention policies
129         first.retention &&
130           Object.entries(first.retention).forEach(([frequency, interval], idx) => {
131             const freqKey = Object.keys(RetentionFrequency)[
132               Object.values(RetentionFrequency).indexOf(frequency as any)
133             ];
134             this.retentionPolicies.push(
135               new FormGroup({
136                 retentionInterval: new FormControl(interval),
137                 retentionFrequency: new FormControl(RetentionFrequency[freqKey])
138               })
139             );
140             this.retentionPolicies.controls[idx].get('retentionInterval').disable();
141             this.retentionPolicies.controls[idx].get('retentionFrequency').disable();
142           });
143         this.loadingReady();
144       }
145     });
146   }
147
148   createForm() {
149     this.snapScheduleForm = new CdFormGroup(
150       {
151         directory: new FormControl(undefined, {
152           validators: [Validators.required]
153         }),
154         startDate: new FormControl(this.minDate, {
155           validators: [Validators.required]
156         }),
157         startTime: new FormControl(this.currentTime, {
158           validators: [Validators.required]
159         }),
160         repeatInterval: new FormControl(1, {
161           validators: [Validators.required, Validators.min(1)]
162         }),
163         repeatFrequency: new FormControl(RepeatFrequency.Daily, {
164           validators: [Validators.required]
165         }),
166         retentionPolicies: new FormArray([])
167       },
168       {
169         asyncValidators: [this.validateSchedule(), this.validateRetention()]
170       }
171     );
172   }
173
174   addRetentionPolicy() {
175     this.retentionPolicies.push(
176       new FormGroup({
177         retentionInterval: new FormControl(1),
178         retentionFrequency: new FormControl(RetentionFrequency.Daily)
179       })
180     );
181     this.cd.detectChanges();
182   }
183
184   removeRetentionPolicy(idx: number) {
185     if (this.isEdit && this.retentionPolicies.at(idx).disabled) {
186       const values = this.retentionPolicies.at(idx).value as RetentionPolicy;
187       this.retentionPoliciesToRemove.push(values);
188     }
189     this.retentionPolicies.removeAt(idx);
190     this.retentionPolicies.controls.forEach((x) =>
191       x.get('retentionFrequency').updateValueAndValidity()
192     );
193     this.cd.detectChanges();
194   }
195
196   parseDatetime(date: NgbDateStruct, time?: NgbTimeStruct): string {
197     if (!date || !time) return null;
198     return `${date.year}-${date.month}-${date.day}T${time.hour || '00'}:${time.minute || '00'}:${
199       time.second || '00'
200     }`;
201   }
202   parseSchedule(interval: number, frequency: string): string {
203     return `${interval}${frequency}`;
204   }
205
206   parseRetentionPolicies(retentionPolicies: RetentionPolicy[]) {
207     return retentionPolicies
208       ?.filter((r) => r?.retentionInterval !== null && r?.retentionFrequency !== null)
209       ?.map?.((r) => `${r.retentionInterval}-${r.retentionFrequency}`)
210       .join('|');
211   }
212
213   submit() {
214     if (this.snapScheduleForm.invalid) {
215       this.snapScheduleForm.setErrors({ cdSubmitButton: true });
216       return;
217     }
218
219     const values = this.snapScheduleForm.value as SnapshotScheduleFormValue;
220
221     if (this.isEdit) {
222       const retentionPoliciesToAdd = (this.snapScheduleForm.get(
223         'retentionPolicies'
224       ) as FormArray).controls
225         ?.filter(
226           (ctrl) =>
227             !ctrl.get('retentionInterval').disabled && !ctrl.get('retentionFrequency').disabled
228         )
229         .map((ctrl) => ({
230           retentionInterval: ctrl.get('retentionInterval').value,
231           retentionFrequency: ctrl.get('retentionFrequency').value
232         }));
233
234       const updateObj = {
235         fs: this.fsName,
236         path: this.path,
237         retention_to_add: this.parseRetentionPolicies(retentionPoliciesToAdd) || null,
238         retention_to_remove: this.parseRetentionPolicies(this.retentionPoliciesToRemove) || null
239       };
240
241       this.taskWrapper
242         .wrapTaskAroundCall({
243           task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.EDIT, {
244             path: this.path
245           }),
246           call: this.snapScheduleService.update(updateObj)
247         })
248         .subscribe({
249           error: () => {
250             this.snapScheduleForm.setErrors({ cdSubmitButton: true });
251           },
252           complete: () => {
253             this.activeModal.close();
254           }
255         });
256     } else {
257       const snapScheduleObj = {
258         fs: this.fsName,
259         path: values.directory,
260         snap_schedule: this.parseSchedule(values?.repeatInterval, values?.repeatFrequency),
261         start: this.parseDatetime(values?.startDate, values?.startTime)
262       };
263
264       const retentionPoliciesValues = this.parseRetentionPolicies(values?.retentionPolicies);
265       if (retentionPoliciesValues) {
266         snapScheduleObj['retention_policy'] = retentionPoliciesValues;
267       }
268       this.taskWrapper
269         .wrapTaskAroundCall({
270           task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.CREATE, {
271             path: snapScheduleObj.path
272           }),
273           call: this.snapScheduleService.create(snapScheduleObj)
274         })
275         .subscribe({
276           error: () => {
277             this.snapScheduleForm.setErrors({ cdSubmitButton: true });
278           },
279           complete: () => {
280             this.activeModal.close();
281           }
282         });
283     }
284   }
285
286   validateSchedule() {
287     return (frm: AbstractControl) => {
288       const directory = frm.get('directory');
289       const repeatFrequency = frm.get('repeatFrequency');
290       const repeatInterval = frm.get('repeatInterval');
291
292       if (this.isEdit) {
293         return of(null);
294       }
295
296       return timer(VALIDATON_TIMER).pipe(
297         switchMap(() =>
298           this.snapScheduleService
299             .checkScheduleExists(
300               directory?.value,
301               this.fsName,
302               repeatInterval?.value,
303               repeatFrequency?.value
304             )
305             .pipe(
306               map((exists: boolean) => {
307                 if (exists) {
308                   repeatFrequency?.setErrors({ notUnique: true }, { emitEvent: true });
309                 } else {
310                   repeatFrequency?.setErrors(null);
311                 }
312                 return null;
313               })
314             )
315         )
316       );
317     };
318   }
319
320   getFormArrayItem(frm: FormGroup, frmArrayName: string, ctrl: string, idx: number) {
321     return (frm.get(frmArrayName) as FormArray)?.controls?.[idx]?.get?.(ctrl);
322   }
323
324   validateRetention() {
325     return (frm: FormGroup) => {
326       return timer(VALIDATON_TIMER).pipe(
327         switchMap(() => {
328           const retentionList = (frm.get('retentionPolicies') as FormArray).controls?.map(
329             (ctrl) => {
330               return ctrl.get('retentionFrequency').value;
331             }
332           );
333           if (uniq(retentionList)?.length !== retentionList?.length) {
334             this.getFormArrayItem(
335               frm,
336               'retentionPolicies',
337               'retentionFrequency',
338               retentionList.length - 1
339             )?.setErrors?.({
340               notUnique: true
341             });
342             return null;
343           }
344           return this.snapScheduleService
345             .checkRetentionPolicyExists(
346               frm.get('directory').value,
347               this.fsName,
348               retentionList,
349               this.retentionPoliciesToRemove?.map?.((rp) => rp.retentionFrequency) || []
350             )
351             .pipe(
352               map(({ exists, errorIndex }) => {
353                 if (exists) {
354                   this.getFormArrayItem(
355                     frm,
356                     'retentionPolicies',
357                     'retentionFrequency',
358                     errorIndex
359                   )?.setErrors?.({ notUnique: true });
360                 } else {
361                   (frm.get('retentionPolicies') as FormArray).controls?.forEach?.((_, i) => {
362                     this.getFormArrayItem(
363                       frm,
364                       'retentionPolicies',
365                       'retentionFrequency',
366                       i
367                     )?.setErrors?.(null);
368                   });
369                 }
370                 return null;
371               })
372             );
373         })
374       );
375     };
376   }
377 }