]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add cephfs mirroring routes and tabs for mirrored filesystem 69471/head
authorPedro Gonzalez Gomez <pegonzal@ibm.com>
Mon, 15 Jun 2026 10:01:10 +0000 (12:01 +0200)
committerPedro Gonzalez Gomez <pegonzal@ibm.com>
Wed, 24 Jun 2026 17:21:31 +0000 (19:21 +0200)
Fixes: https://tracker.ceph.com/issues/77404
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@ibm.com>
23 files changed:
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-mirror-paths/cephfs-mirroring-fs-mirror-paths.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-mirror-paths/cephfs-mirroring-fs-mirror-paths.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-mirror-paths/cephfs-mirroring-fs-mirror-paths.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-schedules/cephfs-mirroring-fs-schedules.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-schedules/cephfs-mirroring-fs-schedules.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-schedules/cephfs-mirroring-fs-schedules.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-breadcrumb.resolver.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-list/cephfs-mirroring-list.component.ts
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/components/page-header/page-header.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/page-header/page-header.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/page-header/page-header.component.ts

index 0a18e3caabcd217a0a82212b0ccf3fa8014ade8b..450f048b62755f9bb1d01defc7113db25cfa72bd 100644 (file)
@@ -66,6 +66,11 @@ import { SmbUsersgroupsListComponent } from './ceph/smb/smb-usersgroups-list/smb
 import { SmbOverviewComponent } from './ceph/smb/smb-overview/smb-overview.component';
 import { MultiClusterFormComponent } from './ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component';
 import { CephfsMirroringListComponent } from './ceph/cephfs/cephfs-mirroring-list/cephfs-mirroring-list.component';
+import { CephfsMirroringFsTabsComponent } from './ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component';
+import { CephfsMirroringFsOverviewComponent } from './ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component';
+import { CephfsMirroringFsMirrorPathsComponent } from './ceph/cephfs/cephfs-mirroring-fs-mirror-paths/cephfs-mirroring-fs-mirror-paths.component';
+import { CephfsMirroringFsSchedulesComponent } from './ceph/cephfs/cephfs-mirroring-fs-schedules/cephfs-mirroring-fs-schedules.component';
+import { CephfsMirroringFsBreadcrumbResolver } from './ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-breadcrumb.resolver';
 import { NotificationsPageComponent } from './core/navigation/notification-panel/notifications-page/notifications-page.component';
 import { CephfsMirroringErrorComponent } from './ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component';
 import { OverviewComponent } from './ceph/overview/overview.component';
@@ -476,17 +481,48 @@ const routes: Routes = [
           {
             path: 'mirroring',
             canActivate: [ModuleStatusGuardService],
-            component: CephfsMirroringListComponent,
             data: {
               moduleStatusGuardConfig: {
                 uiApiPath: 'cephfs/mirror',
                 redirectTo: 'cephfs/mirroring/error',
                 module_name: 'mirroring',
                 navigate_to: 'File/Mirroring'
+              }
+            },
+            children: [
+              {
+                path: '',
+                component: CephfsMirroringListComponent,
+                data: {
+                  breadcrumbs: 'File/Mirroring',
+                  pageHeader: CEPHFS_MIRRORING_PAGE_HEADER
+                }
               },
-              breadcrumbs: 'File/Mirroring',
-              pageHeader: CEPHFS_MIRRORING_PAGE_HEADER
-            }
+              {
+                path: ':fsName',
+                component: CephfsMirroringFsTabsComponent,
+                data: {
+                  breadcrumbs: CephfsMirroringFsBreadcrumbResolver,
+                  // PageHeader included within the CephfsMirroringFsTabsComponent
+                  pageHeaderHidden: true
+                },
+                children: [
+                  { path: '', redirectTo: 'overview', pathMatch: 'full' },
+                  {
+                    path: 'overview',
+                    component: CephfsMirroringFsOverviewComponent
+                  },
+                  {
+                    path: 'mirror-paths',
+                    component: CephfsMirroringFsMirrorPathsComponent
+                  },
+                  {
+                    path: 'schedules',
+                    component: CephfsMirroringFsSchedulesComponent
+                  }
+                ]
+              }
+            ]
           },
           {
             path: 'nfs',
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-mirror-paths/cephfs-mirroring-fs-mirror-paths.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-mirror-paths/cephfs-mirroring-fs-mirror-paths.component.html
new file mode 100644 (file)
index 0000000..f7d6e4f
--- /dev/null
@@ -0,0 +1,6 @@
+<main class="cephfs-mirroring-fs-mirror-paths">
+  <cds-tile>
+    <p class="cds--type-body-01"
+       i18n>Mirror paths content will be available here.</p>
+  </cds-tile>
+</main>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-mirror-paths/cephfs-mirroring-fs-mirror-paths.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-mirror-paths/cephfs-mirroring-fs-mirror-paths.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-mirror-paths/cephfs-mirroring-fs-mirror-paths.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-mirror-paths/cephfs-mirroring-fs-mirror-paths.component.ts
new file mode 100644 (file)
index 0000000..b069302
--- /dev/null
@@ -0,0 +1,10 @@
+import { Component, ViewEncapsulation } from '@angular/core';
+
+@Component({
+  selector: 'cd-cephfs-mirroring-fs-mirror-paths',
+  templateUrl: './cephfs-mirroring-fs-mirror-paths.component.html',
+  styleUrls: ['./cephfs-mirroring-fs-mirror-paths.component.scss'],
+  standalone: false,
+  encapsulation: ViewEncapsulation.None
+})
+export class CephfsMirroringFsMirrorPathsComponent {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.html
new file mode 100644 (file)
index 0000000..8bdd4bb
--- /dev/null
@@ -0,0 +1,6 @@
+<main class="cephfs-mirroring-fs-schedules">
+  <cds-tile>
+    <p class="cds--type-body-01"
+       i18n>Overview content will be available here.</p>
+  </cds-tile>
+</main>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.spec.ts
new file mode 100644 (file)
index 0000000..2312db9
--- /dev/null
@@ -0,0 +1,41 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, convertToParamMap } from '@angular/router';
+import { of } from 'rxjs';
+
+import { CephfsMirroringFsOverviewComponent } from './cephfs-mirroring-fs-overview.component';
+
+describe('CephfsMirroringFsOverviewComponent', () => {
+  let component: CephfsMirroringFsOverviewComponent;
+  let fixture: ComponentFixture<CephfsMirroringFsOverviewComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [CephfsMirroringFsOverviewComponent],
+      providers: [
+        {
+          provide: ActivatedRoute,
+          useValue: {
+            parent: {
+              paramMap: of(convertToParamMap({ fsName: 'myfs' }))
+            }
+          }
+        }
+      ],
+      schemas: [NO_ERRORS_SCHEMA]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(CephfsMirroringFsOverviewComponent);
+    component = fixture.componentInstance;
+
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should read fsName from parent route params', () => {
+    expect(component.fsName).toBe('myfs');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.ts
new file mode 100644 (file)
index 0000000..52e401a
--- /dev/null
@@ -0,0 +1,21 @@
+import { Component, OnInit, ViewEncapsulation } from '@angular/core';
+import { ActivatedRoute, ParamMap } from '@angular/router';
+
+@Component({
+  selector: 'cd-cephfs-mirroring-fs-overview',
+  templateUrl: './cephfs-mirroring-fs-overview.component.html',
+  styleUrls: ['./cephfs-mirroring-fs-overview.component.scss'],
+  standalone: false,
+  encapsulation: ViewEncapsulation.None
+})
+export class CephfsMirroringFsOverviewComponent implements OnInit {
+  fsName = '';
+
+  constructor(private route: ActivatedRoute) {}
+
+  ngOnInit(): void {
+    this.route.parent?.paramMap.subscribe((paramMap: ParamMap) => {
+      this.fsName = paramMap.get('fsName') ?? '';
+    });
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-schedules/cephfs-mirroring-fs-schedules.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-schedules/cephfs-mirroring-fs-schedules.component.html
new file mode 100644 (file)
index 0000000..0990cf5
--- /dev/null
@@ -0,0 +1,6 @@
+<main class="cephfs-mirroring-fs-schedules">
+  <cds-tile>
+    <p class="cds--type-body-01"
+       i18n>Schedules content will be available here.</p>
+  </cds-tile>
+</main>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-schedules/cephfs-mirroring-fs-schedules.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-schedules/cephfs-mirroring-fs-schedules.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-schedules/cephfs-mirroring-fs-schedules.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-schedules/cephfs-mirroring-fs-schedules.component.ts
new file mode 100644 (file)
index 0000000..e52bdc7
--- /dev/null
@@ -0,0 +1,10 @@
+import { Component, ViewEncapsulation } from '@angular/core';
+
+@Component({
+  selector: 'cd-cephfs-mirroring-fs-schedules',
+  templateUrl: './cephfs-mirroring-fs-schedules.component.html',
+  styleUrls: ['./cephfs-mirroring-fs-schedules.component.scss'],
+  standalone: false,
+  encapsulation: ViewEncapsulation.None
+})
+export class CephfsMirroringFsSchedulesComponent {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-breadcrumb.resolver.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-breadcrumb.resolver.ts
new file mode 100644 (file)
index 0000000..750c7fb
--- /dev/null
@@ -0,0 +1,24 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot } from '@angular/router';
+
+import { CEPHFS_MIRRORING_PAGE_HEADER } from '~/app/shared/constants/app.constants';
+import { BreadcrumbsResolver, IBreadcrumb } from '~/app/shared/models/breadcrumbs';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class CephfsMirroringFsBreadcrumbResolver extends BreadcrumbsResolver {
+  resolve(route: ActivatedRouteSnapshot): IBreadcrumb[] {
+    const fsName = route.params?.fsName || '';
+    let decodedFsName = fsName;
+    try {
+      decodedFsName = decodeURIComponent(fsName);
+    } catch {
+      // Fallback to raw value if decoding fails
+    }
+    return [
+      { text: CEPHFS_MIRRORING_PAGE_HEADER.title, path: '/cephfs/mirroring' },
+      { text: decodedFsName, path: null }
+    ];
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.html
new file mode 100644 (file)
index 0000000..f211547
--- /dev/null
@@ -0,0 +1,27 @@
+<cd-page-header [title]="displayFsName"
+               [tabsLayout]="true">
+  <cds-tabs [followFocus]="true"
+            [isNavigation]="true"
+            [cacheActive]="false"
+            class="cds-mb-0">
+    <cds-tab heading="Overview"
+             i18n-heading
+             [active]="activeTab === Tabs.overview"
+             (selected)="onSelected(Tabs.overview)">
+    </cds-tab>
+    <cds-tab heading="Mirror paths"
+             i18n-heading
+             [active]="activeTab === Tabs.mirrorPaths"
+             (selected)="onSelected(Tabs.mirrorPaths)">
+    </cds-tab>
+    <cds-tab heading="Schedules"
+             i18n-heading
+             [active]="activeTab === Tabs.schedules"
+             (selected)="onSelected(Tabs.schedules)">
+    </cds-tab>
+  </cds-tabs>
+</cd-page-header>
+
+<div class="cds-mt-5">
+  <router-outlet></router-outlet>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.spec.ts
new file mode 100644 (file)
index 0000000..d98db77
--- /dev/null
@@ -0,0 +1,41 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, convertToParamMap } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { of } from 'rxjs';
+
+import { CephfsMirroringFsTabsComponent } from './cephfs-mirroring-fs-tabs.component';
+
+describe('CephfsMirroringFsTabsComponent', () => {
+  let component: CephfsMirroringFsTabsComponent;
+  let fixture: ComponentFixture<CephfsMirroringFsTabsComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [RouterTestingModule],
+      declarations: [CephfsMirroringFsTabsComponent],
+      providers: [
+        {
+          provide: ActivatedRoute,
+          useValue: {
+            paramMap: of(convertToParamMap({ fsName: 'testfs' }))
+          }
+        }
+      ],
+      schemas: [NO_ERRORS_SCHEMA]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(CephfsMirroringFsTabsComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should read fsName from route params', () => {
+    expect(component.fsName).toBe('testfs');
+    expect(component.displayFsName).toBe('testfs');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.ts
new file mode 100644 (file)
index 0000000..5abdd2a
--- /dev/null
@@ -0,0 +1,68 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { ActivatedRoute, NavigationEnd, ParamMap, Router } from '@angular/router';
+import { Subscription } from 'rxjs';
+import { filter } from 'rxjs/operators';
+
+enum TABS {
+  overview = 'overview',
+  mirrorPaths = 'mirror-paths',
+  schedules = 'schedules'
+}
+
+@Component({
+  selector: 'cd-cephfs-mirroring-fs-tabs',
+  templateUrl: './cephfs-mirroring-fs-tabs.component.html',
+  styleUrls: ['./cephfs-mirroring-fs-tabs.component.scss'],
+  standalone: false
+})
+export class CephfsMirroringFsTabsComponent implements OnInit, OnDestroy {
+  fsName = '';
+  displayFsName = '';
+  activeTab: TABS = TABS.overview;
+
+  private subs = new Subscription();
+
+  constructor(private route: ActivatedRoute, private router: Router) {}
+
+  ngOnInit(): void {
+    this.subs.add(
+      this.route.paramMap.subscribe((paramMap: ParamMap) => {
+        this.fsName = paramMap.get('fsName') ?? '';
+        this.displayFsName = this.decodeFsName(this.fsName);
+      })
+    );
+    this.updateActiveTab(this.router.url);
+    this.subs.add(
+      this.router.events
+        .pipe(filter((event) => event instanceof NavigationEnd))
+        .subscribe(() => this.updateActiveTab(this.router.url))
+    );
+  }
+
+  ngOnDestroy(): void {
+    this.subs.unsubscribe();
+  }
+
+  onSelected(tab: TABS): void {
+    this.router.navigate(['/cephfs/mirroring', this.fsName, tab]);
+  }
+
+  get Tabs(): typeof TABS {
+    return TABS;
+  }
+
+  private updateActiveTab(url: string): void {
+    this.activeTab = Object.values(TABS).find((tab) => url.includes(`/${tab}`)) || TABS.overview;
+  }
+
+  private decodeFsName(fsName: string): string {
+    if (!fsName) {
+      return '';
+    }
+    try {
+      return decodeURIComponent(fsName);
+    } catch {
+      return fsName;
+    }
+  }
+}
index 355f6dc951bfa4c3dcbaf9b89e90ad755e45043e..02f1cbe55baf7e68a95e9af19306193e4def9762 100644 (file)
@@ -4,6 +4,7 @@ import { catchError, map, switchMap } from 'rxjs/operators';
 
 import { CephfsService } from '~/app/shared/api/cephfs.service';
 import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
 import { Daemon, Filesystem, MirroringRow, Peer } from '~/app/shared/models/cephfs.model';
@@ -34,7 +35,15 @@ export class CephfsMirroringListComponent implements OnInit {
 
   ngOnInit() {
     this.columns = [
-      { name: $localize`Filesystem`, prop: 'local_fs_name', flexGrow: 2 },
+      {
+        name: $localize`Filesystem`,
+        prop: 'local_fs_name',
+        flexGrow: 2,
+        cellTransformation: CellTemplate.redirect,
+        customTemplateConfig: {
+          redirectLink: ['/cephfs/mirroring', '::prop', 'overview']
+        }
+      },
       { name: $localize`Destination cluster`, prop: 'remote_cluster_name', flexGrow: 2 },
       { name: $localize`Mirroring status`, prop: 'mirroring_status', flexGrow: 2 },
       { name: $localize`Bytes replicated`, prop: 'bytes_replicated', flexGrow: 2 },
index a8e17adbefcc2f661e182389fcbdcf8ea6120b4b..f5b0fc1590dfd04de9be3f1a3ddbba0c64220fad 100644 (file)
@@ -33,6 +33,10 @@ import { CephfsMountDetailsComponent } from './cephfs-mount-details/cephfs-mount
 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 { 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';
+import { CephfsMirroringFsSchedulesComponent } from './cephfs-mirroring-fs-schedules/cephfs-mirroring-fs-schedules.component';
 import {
   ButtonModule,
   CheckboxModule,
@@ -128,7 +132,11 @@ import ShareIcon24 from '@carbon/icons/es/share/24';
     CephfsMountDetailsComponent,
     CephfsAuthModalComponent,
     CephfsMirroringListComponent,
-    CephfsMirroringErrorComponent
+    CephfsMirroringErrorComponent,
+    CephfsMirroringFsTabsComponent,
+    CephfsMirroringFsOverviewComponent,
+    CephfsMirroringFsMirrorPathsComponent,
+    CephfsMirroringFsSchedulesComponent
   ],
   providers: [provideCharts(withDefaultRegisterables())]
 })
index da63bd561f2bcf009adc491e1d0170fbd1f58c4c..895e45fcdc15f085ec479785bdf4c5c24d91ddf1 100644 (file)
@@ -19,7 +19,7 @@
       <div class="breadcrumbs--padding">
         <cd-breadcrumbs></cd-breadcrumbs>
       </div>
-      @if(pageHeaderTitle) {
+      @if(pageHeaderTitle && !pageHeaderHidden) {
       <cd-page-header
                       [title]="pageHeaderTitle"
                       [subtitle]="pageHeaderSubtitle"
index 7c149092d1bc08f8f9842e270a28781c52a972d8..466bd1a20b95c4f65975d640b94f61346ce77efe 100644 (file)
@@ -32,6 +32,7 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy {
   pageHeaderTitle: string | null = null;
   pageHeaderSubtitle: string | null = null;
   pageHeaderDescription: string | null = null;
+  pageHeaderHidden = false;
   enabledFeature$: Observable<FeatureTogglesMap>;
 
   @HostBinding('class') get class(): string {
@@ -91,6 +92,34 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy {
     while (route?.firstChild) {
       route = route.firstChild;
     }
+
+    const hiddenRoute = this.findRouteWithData(route, 'pageHeaderHidden');
+    if (hiddenRoute?.routeConfig?.data?.['pageHeaderHidden']) {
+      this.pageHeaderHidden = true;
+      this.pageHeaderTitle = null;
+      this.pageHeaderSubtitle = null;
+      this.pageHeaderDescription = null;
+      return;
+    }
+
+    this.pageHeaderHidden = false;
+
+    const titleFromParamRoute = this.findRouteWithData(route, 'pageHeaderTitleFromParam');
+    const titleFromParam = titleFromParamRoute?.routeConfig?.data?.['pageHeaderTitleFromParam'] as
+      | string
+      | undefined;
+
+    if (titleFromParam && titleFromParamRoute?.params[titleFromParam]) {
+      try {
+        this.pageHeaderTitle = decodeURIComponent(titleFromParamRoute.params[titleFromParam]);
+      } catch {
+        this.pageHeaderTitle = titleFromParamRoute.params[titleFromParam];
+      }
+      this.pageHeaderSubtitle = null;
+      this.pageHeaderDescription = null;
+      return;
+    }
+
     const pageHeader = route?.routeConfig?.data?.['pageHeader'] as
       | { title?: string; subtitle?: string; description?: string }
       | undefined;
@@ -99,6 +128,20 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy {
     this.pageHeaderDescription = pageHeader?.description ?? null;
   }
 
+  private findRouteWithData(
+    route: ActivatedRouteSnapshot | null,
+    key: string
+  ): ActivatedRouteSnapshot | null {
+    let current = route;
+    while (current) {
+      if (current.routeConfig?.data?.[key]) {
+        return current;
+      }
+      current = current.parent;
+    }
+    return null;
+  }
+
   showTopNotification(name: string, isDisplayed: boolean) {
     if (isDisplayed) {
       if (!this.notifications.includes(name)) {
index 069ccd78b0056335ea59c11681dc2cf80b859631..70a6f9102e32eaf408e16b8625b0999ed6ecab71 100644 (file)
@@ -1,13 +1,24 @@
 <header data-testid="page-header">
   <div>
-    <cds-tile class="border-top padding-inline-0">
-      <p class="cds--type-heading-04">{{ title }}</p>
-      @if(subtitle) {
-      <p class="cds--type-heading-03">{{ subtitle }}</p>
-      }
-      @if(description) {
-      <p class="cds--type-body-01">{{ description }}</p>
-      }
+    <cds-tile class="border-top padding-inline-0"
+              [class.cd-page-header__tile--tabs]="tabsLayout">
+      <div cdsStack="vertical"
+           [gap]="6"
+           [class.cd-page-header--tabs-layout]="tabsLayout">
+        <div cdsStack="vertical"
+             [gap]="2">
+          <p class="cds--type-heading-04 cds-m-0">{{ title }}</p>
+          @if(subtitle) {
+          <p class="cds--type-heading-03 cds-m-0">{{ subtitle }}</p>
+          }
+          @if(description) {
+          <p class="cds--type-body-01 cds-m-0">{{ description }}</p>
+          }
+        </div>
+        <div [class.cd-page-header__tabs]="tabsLayout">
+          <ng-content></ng-content>
+        </div>
+      </div>
     </cds-tile>
   </div>
 </header>
index 938aec43d3381e0b57ef80d47effedde448863f8..f6c8344996357584c51f23535e658c3d35390fcd 100644 (file)
@@ -14,4 +14,14 @@ cd-page-header {
   .border-top {
     border-top: 1px solid var(--cds-border-subtle-01);
   }
+
+  // Remove tile bottom padding so tabs sit on the tile edge
+  .cd-page-header__tile--tabs {
+    padding-bottom: 0;
+  }
+
+  // Align tabs with the title
+  .cd-page-header__tabs {
+    margin-inline-start: calc(-1 * var(--cds-layout-density-padding-inline-local, 1rem));
+  }
 }
index f700127f3905b8dc7d27cfca244ab8946e60de44..78193959202754514dcc39b743893c7729b7b5d4 100644 (file)
@@ -19,4 +19,5 @@ export class PageHeaderComponent {
   @Input({ required: true }) title: string;
   @Input() subtitle: string = '';
   @Input() description: string = '';
+  @Input() tabsLayout = false;
 }