]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add CephFS Mirroring enablement page 67112/head
authorPedro Gonzalez Gomez <pegonzal@ibm.com>
Wed, 28 Jan 2026 21:23:25 +0000 (22:23 +0100)
committerPedro Gonzalez Gomez <pegonzal@ibm.com>
Fri, 13 Feb 2026 08:48:24 +0000 (09:48 +0100)
Fixes: https://tracker.ceph.com/issues/74633
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@ibm.com>
12 files changed:
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-list/cephfs-mirroring-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts
src/pybind/mgr/dashboard/frontend/src/assets/empty-state.png [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/styles/_carbon-defaults.scss

index 32d910a031ec0d2b1fd5dd367487c8261adfff60..9360cbdb302fbadca17c5771f396b5ea528a71a5 100644 (file)
@@ -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 (file)
index 0000000..5141bb2
--- /dev/null
@@ -0,0 +1,33 @@
+<cds-inline-notification
+  [notificationObj]="{
+                      type: 'warning',
+                      title: 'CephFS Mirroring module is not enabled',
+                      message: 'To create mirror links and configure replication, the CephFS Mirroring module must be enabled on this cluster.',
+                      lowContrast: true,
+                      showClose: false
+                    }"
+  class="mt-2 mb-2 full-width padding-inline-0"
+  i18n></cds-inline-notification>
+<cds-tile>
+  <p class="cds--type-heading-compact-01"
+     i18n>Enable CephFS Mirroring</p>
+  <p class="cds--type-body-compact-01"
+     i18n>Turn on CephFS Mirroring to start creating mirror links and synchronizing data across clusters. After enabling, you can add mirror links.</p>
+  <button cdsButton="primary"
+          (click)="enableModule()"
+          i18n>Enable CephFS Mirroring</button>
+</cds-tile>
+<div cdsGrid
+     class="mt-5">
+  <div cdsRow>
+    <div cdsCol>
+      <img src="assets/empty-state.png"
+           alt="no-mirror-links" />
+    </div>
+  </div>
+  <div cdsRow>
+    <div cdsCol>
+      <p class="cds--body-compact-02">No CephFS mirror links available</p>
+    </div>
+  </div>
+</div>
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 (file)
index 0000000..0f95137
--- /dev/null
@@ -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 (file)
index 0000000..3752073
--- /dev/null
@@ -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<CephfsMirroringErrorComponent>;
+
+  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 (file)
index 0000000..c21b1d2
--- /dev/null
@@ -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 ...`
+    );
+  }
+}
index 54f6d9823e36ba9cef9f41cf8be68c29887cb204..2ba0db47e3344731e9a43c4d3c3ebf3e58140523 100644 (file)
@@ -1,12 +1,4 @@
-<cd-page-header
-  i18n-title
-  title="CephFS Mirroring"
-  i18n-description
-  description="Centralised view of all CephFS Mirroring relationships.">
-</cd-page-header>
-
-<ng-container *ngIf="daemonStatus$ | async as daemonStatus">
-
+@if (daemonStatus$ | async; as daemonStatus) {
   <cd-table
     #table
     [data]="daemonStatus"
     selectionType="single"
     (updateSelection)="updateSelection($event)"
     (fetchData)="loadDaemonStatus()">
-  <cd-table-actions class="table-actions"
-                    [permission]="permission"
-                    [selection]="selection"
-                    [tableActions]="tableActions">
-  </cd-table-actions>
+    <cd-table-actions class="table-actions"
+                      [permission]="permission"
+                      [selection]="selection"
+                      [tableActions]="tableActions">
+    </cd-table-actions>
   </cd-table>
-</ng-container>
+}
index 47ec4f9b355294e0d0122534e8de8ada9c6dd322..b6461be67ff6e0ea8f6cbcdde648606042925a12 100644 (file)
@@ -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())]
 })
index dc906d4ee69295ea2adee29f28560ce729624140..458335c77cf0a9aeec0f9952316ab82511ff8d13 100644 (file)
       <div class="breadcrumbs--padding">
         <cd-breadcrumbs></cd-breadcrumbs>
       </div>
+      @if(pageHeaderTitle) {
+      <cd-page-header
+                      [title]="pageHeaderTitle"
+                      [description]="pageHeaderDescription">
+      </cd-page-header>
+      }
       <router-outlet></router-outlet>
       <cds-placeholder></cds-placeholder>
     </div>
index a162e5f067d76d398c618a5f4372d81cd1d8babc..3cc7faf0eb633b49c6fde98597ac24a0f99e824c 100644 (file)
@@ -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)) {
index 7de826aa2e3a2d66c0620264fcce20e8be957136..e7b16fb3b1fd2b1ad36739f43c2188dc6b1f8eb9 100644 (file)
@@ -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 (file)
index 0000000..38ed1e0
Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/assets/empty-state.png differ
index ba5b2980193b623e266f1ba5e04c24b70f0111eb..44cf6869a38a75f704e03154afea4331e7d82114 100644 (file)
@@ -169,6 +169,10 @@ Forms
   padding-inline: 0;
 }
 
+.padding-inline-0 {
+  padding-inline: 0;
+}
+
 /******************************************
 Breadcrumbs
 ******************************************/