From: Pedro Gonzalez Gomez Date: Wed, 1 Jul 2026 11:34:35 +0000 (+0200) Subject: mgr/dashboard: cephs add step 2 & 3 schedule form and summary in add mirror path... X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=0ffc20f9a96119688b91fffafbaf57ddfa4da75b;p=ceph.git mgr/dashboard: cephs add step 2 & 3 schedule form and summary in add mirror path wizard Signed-off-by: Pedro Gonzalez Gomez --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/cephfs-add-mirroring-path.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/cephfs-add-mirroring-path.component.html index 5e6c207fdc6..012b94f5bfc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/cephfs-add-mirroring-path.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/cephfs-add-mirroring-path.component.html @@ -19,6 +19,27 @@ [fsId]="fsId"> - - + +

Snapshot schedule

+

Define when snapshots are taken for the mirrored path(s).

+ + +
+ + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/cephfs-add-mirroring-path.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/cephfs-add-mirroring-path.component.spec.ts index b1d0ae45c37..c86bfb0aef1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/cephfs-add-mirroring-path.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/cephfs-add-mirroring-path.component.spec.ts @@ -6,8 +6,10 @@ import { observeOn } from 'rxjs/operators'; 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; @@ -18,6 +20,14 @@ describe('CephfsAddMirroringPathComponent', () => { addMirrorDirectory: jest.fn() }; + const snapshotScheduleServiceMock = { + create: jest.fn() + }; + + const taskWrapperMock = { + wrapTaskAroundCall: jest.fn(({ call }) => call) + }; + const notificationServiceMock = { show: jest.fn() }; @@ -42,6 +52,8 @@ describe('CephfsAddMirroringPathComponent', () => { useValue: { navigate: routerNavigateSpy } }, { provide: CephfsService, useValue: cephfsServiceMock }, + { provide: CephfsSnapshotScheduleService, useValue: snapshotScheduleServiceMock }, + { provide: TaskWrapperService, useValue: taskWrapperMock }, { provide: NotificationService, useValue: notificationServiceMock } ], schemas: [NO_ERRORS_SCHEMA] @@ -74,7 +86,7 @@ describe('CephfsAddMirroringPathComponent', () => { }); 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(), @@ -87,8 +99,7 @@ describe('CephfsAddMirroringPathComponent', () => { 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); @@ -107,6 +118,10 @@ describe('CephfsAddMirroringPathComponent', () => { 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(() => { @@ -234,6 +249,8 @@ describe('CephfsAddMirroringPathComponent', () => { useValue: { navigate: routerNavigateSpy } }, { provide: CephfsService, useValue: cephfsServiceMock }, + { provide: CephfsSnapshotScheduleService, useValue: snapshotScheduleServiceMock }, + { provide: TaskWrapperService, useValue: taskWrapperMock }, { provide: NotificationService, useValue: notificationServiceMock } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/cephfs-add-mirroring-path.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/cephfs-add-mirroring-path.component.ts index e1b99e46bf2..d119693a900 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/cephfs-add-mirroring-path.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/cephfs-add-mirroring-path.component.ts @@ -6,11 +6,16 @@ import { from, of } from 'rxjs'; 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'; @@ -22,10 +27,13 @@ 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); @@ -40,6 +48,10 @@ export class CephfsAddMirroringPathComponent implements OnInit { ]; 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') ?? ''; @@ -56,12 +68,6 @@ export class CephfsAddMirroringPathComponent implements OnInit { return; } - pathsStep.formGroup.markAllAsTouched(); - pathsStep.formGroup.updateValueAndValidity(); - if (pathsStep.formGroup.invalid) { - return; - } - this.isSubmitLoading = true; pathsStep @@ -77,7 +83,12 @@ export class CephfsAddMirroringPathComponent implements OnInit { skippedByServer: [], succeeded: [] }); - return of([] as (string | null)[]); + return of({ + failed: [], + alreadyMirrored, + skippedByServer: [], + succeeded: [] + }); } const skippedByServer: string[] = []; @@ -109,15 +120,13 @@ export class CephfsAddMirroringPathComponent implements OnInit { ); }), 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(() => { @@ -125,14 +134,45 @@ export class CephfsAddMirroringPathComponent implements OnInit { }), 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); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/mirroring-review-step/mirroring-review-step.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/mirroring-review-step/mirroring-review-step.component.html new file mode 100644 index 00000000000..3319d3c711b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/mirroring-review-step/mirroring-review-step.component.html @@ -0,0 +1,31 @@ +

Review

+

Confirm the paths and snapshot schedule before adding the mirror path(s).

+ +
+
+
Filesystem
+
{{ fsName }}
+
+ +
+
Paths
+ @if (pathsToAdd.length) { + @for (path of pathsToAdd; track path) { +
{{ path }}
+ } + } @else { +
—
+ } +
+ +
+
Snapshot schedule
+
{{ scheduleSummary || '—' }}
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/mirroring-review-step/mirroring-review-step.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/mirroring-review-step/mirroring-review-step.component.ts new file mode 100644 index 00000000000..0028f6a7442 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-add-mirroring-path/mirroring-review-step/mirroring-review-step.component.ts @@ -0,0 +1,31 @@ +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() ?? ''; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.html index f1a2dcb6b4b..b8af71bef90 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.html @@ -1,165 +1,177 @@ - - -

- {{ action | titlecase }} {{ resource | upperFirst }} -

-
+@if (!embedded) { + + +

+ {{ action | titlecase }} {{ resource | upperFirst }} +

+
+ +
+ +
+ +
+
+} @else { -
-
- -
- - Directory (required) - - - - This field is required. - A snapshot schedule for this path already exists. - -
+ + +} - - + + + @if (!hideDirectory) { + +
+ + Directory (required) + + + This field is required. + A snapshot schedule for this path already exists. + +
+ } - -
-
- + + This field is required. + + +
+
+ +
+
+ + + + + This schedule already exists for the selected directory. + + + This field is required. + Choose a value greater than 0. + +
+
+ + + + +
+
+ + [invalid]="snapScheduleForm.controls['retentionPolicies'].controls[i].invalid && snapScheduleForm.controls['retentionPolicies'].dirty" + [invalidText]="retentionPolicyError">
-
- + - + *ngIf="retentionFrequencies"> + + - - This schedule already exists for the selected directory. - - - This field is required. - Choose a value greater than 0. - +
+
+ + +
+ + This retention policy already exists for the selected directory. + +
+
- - - -
-
- -
-
- - - - -
-
- - - -
-
- - This retention policy already exists for the selected directory. - -
-
- -
- -
- +
+
- - - + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.ts index 91a6deb95d8..9e2ee920cd4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.ts @@ -1,4 +1,13 @@ -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'; @@ -33,6 +42,7 @@ import { 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; @@ -43,7 +53,37 @@ 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; @@ -59,6 +99,10 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni snapScheduleForm!: CdFormGroup; + get formGroup(): CdFormGroup { + return this.snapScheduleForm; + } + action!: string; resource!: string; @@ -90,16 +134,27 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni }-${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; @@ -273,6 +328,51 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni .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 { + const values = this.snapScheduleForm.getRawValue() as SnapshotScheduleFormValue; + const path = targetPath ?? values.directory; + const snapScheduleObj: Record = { + 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: () => { @@ -281,8 +381,6 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni return; } - const values = this.snapScheduleForm.value as SnapshotScheduleFormValue; - if (this.isEdit) { const retentionPoliciesToAdd = (this.snapScheduleForm.get( 'retentionPolicies' @@ -321,28 +419,7 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni } }); } 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, { @@ -368,6 +445,9 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni 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); @@ -377,7 +457,7 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni switchMap(() => this.snapScheduleService .checkScheduleExists( - directory?.value, + directoryPath, this.fsName, repeatInterval?.value, repeatFrequency?.value, @@ -403,6 +483,29 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni 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( @@ -425,7 +528,7 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni } 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) || [], diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts index 155669bc73e..9d15119bf28 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts @@ -35,6 +35,7 @@ import { CephfsMirroringListComponent } from './cephfs-mirroring-list/cephfs-mir 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'; @@ -155,7 +156,8 @@ import FolderIcon16 from '@carbon/icons/es/folder/16'; CephfsDownloadTokenComponent, CephfsSetupMirroringComponent, CephfsAddMirroringPathComponent, - MirroringPathsStepComponent + MirroringPathsStepComponent, + MirroringReviewStepComponent ], providers: [provideCharts(withDefaultRegisterables())], schemas: [CUSTOM_ELEMENTS_SCHEMA]