[fsId]="fsId">
</cd-mirroring-paths-step>
</cd-tearsheet-step>
- <cd-tearsheet-step></cd-tearsheet-step>
- <cd-tearsheet-step></cd-tearsheet-step>
+ <cd-tearsheet-step>
+ <p class="cds--type-heading-03 cds-mb-3"
+ i18n>Snapshot schedule</p>
+ <p class="cds--type-body-compact-01 cds-mb-5"
+ i18n>Define when snapshots are taken for the mirrored path(s).</p>
+ <cd-cephfs-snapshotschedule-form
+ #scheduleStep
+ #tearsheetStep
+ [embedded]="true"
+ [hideDirectory]="true"
+ [fsNameInput]="fsName"
+ [fsIdInput]="fsId"
+ [schedulePath]="schedulePath">
+ </cd-cephfs-snapshotschedule-form>
+ </cd-tearsheet-step>
+ <cd-tearsheet-step>
+ <cd-mirroring-review-step
+ #tearsheetStep
+ [fsName]="fsName"
+ [pathsStep]="pathsStep"
+ [scheduleStep]="scheduleStep">
+ </cd-mirroring-review-step>
+ </cd-tearsheet-step>
</cd-tearsheet>
import { CephfsAddMirroringPathComponent } from './cephfs-add-mirroring-path.component';
import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { CephfsSnapshotScheduleService } from '~/app/shared/api/cephfs-snapshot-schedule.service';
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
import { NotificationService } from '~/app/shared/services/notification.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
describe('CephfsAddMirroringPathComponent', () => {
let component: CephfsAddMirroringPathComponent;
addMirrorDirectory: jest.fn()
};
+ const snapshotScheduleServiceMock = {
+ create: jest.fn()
+ };
+
+ const taskWrapperMock = {
+ wrapTaskAroundCall: jest.fn(({ call }) => call)
+ };
+
const notificationServiceMock = {
show: jest.fn()
};
useValue: { navigate: routerNavigateSpy }
},
{ provide: CephfsService, useValue: cephfsServiceMock },
+ { provide: CephfsSnapshotScheduleService, useValue: snapshotScheduleServiceMock },
+ { provide: TaskWrapperService, useValue: taskWrapperMock },
{ provide: NotificationService, useValue: notificationServiceMock }
],
schemas: [NO_ERRORS_SCHEMA]
});
it('should skip API calls when the paths step form is invalid', () => {
- const refreshTrackedPaths = jest.fn();
+ const refreshTrackedPaths = jest.fn(() => of(undefined));
component.pathsStep = {
formGroup: {
markAllAsTouched: jest.fn(),
component.onSubmit();
- expect(component.pathsStep.formGroup.markAllAsTouched).toHaveBeenCalled();
- expect(refreshTrackedPaths).not.toHaveBeenCalled();
+ expect(refreshTrackedPaths).toHaveBeenCalled();
expect(notificationServiceMock.show).not.toHaveBeenCalled();
expect(cephfsServiceMock.addMirrorDirectory).not.toHaveBeenCalled();
expect(component.isSubmitLoading).toBe(false);
addTrackedPath: jest.fn(),
...overrides
} as any;
+ component.scheduleStep = {
+ buildCreatePayload: jest.fn((path: string) => ({ path, fs: 'testfs' }))
+ } as any;
+ snapshotScheduleServiceMock.create.mockReturnValue(of({}));
}
it('should add mirror directories and close modal on success', fakeAsync(() => {
useValue: { navigate: routerNavigateSpy }
},
{ provide: CephfsService, useValue: cephfsServiceMock },
+ { provide: CephfsSnapshotScheduleService, useValue: snapshotScheduleServiceMock },
+ { provide: TaskWrapperService, useValue: taskWrapperMock },
{ provide: NotificationService, useValue: notificationServiceMock }
],
schemas: [NO_ERRORS_SCHEMA]
import { catchError, concatMap, finalize, map, switchMap, tap, toArray } from 'rxjs/operators';
import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { CephfsSnapshotScheduleService } from '~/app/shared/api/cephfs-snapshot-schedule.service';
+import { URLVerbs } from '~/app/shared/constants/app.constants';
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { FinishedTask } from '~/app/shared/models/finished-task';
import { NotificationService } from '~/app/shared/services/notification.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { MirroringPathUtils } from './mirroring-path-utils';
import { PathSubmitFailure, PathSubmitOutput } from './mirroring-path.model';
import { MirroringPathsStepComponent } from './mirroring-paths-step/mirroring-paths-step.component';
+import { CephfsSnapshotscheduleFormComponent } from '../cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component';
import { CEPHFS_MIRRORING_URL } from '~/app/shared/constants/cephfs.constant';
})
export class CephfsAddMirroringPathComponent implements OnInit {
@ViewChild('pathsStep') pathsStep!: MirroringPathsStepComponent;
+ @ViewChild('scheduleStep') scheduleStep?: CephfsSnapshotscheduleFormComponent;
private route = inject(ActivatedRoute);
private router = inject(Router);
private cephfsService = inject(CephfsService);
+ private snapshotScheduleService = inject(CephfsSnapshotScheduleService);
+ private taskWrapper = inject(TaskWrapperService);
private notificationService = inject(NotificationService);
private destroyRef = inject(DestroyRef);
];
isSubmitLoading = false;
+ get schedulePath(): string {
+ return this.pathsStep?.getSubmitPaths()?.toAdd?.[0] ?? '';
+ }
+
ngOnInit(): void {
this.fsId = Number(this.route.snapshot.paramMap.get('fsId'));
const fsName = this.route.snapshot.paramMap.get('fsName') ?? '';
return;
}
- pathsStep.formGroup.markAllAsTouched();
- pathsStep.formGroup.updateValueAndValidity();
- if (pathsStep.formGroup.invalid) {
- return;
- }
-
this.isSubmitLoading = true;
pathsStep
skippedByServer: [],
succeeded: []
});
- return of([] as (string | null)[]);
+ return of({
+ failed: [],
+ alreadyMirrored,
+ skippedByServer: [],
+ succeeded: []
+ });
}
const skippedByServer: string[] = [];
);
}),
toArray(),
- tap((results) => {
+ switchMap((results) => {
const succeeded = results.filter((path): path is string => !!path);
- this.showSubmitSummary({
- failed,
- alreadyMirrored,
- skippedByServer,
- succeeded
- });
- })
+ return this.createSnapshotSchedules(succeeded).pipe(
+ map(() => ({ failed, alreadyMirrored, skippedByServer, succeeded }))
+ );
+ }),
+ tap((outcome) => this.showSubmitSummary(outcome))
);
}),
finalize(() => {
}),
takeUntilDestroyed(this.destroyRef)
)
- .subscribe((results) => {
- const succeeded = results.filter((path): path is string => !!path);
- if (succeeded.length) {
+ .subscribe((outcome) => {
+ if (outcome?.succeeded?.length) {
this.closeTearsheet(true);
}
});
}
+ private createSnapshotSchedules(paths: string[]) {
+ if (!this.scheduleStep || !paths.length) {
+ return of(undefined);
+ }
+
+ return from(paths).pipe(
+ concatMap((path) =>
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.CREATE, { path }),
+ call: this.snapshotScheduleService.create(this.scheduleStep.buildCreatePayload(path))
+ })
+ .pipe(
+ catchError((error) => {
+ const detail =
+ error?.error?.detail ||
+ error?.message ||
+ $localize`Failed to create snapshot schedule for '${path}'`;
+ this.notificationService.show(
+ NotificationType.error,
+ $localize`Failed to create snapshot schedule`,
+ detail
+ );
+ return of(undefined);
+ })
+ )
+ ),
+ toArray(),
+ map(() => undefined)
+ );
+ }
+
onCancel(): void {
this.closeTearsheet(false);
}
--- /dev/null
+<p class="cds--type-heading-03 cds-mb-3"
+ i18n>Review</p>
+<p class="cds--type-body-compact-01 cds-mb-5"
+ i18n>Confirm the paths and snapshot schedule before adding the mirror path(s).</p>
+
+<div cdsStack="vertical"
+ [gap]="5">
+ <div>
+ <div class="cds--type-label-01 cds-mb-2"
+ i18n>Filesystem</div>
+ <div class="cds--type-body-01">{{ fsName }}</div>
+ </div>
+
+ <div>
+ <div class="cds--type-label-01 cds-mb-2"
+ i18n>Paths</div>
+ @if (pathsToAdd.length) {
+ @for (path of pathsToAdd; track path) {
+ <div class="cds--type-body-01">{{ path }}</div>
+ }
+ } @else {
+ <div class="cds--type-body-01">—</div>
+ }
+ </div>
+
+ <div>
+ <div class="cds--type-label-01 cds-mb-2"
+ i18n>Snapshot schedule</div>
+ <div class="cds--type-body-01">{{ scheduleSummary || '—' }}</div>
+ </div>
+</div>
--- /dev/null
+import { Component, Input, OnInit } from '@angular/core';
+
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { TearsheetStep } from '~/app/shared/models/tearsheet-step';
+import { CephfsSnapshotscheduleFormComponent } from '../../cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component';
+import { MirroringPathsStepComponent } from '../mirroring-paths-step/mirroring-paths-step.component';
+
+@Component({
+ selector: 'cd-mirroring-review-step',
+ templateUrl: './mirroring-review-step.component.html',
+ standalone: false
+})
+export class MirroringReviewStepComponent implements OnInit, TearsheetStep {
+ @Input() fsName = '';
+ @Input() pathsStep?: MirroringPathsStepComponent;
+ @Input() scheduleStep?: CephfsSnapshotscheduleFormComponent;
+
+ formGroup!: CdFormGroup;
+
+ ngOnInit(): void {
+ this.formGroup = new CdFormGroup({});
+ }
+
+ get pathsToAdd(): string[] {
+ return this.pathsStep?.getSubmitPaths()?.toAdd ?? [];
+ }
+
+ get scheduleSummary(): string {
+ return this.scheduleStep?.getScheduleSummary() ?? '';
+ }
+}
-<cds-modal size="lg"
- [open]="open"
- [hasScrollingContent]="true"
- (overlaySelected)="closeModal()">
- <cds-modal-header (closeSelect)="closeModal()">
- <h3 cdsModalHeaderHeading
- i18n>
- {{ action | titlecase }} {{ resource | upperFirst }}
- </h3>
- </cds-modal-header>
+@if (!embedded) {
+ <cds-modal size="lg"
+ [open]="open"
+ [hasScrollingContent]="true"
+ (overlaySelected)="closeModal()">
+ <cds-modal-header (closeSelect)="closeModal()">
+ <h3 cdsModalHeaderHeading
+ i18n>
+ {{ action | titlecase }} {{ resource | upperFirst }}
+ </h3>
+ </cds-modal-header>
+ <ng-container *cdFormLoading="loading">
+ <div cdsModalContent>
+ <ng-container *ngTemplateOutlet="formBody"></ng-container>
+ </div>
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="snapScheduleForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ [modalForm]="true"></cd-form-button-panel>
+ </ng-container>
+ </cds-modal>
+} @else {
<ng-container *cdFormLoading="loading">
- <div cdsModalContent>
- <form name="snapScheduleForm"
- #formDir="ngForm"
- [formGroup]="snapScheduleForm"
- novalidate>
- <!-- Directory -->
- <div class="form-item">
- <cds-text-label for="directory"
- i18n
- cdRequiredField="Directory"
- [invalid]="snapScheduleForm.controls.directory.invalid && (snapScheduleForm.controls.directory.dirty)"
- [invalidText]="directoryError"
- [skeleton]="directoryStore.isLoading"
- modal-primary-focus>
- <ng-container *ngIf="!directoryStore.isLoading">Directory (required)</ng-container>
- <input cdsText
- type="text"
- formControlName="directory"
- name="directory"
- [ngbTypeahead]="search"
- [invalid]="snapScheduleForm.controls.directory.invalid && (snapScheduleForm.controls.directory.dirty)"
- [placeholder]="directoryStore.isLoading ? '' : 'Directory path'"
- [skeleton]="directoryStore.isLoading"/>
- </cds-text-label>
- <ng-template #directoryError>
- <span class="invalid-feedback"
- *ngIf="snapScheduleForm.showError('directory', formDir, 'required')"
- i18n>This field is required.</span>
- <span class="invalid-feedback"
- *ngIf="snapScheduleForm.showError('directory', formDir, 'notUnique')"
- i18n>A snapshot schedule for this path already exists.</span>
- </ng-template>
- </div>
+ <ng-container *ngTemplateOutlet="formBody"></ng-container>
+ </ng-container>
+}
- <!--Start date -->
- <cd-date-time-picker
- name="Start Date"
- helperText="The time zone is assumed to be UTC"
- [control]="snapScheduleForm.get('startDate')"
- [disabled]="isEdit"></cd-date-time-picker>
+<ng-template #formBody>
+ <form name="snapScheduleForm"
+ #formDir="ngForm"
+ [formGroup]="snapScheduleForm"
+ novalidate>
+ @if (!hideDirectory) {
+ <!-- Directory -->
+ <div class="form-item">
+ <cds-text-label for="directory"
+ i18n
+ cdRequiredField="Directory"
+ [invalid]="snapScheduleForm.controls.directory.invalid && (snapScheduleForm.controls.directory.dirty)"
+ [invalidText]="directoryError"
+ [skeleton]="directoryStore.isLoading"
+ modal-primary-focus>
+ <ng-container *ngIf="!directoryStore.isLoading">Directory (required)</ng-container>
+ <input cdsText
+ type="text"
+ formControlName="directory"
+ name="directory"
+ [ngbTypeahead]="search"
+ [invalid]="snapScheduleForm.controls.directory.invalid && (snapScheduleForm.controls.directory.dirty)"
+ [placeholder]="directoryStore.isLoading ? '' : 'Directory path'"
+ [skeleton]="directoryStore.isLoading"/>
+ </cds-text-label>
+ <ng-template #directoryError>
<span class="invalid-feedback"
- *ngIf="snapScheduleForm.showError('startDate', formDir, 'required')"
+ *ngIf="snapScheduleForm.showError('directory', formDir, 'required')"
i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="snapScheduleForm.showError('directory', formDir, 'notUnique')"
+ i18n>A snapshot schedule for this path already exists.</span>
+ </ng-template>
+ </div>
+ }
- <!-- Repeat interval -->
- <div class="form-item form-item-append"
- cdsRow>
- <div cdsCol>
- <cds-number [id]="'repeatInterval'"
- [name]="'repeatInterval'"
- [formControlName]="'repeatInterval'"
- [label]="'Schedule'"
+ <!--Start date -->
+ <cd-date-time-picker
+ name="Start Date"
+ helperText="The time zone is assumed to be UTC"
+ [control]="snapScheduleForm.get('startDate')"
+ [disabled]="isEdit"></cd-date-time-picker>
+ <span class="invalid-feedback"
+ *ngIf="snapScheduleForm.showError('startDate', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <!-- Repeat interval -->
+ <div class="form-item form-item-append"
+ cdsRow>
+ <div cdsCol>
+ <cds-number [id]="'repeatInterval'"
+ [name]="'repeatInterval'"
+ [formControlName]="'repeatInterval'"
+ [label]="'Schedule'"
+ [min]="1"
+ [invalid]="!snapScheduleForm.controls.repeatInterval.valid && (snapScheduleForm.controls.repeatInterval.dirty)"
+ [invalidText]="repeatIntervalError"
+ cdRequiredField="Schedule"></cds-number>
+ </div>
+ <div cdsCol>
+ <cds-select id="repeatFrequency"
+ name="repeatFrequency"
+ formControlName="repeatFrequency"
+ label="Frequency"
+ [invalid]="!snapScheduleForm.controls.repeatFrequency.valid && (snapScheduleForm.controls.repeatFrequency.dirty)"
+ [invalidText]="repeatFrequencyError"
+ *ngIf="repeatFrequencies">
+ <option *ngFor="let freq of repeatFrequencies"
+ [value]="freq[1]">{{ freq[0] }}
+ </option>
+ </cds-select>
+ <ng-template #repeatFrequencyError>
+ <span class="invalid-feedback"
+ *ngIf="snapScheduleForm.showError('repeatFrequency', formDir, 'notUnique')"
+ i18n>This schedule already exists for the selected directory.</span>
+ </ng-template>
+ <ng-template #repeatIntervalError>
+ <span class="invalid-feedback"
+ *ngIf="snapScheduleForm.showError('repeatInterval', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="snapScheduleForm.showError('repeatInterval', formDir, 'min')"
+ i18n>Choose a value greater than 0.</span>
+ </ng-template>
+ </div>
+ </div>
+
+ <!-- Retention policies -->
+ <ng-container formArrayName="retentionPolicies"
+ *ngFor="let retentionPolicy of retentionPolicies.controls; index as i">
+ <ng-container [formGroupName]="i">
+ <div cdsRow
+ class="form-item form-item-append">
+ <div cdsCol
+ [columnNumbers]="{lg: 8}">
+ <cds-number [id]="'retentionInterval' + i"
+ [name]="'retentionInterval' + i"
+ [formControlName]="'retentionInterval'"
+ [label]="'Retention policy'"
[min]="1"
- [invalid]="!snapScheduleForm.controls.repeatInterval.valid && (snapScheduleForm.controls.repeatInterval.dirty)"
- [invalidText]="repeatIntervalError"
- cdRequiredField="Schedule"></cds-number>
+ [invalid]="snapScheduleForm.controls['retentionPolicies'].controls[i].invalid && snapScheduleForm.controls['retentionPolicies'].dirty"
+ [invalidText]="retentionPolicyError"></cds-number>
</div>
- <div cdsCol>
- <cds-select id="repeatFrequency"
- name="repeatFrequency"
- formControlName="repeatFrequency"
+ <div cdsCol
+ [columnNumbers]="{lg: 7}">
+ <cds-select id="retentionFrequency"
+ name="retentionFrequency"
+ formControlName="retentionFrequency"
label="Frequency"
- [invalid]="!snapScheduleForm.controls.repeatFrequency.valid && (snapScheduleForm.controls.repeatFrequency.dirty)"
- [invalidText]="repeatFrequencyError"
- *ngIf="repeatFrequencies">
- <option *ngFor="let freq of repeatFrequencies"
- [value]="freq[1]">{{ freq[0] }}
- </option>
+ *ngIf="retentionFrequencies">
+ <option *ngFor="let freq of retentionFrequencies"
+ [value]="freq[1]">{{ freq[0] }}</option>
+
</cds-select>
- <ng-template #repeatFrequencyError>
- <span class="invalid-feedback"
- *ngIf="snapScheduleForm.showError('repeatFrequency', formDir, 'notUnique')"
- i18n>This schedule already exists for the selected directory.</span>
- </ng-template>
- <ng-template #repeatIntervalError>
- <span class="invalid-feedback"
- *ngIf="snapScheduleForm.showError('repeatInterval', formDir, 'required')"
- i18n>This field is required.</span>
- <span class="invalid-feedback"
- *ngIf="snapScheduleForm.showError('repeatInterval', formDir, 'min')"
- i18n>Choose a value greater than 0.</span>
- </ng-template>
+ </div>
+ <div cdsCol
+ [columnNumbers]="{lg: 1}"
+ class="item-action-btn">
+ <cds-icon-button kind="danger"
+ size="sm"
+ (click)="removeRetentionPolicy(i)">
+ <svg cdsIcon="trash-can"
+ size="32"
+ class="cds--btn__icon"></svg>
+ </cds-icon-button>
</div>
</div>
+ <ng-template #retentionPolicyError>
+ <span class="invalid-feedback"
+ *ngIf="snapScheduleForm.controls['retentionPolicies'].controls[i].invalid"
+ i18n>This retention policy already exists for the selected directory.</span>
+ </ng-template>
+ </ng-container>
+ </ng-container>
- <!-- Retention policies -->
- <ng-container formArrayName="retentionPolicies"
- *ngFor="let retentionPolicy of retentionPolicies.controls; index as i">
- <ng-container [formGroupName]="i">
- <div cdsRow
- class="form-item form-item-append">
- <div cdsCol
- [columnNumbers]="{lg: 8}">
- <cds-number [id]="'retentionInterval' + i"
- [name]="'retentionInterval' + i"
- [formControlName]="'retentionInterval'"
- [label]="'Retention policy'"
- [min]="1"
- [invalid]="snapScheduleForm.controls['retentionPolicies'].controls[i].invalid && snapScheduleForm.controls['retentionPolicies'].dirty"
- [invalidText]="retentionPolicyError"></cds-number>
- </div>
- <div cdsCol
- [columnNumbers]="{lg: 7}">
- <cds-select id="retentionFrequency"
- name="retentionFrequency"
- formControlName="retentionFrequency"
- label="Frequency"
- *ngIf="retentionFrequencies">
- <option *ngFor="let freq of retentionFrequencies"
- [value]="freq[1]">{{ freq[0] }}</option>
-
- </cds-select>
- </div>
- <div cdsCol
- [columnNumbers]="{lg: 1}"
- class="item-action-btn">
- <cds-icon-button kind="danger"
- size="sm"
- (click)="removeRetentionPolicy(i)">
- <svg cdsIcon="trash-can"
- size="32"
- class="cds--btn__icon"></svg>
- </cds-icon-button>
- </div>
- </div>
- <ng-template #retentionPolicyError>
- <span class="invalid-feedback"
- *ngIf="snapScheduleForm.controls['retentionPolicies'].controls[i].invalid"
- i18n>This retention policy already exists for the selected directory.</span>
- </ng-template>
- </ng-container>
- </ng-container>
-
- <div class="form-item">
- <button cdsButton="tertiary"
- type="button"
- (click)="addRetentionPolicy()"
- i18n>
- Add retention policy
- <svg cdsIcon="add"
- size="32"
- class="cds--btn__icon"
- icon></svg>
- </button>
- </div>
- </form>
+ <div class="form-item">
+ <button cdsButton="tertiary"
+ type="button"
+ (click)="addRetentionPolicy()"
+ i18n>
+ Add retention policy
+ <svg cdsIcon="add"
+ size="32"
+ class="cds--btn__icon"
+ icon></svg>
+ </button>
</div>
- <cd-form-button-panel (submitActionEvent)="submit()"
- [form]="snapScheduleForm"
- [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
- [modalForm]="true"></cd-form-button-panel>
- </ng-container>
-</cds-modal>
+ </form>
+</ng-template>
-import { ChangeDetectorRef, Component, Inject, OnInit, Optional } from '@angular/core';
+import {
+ ChangeDetectorRef,
+ Component,
+ Inject,
+ Input,
+ OnChanges,
+ OnInit,
+ Optional,
+ SimpleChanges
+} from '@angular/core';
import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
import { NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap';
import { padStart, uniq } from 'lodash';
SnapshotScheduleFormValue
} from '~/app/shared/models/snapshot-schedule';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { TearsheetStep } from '~/app/shared/models/tearsheet-step';
const VALIDATON_TIMER = 300;
const DEBOUNCE_TIMER = 300;
styleUrls: ['./cephfs-snapshotschedule-form.component.scss'],
standalone: false
})
-export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnInit {
+export class CephfsSnapshotscheduleFormComponent
+ extends CdForm
+ implements OnInit, OnChanges, TearsheetStep
+{
+ @Input() embedded = false;
+ @Input() hideDirectory = false;
+
+ @Input()
+ set fsIdInput(value: number) {
+ if (value) {
+ this.id = value;
+ }
+ }
+
+ @Input()
+ set fsNameInput(value: string) {
+ if (value) {
+ this.fsName = value;
+ }
+ }
+
+ @Input()
+ set schedulePath(value: string) {
+ if (value) {
+ this.path = value;
+ if (this.snapScheduleForm && this.hideDirectory) {
+ this.applyDirectoryPath(value);
+ }
+ }
+ }
+
subvol!: string;
group!: string;
icons = Icons;
snapScheduleForm!: CdFormGroup;
+ get formGroup(): CdFormGroup {
+ return this.snapScheduleForm;
+ }
+
action!: string;
resource!: string;
}-${currentDatetime.getUTCDate()}`;
}
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.schedulePath && this.snapScheduleForm && this.hideDirectory && this.path) {
+ this.applyDirectoryPath(this.path);
+ }
+ }
+
ngOnInit(): void {
this.action = this.actionLabels.CREATE;
- this.directoryStore.loadDirectories(this.id, '/', 3);
+ if (this.id) {
+ this.directoryStore.loadDirectories(this.id, '/', 3);
+ }
this.createForm();
+ if (this.hideDirectory && this.path) {
+ this.applyDirectoryPath(this.path);
+ }
this.isEdit ? this.populateForm() : this.loadingReady();
this.snapScheduleForm
.get('directory')
.valueChanges.pipe(
- filter(() => !this.isEdit),
+ filter(() => !this.isEdit && !this.hideDirectory),
debounceTime(DEBOUNCE_TIMER),
tap(() => {
this.isSubvolume = false;
.join('|');
}
+ getScheduleSummary(): string {
+ if (!this.snapScheduleForm) {
+ return '';
+ }
+
+ const values = this.snapScheduleForm.getRawValue() as SnapshotScheduleFormValue;
+ const schedule = this.parseSchedule(values?.repeatInterval, values?.repeatFrequency);
+ const retention = this.parseRetentionPolicies(values?.retentionPolicies);
+ const parts = [$localize`Every ${schedule}`];
+
+ if (retention) {
+ parts.push($localize`Retention: ${retention}`);
+ }
+
+ return parts.join(' · ');
+ }
+
+ buildCreatePayload(targetPath?: string): Record<string, unknown> {
+ const values = this.snapScheduleForm.getRawValue() as SnapshotScheduleFormValue;
+ const path = targetPath ?? values.directory;
+ const snapScheduleObj: Record<string, unknown> = {
+ fs: this.fsName,
+ path,
+ snap_schedule: this.parseSchedule(values?.repeatInterval, values?.repeatFrequency),
+ start: new Date(values?.startDate.replace(/\//g, '-').replace(' ', 'T'))
+ .toISOString()
+ .slice(0, 19)
+ };
+
+ const retentionPoliciesValues = this.parseRetentionPolicies(values?.retentionPolicies);
+ if (retentionPoliciesValues) {
+ snapScheduleObj['retention_policy'] = retentionPoliciesValues;
+ }
+
+ if (this.isSubvolume) {
+ snapScheduleObj['subvol'] = this.subvolume;
+ }
+
+ if (this.isSubvolume && !this.isDefaultSubvolumeGroup) {
+ snapScheduleObj['group'] = this.subvolumeGroup;
+ }
+
+ return snapScheduleObj;
+ }
+
submit() {
this.validateSchedule()(this.snapScheduleForm).subscribe({
next: () => {
return;
}
- const values = this.snapScheduleForm.value as SnapshotScheduleFormValue;
-
if (this.isEdit) {
const retentionPoliciesToAdd = (this.snapScheduleForm.get(
'retentionPolicies'
}
});
} else {
- const snapScheduleObj = {
- fs: this.fsName,
- path: values.directory,
- snap_schedule: this.parseSchedule(values?.repeatInterval, values?.repeatFrequency),
- start: new Date(values?.startDate.replace(/\//g, '-').replace(' ', 'T'))
- .toISOString()
- .slice(0, 19)
- };
-
- const retentionPoliciesValues = this.parseRetentionPolicies(values?.retentionPolicies);
-
- if (retentionPoliciesValues) {
- snapScheduleObj['retention_policy'] = retentionPoliciesValues;
- }
-
- if (this.isSubvolume) {
- snapScheduleObj['subvol'] = this.subvolume;
- }
-
- if (this.isSubvolume && !this.isDefaultSubvolumeGroup) {
- snapScheduleObj['group'] = this.subvolumeGroup;
- }
+ const snapScheduleObj = this.buildCreatePayload();
this.taskWrapper
.wrapTaskAroundCall({
task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.CREATE, {
const directory = frm.get('directory');
const repeatFrequency = frm.get('repeatFrequency');
const repeatInterval = frm.get('repeatInterval');
+ const directoryPath = this.hideDirectory
+ ? directory?.getRawValue?.() ?? directory?.value
+ : directory?.value;
if (this.isEdit) {
return of(null);
switchMap(() =>
this.snapScheduleService
.checkScheduleExists(
- directory?.value,
+ directoryPath,
this.fsName,
repeatInterval?.value,
repeatFrequency?.value,
return (frm.get(frmArrayName) as FormArray)?.controls?.[idx]?.get?.(ctrl);
}
+ private applyDirectoryPath(path: string): void {
+ const directoryControl = this.snapScheduleForm.get('directory');
+ directoryControl.setValue(path);
+ directoryControl.disable();
+ this.subvolumeGroup = path?.split?.('/')?.[2];
+ this.subvolume = path?.split?.('/')?.[3];
+
+ if (!this.subvolume || !this.subvolumeGroup || !this.fsName) {
+ return;
+ }
+
+ this.subvolumeService
+ .exists(
+ this.subvolume,
+ this.fsName,
+ this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP ? '' : this.subvolumeGroup
+ )
+ .subscribe((exists: boolean) => {
+ this.isSubvolume = exists;
+ this.isDefaultSubvolumeGroup = exists && this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP;
+ });
+ }
+
validateRetention() {
return (frm: FormGroup) => {
return timer(VALIDATON_TIMER).pipe(
}
return this.snapScheduleService
.checkRetentionPolicyExists(
- frm.get('directory').value,
+ frm.get('directory').getRawValue?.() ?? frm.get('directory').value,
this.fsName,
retentionList,
this.retentionPoliciesToRemove?.map?.((rp) => rp.retentionFrequency) || [],
import { CephfsMirroringErrorComponent } from './cephfs-mirroring-error/cephfs-mirroring-error.component';
import { CephfsAddMirroringPathComponent } from './cephfs-add-mirroring-path/cephfs-add-mirroring-path.component';
import { MirroringPathsStepComponent } from './cephfs-add-mirroring-path/mirroring-paths-step/mirroring-paths-step.component';
+import { MirroringReviewStepComponent } from './cephfs-add-mirroring-path/mirroring-review-step/mirroring-review-step.component';
import { CephfsMirroringFsTabsComponent } from './cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component';
import { CephfsMirroringFsOverviewComponent } from './cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component';
import { CephfsMirroringFsMirrorPathsComponent } from './cephfs-mirroring-fs-mirror-paths/cephfs-mirroring-fs-mirror-paths.component';
CephfsDownloadTokenComponent,
CephfsSetupMirroringComponent,
CephfsAddMirroringPathComponent,
- MirroringPathsStepComponent
+ MirroringPathsStepComponent,
+ MirroringReviewStepComponent
],
providers: [provideCharts(withDefaultRegisterables())],
schemas: [CUSTOM_ELEMENTS_SCHEMA]