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';
16 } from 'rxjs/operators';
17 import { CephfsSnapshotScheduleService } from '~/app/shared/api/cephfs-snapshot-schedule.service';
18 import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
19 import { DirectoryStoreService } from '~/app/shared/api/directory-store.service';
20 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
21 import { Icons } from '~/app/shared/enum/icons.enum';
22 import { RepeatFrequency } from '~/app/shared/enum/repeat-frequency.enum';
23 import { RetentionFrequency } from '~/app/shared/enum/retention-frequency.enum';
24 import { CdForm } from '~/app/shared/forms/cd-form';
25 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
26 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
27 import { FinishedTask } from '~/app/shared/models/finished-task';
31 SnapshotScheduleFormValue
32 } from '~/app/shared/models/snapshot-schedule';
33 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
35 const VALIDATON_TIMER = 300;
36 const DEBOUNCE_TIMER = 300;
37 const DEFAULT_SUBVOLUME_GROUP = '_nogroup';
40 selector: 'cd-cephfs-snapshotschedule-form',
41 templateUrl: './cephfs-snapshotschedule-form.component.html',
42 styleUrls: ['./cephfs-snapshotschedule-form.component.scss']
44 export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnInit {
56 repeatFrequencies = Object.entries(RepeatFrequency);
57 retentionFrequencies = Object.entries(RetentionFrequency);
58 retentionPoliciesToRemove: RetentionPolicy[] = [];
59 isDefaultSubvolumeGroup = false;
60 subvolumeGroup!: string;
64 currentTime!: NgbTimeStruct;
65 minDate!: NgbDateStruct;
67 snapScheduleForm!: CdFormGroup;
72 columns!: CdTableColumn[];
75 public activeModal: NgbActiveModal,
76 private actionLabels: ActionLabelsI18n,
77 private snapScheduleService: CephfsSnapshotScheduleService,
78 private taskWrapper: TaskWrapperService,
79 private cd: ChangeDetectorRef,
80 public directoryStore: DirectoryStoreService,
81 private subvolumeService: CephfsSubvolumeService
84 this.resource = $localize`Snapshot schedule`;
86 const currentDatetime = new Date();
88 year: currentDatetime.getUTCFullYear(),
89 month: currentDatetime.getUTCMonth() + 1,
90 day: currentDatetime.getUTCDate()
93 hour: currentDatetime.getUTCHours(),
94 minute: currentDatetime.getUTCMinutes(),
95 second: currentDatetime.getUTCSeconds()
100 this.action = this.actionLabels.CREATE;
101 this.directoryStore.loadDirectories(this.id, '/', 3);
103 this.isEdit ? this.populateForm() : this.loadingReady();
105 this.snapScheduleForm
108 filter(() => !this.isEdit),
109 debounceTime(DEBOUNCE_TIMER),
111 this.isSubvolume = false;
113 tap((value: string) => {
114 this.subvolumeGroup = value?.split?.('/')?.[2];
115 this.subvolume = value?.split?.('/')?.[3];
117 filter(() => !!this.subvolume && !!this.subvolumeGroup),
119 this.subvolumeService
123 this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP ? '' : this.subvolumeGroup
126 tap((exists: boolean) => (this.isSubvolume = exists)),
129 (this.isDefaultSubvolumeGroup =
130 exists && this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP)
134 filter((exists: boolean) => exists),
136 this.subvolumeService
140 this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP ? '' : this.subvolumeGroup
144 filter((path: string) => path !== this.snapScheduleForm.get('directory').value)
147 next: (path: string) => this.snapScheduleForm.get('directory').setValue(path)
151 get retentionPolicies() {
152 return this.snapScheduleForm.get('retentionPolicies') as FormArray;
155 search: OperatorFunction<string, readonly string[]> = (input: Observable<string>) =>
157 debounceTime(DEBOUNCE_TIMER),
158 distinctUntilChanged(),
160 this.directoryStore.search(term, this.id).pipe(
169 this.action = this.actionLabels.EDIT;
170 this.snapScheduleService.getSnapshotSchedule(this.path, this.fsName, false).subscribe({
171 next: (response: SnapshotSchedule[]) => {
172 const first = response.find((x) => x.path === this.path);
173 this.snapScheduleForm.get('directory').disable();
174 this.snapScheduleForm.get('directory').setValue(first.path);
175 this.snapScheduleForm.get('startDate').disable();
176 this.snapScheduleForm.get('startDate').setValue({
177 year: new Date(first.start).getUTCFullYear(),
178 month: new Date(first.start).getUTCMonth() + 1,
179 day: new Date(first.start).getUTCDate()
181 this.snapScheduleForm.get('startTime').disable();
182 this.snapScheduleForm.get('startTime').setValue({
183 hour: new Date(first.start).getUTCHours(),
184 minute: new Date(first.start).getUTCMinutes(),
185 second: new Date(first.start).getUTCSeconds()
187 this.snapScheduleForm.get('repeatInterval').disable();
188 this.snapScheduleForm.get('repeatInterval').setValue(first.schedule.split('')?.[0]);
189 this.snapScheduleForm.get('repeatFrequency').disable();
190 this.snapScheduleForm.get('repeatFrequency').setValue(first.schedule.split('')?.[1]);
192 // retention policies
194 Object.entries(first.retention).forEach(([frequency, interval], idx) => {
195 const freqKey = Object.keys(RetentionFrequency)[
196 Object.values(RetentionFrequency).indexOf(frequency as any)
198 this.retentionPolicies.push(
200 retentionInterval: new FormControl(interval),
201 retentionFrequency: new FormControl(RetentionFrequency[freqKey])
204 this.retentionPolicies.controls[idx].get('retentionInterval').disable();
205 this.retentionPolicies.controls[idx].get('retentionFrequency').disable();
213 this.snapScheduleForm = new CdFormGroup(
215 directory: new FormControl(undefined, {
217 validators: [Validators.required]
219 startDate: new FormControl(this.minDate, {
220 validators: [Validators.required]
222 startTime: new FormControl(this.currentTime, {
223 validators: [Validators.required]
225 repeatInterval: new FormControl(1, {
226 validators: [Validators.required, Validators.min(1)]
228 repeatFrequency: new FormControl(RepeatFrequency.Daily, {
229 validators: [Validators.required]
231 retentionPolicies: new FormArray([])
234 asyncValidators: [this.validateSchedule(), this.validateRetention()]
239 addRetentionPolicy() {
240 this.retentionPolicies.push(
242 retentionInterval: new FormControl(1),
243 retentionFrequency: new FormControl(RetentionFrequency.Daily)
246 this.cd.detectChanges();
249 removeRetentionPolicy(idx: number) {
250 if (this.isEdit && this.retentionPolicies.at(idx).disabled) {
251 const values = this.retentionPolicies.at(idx).value as RetentionPolicy;
252 this.retentionPoliciesToRemove.push(values);
254 this.retentionPolicies.removeAt(idx);
255 this.retentionPolicies.controls.forEach((x) =>
256 x.get('retentionFrequency').updateValueAndValidity()
258 this.cd.detectChanges();
261 convertNumberToString(input: number, length = 2, format = '0'): string {
262 return padStart(input.toString(), length, format);
265 parseDatetime(date: NgbDateStruct, time?: NgbTimeStruct): string {
266 if (!date || !time) return null;
267 return `${date.year}-${this.convertNumberToString(date.month)}-${this.convertNumberToString(
269 )}T${this.convertNumberToString(time.hour)}:${this.convertNumberToString(
271 )}:${this.convertNumberToString(time.second)}`;
273 parseSchedule(interval: number, frequency: string): string {
274 return `${interval}${frequency}`;
277 parseRetentionPolicies(retentionPolicies: RetentionPolicy[]) {
278 return retentionPolicies
279 ?.filter((r) => r?.retentionInterval !== null && r?.retentionFrequency !== null)
280 ?.map?.((r) => `${r.retentionInterval}-${r.retentionFrequency}`)
285 if (this.snapScheduleForm.invalid) {
286 this.snapScheduleForm.setErrors({ cdSubmitButton: true });
290 const values = this.snapScheduleForm.value as SnapshotScheduleFormValue;
293 const retentionPoliciesToAdd = (this.snapScheduleForm.get(
295 ) as FormArray).controls
298 !ctrl.get('retentionInterval').disabled && !ctrl.get('retentionFrequency').disabled
301 retentionInterval: ctrl.get('retentionInterval').value,
302 retentionFrequency: ctrl.get('retentionFrequency').value
310 retention_to_add: this.parseRetentionPolicies(retentionPoliciesToAdd) || null,
311 retention_to_remove: this.parseRetentionPolicies(this.retentionPoliciesToRemove) || null
315 .wrapTaskAroundCall({
316 task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.EDIT, {
319 call: this.snapScheduleService.update(updateObj)
323 this.snapScheduleForm.setErrors({ cdSubmitButton: true });
326 this.activeModal.close();
330 const snapScheduleObj = {
332 path: values.directory,
333 snap_schedule: this.parseSchedule(values?.repeatInterval, values?.repeatFrequency),
334 start: this.parseDatetime(values?.startDate, values?.startTime)
337 const retentionPoliciesValues = this.parseRetentionPolicies(values?.retentionPolicies);
339 if (retentionPoliciesValues) snapScheduleObj['retention_policy'] = retentionPoliciesValues;
341 if (this.isSubvolume) snapScheduleObj['subvol'] = this.subvolume;
343 if (this.isSubvolume && !this.isDefaultSubvolumeGroup) {
344 snapScheduleObj['group'] = this.subvolumeGroup;
348 .wrapTaskAroundCall({
349 task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.CREATE, {
350 path: snapScheduleObj.path
352 call: this.snapScheduleService.create(snapScheduleObj)
356 this.snapScheduleForm.setErrors({ cdSubmitButton: true });
359 this.activeModal.close();
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
385 map((exists: boolean) => {
387 repeatFrequency?.setErrors({ notUnique: true }, { emitEvent: true });
389 repeatFrequency?.setErrors(null);
399 getFormArrayItem(frm: FormGroup, frmArrayName: string, ctrl: string, idx: number) {
400 return (frm.get(frmArrayName) as FormArray)?.controls?.[idx]?.get?.(ctrl);
403 validateRetention() {
404 return (frm: FormGroup) => {
405 return timer(VALIDATON_TIMER).pipe(
407 const retentionList = (frm.get('retentionPolicies') as FormArray).controls?.map(
409 return ctrl.get('retentionFrequency').value;
412 if (uniq(retentionList)?.length !== retentionList?.length) {
413 this.getFormArrayItem(
416 'retentionFrequency',
417 retentionList.length - 1
423 return this.snapScheduleService
424 .checkRetentionPolicyExists(
425 frm.get('directory').value,
428 this.retentionPoliciesToRemove?.map?.((rp) => rp.retentionFrequency) || [],
432 map(({ exists, errorIndex }) => {
434 this.getFormArrayItem(
437 'retentionFrequency',
439 )?.setErrors?.({ notUnique: true });
441 (frm.get('retentionPolicies') as FormArray).controls?.forEach?.((_, i) => {
442 this.getFormArrayItem(
445 'retentionFrequency',
447 )?.setErrors?.(null);