From: Aashish Sharma Date: Tue, 23 Jun 2026 12:30:19 +0000 (+0530) Subject: mgr/dashboard: Add cephfs mirror path listing X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=725bff73db9125575997b54d5bb96caec0144b4a;p=ceph.git mgr/dashboard: Add cephfs mirror path listing Signed-off-by: Aashish Sharma --- diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index f8cfcd0e1d3..62f994b81fc 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -1409,6 +1409,18 @@ class CephFSMirror(RESTController): ) return json.loads(out) + @Endpoint('GET', path='/snapshot-mirror-status') + @ReadPermission + def snapshot_mirror_status(self, fs_name: str, mirrored_dir_path=None, peer_uuid=None): + error_code, out, err = mgr.remote('mirroring', 'snapshot_mirror_status', fs_name, mirrored_dir_path, peer_uuid) + if error_code != 0: + raise DashboardException( + msg=f'Failed to get Cephfs snapshot mirror status: {err}', + code=error_code, + component='cephfs.mirror' + ) + return json.loads(out) + @UIRouter('/cephfs/mirror') class CephFSMirrorStatus(RESTController): 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 index f7d6e4f3b4b..f1279f35f49 100644 --- 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 @@ -1,6 +1,57 @@ -
- -

Mirror paths content will be available here.

-
-
+
+ Mirror paths +
+ + + + + {{ row.path }} + + + + +
+ + {{ row.syncStatus | titlecase }} +
+
+ + +
+
{{ row.currentSyncSnapshot }}
+
+ ETA: {{ row.currentSyncEta }} +
+
+
+
+
+ + + +
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 index e69de29bb2d..339a8fcc78c 100644 --- 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 @@ -0,0 +1,29 @@ +.cephfs-mirroring-fs-mirror-paths { + .path-link { + color: var(--cds-link-primary); + text-decoration: none; + cursor: pointer; + + &:hover { + text-decoration: none; + } + } + + .text-success { + fill: var(--cds-support-success); + } + + .text-info { + fill: var(--cds-support-info); + } + + .text-danger { + fill: var(--cds-support-error); + } + + .text-muted { + fill: var(--cds-text-secondary); + } +} + +// Made with Bob 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 index b0693024a77..69730037ede 100644 --- 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 @@ -1,4 +1,26 @@ -import { Component, ViewEncapsulation } from '@angular/core'; +import { + Component, + OnInit, + OnDestroy, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { CephfsService } from '~/app/shared/api/cephfs.service'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; +import { Icons } from '~/app/shared/enum/icons.enum'; + +interface MirrorPath { + path: string; + syncStatus: 'syncing' | 'idle' | 'failed'; + currentSyncSnapshot: string; + currentSyncEta?: string; + lastSyncedSnapshot: string; + lastSyncedTime?: string; +} @Component({ selector: 'cd-cephfs-mirroring-fs-mirror-paths', @@ -7,4 +29,174 @@ import { Component, ViewEncapsulation } from '@angular/core'; standalone: false, encapsulation: ViewEncapsulation.None }) -export class CephfsMirroringFsMirrorPathsComponent {} +export class CephfsMirroringFsMirrorPathsComponent implements OnInit, OnDestroy { + @ViewChild('syncStatusTpl', { static: true }) + syncStatusTpl!: TemplateRef; + + @ViewChild('pathTpl', { static: true }) + pathTpl!: TemplateRef; + + @ViewChild('currentSyncSnapshotTpl', { static: true }) + currentSyncSnapshotTpl!: TemplateRef; + + columns: CdTableColumn[] = []; + mirrorPaths: MirrorPath[] = []; + selection = new CdTableSelection(); + selectedPath: MirrorPath | null = null; + sidePanelOpen = false; + icons = Icons; + fsName: string = ''; + + private subscriptions = new Subscription(); + + constructor(private cephfsService: CephfsService, private route: ActivatedRoute) {} + + ngOnInit(): void { + this.initializeColumns(); + } + + ngOnDestroy(): void { + this.subscriptions.unsubscribe(); + } + + initializeColumns(): void { + this.columns = [ + { + name: $localize`Path`, + prop: 'path', + flexGrow: 2, + cellTemplate: this.pathTpl, + sortable: true + }, + { + name: $localize`Sync status`, + prop: 'syncStatus', + flexGrow: 1.5, + cellTemplate: this.syncStatusTpl, + sortable: true + }, + { + name: $localize`Current sync snapshot`, + prop: 'currentSyncSnapshot', + flexGrow: 1.5, + cellTemplate: this.currentSyncSnapshotTpl, + sortable: true + }, + { + name: $localize`Last synced snapshot`, + prop: 'lastSyncedSnapshot', + flexGrow: 1.5, + sortable: true + } + ]; + } + + fetchFsName(): void { + this.subscriptions.add( + this.route.parent?.paramMap.subscribe((paramMap) => { + this.fsName = paramMap.get('fsName') || ''; + if (this.fsName) { + this.loadMirrorPaths(); + } + }) || new Subscription() + ); + } + + loadMirrorPaths(): void { + if (!this.fsName) { + return; + } + + this.subscriptions.add( + this.cephfsService.getSnapshotMirrorStatus(this.fsName).subscribe( + (data: any) => { + this.mirrorPaths = this.parseMirrorStatus(data); + }, + (_) => { + this.mirrorPaths = []; + } + ) + ); + } + + parseMirrorStatus(data: any): MirrorPath[] { + const paths: MirrorPath[] = []; + + if (!data || !data.metrics) { + return paths; + } + + for (const [path, pathData] of Object.entries(data.metrics)) { + if (pathData && typeof pathData === 'object' && 'peer' in pathData) { + const peerData: any = pathData; + + const peerEntries = Object.entries(peerData.peer || {}); + if (peerEntries.length > 0) { + const [, peerInfo]: [string, any] = peerEntries[0]; + + const syncStatus = peerInfo.state || 'idle'; + const lastSyncedSnap = peerInfo.last_synced_snap?.name || '-'; + const currentSyncSnap = peerInfo.current_sync_snap?.name || '-'; + let currentSyncEta: string | undefined; + if (peerInfo.current_sync_snap?.eta_completion) { + currentSyncEta = peerInfo.current_sync_snap.eta_completion; + } + + const mirrorPath: MirrorPath = { + path: path, + syncStatus: syncStatus, + currentSyncSnapshot: currentSyncSnap, + currentSyncEta: currentSyncEta, + lastSyncedSnapshot: lastSyncedSnap + }; + + paths.push(mirrorPath); + } + } + } + + return paths; + } + + onSelectionChange(selection: CdTableSelection): void { + this.selection = selection; + } + + onPathClick(path: MirrorPath): void { + this.selectedPath = path; + this.sidePanelOpen = true; + } + + closeSidePanel(): void { + this.sidePanelOpen = false; + this.selectedPath = null; + } + + getSyncStatusIcon(status: string): string { + switch (status) { + case 'syncing': + return Icons.inProgress; + case 'idle': + return Icons.pendingFilled; + case 'failed': + return Icons.danger; + default: + return Icons.circle; + } + } + + getSyncStatusClass(status: string): string { + switch (status) { + case 'syncing': + return 'text-info'; + case 'completed': + return 'text-success'; + case 'idle': + return 'text-muted'; + case 'failed': + return 'text-danger'; + default: + return ''; + } + } +} 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 219662364db..6a65257472f 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 @@ -75,6 +75,7 @@ import ReplicateIcon from '@carbon/icons/es/replicate/32'; import ReplicateIcon24 from '@carbon/icons/es/replicate/24'; import ShareIcon from '@carbon/icons/es/share/32'; import ShareIcon24 from '@carbon/icons/es/share/24'; +import PendingFilled from '@carbon/icons/es/pending--filled/16'; @NgModule({ imports: [ @@ -157,7 +158,8 @@ export class CephfsModule { ReplicateIcon, ReplicateIcon24, ShareIcon, - ShareIcon24 + ShareIcon24, + PendingFilled ]); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts index 15bab5f3e42..616811e4476 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts @@ -152,4 +152,19 @@ export class CephfsService { token: token }); } + + getSnapshotMirrorStatus( + fsName: string, + mirroredDirPath?: string, + peerUuid?: string + ): Observable { + let params = new HttpParams(); + if (mirroredDirPath) { + params = params.append('mirrored_dir_path', mirroredDirPath); + } + if (peerUuid) { + params = params.append('peer_uuid', peerUuid); + } + return this.http.get(`${this.baseURL}/mirror/snapshot-mirror-status/${fsName}`, { params }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts index 5a304fc43ac..53076bf8a3a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts @@ -125,7 +125,8 @@ export enum Icons { inProgress = 'in-progress', arrowDown = 'arrow--down', locked = 'locked', // Access denied, locked state - cloudMonitoring = 'cloud--monitoring' + cloudMonitoring = 'cloud--monitoring', + pendingFilled = 'pending--filled' } export enum IconSize { @@ -176,7 +177,10 @@ export const ICON_TYPE = { rightArrow: 'caret--right', locked: 'locked', cloudMonitoring: 'cloud--monitoring', - trash: 'trash-can' + trash: 'trash-can', + replicate: 'replicate', + share: 'share', + pendingFilled: 'pending--filled' } as const; export const EMPTY_STATE_IMAGE = {