1 import { ChangeDetectorRef, Component, Inject, OnInit, Optional } from '@angular/core';
2 import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
3 import { NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap';
4 import { padStart, uniq } from 'lodash';
5 import moment from 'moment';
6 import { Observable, OperatorFunction, of, timer } from 'rxjs';
17 } from 'rxjs/operators';
18 import { CephfsSnapshotScheduleService } from '~/app/shared/api/cephfs-snapshot-schedule.service';
19 import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
20 import { DirectoryStoreService } from '~/app/shared/api/directory-store.service';
21 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
22 import { Icons } from '~/app/shared/enum/icons.enum';
23 import { RepeatFrequency } from '~/app/shared/enum/repeat-frequency.enum';
24 import { RetentionFrequency } from '~/app/shared/enum/retention-frequency.enum';
25 import { CdForm } from '~/app/shared/forms/cd-form';
26 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
27 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
28 import { FinishedTask } from '~/app/shared/models/finished-task';
32 SnapshotScheduleFormValue
33 } from '~/app/shared/models/snapshot-schedule';
34 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
36 const VALIDATON_TIMER = 300;
37 const DEBOUNCE_TIMER = 300;
38 const DEFAULT_SUBVOLUME_GROUP = '_nogroup';
41 selector: 'cd-cephfs-snapshotschedule-form',
42 templateUrl: './cephfs-snapshotschedule-form.component.html',
43 styleUrls: ['./cephfs-snapshotschedule-form.component.scss']
45 export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnInit {
49 repeatFrequencies = Object.entries(RepeatFrequency);
50 retentionFrequencies = Object.entries(RetentionFrequency);
51 retentionPoliciesToRemove: RetentionPolicy[] = [];
52 isDefaultSubvolumeGroup = false;
53 subvolumeGroup!: string;
59 snapScheduleForm!: CdFormGroup;
64 columns!: CdTableColumn[];
67 private actionLabels: ActionLabelsI18n,
68 private snapScheduleService: CephfsSnapshotScheduleService,
69 private taskWrapper: TaskWrapperService,
70 private cd: ChangeDetectorRef,
71 public directoryStore: DirectoryStoreService,
72 private subvolumeService: CephfsSubvolumeService,
74 @Optional() @Inject('fsName') public fsName: string,
75 @Optional() @Inject('id') public id: number,
76 @Optional() @Inject('path') public path: string,
77 @Optional() @Inject('schedule') public schedule: string,
78 @Optional() @Inject('retention') public retention: string,
79 @Optional() @Inject('start') public start: string,
80 @Optional() @Inject('status') public status: string,
81 @Optional() @Inject('isEdit') public isEdit = false
84 this.resource = $localize`Snapshot schedule`;
86 const currentDatetime = new Date();
87 this.minDate = `${currentDatetime.getUTCFullYear()}-${
88 currentDatetime.getUTCMonth() + 1
89 }-${currentDatetime.getUTCDate()}`;
93 this.action = this.actionLabels.CREATE;
94 this.directoryStore.loadDirectories(this.id, '/', 3);
96 this.isEdit ? this.populateForm() : this.loadingReady();
101 filter(() => !this.isEdit),
102 debounceTime(DEBOUNCE_TIMER),
104 this.isSubvolume = false;
106 tap((value: string) => {
107 this.subvolumeGroup = value?.split?.('/')?.[2];
108 this.subvolume = value?.split?.('/')?.[3];
110 filter(() => !!this.subvolume && !!this.subvolumeGroup),
112 this.isSubvolume = !!this.subvolume && !!this.subvolumeGroup;
113 this.snapScheduleForm.get('repeatFrequency').setErrors(null);
116 this.subvolumeService
120 this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP ? '' : this.subvolumeGroup
123 tap((exists: boolean) => (this.isSubvolume = exists)),
126 (this.isDefaultSubvolumeGroup =
127 exists && this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP)
131 filter((exists: boolean) => exists),
133 this.subvolumeService
137 this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP ? '' : this.subvolumeGroup
141 filter((path: string) => path !== this.snapScheduleForm.get('directory').value)
144 next: (path: string) => this.snapScheduleForm.get('directory').setValue(path)
148 get retentionPolicies() {
149 return this.snapScheduleForm.get('retentionPolicies') as FormArray;
152 search: OperatorFunction<string, readonly string[]> = (input: Observable<string>) =>
154 debounceTime(DEBOUNCE_TIMER),
155 distinctUntilChanged(),
157 this.directoryStore.search(term, this.id).pipe(
166 this.action = this.actionLabels.EDIT;
167 this.snapScheduleService.getSnapshotSchedule(this.path, this.fsName, false).subscribe({
168 next: (response: SnapshotSchedule[]) => {
169 const schedule = response.find((x) => x.path === this.path);
170 const offset = moment().utcOffset();
171 const startDate = moment
172 .parseZone(schedule.start)
176 .format('YYYY-MM-DD HH:mm:ss');
177 this.snapScheduleForm.get('directory').disable();
178 this.snapScheduleForm.get('directory').setValue(schedule.path);
179 this.snapScheduleForm.get('startDate').disable();
180 this.snapScheduleForm.get('startDate').setValue(startDate);
181 this.snapScheduleForm.get('repeatInterval').disable();
182 this.snapScheduleForm.get('repeatInterval').setValue(schedule.schedule.split('')?.[0]);
183 this.snapScheduleForm.get('repeatFrequency').disable();
184 this.snapScheduleForm.get('repeatFrequency').setValue(schedule.schedule.split('')?.[1]);
186 // retention policies
187 schedule.retention &&
188 Object.entries(schedule.retention).forEach(([frequency, interval], idx) => {
189 const freqKey = Object.keys(RetentionFrequency)[
190 Object.values(RetentionFrequency).indexOf(frequency as any)
192 this.retentionPolicies.push(
194 retentionInterval: new FormControl(interval),
195 retentionFrequency: new FormControl(RetentionFrequency[freqKey])
198 this.retentionPolicies.controls[idx].get('retentionInterval').disable();
199 this.retentionPolicies.controls[idx].get('retentionFrequency').disable();
207 this.snapScheduleForm = new CdFormGroup(
209 directory: new FormControl(undefined, {
211 validators: [Validators.required]
213 startDate: new FormControl(this.minDate, {
214 validators: [Validators.required]
216 repeatInterval: new FormControl(1, {
217 validators: [Validators.required, Validators.min(1)]
219 repeatFrequency: new FormControl(RepeatFrequency.Daily, {
220 validators: [Validators.required]
222 retentionPolicies: new FormArray([])
225 asyncValidators: [this.validateSchedule(), this.validateRetention()]
230 addRetentionPolicy() {
231 this.retentionPolicies.push(
233 retentionInterval: new FormControl(1),
234 retentionFrequency: new FormControl(RetentionFrequency.Daily)
237 this.cd.detectChanges();
240 removeRetentionPolicy(idx: number) {
241 if (this.isEdit && this.retentionPolicies.at(idx).disabled) {
242 const values = this.retentionPolicies.at(idx).value as RetentionPolicy;
243 this.retentionPoliciesToRemove.push(values);
245 this.retentionPolicies.removeAt(idx);
246 this.retentionPolicies.controls.forEach((x) =>
247 x.get('retentionFrequency').updateValueAndValidity()
249 this.cd.detectChanges();
252 convertNumberToString(input: number, length = 2, format = '0'): string {
253 return padStart(input.toString(), length, format);
256 parseDatetime(date: NgbDateStruct, time?: NgbTimeStruct): string {
257 if (!date || !time) return null;
258 return `${date.year}-${this.convertNumberToString(date.month)}-${this.convertNumberToString(
260 )}T${this.convertNumberToString(time.hour)}:${this.convertNumberToString(
262 )}:${this.convertNumberToString(time.second)}`;
264 parseSchedule(interval: number, frequency: string): string {
265 return `${interval}${frequency}`;
268 parseRetentionPolicies(retentionPolicies: RetentionPolicy[]) {
269 return retentionPolicies
270 ?.filter((r) => r?.retentionInterval !== null && r?.retentionFrequency !== null)
271 ?.map?.((r) => `${r.retentionInterval}-${r.retentionFrequency}`)
276 this.validateSchedule()(this.snapScheduleForm).subscribe({
278 if (this.snapScheduleForm.invalid) {
279 this.snapScheduleForm.setErrors({ cdSubmitButton: true });
283 const values = this.snapScheduleForm.value as SnapshotScheduleFormValue;
286 const retentionPoliciesToAdd = (this.snapScheduleForm.get(
288 ) as FormArray).controls
291 !ctrl.get('retentionInterval').disabled && !ctrl.get('retentionFrequency').disabled
294 retentionInterval: ctrl.get('retentionInterval').value,
295 retentionFrequency: ctrl.get('retentionFrequency').value
303 retention_to_add: this.parseRetentionPolicies(retentionPoliciesToAdd) || null,
304 retention_to_remove: this.parseRetentionPolicies(this.retentionPoliciesToRemove) || null
308 .wrapTaskAroundCall({
309 task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.EDIT, {
312 call: this.snapScheduleService.update(updateObj)
316 this.snapScheduleForm.setErrors({ cdSubmitButton: true });
323 const snapScheduleObj = {
325 path: values.directory,
326 snap_schedule: this.parseSchedule(values?.repeatInterval, values?.repeatFrequency),
327 start: new Date(values?.startDate.replace(/\//g, '-').replace(' ', 'T'))
332 const retentionPoliciesValues = this.parseRetentionPolicies(values?.retentionPolicies);
334 if (retentionPoliciesValues) {
335 snapScheduleObj['retention_policy'] = retentionPoliciesValues;
338 if (this.isSubvolume) {
339 snapScheduleObj['subvol'] = this.subvolume;
342 if (this.isSubvolume && !this.isDefaultSubvolumeGroup) {
343 snapScheduleObj['group'] = this.subvolumeGroup;
346 .wrapTaskAroundCall({
347 task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.CREATE, {
348 path: snapScheduleObj.path
350 call: this.snapScheduleService.create(snapScheduleObj)
354 this.snapScheduleForm.setErrors({ cdSubmitButton: true });
366 return (frm: AbstractControl) => {
367 const directory = frm.get('directory');
368 const repeatFrequency = frm.get('repeatFrequency');
369 const repeatInterval = frm.get('repeatInterval');
375 return timer(VALIDATON_TIMER).pipe(
377 this.snapScheduleService
378 .checkScheduleExists(
381 repeatInterval?.value,
382 repeatFrequency?.value,
386 map((exists: boolean) => {
388 repeatFrequency?.markAsDirty();
389 repeatFrequency?.setErrors({ notUnique: true }, { emitEvent: true });
391 repeatFrequency?.setErrors(null);
401 getFormArrayItem(frm: FormGroup, frmArrayName: string, ctrl: string, idx: number) {
402 return (frm.get(frmArrayName) as FormArray)?.controls?.[idx]?.get?.(ctrl);
405 validateRetention() {
406 return (frm: FormGroup) => {
407 return timer(VALIDATON_TIMER).pipe(
409 const retentionList = (frm.get('retentionPolicies') as FormArray).controls?.map(
411 return ctrl.get('retentionFrequency').value;
414 if (uniq(retentionList)?.length !== retentionList?.length) {
415 this.getFormArrayItem(
418 'retentionFrequency',
419 retentionList.length - 1
425 return this.snapScheduleService
426 .checkRetentionPolicyExists(
427 frm.get('directory').value,
430 this.retentionPoliciesToRemove?.map?.((rp) => rp.retentionFrequency) || [],
434 map(({ exists, errorIndex }) => {
436 this.getFormArrayItem(
439 'retentionFrequency',
441 )?.setErrors?.({ notUnique: true });
443 (frm.get('retentionPolicies') as FormArray).controls?.forEach?.((_, i) => {
444 this.getFormArrayItem(
447 'retentionFrequency',
449 )?.setErrors?.(null);