]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard : Cephfs Mirroring Listing
authorDnyaneshwari Talwekar <dtalwekar@li-4c4c4544-0038-3510-8056-b5c04f473234.ibm.com>
Fri, 15 May 2026 11:22:27 +0000 (16:52 +0530)
committerPedro Gonzalez Gomez <pegonzal@ibm.com>
Wed, 24 Jun 2026 11:15:19 +0000 (13:15 +0200)
Fixes: https://tracker.ceph.com/issues/76346
Signed-off-by: Dnyaneshwari Talwekar <dtalweka@redhat.com>
25 files changed:
src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/page-header.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.html
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-mirroring-list/cephfs-mirroring-list.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-list/cephfs-mirroring-list.component.spec.ts
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-mirroring-wizard/cephfs-mirroring-wizard-step.enum.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.html
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.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts
src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss

index 9fb83fa4cecef379d918ace6bbca409d7837e439..3c00fb530bfcf5715764ba797ecb99571c9b6933 100644 (file)
@@ -8,19 +8,21 @@ describe('Page header component', () => {
     pageHeader.navigateToCephfsMirroring();
   });
 
-  it('should display the page header on CephFS Mirroring page', () => {
+  it('should display the page header on Filesystem Mirroring page', () => {
     pageHeader.getPageHeader().should('be.visible');
   });
 
   it('should show the expected title in the page header', () => {
     pageHeader.getHeaderTitle().then((text) => {
-      expect(text.trim()).to.equal('CephFS Mirroring');
+      expect(text.trim()).to.equal('Filesystem Mirroring');
     });
   });
 
   it('should show the expected description in the page header', () => {
     pageHeader.getHeaderDescription().then((text) => {
-      expect(text.trim()).to.equal('Centralised view of all CephFS Mirroring relationships.');
+      expect(text.trim()).to.equal(
+        'Configure mirroring between filesystems and monitor replication status.'
+      );
     });
   });
 });
index 5a77bb1f1dba452bced0be966576f9fd3586d560..0a18e3caabcd217a0a82212b0ccf3fa8014ade8b 100644 (file)
@@ -67,7 +67,6 @@ import { SmbOverviewComponent } from './ceph/smb/smb-overview/smb-overview.compo
 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 { 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';
 import { OverviewComponent } from './ceph/overview/overview.component';
 
@@ -489,11 +488,6 @@ const routes: Routes = [
               pageHeader: CEPHFS_MIRRORING_PAGE_HEADER
             }
           },
-          {
-            path: `mirroring/${URLVerbs.CREATE}`,
-            component: CephfsMirroringWizardComponent,
-            data: { breadcrumbs: ActionLabels.CREATE }
-          },
           {
             path: 'nfs',
             canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService],
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.html
deleted file mode 100644 (file)
index 0a2a647..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<div cds-mb="lg">
-  <div class="cds--type-heading-03"
-       i18n>Select filesystem</div>
-</div>
-<div class="cds-mt-5">
-  <cd-alert-panel type="info">
-    <div [cdsStack]="'vertical'"
-         [gap]="2">
-      <div class="cds--type-heading-compact-01"
-           i18n>Selection requirements</div>
-      <ul class="cds--type-body-compact-01 requirements-list">
-        <li i18n>Only one filesystem can be selected as the mirroring target</li>
-        <li i18n>The selected filesystem must have an active MDS (Metadata Server)</li>
-        <li i18n>Ensure sufficient storage capacity for incoming mirrored snapshots</li>
-      </ul>
-    </div>
-  </cd-alert-panel>
-</div>
-
-<div class="cds-mt-5">
-  <cd-table [data]="(filesystems$ | async) ?? []"
-            [columns]="columns"
-            columnMode="flex"
-            selectionType="singleRadio"
-            headerTitle="Select target filesystem"
-            headerDescription="Choose the filesystem that will receive mirrored data from the local cluster."
-            (updateSelection)="updateSelection($event)">
-    <ng-template #mdsStatus
-                 let-value="data.value"
-                 let-row="data.row">
-      <div [cdsStack]="'horizontal'"
-           gap="2">
-        @if (value === 'Active') {
-        <cd-icon type="success"></cd-icon>
-        }
-        @if (value === 'Warning') {
-        <cd-icon type="warning"></cd-icon>
-        }
-        @if (value === 'Inactive') {
-        <cd-icon type="error"></cd-icon>
-        }
-        <span class="cds--type-body-compact-01 cds-pt-2px">{{ mdsStatusLabels[value] }}</span>
-      </div>
-    </ng-template>
-
-    <ng-template #mirroringStatus
-                 let-value="data.value"
-                 let-row="data.row">
-      <div [cdsStack]="'horizontal'"
-           gap="2">
-        @if (value === 'Enabled') {
-        <cd-icon type="success"></cd-icon>
-        }
-        @if (value === 'Disabled') {
-        <cd-icon type="warning"></cd-icon>
-        }
-        <span class="cds--type-body-compact-01 cds-pt-2px">{{ mirroringStatusLabels[value] }}</span>
-      </div>
-    </ng-template>
-
-    <ng-template let-row="row"
-                 let-col="col">
-      <ng-container [ngSwitch]="col.prop">
-        <ng-container *ngSwitchCase="'pools'">
-          @for (pool of row.pools; track $index; let last = $last) {
-          <cd-badge>{{ pool }}</cd-badge>
-          @if (!last) {
-          <span>, </span>
-          }
-          }
-        </ng-container>
-        <ng-container *ngSwitchDefault>
-          {{ row[col.prop] }}
-        </ng-container>
-      </ng-container>
-    </ng-template>
-  </cd-table>
-</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.scss
deleted file mode 100644 (file)
index 46d4398..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-@use '@carbon/layout';
-
-.requirements-list {
-  list-style-type: disc;
-  padding-left: var(--cds-spacing-05);
-  margin-left: layout.$spacing-02;
-}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.spec.ts
deleted file mode 100644 (file)
index 47f8e05..0000000
+++ /dev/null
@@ -1,147 +0,0 @@
-import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
-import { of, throwError } from 'rxjs';
-
-import { CephfsService } from '~/app/shared/api/cephfs.service';
-import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
-import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
-import { CephfsFilesystemSelectorComponent } from './cephfs-filesystem-selector.component';
-
-const createDetail = (
-  id: number,
-  name: string,
-  pools: Array<{ pool: string; used: number }>,
-  enabled = true,
-  peers: Record<string, unknown> = { peer: {} }
-) => ({
-  cephfs: {
-    id,
-    name,
-    pools,
-    flags: { enabled },
-    mirror_info: { peers }
-  }
-});
-
-describe('CephfsFilesystemSelectorComponent', () => {
-  let component: CephfsFilesystemSelectorComponent;
-  let fixture: ComponentFixture<CephfsFilesystemSelectorComponent>;
-  let cephfsServiceMock: jest.Mocked<Pick<CephfsService, 'list' | 'getCephfs'>>;
-
-  beforeEach(async () => {
-    cephfsServiceMock = {
-      list: jest.fn(),
-      getCephfs: jest.fn()
-    };
-
-    cephfsServiceMock.list.mockReturnValue(of([]));
-    cephfsServiceMock.getCephfs.mockReturnValue(of(null));
-
-    await TestBed.configureTestingModule({
-      declarations: [CephfsFilesystemSelectorComponent],
-      providers: [{ provide: CephfsService, useValue: cephfsServiceMock }, DimlessBinaryPipe],
-      schemas: [NO_ERRORS_SCHEMA]
-    }).compileComponents();
-
-    fixture = TestBed.createComponent(CephfsFilesystemSelectorComponent);
-    component = fixture.componentInstance;
-  });
-
-  afterEach(() => {
-    jest.clearAllMocks();
-  });
-
-  it('should configure columns on init', () => {
-    fixture.detectChanges();
-
-    expect(component.columns.map((c) => c.prop)).toEqual([
-      'name',
-      'used',
-      'pools',
-      'mdsStatus',
-      'mirroringStatus'
-    ]);
-  });
-
-  it('should populate filesystems from service data', fakeAsync(() => {
-    cephfsServiceMock.list.mockReturnValue(of([{ id: 1 }]));
-    cephfsServiceMock.getCephfs.mockReturnValue(
-      of(
-        createDetail(1, 'fs1', [
-          { pool: 'data', used: 100 },
-          { pool: 'meta', used: 50 }
-        ])
-      )
-    );
-
-    fixture.detectChanges();
-
-    let filesystems: any[] = [];
-    component.filesystems$.subscribe((rows) => {
-      filesystems = rows;
-    });
-    tick();
-
-    expect(filesystems).toEqual([
-      {
-        id: 1,
-        name: 'fs1',
-        pools: ['data', 'meta'],
-        used: '150',
-        mdsStatus: 'Inactive',
-        mirroringStatus: 'Disabled'
-      }
-    ]);
-  }));
-
-  it('should set mirroring status to Disabled when list response has no mirror info', fakeAsync(() => {
-    cephfsServiceMock.list.mockReturnValue(of([{ id: 2 }]));
-    cephfsServiceMock.getCephfs.mockReturnValue(of(createDetail(2, 'fs2', [])));
-
-    fixture.detectChanges();
-
-    let filesystems: any[] = [];
-    component.filesystems$.subscribe((rows) => {
-      filesystems = rows;
-    });
-    tick();
-
-    expect(filesystems[0].mirroringStatus).toBe('Disabled');
-  }));
-
-  it('should produce empty filesystems when list is empty', fakeAsync(() => {
-    cephfsServiceMock.list.mockReturnValue(of([]));
-
-    fixture.detectChanges();
-
-    let filesystems: any[] = [];
-    component.filesystems$.subscribe((rows) => {
-      filesystems = rows;
-    });
-    tick();
-
-    expect(filesystems).toEqual([]);
-    expect(cephfsServiceMock.getCephfs).not.toHaveBeenCalled();
-  }));
-
-  it('should skip null details when getCephfs errors', fakeAsync(() => {
-    cephfsServiceMock.list.mockReturnValue(of([{ id: 3 }]));
-    cephfsServiceMock.getCephfs.mockReturnValue(throwError(() => new Error('boom')));
-
-    fixture.detectChanges();
-
-    let filesystems: any[] = [];
-    component.filesystems$.subscribe((rows) => {
-      filesystems = rows;
-    });
-    tick();
-
-    expect(filesystems).toEqual([]);
-  }));
-
-  it('should update selection reference', () => {
-    const selection = new CdTableSelection([{ id: 1 }]);
-    component.updateSelection(selection);
-    expect(component.selection).toBe(selection);
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.ts
deleted file mode 100644 (file)
index d22fb82..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-import {
-  Component,
-  OnInit,
-  TemplateRef,
-  ViewChild,
-  inject,
-  Output,
-  EventEmitter
-} from '@angular/core';
-
-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';
-import { catchError, map, switchMap } from 'rxjs/operators';
-import { forkJoin, of, Observable } from 'rxjs';
-import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
-import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
-import {
-  CephfsDetail,
-  FilesystemRow,
-  MdsStatus,
-  MirroringStatus,
-  MIRRORING_STATUS,
-  mdsStateToStatus
-} from '~/app/shared/models/cephfs.model';
-
-@Component({
-  selector: 'cd-cephfs-filesystem-selector',
-  templateUrl: './cephfs-filesystem-selector.component.html',
-  standalone: false,
-  styleUrls: ['./cephfs-filesystem-selector.component.scss']
-})
-export class CephfsFilesystemSelectorComponent implements OnInit {
-  @ViewChild('mdsStatus', { static: true })
-  mdsStatus: TemplateRef<any>;
-  @ViewChild('mirroringStatus', { static: true })
-  mirroringStatus: TemplateRef<any>;
-  columns: CdTableColumn[] = [];
-  filesystems$: Observable<FilesystemRow[]> = of([]);
-  selection = new CdTableSelection();
-  icons = Icons;
-  @Output() filesystemSelected = new EventEmitter<FilesystemRow | null>();
-  mdsStatusLabels: Record<MdsStatus, string> = {
-    Active: $localize`Active`,
-    Warning: $localize`Warning`,
-    Inactive: $localize`Inactive`
-  };
-  mirroringStatusLabels: Record<MirroringStatus, string> = {
-    Enabled: $localize`Enabled`,
-    Disabled: $localize`Disabled`
-  };
-
-  private cephfsService = inject(CephfsService);
-  private dimlessBinaryPipe = inject(DimlessBinaryPipe);
-
-  ngOnInit(): void {
-    this.columns = [
-      { name: $localize`Filesystem name`, prop: 'name', flexGrow: 2 },
-      { name: $localize`Usage`, prop: 'used', flexGrow: 1, pipe: this.dimlessBinaryPipe },
-      {
-        prop: $localize`pools`,
-        name: 'Pools used',
-        cellTransformation: CellTemplate.tag,
-        customTemplateConfig: {
-          class: 'tag-background-primary'
-        },
-        flexGrow: 1.3
-      },
-      { name: $localize`Status`, prop: 'mdsStatus', flexGrow: 0.8, cellTemplate: this.mdsStatus },
-
-      {
-        name: $localize`Mirroring status`,
-        prop: 'mirroringStatus',
-        flexGrow: 0.8,
-        cellTemplate: this.mirroringStatus
-      }
-    ];
-
-    this.filesystems$ = this.cephfsService.list().pipe(
-      switchMap((listResponse: Array<CephfsDetail>) => {
-        if (!listResponse?.length) {
-          return of([]);
-        }
-        const detailRequests = listResponse.map(
-          (fs): Observable<CephfsDetail | null> =>
-            this.cephfsService.getCephfs(fs.id).pipe(catchError(() => of(null)))
-        );
-        return forkJoin(detailRequests).pipe(
-          map((details: Array<CephfsDetail | null>) =>
-            details
-              .map((detail, index) => {
-                if (!detail?.cephfs) {
-                  return null;
-                }
-                const listItem = listResponse[index];
-                const pools = detail.cephfs.pools || [];
-                const poolNames = pools.map((p) => p.pool);
-                const totalUsed = pools.reduce((sum, p) => sum + p.used, 0);
-                const mdsInfo = listItem?.mdsmap?.info ?? {};
-                const firstMdsGid = Object.keys(mdsInfo)[0];
-                const mdsState = firstMdsGid ? mdsInfo[firstMdsGid]?.state : undefined;
-                return {
-                  id: detail.cephfs.id,
-                  name: detail.cephfs.name,
-                  pools: poolNames,
-                  used: `${totalUsed}`,
-                  mdsStatus: mdsStateToStatus(mdsState),
-                  mirroringStatus: listItem?.mirror_info
-                    ? MIRRORING_STATUS.Enabled
-                    : MIRRORING_STATUS.Disabled
-                } as FilesystemRow;
-              })
-              .filter((row): row is FilesystemRow => row !== null)
-          )
-        );
-      })
-    );
-  }
-
-  updateSelection(selection: CdTableSelection) {
-    this.selection = selection;
-    const selectedRow = typeof selection?.first === 'function' ? selection.first() : null;
-    this.filesystemSelected.emit(selectedRow as FilesystemRow | null);
-  }
-}
index cb8f101a847d52762816268ecca51f8a9988f120..bcbe6b1e510dcd38564441934d34dcd7a60e66f9 100644 (file)
@@ -6,7 +6,7 @@
           #formDir="ngForm"
           [formGroup]="form"
           novalidate>
-      <div i18n="form title|Example: Create Volume@@formTitle"
+      <div i18n="form title|Example: Create Volume@@cephfsFormTitle"
            class="form-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
 
       <div class="form-item">
index 2ba0db47e3344731e9a43c4d3c3ebf3e58140523..c0c55839e30ea58da6c2f52cc98afd9d95833ea8 100644 (file)
@@ -1,16 +1,68 @@
-@if (daemonStatus$ | async; as daemonStatus) {
+<div cdsStack="vertical"
+     gap="5"
+     class="cds-mt-6">
+  <h4 class="cds--type-heading-03"
+      i18n>Jump in</h4>
+  <div cdsGrid
+       [narrow]="true">
+    <div cdsRow
+         class="jump-in-row">
+      <div cdsCol
+           [columnNumbers]="{lg: 4, md: 4, sm: 4}">
+        <cds-clickable-tile>
+          <div cdsStack="vertical"
+               gap="5">
+            <p class="cds--type-heading-compact-01"
+               i18n>Set up mirroring</p>
+            <p class="cds--type-body-compact-01"
+               i18n>
+              Configure mirroring for a filesystem by importing a token from a peer cluster and adding paths to replicate.
+            </p>
+            <div [cdsStack]="'horizontal'">
+              <cd-icon type="replicate"
+                       size="24"
+                       class="cds-ml-1 cds-mr-12"></cd-icon>
+              <span class="cds--text-right">&rarr;</span>
+            </div>
+          </div>
+        </cds-clickable-tile>
+      </div>
+      <div cdsCol
+           [columnNumbers]="{lg: 4, md: 4, sm: 4}">
+        <cds-clickable-tile>
+          <div cdsStack="vertical"
+               gap="5">
+            <p class="cds--type-heading-compact-01"
+               i18n>Prepare to receive</p>
+            <p class="cds--type-body-compact-01"
+               i18n>
+              Generate a bootstrap token for a filesystem to allow a peer cluster to replicate data to it.
+            </p>
+            <div [cdsStack]="'horizontal'">
+              <cd-icon type="share"
+                       size="24"
+                       class="cds-ml-3 cds-mr-12"></cd-icon>
+              <span class="cds--text-right">&rarr;</span>
+            </div>
+          </div>
+        </cds-clickable-tile>
+      </div>
+    </div>
+  </div>
+</div>
+
+<!-- Mirrored filesystems table -->
+<section class="cds-mt-6">
+  <h3 class="cds--type-heading-03 cds-mb-3"
+      i18n>Mirrored filesystems</h3>
   <cd-table
     #table
-    [data]="daemonStatus"
+    [data]="daemonStatus$ | async"
     [columns]="columns"
     columnMode="flex"
     selectionType="single"
-    (updateSelection)="updateSelection($event)"
-    (fetchData)="loadDaemonStatus()">
-    <cd-table-actions class="table-actions"
-                      [permission]="permission"
-                      [selection]="selection"
-                      [tableActions]="tableActions">
-    </cd-table-actions>
+    (fetchData)="loadDaemonStatus()"
+    (updateSelection)="updateSelection($event)">
   </cd-table>
-}
+</section>
+
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..15b64672187ad5d09f2c074224ff35145b7cc597 100644 (file)
@@ -0,0 +1,13 @@
+.cds--grid.cds--grid--narrow {
+  margin-left: 20px;
+  padding-left: 0;
+}
+
+.jump-in-row {
+  align-items: stretch;
+
+  cds-clickable-tile,
+  .cds--tile {
+    height: 100%;
+  }
+}
index bd2f222b24cdf0a1a75d6649b6c208a7ba5a716c..d7fa1dbc1481174bf8544e993ca56cf458c0fd5a 100644 (file)
@@ -1,9 +1,9 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { of } from 'rxjs';
 
 import { CephfsMirroringListComponent } from './cephfs-mirroring-list.component';
 import { CephfsService } from '~/app/shared/api/cephfs.service';
-import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { Daemon, MirroringRow } from '~/app/shared/models/cephfs.model';
 
 describe('CephfsMirroringListComponent', () => {
@@ -19,7 +19,8 @@ describe('CephfsMirroringListComponent', () => {
 
     await TestBed.configureTestingModule({
       declarations: [CephfsMirroringListComponent],
-      providers: [ActionLabelsI18n, { provide: CephfsService, useValue: cephfsServiceMock }]
+      providers: [{ provide: CephfsService, useValue: cephfsServiceMock }],
+      schemas: [NO_ERRORS_SCHEMA]
     }).compileComponents();
 
     fixture = TestBed.createComponent(CephfsMirroringListComponent);
@@ -29,14 +30,15 @@ describe('CephfsMirroringListComponent', () => {
   it('should initialize columns correctly on ngOnInit', () => {
     component.ngOnInit();
 
-    expect(component.columns.length).toBe(5);
-    expect(component.columns[0].prop).toBe('remote_cluster_name');
+    expect(component.columns.length).toBe(6);
+    expect(component.columns[0].prop).toBe('local_fs_name');
   });
 
-  it('should call loadDaemonStatus inside ngOnInit', () => {
-    const loadSpy = jest.spyOn(component, 'loadDaemonStatus');
-    component.ngOnInit();
-    expect(loadSpy).toHaveBeenCalledTimes(1);
+  it('should fetch daemon status when loadDaemonStatus() is called', () => {
+    cephfsServiceMock.listDaemonStatus.mockReturnValue(of([]));
+    component.daemonStatus$.subscribe();
+    component.loadDaemonStatus();
+    expect(cephfsServiceMock.listDaemonStatus).toHaveBeenCalledTimes(1);
   });
 
   it('should map daemon status to MirroringRow[] correctly', () => {
index 7d9e04bb6a38e5b83e3af58aaf71dd1d67b3e32f..355f6dc951bfa4c3dcbaf9b89e90ad755e45043e 100644 (file)
-import { Component, ViewChild, OnInit } from '@angular/core';
-import { BehaviorSubject, Observable, of } from 'rxjs';
-import { catchError, switchMap } from 'rxjs/operators';
+import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
+import { Subject, of } from 'rxjs';
+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 { CdTableColumn } from '~/app/shared/models/cd-table-column';
 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
-import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
-import { CephfsService } from '~/app/shared/api/cephfs.service';
-import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
-import { CdTableAction } from '~/app/shared/models/cd-table-action';
-import { URLBuilderService } from '~/app/shared/services/url-builder.service';
-import { Daemon, MirroringRow } from '~/app/shared/models/cephfs.model';
-import { Icons } from '~/app/shared/enum/icons.enum';
-import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
-import { Permission } from '~/app/shared/models/permissions';
+import { Daemon, Filesystem, MirroringRow, Peer } from '~/app/shared/models/cephfs.model';
 
-export const MIRRORING_PATH = 'cephfs/mirroring';
 @Component({
   selector: 'cd-cephfs-mirroring-list',
   templateUrl: './cephfs-mirroring-list.component.html',
   styleUrls: ['./cephfs-mirroring-list.component.scss'],
   standalone: false,
-  providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(MIRRORING_PATH) }]
+  encapsulation: ViewEncapsulation.None
 })
 export class CephfsMirroringListComponent implements OnInit {
   @ViewChild('table', { static: true }) table: TableComponent;
 
   columns: CdTableColumn[];
   selection = new CdTableSelection();
-  subject$ = new BehaviorSubject<MirroringRow[]>([]);
-  daemonStatus$: Observable<MirroringRow[]>;
-  context: CdTableFetchDataContext;
-  tableActions: CdTableAction[];
-  permission: Permission;
 
-  constructor(
-    public actionLabels: ActionLabelsI18n,
-    private authStorageService: AuthStorageService,
-    private cephfsService: CephfsService,
-    private urlBuilder: URLBuilderService
-  ) {
-    this.permission = this.authStorageService.getPermissions().cephfs;
-  }
+  private subject$ = new Subject<void>();
+
+  daemonStatus$ = this.subject$.pipe(
+    switchMap(() =>
+      this.cephfsService.listDaemonStatus().pipe(catchError(() => of([] as Daemon[])))
+    ),
+    map((daemons) => this.buildRows(daemons))
+  );
+
+  constructor(private cephfsService: CephfsService) {}
 
   ngOnInit() {
     this.columns = [
-      {
-        name: $localize`Remote cluster`,
-        prop: 'remote_cluster_name',
-        flexGrow: 2
-      },
-      { name: $localize`Local filesystem`, prop: 'local_fs_name', flexGrow: 2 },
-      { name: $localize`Remote filesystem`, prop: 'fs_name', flexGrow: 2 },
-      { name: $localize`Remote client`, prop: 'client_name', flexGrow: 2 },
-      { name: $localize`Snapshot directories`, prop: 'directory_count', flexGrow: 1 }
+      { name: $localize`Filesystem`, prop: 'local_fs_name', flexGrow: 2 },
+      { 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 },
+      { name: $localize`Last sync`, prop: 'last_sync', flexGrow: 2 },
+      { name: $localize`Replicated paths`, prop: 'directory_count', flexGrow: 2 }
     ];
+  }
 
-    const createAction: CdTableAction = {
-      permission: 'create',
-      icon: Icons.add,
-      routerLink: () => this.urlBuilder.getCreate(),
-      name: this.actionLabels.CREATE,
-      canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
-    };
+  loadDaemonStatus() {
+    this.subject$.next();
+  }
 
-    this.tableActions = [createAction];
-    this.daemonStatus$ = this.subject$.pipe(
-      switchMap(() =>
-        this.cephfsService.listDaemonStatus()?.pipe(
-          switchMap((daemons: Daemon[]) => {
-            const result: MirroringRow[] = [];
+  updateSelection(selection: CdTableSelection) {
+    this.selection = selection;
+  }
 
-            daemons.forEach((d) => {
-              d.filesystems.forEach((fs) => {
-                if (!fs.peers || fs.peers.length === 0) {
-                  result.push({
-                    remote_cluster_name: '-',
-                    local_fs_name: fs.name,
-                    fs_name: fs.name,
-                    client_name: '-',
-                    directory_count: fs.directory_count,
-                    peerId: '-',
-                    id: `${d.daemon_id}-${fs.filesystem_id}`
-                  });
-                } else {
-                  fs.peers.forEach((peer) => {
-                    result.push({
-                      remote_cluster_name: peer.remote.cluster_name,
-                      local_fs_name: fs.name,
-                      fs_name: peer.remote.fs_name,
-                      client_name: peer.remote.client_name,
-                      directory_count: fs.directory_count,
-                      id: `${d.daemon_id}-${fs.filesystem_id}`
-                    });
-                  });
-                }
-              });
-            });
-            return of(result);
-          }),
-          catchError(() => {
-            this.context?.error();
-            return of(null);
-          })
-        )
-      )
-    );
+  private buildRows(daemons: Daemon[]): MirroringRow[] {
+    const rows: MirroringRow[] = [];
+    if (!daemons?.length) {
+      return rows;
+    }
 
-    this.loadDaemonStatus();
+    for (const daemon of daemons) {
+      if (!daemon?.filesystems) continue;
+      for (const fs of daemon.filesystems) {
+        if (fs.peers?.length) {
+          for (const peer of fs.peers) {
+            rows.push(this.peerToRow(daemon, fs, peer));
+          }
+        } else {
+          rows.push(this.noPeerRow(daemon, fs));
+        }
+      }
+    }
+    return rows;
   }
 
-  loadDaemonStatus() {
-    this.subject$.next([]);
+  private peerToRow(daemon: Daemon, fs: Filesystem, peer: Peer): MirroringRow {
+    return {
+      remote_cluster_name: peer.remote?.cluster_name ?? '-',
+      local_fs_name: fs.name,
+      fs_name: peer.remote?.fs_name ?? '-',
+      client_name: peer.remote?.client_name ?? '-',
+      directory_count: fs.directory_count ?? 0,
+      id: `${daemon.daemon_id}-${fs.filesystem_id}`
+    };
   }
 
-  updateSelection(selection: CdTableSelection) {
-    this.selection = selection;
+  private noPeerRow(daemon: Daemon, fs: Filesystem): MirroringRow {
+    return {
+      remote_cluster_name: '-',
+      local_fs_name: fs.name,
+      fs_name: fs.name,
+      client_name: '-',
+      directory_count: fs.directory_count ?? 0,
+      peerId: '-',
+      id: `${daemon.daemon_id}-${fs.filesystem_id}`
+    };
   }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard-step.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard-step.enum.ts
deleted file mode 100644 (file)
index 2b712d8..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-export const StepTitles = {
-  ChooseMirrorPeerRole: $localize`Choose mirror peer role`,
-  SelectFilesystem: $localize`Select filesystem`,
-  CreateOrSelectEntity: $localize`Create or select entity`,
-  GenerateBootstrapToken: $localize`Generate bootstrap token`,
-  Review: $localize`Review`
-} as const;
-
-export const STEP_TITLES_MIRRORING_CONFIGURED = [
-  StepTitles.ChooseMirrorPeerRole,
-  StepTitles.SelectFilesystem,
-  StepTitles.CreateOrSelectEntity,
-  StepTitles.GenerateBootstrapToken
-];
-
-export const LOCAL_ROLE = 'local';
-export const REMOTE_ROLE = 'remote';
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.html
deleted file mode 100644 (file)
index 5c0f99e..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-<cd-tearsheet
-  [steps]="steps"
-  [title]="title"
-  [description]="description"
-  (submitRequested)="onSubmit()"
-  (closeRequested)="onCancel()">
-  <cd-tearsheet-step>
-    <form [formGroup]="form">
-      <div>
-        <div class="cds--type-heading-03"
-             i18n>Choose mirror peer role</div>
-        <p i18n>Select how the cluster will participate in the CephFS mirroring relationship.</p>
-      </div>
-
-      <div cdsStack="horizontal">
-        <div class="cds-mr-5">
-          <cds-tile>
-            <cds-radio-group formControlName="localRole">
-              <cds-radio
-                [value]="LOCAL_ROLE"
-                [checked]="form.get('localRole')?.value === LOCAL_ROLE"
-                (click)="onLocalRoleChange()">
-                <div>
-                  <div class="cds--type-heading-compact-02"
-                       i18n>Configure local peer</div>
-                  <div class="cds--type-label-01 cds-mt-3"
-                       i18n>
-                    This cluster will act as the initiating peer and send snapshots to a remote
-                    peer.
-                  </div>
-                  <ul class="cds--type-body-compact-01 cds-mt-6">
-                    @for (item of sourceList; track $index) {
-                    <li class="cds-mb-6 cds-mt-3">&rarr; {{ item }}</li>
-                    }
-                  </ul>
-                </div>
-              </cds-radio>
-            </cds-radio-group>
-          </cds-tile>
-        </div>
-        <cds-tile>
-          <cds-radio-group formControlName="remoteRole">
-            <cds-radio
-              [value]="REMOTE_ROLE"
-              [checked]="form.get('remoteRole')?.value === REMOTE_ROLE"
-              (click)="onRemoteRoleChange()">
-              <div>
-                <div class="cds--type-heading-compact-02"
-                     i18n>Configure remote peer</div>
-                <div class="cds--type-label-01 cds-mt-3"
-                     i18n>
-                  A remote cluster will act as the receiving peer and store replicated snapshots.
-                </div>
-                <ul class="cds--type-body-compact-01 cds-mt-6">
-                  @for (item of targetList; track $index) {
-                  <li class="cds-mb-6 cds-mt-3">&rarr; {{ item }}</li>
-                  }
-                </ul>
-              </div>
-            </cds-radio>
-          </cds-radio-group>
-        </cds-tile>
-      </div>
-
-      @if (form.get('localRole')?.value !== LOCAL_ROLE && showMessage) {
-      <cd-alert-panel
-        type="info"
-        spacingClass="mb-3 mt-3"
-        dismissible="true"
-        (dismissed)="showMessage = false">
-        <div>
-          <div
-            class="cds--type-heading-compact-01 cds-mb-2"
-            i18n>About remote peer setup</div>
-          <div
-            class="cds--type-body-compact-01 cds-mb-3"
-            i18n>
-            As a remote peer, this cluster prepares to receive mirrored data from an initiating
-            cluster. The setup includes environment validation, enabling filesystem mirroring,
-            creating required Ceph users, and generating a bootstrap token.
-          </div>
-          <div
-            class="cds--type-heading-compact-01 cds-mb-1 cds-mt-6"
-            i18n>What happens next:</div>
-          <ul class="list-disc cds-ml-5 cds--type-body-compact-01">
-            <li i18n>Environment validation</li>
-            <li i18n>Ceph user creation</li>
-            <li i18n>Filesystem mirroring activation</li>
-            <li i18n>Bootstrap token generation</li>
-          </ul>
-        </div>
-      </cd-alert-panel>
-      }
-    </form>
-  </cd-tearsheet-step>
-
-  <!-- Step 1 -->
-  <cd-tearsheet-step>
-    <cd-cephfs-filesystem-selector (filesystemSelected)="onFilesystemSelected($event)">
-    </cd-cephfs-filesystem-selector>
-  </cd-tearsheet-step>
-
-  <!-- Step 2 -->
-  <cd-tearsheet-step>
-    <cd-cephfs-mirroring-entity [selectedFilesystem]="selectedFilesystem"
-                                (entitySelected)="onEntitySelected($event)">
-    </cd-cephfs-mirroring-entity>
-  </cd-tearsheet-step>
-
-  <!-- Step 3 -->
-  <cd-tearsheet-step>
-    <div>
-      Test 3
-    </div>
-  </cd-tearsheet-step>
-</cd-tearsheet>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.scss
deleted file mode 100644 (file)
index c327f0f..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-form {
-  max-width: 77%;
-}
-
-.list-disc {
-  list-style-type: disc;
-}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.spec.ts
deleted file mode 100644 (file)
index 67397af..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { CephfsMirroringWizardComponent } from './cephfs-mirroring-wizard.component';
-import { WizardStepsService } from '~/app/shared/services/wizard-steps.service';
-import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
-import { Router } from '@angular/router';
-import { BehaviorSubject } from 'rxjs';
-import {
-  STEP_TITLES_MIRRORING_CONFIGURED,
-  LOCAL_ROLE,
-  REMOTE_ROLE
-} from './cephfs-mirroring-wizard-step.enum';
-import { WizardStepModel } from '~/app/shared/models/wizard-steps';
-import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { RadioModule } from 'carbon-components-angular';
-
-describe('CephfsMirroringWizardComponent', () => {
-  let component: CephfsMirroringWizardComponent;
-  let fixture: ComponentFixture<CephfsMirroringWizardComponent>;
-  let wizardStepsService: jest.Mocked<WizardStepsService>;
-  let router: jest.Mocked<Router>;
-
-  const mockSteps: WizardStepModel[] = [
-    { stepIndex: 0, isComplete: false },
-    { stepIndex: 1, isComplete: false }
-  ];
-
-  beforeEach(async () => {
-    wizardStepsService = ({
-      setTotalSteps: jest.fn(),
-      setCurrentStep: jest.fn(),
-      steps$: new BehaviorSubject<WizardStepModel[]>(mockSteps)
-    } as unknown) as jest.Mocked<WizardStepsService>;
-
-    router = ({
-      navigate: jest.fn()
-    } as unknown) as jest.Mocked<Router>;
-
-    await TestBed.configureTestingModule({
-      imports: [ReactiveFormsModule, RadioModule],
-      declarations: [CephfsMirroringWizardComponent],
-      providers: [
-        FormBuilder,
-        { provide: WizardStepsService, useValue: wizardStepsService },
-        { provide: Router, useValue: router }
-      ],
-      schemas: [NO_ERRORS_SCHEMA]
-    }).compileComponents();
-
-    fixture = TestBed.createComponent(CephfsMirroringWizardComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create the component', () => {
-    expect(component).toBeTruthy();
-  });
-
-  it('should initialize wizard steps on ngOnInit', () => {
-    expect(wizardStepsService.setTotalSteps).toHaveBeenCalledWith(
-      STEP_TITLES_MIRRORING_CONFIGURED.length
-    );
-
-    expect(component.steps.length).toBe(STEP_TITLES_MIRRORING_CONFIGURED.length);
-  });
-
-  it('should navigate to step when goToStep is called', () => {
-    component.goToStep(mockSteps[0]);
-
-    expect(wizardStepsService.setCurrentStep).toHaveBeenCalledWith(mockSteps[0]);
-  });
-
-  it('should initialize form with local role selected', () => {
-    expect(component.form.value).toEqual({
-      localRole: LOCAL_ROLE,
-      remoteRole: null
-    });
-  });
-
-  it('should update form on local role change', () => {
-    component.onLocalRoleChange();
-
-    expect(component.form.value).toEqual({
-      localRole: LOCAL_ROLE,
-      remoteRole: null
-    });
-  });
-
-  it('should update form on remote role change', () => {
-    component.onRemoteRoleChange();
-
-    expect(component.form.value).toEqual({
-      localRole: null,
-      remoteRole: REMOTE_ROLE
-    });
-  });
-
-  it('should navigate to mirroring list on cancel', () => {
-    component.onCancel();
-    expect(router.navigate).toHaveBeenCalledWith(['/cephfs/mirroring']);
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.ts
deleted file mode 100644 (file)
index 7555460..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-import { Component, OnInit, inject } from '@angular/core';
-import { Step } from 'carbon-components-angular';
-import { Router } from '@angular/router';
-import {
-  STEP_TITLES_MIRRORING_CONFIGURED,
-  LOCAL_ROLE,
-  REMOTE_ROLE
-} from './cephfs-mirroring-wizard-step.enum';
-import { WizardStepsService } from '~/app/shared/services/wizard-steps.service';
-import { WizardStepModel } from '~/app/shared/models/wizard-steps';
-import { FormBuilder, FormGroup } from '@angular/forms';
-import { FilesystemRow } from '~/app/shared/models/cephfs.model';
-@Component({
-  selector: 'cd-cephfs-mirroring-wizard',
-  templateUrl: './cephfs-mirroring-wizard.component.html',
-  standalone: false,
-  styleUrls: ['./cephfs-mirroring-wizard.component.scss']
-})
-export class CephfsMirroringWizardComponent implements OnInit {
-  steps: Step[] = [];
-  title: string = $localize`Create new CephFS Mirroring`;
-  description: string = $localize`Configure a new mirroring relationship between clusters`;
-  form: FormGroup;
-  showMessage: boolean = true;
-  selectedFilesystem: FilesystemRow | null = null;
-  selectedEntity: string | null = null;
-
-  LOCAL_ROLE = LOCAL_ROLE;
-  REMOTE_ROLE = REMOTE_ROLE;
-
-  private wizardStepsService = inject(WizardStepsService);
-  private fb = inject(FormBuilder);
-  private router = inject(Router);
-
-  sourceList: string[] = [
-    $localize`Sends data to remote clusters`,
-    $localize`Requires bootstrap token from target`,
-    $localize`Manages snapshot schedules`
-  ];
-
-  targetList: string[] = [
-    $localize`Receives data from source clusters`,
-    $localize`Generates bootstrap token`,
-    $localize`Stores replicated snapshots`
-  ];
-
-  constructor() {
-    this.form = this.fb.group({
-      localRole: [LOCAL_ROLE],
-      remoteRole: [null]
-    });
-  }
-
-  ngOnInit() {
-    this.wizardStepsService.setTotalSteps(STEP_TITLES_MIRRORING_CONFIGURED.length);
-
-    const stepsData = this.wizardStepsService.steps$.value;
-    this.steps = STEP_TITLES_MIRRORING_CONFIGURED.map((title, index) => ({
-      label: title,
-      onClick: () => this.goToStep(stepsData[index]),
-      invalid: true
-    }));
-  }
-
-  onFilesystemSelected(filesystem: FilesystemRow) {
-    this.selectedFilesystem = filesystem;
-    if (this.steps[1]) {
-      this.steps[1].invalid = !filesystem;
-    }
-  }
-
-  onEntitySelected(entity: string) {
-    this.selectedEntity = entity;
-    if (this.steps[2]) {
-      this.steps[2].invalid = !entity;
-    }
-  }
-
-  goToStep(step: WizardStepModel) {
-    if (step) {
-      this.wizardStepsService.setCurrentStep(step);
-    }
-  }
-
-  onLocalRoleChange() {
-    this.form.patchValue({ localRole: LOCAL_ROLE, remoteRole: null });
-    this.showMessage = false;
-    if (this.steps[0]) {
-      this.steps[0].invalid = false;
-    }
-  }
-
-  onRemoteRoleChange() {
-    this.form.patchValue({ localRole: null, remoteRole: REMOTE_ROLE });
-    this.showMessage = true;
-    if (this.steps[0]) {
-      this.steps[0].invalid = false;
-    }
-  }
-
-  onSubmit() {}
-
-  onCancel() {
-    this.router.navigate(['/cephfs/mirroring']);
-  }
-}
index 92d39708d09d61731724849752c2edb4584989a1..a8e17adbefcc2f661e182389fcbdcf8ea6120b4b 100644 (file)
@@ -64,9 +64,10 @@ 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 Renew16 from '@carbon/icons/es/renew/16';
-import { CephfsMirroringWizardComponent } from './cephfs-mirroring-wizard/cephfs-mirroring-wizard.component';
-import { CephfsFilesystemSelectorComponent } from './cephfs-filesystem-selector/cephfs-filesystem-selector.component';
-import { CephfsMirroringEntityComponent } from './cephfs-mirroring-entity/cephfs-mirroring-entity.component';
+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';
 
 @NgModule({
   imports: [
@@ -127,15 +128,22 @@ import { CephfsMirroringEntityComponent } from './cephfs-mirroring-entity/cephfs
     CephfsMountDetailsComponent,
     CephfsAuthModalComponent,
     CephfsMirroringListComponent,
-    CephfsMirroringWizardComponent,
-    CephfsFilesystemSelectorComponent,
-    CephfsMirroringErrorComponent,
-    CephfsMirroringEntityComponent
+    CephfsMirroringErrorComponent
   ],
   providers: [provideCharts(withDefaultRegisterables())]
 })
 export class CephfsModule {
   constructor(private iconService: IconService) {
-    this.iconService.registerAll([AddIcon, LaunchIcon, Close, Trash, Renew16]);
+    this.iconService.registerAll([
+      AddIcon,
+      LaunchIcon,
+      Close,
+      Trash,
+      Renew16,
+      ReplicateIcon,
+      ReplicateIcon24,
+      ShareIcon,
+      ShareIcon24
+    ]);
   }
 }
index dfcd7523a08c7a7012fc1fc74888e02007ab274b..eca9ce8442d41c501402e36bba83d00db5c29e62 100644 (file)
@@ -11,7 +11,7 @@
       novalidate
     >
       <div
-        i18n="form title|Example: Create Pool@@formTitle"
+        i18n="form title|Example: Create Pool@@poolFormTitle"
         class="form-header"
       >
         {{ action | titlecase }} {{ resource | upperFirst }}
index 95d610d26d3a7360095780f48cb9c7154c57bf77..da63bd561f2bcf009adc491e1d0170fbd1f58c4c 100644 (file)
@@ -22,6 +22,7 @@
       @if(pageHeaderTitle) {
       <cd-page-header
                       [title]="pageHeaderTitle"
+                      [subtitle]="pageHeaderSubtitle"
                       [description]="pageHeaderDescription">
       </cd-page-header>
       }
index d7983d6247f7958204eafcf70f25db63de15aff5..7c149092d1bc08f8f9842e270a28781c52a972d8 100644 (file)
@@ -30,6 +30,7 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy {
   private subs = new Subscription();
   permissions: Permissions;
   pageHeaderTitle: string | null = null;
+  pageHeaderSubtitle: string | null = null;
   pageHeaderDescription: string | null = null;
   enabledFeature$: Observable<FeatureTogglesMap>;
 
@@ -91,9 +92,10 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy {
       route = route.firstChild;
     }
     const pageHeader = route?.routeConfig?.data?.['pageHeader'] as
-      | { title?: string; description?: string }
+      | { title?: string; subtitle?: string; description?: string }
       | undefined;
     this.pageHeaderTitle = pageHeader?.title ?? null;
+    this.pageHeaderSubtitle = pageHeader?.subtitle ?? null;
     this.pageHeaderDescription = pageHeader?.description ?? null;
   }
 
index 3b51bd5a7e14a06d2ba6360ff90721a65a4051dc..069ccd78b0056335ea59c11681dc2cf80b859631 100644 (file)
@@ -2,6 +2,9 @@
   <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>
       }
index cb2ac7d3fdb96d88e10caa68c84f80de3f4013db..f700127f3905b8dc7d27cfca244ab8946e60de44 100644 (file)
@@ -5,7 +5,7 @@ import { Component, Input, ViewEncapsulation } from '@angular/core';
  * @see https://ibm-products.carbondesignsystem.com/?path=/docs/components-pageheader--overview
  *
  * Usage:
- * <cd-page-header title="Page title" description="Optional description">
+ * <cd-page-header title="Page title" subtitle="Optional subtitle" description="Optional description">
  * </cd-page-header>
  */
 @Component({
@@ -17,5 +17,6 @@ import { Component, Input, ViewEncapsulation } from '@angular/core';
 })
 export class PageHeaderComponent {
   @Input({ required: true }) title: string;
+  @Input() subtitle: string = '';
   @Input() description: string = '';
 }
index 22673b0ac2d46e3cd969cd11d3a410234d4fec17..4786fde842517e1e2d3a5334b75ccb116081e503 100644 (file)
@@ -388,6 +388,7 @@ 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.`
+  title: $localize`Filesystem Mirroring`,
+  subtitle: $localize`Manage snapshot-based replication for CephFS across clusters.`,
+  description: $localize`Configure mirroring between filesystems and monitor replication status.`
 };
index c5190555b6a02294f92f01a4483d3c1baeb953b4..e95cd5fc44e095adb219b54045e9b52f339e3772 100644 (file)
@@ -29,6 +29,7 @@ export enum Icons {
   expand = 'maximize', // Expand cluster
   user = 'user', // User, Initiators
   users = 'user--multiple', // Users, Groups
+  replicate = 'replicate', // replicate
   share = 'share', // share
   key = 'password', // S3 Keys, Swift Keys, Authentication
   warning = 'warning--alt--filled', // Notification warning
@@ -175,7 +176,9 @@ export const ICON_TYPE = {
   rightArrow: 'caret--right',
   locked: 'locked',
   cloudMonitoring: 'cloud--monitoring',
-  trash: 'trash-can'
+  trash: 'trash-can',
+  replicate: 'replicate',
+  share: 'share'
 } as const;
 
 export const EMPTY_STATE_IMAGE = {
index ac3a20cea999241dde26a177ab8fa5ed14239c3a..ef04f9b6fa73df968d243938dc93967e803eabfe 100644 (file)
   margin-right: layout.$spacing-05;
 }
 
+.cds-mr-12 {
+  margin-right: layout.$spacing-12;
+}
+
 .cds-pt-6 {
   padding-top: layout.$spacing-06;
 }