From: Pedro Gonzalez Gomez Date: Mon, 15 Jun 2026 10:01:10 +0000 (+0200) Subject: mgr/dashboard: add cephfs mirroring routes and tabs for mirrored filesystem X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=9a4d729d716c44b4647ee82b8c433ae5e3c4b273;p=ceph.git mgr/dashboard: add cephfs mirroring routes and tabs for mirrored filesystem Fixes: https://tracker.ceph.com/issues/77404 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 0a18e3caabc..450f048b627 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 @@ -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 index 00000000000..f7d6e4f3b4b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-mirror-paths/cephfs-mirroring-fs-mirror-paths.component.html @@ -0,0 +1,6 @@ +
+ +

Mirror paths content will be available here.

+
+
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 index 00000000000..e69de29bb2d 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 index 00000000000..b0693024a77 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-mirror-paths/cephfs-mirroring-fs-mirror-paths.component.ts @@ -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 index 00000000000..8bdd4bb2146 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.html @@ -0,0 +1,6 @@ +
+ +

Overview content will be available here.

+
+
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 index 00000000000..e69de29bb2d 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 index 00000000000..2312db990e5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.spec.ts @@ -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; + + 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 index 00000000000..52e401a699a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-overview/cephfs-mirroring-fs-overview.component.ts @@ -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 index 00000000000..0990cf5bd3a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-schedules/cephfs-mirroring-fs-schedules.component.html @@ -0,0 +1,6 @@ +
+ +

Schedules content will be available here.

+
+
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 index 00000000000..e69de29bb2d 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 index 00000000000..e52bdc795c5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-schedules/cephfs-mirroring-fs-schedules.component.ts @@ -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 index 00000000000..750c7fbd0ad --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-breadcrumb.resolver.ts @@ -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 index 00000000000..f2115477e61 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.html @@ -0,0 +1,27 @@ + + + + + + + + + + + +
+ +
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 index 00000000000..e69de29bb2d 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 index 00000000000..d98db770e69 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.spec.ts @@ -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; + + 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 index 00000000000..5abdd2aac3d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-fs-tabs/cephfs-mirroring-fs-tabs.component.ts @@ -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; + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-list/cephfs-mirroring-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-list/cephfs-mirroring-list.component.ts index 355f6dc951b..02f1cbe55ba 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-list/cephfs-mirroring-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-list/cephfs-mirroring-list.component.ts @@ -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 }, 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 a8e17adbefc..f5b0fc1590d 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 @@ -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())] }) 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 da63bd561f2..895e45fcdc1 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 @@ -19,7 +19,7 @@ - @if(pageHeaderTitle) { + @if(pageHeaderTitle && !pageHeaderHidden) { ; @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)) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/page-header/page-header.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/page-header/page-header.component.html index 069ccd78b00..70a6f9102e3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/page-header/page-header.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/page-header/page-header.component.html @@ -1,13 +1,24 @@
- -

{{ title }}

- @if(subtitle) { -

{{ subtitle }}

- } - @if(description) { -

{{ description }}

- } + +
+
+

{{ title }}

+ @if(subtitle) { +

{{ subtitle }}

+ } + @if(description) { +

{{ description }}

+ } +
+
+ +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/page-header/page-header.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/page-header/page-header.component.scss index 938aec43d33..f6c83449963 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/page-header/page-header.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/page-header/page-header.component.scss @@ -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)); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/page-header/page-header.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/page-header/page-header.component.ts index f700127f390..78193959202 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/page-header/page-header.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/page-header/page-header.component.ts @@ -19,4 +19,5 @@ export class PageHeaderComponent { @Input({ required: true }) title: string; @Input() subtitle: string = ''; @Input() description: string = ''; + @Input() tabsLayout = false; }