From: Pedro Gonzalez Gomez Date: Wed, 28 Jan 2026 21:23:25 +0000 (+0100) Subject: mgr/dashboard: add CephFS Mirroring enablement page X-Git-Tag: testing/wip-vshankar-testing-20260218.045142~5^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=eb3658a425a86b5826c49de51d934d86df6d00ba;p=ceph-ci.git mgr/dashboard: add CephFS Mirroring enablement page Fixes: https://tracker.ceph.com/issues/74633 Signed-off-by: Pedro Gonzalez Gomez --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 32d910a031e..9360cbdb302 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -35,7 +35,11 @@ import { BlankLayoutComponent } from './core/layouts/blank-layout/blank-layout.c import { LoginLayoutComponent } from './core/layouts/login-layout/login-layout.component'; import { WorkbenchLayoutComponent } from './core/layouts/workbench-layout/workbench-layout.component'; import { ApiDocsComponent } from './core/navigation/api-docs/api-docs.component'; -import { ActionLabels, URLVerbs } from './shared/constants/app.constants'; +import { + ActionLabels, + CEPHFS_MIRRORING_PAGE_HEADER, + URLVerbs +} from './shared/constants/app.constants'; import { CrudFormComponent } from './shared/forms/crud-form/crud-form.component'; import { CRUDTableComponent } from './shared/datatable/crud-table/crud-table.component'; import { BreadcrumbsResolver, IBreadcrumb } from './shared/models/breadcrumbs'; @@ -62,6 +66,7 @@ import { MultiClusterFormComponent } from './ceph/cluster/multi-cluster/multi-cl import { CephfsMirroringListComponent } from './ceph/cephfs/cephfs-mirroring-list/cephfs-mirroring-list.component'; import { NotificationsPageComponent } from './core/navigation/notification-panel/notifications-page/notifications-page.component'; import { CephfsMirroringWizardComponent } from './ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component'; +import { CephfsMirroringErrorComponent } from './ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component'; @Injectable() export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver { @@ -107,6 +112,15 @@ const routes: Routes = [ children: [ { path: 'overview', component: DashboardComponent }, { path: 'error', component: ErrorComponent }, + { + path: 'cephfs/mirroring/error', + component: CephfsMirroringErrorComponent, + data: { + breadcrumbs: 'File/Mirroring', + pageHeader: CEPHFS_MIRRORING_PAGE_HEADER + } + }, + // Cluster { path: 'notifications', @@ -427,8 +441,18 @@ const routes: Routes = [ }, { path: 'mirroring', + canActivate: [ModuleStatusGuardService], component: CephfsMirroringListComponent, - data: { breadcrumbs: 'File/Mirroring' } + data: { + moduleStatusGuardConfig: { + uiApiPath: 'cephfs/mirror', + redirectTo: 'cephfs/mirroring/error', + module_name: 'mirroring', + navigate_to: 'File/Mirroring' + }, + breadcrumbs: 'File/Mirroring', + pageHeader: CEPHFS_MIRRORING_PAGE_HEADER + } }, { path: `mirroring/${URLVerbs.CREATE}`, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.html new file mode 100644 index 00000000000..5141bb2460d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.html @@ -0,0 +1,33 @@ + + +

Enable CephFS Mirroring

+

Turn on CephFS Mirroring to start creating mirror links and synchronizing data across clusters. After enabling, you can add mirror links.

+ +
+
+
+
+ no-mirror-links +
+
+
+
+

No CephFS mirror links available

+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.scss new file mode 100644 index 00000000000..0f95137319a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.scss @@ -0,0 +1,16 @@ +// Stack title above description when notification is full-width (Carbon lays them +// in a row by default). +cds-inline-notification.full-width { + max-inline-size: 100%; + + [class*='inline-notification__text-wrapper'] { + display: flex; + flex-direction: column; + align-items: flex-start; + } + + [class*='inline-notification__title'], + [class*='inline-notification__subtitle'] { + display: block; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.spec.ts new file mode 100644 index 00000000000..375207376c7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.spec.ts @@ -0,0 +1,61 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; + +import { CephfsMirroringErrorComponent } from './cephfs-mirroring-error.component'; +import { MgrModuleService } from '~/app/shared/api/mgr-module.service'; +import { SharedModule } from '~/app/shared/shared.module'; +import { RouterTestingModule } from '@angular/router/testing'; + +describe('CephfsMirroringErrorComponent', () => { + let component: CephfsMirroringErrorComponent; + let fixture: ComponentFixture; + + const routerMock = { + events: of({}), + onSameUrlNavigation: 'reload' as const, + navigate: jest.fn() + }; + + const mgrModuleServiceMock = { + updateModuleState: jest.fn(), + updateCompleted$: { subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }) } + }; + + beforeEach(async () => { + jest.clearAllMocks(); + + await TestBed.configureTestingModule({ + declarations: [CephfsMirroringErrorComponent], + imports: [SharedModule, RouterTestingModule], + providers: [ + { provide: Router, useValue: routerMock }, + { provide: MgrModuleService, useValue: mgrModuleServiceMock } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + + fixture = TestBed.createComponent(CephfsMirroringErrorComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should call mgrModuleService.updateModuleState when enableModule is called', () => { + fixture.detectChanges(); + component.enableModule(); + expect(mgrModuleServiceMock.updateModuleState).toHaveBeenCalledWith( + 'mirroring', + false, + null, + 'cephfs/mirroring', + expect.any(String), + false, + expect.any(String) + ); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.ts new file mode 100644 index 00000000000..c21b1d2651c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.ts @@ -0,0 +1,25 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { MgrModuleService } from '~/app/shared/api/mgr-module.service'; + +@Component({ + selector: 'cd-cephfs-mirroring-error', + templateUrl: './cephfs-mirroring-error.component.html', + styleUrls: ['./cephfs-mirroring-error.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class CephfsMirroringErrorComponent { + constructor(private mgrModuleService: MgrModuleService) {} + + enableModule(): void { + this.mgrModuleService.updateModuleState( + 'mirroring', + false, + null, + 'cephfs/mirroring', + $localize`CephFS Mirroring module enabled`, + false, + $localize`Enabling CephFS Mirroring. Reconnecting, please wait ...` + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-list/cephfs-mirroring-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-list/cephfs-mirroring-list.component.html index 54f6d9823e3..2ba0db47e33 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-list/cephfs-mirroring-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-list/cephfs-mirroring-list.component.html @@ -1,12 +1,4 @@ - - - - - +@if (daemonStatus$ | async; as daemonStatus) { - - + + - +} 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 47ec4f9b355..b6461be67ff 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 @@ -31,6 +31,8 @@ import { CephfsSubvolumeSnapshotsFormComponent } from './cephfs-subvolume-snapsh import { CephfsSnapshotscheduleFormComponent } from './cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component'; import { CephfsMountDetailsComponent } from './cephfs-mount-details/cephfs-mount-details.component'; import { CephfsAuthModalComponent } from './cephfs-auth-modal/cephfs-auth-modal.component'; +import { CephfsMirroringListComponent } from './cephfs-mirroring-list/cephfs-mirroring-list.component'; +import { CephfsMirroringErrorComponent } from './cephfs-mirroring-error/cephfs-mirroring-error.component'; import { ButtonModule, CheckboxModule, @@ -47,16 +49,17 @@ import { PlaceholderModule, SelectModule, TimePickerModule, + TilesModule, TreeviewModule, TabsModule, - RadioModule + RadioModule, + NotificationModule } from 'carbon-components-angular'; import AddIcon from '@carbon/icons/es/add/32'; import LaunchIcon from '@carbon/icons/es/launch/32'; import Close from '@carbon/icons/es/close/32'; import Trash from '@carbon/icons/es/trash-can/32'; -import { CephfsMirroringListComponent } from './cephfs-mirroring-list/cephfs-mirroring-list.component'; import { CephfsMirroringWizardComponent } from './cephfs-mirroring-wizard/cephfs-mirroring-wizard.component'; import { CephfsFilesystemSelectorComponent } from './cephfs-filesystem-selector/cephfs-filesystem-selector.component'; @@ -91,7 +94,9 @@ import { CephfsFilesystemSelectorComponent } from './cephfs-filesystem-selector/ IconModule, BaseChartDirective, TabsModule, - RadioModule + RadioModule, + TilesModule, + NotificationModule ], declarations: [ CephfsDetailComponent, @@ -114,7 +119,8 @@ import { CephfsFilesystemSelectorComponent } from './cephfs-filesystem-selector/ CephfsAuthModalComponent, CephfsMirroringListComponent, CephfsMirroringWizardComponent, - CephfsFilesystemSelectorComponent + CephfsFilesystemSelectorComponent, + CephfsMirroringErrorComponent ], providers: [provideCharts(withDefaultRegisterables())] }) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html index dc906d4ee69..458335c77cf 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html @@ -15,6 +15,12 @@ + @if(pageHeaderTitle) { + + + } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts index a162e5f067d..3cc7faf0eb6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts @@ -1,7 +1,8 @@ import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; +import { ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; import { Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; import { MultiClusterService } from '~/app/shared/api/multi-cluster.service'; import { Permissions } from '~/app/shared/models/permissions'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; @@ -24,6 +25,9 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy { notifications: string[] = []; private subs = new Subscription(); permissions: Permissions; + pageHeaderTitle: string | null = null; + pageHeaderDescription: string | null = null; + @HostBinding('class') get class(): string { return 'top-notification-' + this.notifications.length; } @@ -65,7 +69,27 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy { }) ); this.faviconService.init(); + + this.updatePageHeaderFromRoute(); + this.subs.add( + this.router.events + .pipe(filter((e) => e instanceof NavigationEnd)) + .subscribe(() => this.updatePageHeaderFromRoute()) + ); } + + private updatePageHeaderFromRoute(): void { + let route: ActivatedRouteSnapshot | null = this.router.routerState.snapshot.root; + while (route?.firstChild) { + route = route.firstChild; + } + const pageHeader = route?.routeConfig?.data?.['pageHeader'] as + | { title?: string; description?: string } + | undefined; + this.pageHeaderTitle = pageHeader?.title ?? null; + this.pageHeaderDescription = pageHeader?.description ?? null; + } + showTopNotification(name: string, isDisplayed: boolean) { if (isDisplayed) { if (!this.notifications.includes(name)) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts index 7de826aa2e3..e7b16fb3b1f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts @@ -386,3 +386,8 @@ export const SSL_CIPHERS = [ export const USER = 'user'; export const VERSION_PREFIX = 'ceph version'; + +export const CEPHFS_MIRRORING_PAGE_HEADER = { + title: $localize`CephFS Mirroring`, + description: $localize`Centralised view of all CephFS Mirroring relationships.` +}; diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/empty-state.png b/src/pybind/mgr/dashboard/frontend/src/assets/empty-state.png new file mode 100644 index 00000000000..38ed1e0b6ae Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/assets/empty-state.png differ diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/_carbon-defaults.scss b/src/pybind/mgr/dashboard/frontend/src/styles/_carbon-defaults.scss index ba5b2980193..44cf6869a38 100644 --- a/src/pybind/mgr/dashboard/frontend/src/styles/_carbon-defaults.scss +++ b/src/pybind/mgr/dashboard/frontend/src/styles/_carbon-defaults.scss @@ -169,6 +169,10 @@ Forms padding-inline: 0; } +.padding-inline-0 { + padding-inline: 0; +} + /****************************************** Breadcrumbs ******************************************/