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 { padStart, 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 { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
9 import { DirectoryStoreService } from '~/app/shared/api/directory-store.service';
10 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
11 import { Icons } from '~/app/shared/enum/icons.enum';
12 import { RepeatFrequency } from '~/app/shared/enum/repeat-frequency.enum';
13 import { RetentionFrequency } from '~/app/shared/enum/retention-frequency.enum';
14 import { CdForm } from '~/app/shared/forms/cd-form';
15 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
16 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
17 import { FinishedTask } from '~/app/shared/models/finished-task';
21 SnapshotScheduleFormValue
22 } from '~/app/shared/models/snapshot-schedule';
23 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
25 const VALIDATON_TIMER = 300;
26 const DEBOUNCE_TIMER = 300;
27 const DEFAULT_SUBVOLUME_GROUP = '_nogroup';
30 selector: 'cd-cephfs-snapshotschedule-form',
31 templateUrl: './cephfs-snapshotschedule-form.component.html',
32 styleUrls: ['./cephfs-snapshotschedule-form.component.scss']
34 export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnInit {
46 repeatFrequencies = Object.entries(RepeatFrequency);
47 retentionFrequencies = Object.entries(RetentionFrequency);
48 retentionPoliciesToRemove: RetentionPolicy[] = [];
49 isDefaultSubvolumeGroup = false;
50 subvolumeGroup!: string;
54 currentTime!: NgbTimeStruct;
55 minDate!: NgbDateStruct;
57 snapScheduleForm!: CdFormGroup;
62 columns!: CdTableColumn[];
65 public activeModal: NgbActiveModal,
66 private actionLabels: ActionLabelsI18n,
67 private snapScheduleService: CephfsSnapshotScheduleService,
68 private taskWrapper: TaskWrapperService,
69 private cd: ChangeDetectorRef,
70 public directoryStore: DirectoryStoreService,
71 private subvolumeService: CephfsSubvolumeService
74 this.resource = $localize`Snapshot schedule`;
76 const currentDatetime = new Date();
78 year: currentDatetime.getUTCFullYear(),
79 month: currentDatetime.getUTCMonth() + 1,
80 day: currentDatetime.getUTCDate()
83 hour: currentDatetime.getUTCHours(),
84 minute: currentDatetime.getUTCMinutes(),
85 second: currentDatetime.getUTCSeconds()
90 this.action = this.actionLabels.CREATE;
91 this.directoryStore.loadDirectories(this.id, '/', 3);
93 this.isEdit ? this.populateForm() : this.loadingReady();
94 this.snapScheduleForm.get('directory').valueChanges.subscribe({
95 next: (value: string) => {
96 this.subvolumeGroup = value?.split?.('/')?.[2];
97 this.subvolume = value?.split?.('/')?.[3];
102 this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP ? '' : this.subvolumeGroup
105 next: (exists: boolean) => {
106 this.isSubvolume = exists;
107 this.isDefaultSubvolumeGroup =
108 exists && this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP;
115 get retentionPolicies() {
116 return this.snapScheduleForm.get('retentionPolicies') as FormArray;
119 search: OperatorFunction<string, readonly string[]> = (input: Observable<string>) =>
121 debounceTime(DEBOUNCE_TIMER),
122 distinctUntilChanged(),
124 this.directoryStore.search(term, this.id).pipe(
133 this.action = this.actionLabels.EDIT;
134 this.snapScheduleService.getSnapshotSchedule(this.path, this.fsName, false).subscribe({
135 next: (response: SnapshotSchedule[]) => {
136 const first = response.find((x) => x.path === this.path);
137 this.snapScheduleForm.get('directory').disable();
138 this.snapScheduleForm.get('directory').setValue(first.path);
139 this.snapScheduleForm.get('startDate').disable();
140 this.snapScheduleForm.get('startDate').setValue({
141 year: new Date(first.start).getUTCFullYear(),
142 month: new Date(first.start).getUTCMonth() + 1,
143 day: new Date(first.start).getUTCDate()
145 this.snapScheduleForm.get('startTime').disable();
146 this.snapScheduleForm.get('startTime').setValue({
147 hour: new Date(first.start).getUTCHours(),
148 minute: new Date(first.start).getUTCMinutes(),
149 second: new Date(first.start).getUTCSeconds()
151 this.snapScheduleForm.get('repeatInterval').disable();
152 this.snapScheduleForm.get('repeatInterval').setValue(first.schedule.split('')?.[0]);
153 this.snapScheduleForm.get('repeatFrequency').disable();
154 this.snapScheduleForm.get('repeatFrequency').setValue(first.schedule.split('')?.[1]);
156 // retention policies
158 Object.entries(first.retention).forEach(([frequency, interval], idx) => {
159 const freqKey = Object.keys(RetentionFrequency)[
160 Object.values(RetentionFrequency).indexOf(frequency as any)
162 this.retentionPolicies.push(
164 retentionInterval: new FormControl(interval),
165 retentionFrequency: new FormControl(RetentionFrequency[freqKey])
168 this.retentionPolicies.controls[idx].get('retentionInterval').disable();
169 this.retentionPolicies.controls[idx].get('retentionFrequency').disable();
177 this.snapScheduleForm = new CdFormGroup(
179 directory: new FormControl(undefined, {
181 validators: [Validators.required]
183 startDate: new FormControl(this.minDate, {
184 validators: [Validators.required]
186 startTime: new FormControl(this.currentTime, {
187 validators: [Validators.required]
189 repeatInterval: new FormControl(1, {
190 validators: [Validators.required, Validators.min(1)]
192 repeatFrequency: new FormControl(RepeatFrequency.Daily, {
193 validators: [Validators.required]
195 retentionPolicies: new FormArray([])
198 asyncValidators: [this.validateSchedule(), this.validateRetention()]
203 addRetentionPolicy() {
204 this.retentionPolicies.push(
206 retentionInterval: new FormControl(1),
207 retentionFrequency: new FormControl(RetentionFrequency.Daily)
210 this.cd.detectChanges();
213 removeRetentionPolicy(idx: number) {
214 if (this.isEdit && this.retentionPolicies.at(idx).disabled) {
215 const values = this.retentionPolicies.at(idx).value as RetentionPolicy;
216 this.retentionPoliciesToRemove.push(values);
218 this.retentionPolicies.removeAt(idx);
219 this.retentionPolicies.controls.forEach((x) =>
220 x.get('retentionFrequency').updateValueAndValidity()
222 this.cd.detectChanges();
225 parseDatetime(date: NgbDateStruct, time?: NgbTimeStruct): string {
226 if (!date || !time) return null;
227 return `${date.year}-${padStart(date.month.toString(), 2, '0')}-${padStart(
231 )}T${time.hour || '00'}:${time.minute || '00'}:${time.second || '00'}`;
233 parseSchedule(interval: number, frequency: string): string {
234 return `${interval}${frequency}`;
237 parseRetentionPolicies(retentionPolicies: RetentionPolicy[]) {
238 return retentionPolicies
239 ?.filter((r) => r?.retentionInterval !== null && r?.retentionFrequency !== null)
240 ?.map?.((r) => `${r.retentionInterval}-${r.retentionFrequency}`)
245 if (this.snapScheduleForm.invalid) {
246 this.snapScheduleForm.setErrors({ cdSubmitButton: true });
250 const values = this.snapScheduleForm.value as SnapshotScheduleFormValue;
253 const retentionPoliciesToAdd = (this.snapScheduleForm.get(
255 ) as FormArray).controls
258 !ctrl.get('retentionInterval').disabled && !ctrl.get('retentionFrequency').disabled
261 retentionInterval: ctrl.get('retentionInterval').value,
262 retentionFrequency: ctrl.get('retentionFrequency').value
270 retention_to_add: this.parseRetentionPolicies(retentionPoliciesToAdd) || null,
271 retention_to_remove: this.parseRetentionPolicies(this.retentionPoliciesToRemove) || null
275 .wrapTaskAroundCall({
276 task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.EDIT, {
279 call: this.snapScheduleService.update(updateObj)
283 this.snapScheduleForm.setErrors({ cdSubmitButton: true });
286 this.activeModal.close();
290 const snapScheduleObj = {
292 path: values.directory,
293 snap_schedule: this.parseSchedule(values?.repeatInterval, values?.repeatFrequency),
294 start: this.parseDatetime(values?.startDate, values?.startTime)
297 const retentionPoliciesValues = this.parseRetentionPolicies(values?.retentionPolicies);
299 if (retentionPoliciesValues) snapScheduleObj['retention_policy'] = retentionPoliciesValues;
301 if (this.isSubvolume) snapScheduleObj['subvol'] = this.subvolume;
303 if (this.isSubvolume && !this.isDefaultSubvolumeGroup) {
304 snapScheduleObj['group'] = this.subvolumeGroup;
308 .wrapTaskAroundCall({
309 task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.CREATE, {
310 path: snapScheduleObj.path
312 call: this.snapScheduleService.create(snapScheduleObj)
316 this.snapScheduleForm.setErrors({ cdSubmitButton: true });
319 this.activeModal.close();
326 return (frm: AbstractControl) => {
327 const directory = frm.get('directory');
328 const repeatFrequency = frm.get('repeatFrequency');
329 const repeatInterval = frm.get('repeatInterval');
335 return timer(VALIDATON_TIMER).pipe(
337 this.snapScheduleService
338 .checkScheduleExists(
341 repeatInterval?.value,
342 repeatFrequency?.value
345 map((exists: boolean) => {
347 repeatFrequency?.setErrors({ notUnique: true }, { emitEvent: true });
349 repeatFrequency?.setErrors(null);
359 getFormArrayItem(frm: FormGroup, frmArrayName: string, ctrl: string, idx: number) {
360 return (frm.get(frmArrayName) as FormArray)?.controls?.[idx]?.get?.(ctrl);
363 validateRetention() {
364 return (frm: FormGroup) => {
365 return timer(VALIDATON_TIMER).pipe(
367 const retentionList = (frm.get('retentionPolicies') as FormArray).controls?.map(
369 return ctrl.get('retentionFrequency').value;
372 if (uniq(retentionList)?.length !== retentionList?.length) {
373 this.getFormArrayItem(
376 'retentionFrequency',
377 retentionList.length - 1
383 return this.snapScheduleService
384 .checkRetentionPolicyExists(
385 frm.get('directory').value,
388 this.retentionPoliciesToRemove?.map?.((rp) => rp.retentionFrequency) || []
391 map(({ exists, errorIndex }) => {
393 this.getFormArrayItem(
396 'retentionFrequency',
398 )?.setErrors?.({ notUnique: true });
400 (frm.get('retentionPolicies') as FormArray).controls?.forEach?.((_, i) => {
401 this.getFormArrayItem(
404 'retentionFrequency',
406 )?.setErrors?.(null);