-<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>
+<div class="cephfs-mirroring-fs-mirror-paths">
+ <span class="cds--type-heading-03 cds-mt-6">Mirror paths</span>
+ <div class="cds-mt-6">
+ <cd-table
+ [data]="mirrorPaths"
+ [columns]="columns"
+ columnMode="flex"
+ selectionType="single"
+ [hasDetails]="false"
+ (updateSelection)="onSelectionChange($event)"
+ [toolHeader]="true"
+ [searchableObjects]="true"
+ (fetchData)="fetchFsName()">
+
+ <ng-template
+ #pathTpl
+ let-row="data.row">
+ <a href="javascript:void(0)"
+ (click)="onPathClick(row)"
+ class="path-link">
+ {{ row.path }}
+ </a>
+ </ng-template>
+
+ <ng-template
+ #syncStatusTpl
+ let-row="data.row">
+ <div class="d-flex align-items-center">
+ <svg
+ [cdsIcon]="getSyncStatusIcon(row.syncStatus)"
+ [size]="icons.size16"
+ [ngClass]="['me-2', getSyncStatusClass(row.syncStatus)]"></svg>
+ <span>{{ row.syncStatus | titlecase }}</span>
+ </div>
+ </ng-template>
+
+ <ng-template
+ #currentSyncSnapshotTpl
+ let-row="data.row">
+ <div>
+ <div>{{ row.currentSyncSnapshot }}</div>
+ <div *ngIf="row.currentSyncEta" class="cds--type-helper-text-01">
+ ETA: {{ row.currentSyncEta }}
+ </div>
+ </div>
+ </ng-template>
+ </cd-table>
+ </div>
+
+ <cd-side-panel
+ *ngIf="selectedPath"
+ [expanded]="sidePanelOpen"
+ [headerText]="selectedPath.path"
+ size="xl"
+ (closed)="closeSidePanel()">
+ </cd-side-panel>
+</div>
-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',
standalone: false,
encapsulation: ViewEncapsulation.None
})
-export class CephfsMirroringFsMirrorPathsComponent {}
+export class CephfsMirroringFsMirrorPathsComponent implements OnInit, OnDestroy {
+ @ViewChild('syncStatusTpl', { static: true })
+ syncStatusTpl!: TemplateRef<any>;
+
+ @ViewChild('pathTpl', { static: true })
+ pathTpl!: TemplateRef<any>;
+
+ @ViewChild('currentSyncSnapshotTpl', { static: true })
+ currentSyncSnapshotTpl!: TemplateRef<any>;
+
+ 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 '';
+ }
+ }
+}