import { LoginLayoutComponent } from './core/layouts/login-layout/login-layout.component';
import { WorkbenchLayoutComponent } from './core/layouts/workbench-layout/workbench-layout.component';
import { ApiDocsComponent } from './core/navigation/api-docs/api-docs.component';
-import { ActionLabels, URLVerbs } from './shared/constants/app.constants';
+import {
+ ActionLabels,
+ CEPHFS_MIRRORING_PAGE_HEADER,
+ URLVerbs
+} from './shared/constants/app.constants';
import { CrudFormComponent } from './shared/forms/crud-form/crud-form.component';
import { CRUDTableComponent } from './shared/datatable/crud-table/crud-table.component';
import { BreadcrumbsResolver, IBreadcrumb } from './shared/models/breadcrumbs';
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';
@Injectable()
export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver {
children: [
{ path: 'overview', component: DashboardComponent },
{ path: 'error', component: ErrorComponent },
+ {
+ path: 'cephfs/mirroring/error',
+ component: CephfsMirroringErrorComponent,
+ data: {
+ breadcrumbs: 'File/Mirroring',
+ pageHeader: CEPHFS_MIRRORING_PAGE_HEADER
+ }
+ },
+
// Cluster
{
path: 'notifications',
},
{
path: 'mirroring',
+ canActivate: [ModuleStatusGuardService],
component: CephfsMirroringListComponent,
- data: { breadcrumbs: 'File/Mirroring' }
+ data: {
+ moduleStatusGuardConfig: {
+ uiApiPath: 'cephfs/mirror',
+ redirectTo: 'cephfs/mirroring/error',
+ module_name: 'mirroring',
+ navigate_to: 'File/Mirroring'
+ },
+ breadcrumbs: 'File/Mirroring',
+ pageHeader: CEPHFS_MIRRORING_PAGE_HEADER
+ }
},
{
path: `mirroring/${URLVerbs.CREATE}`,
--- /dev/null
+<cds-inline-notification
+ [notificationObj]="{
+ type: 'warning',
+ title: 'CephFS Mirroring module is not enabled',
+ message: 'To create mirror links and configure replication, the CephFS Mirroring module must be enabled on this cluster.',
+ lowContrast: true,
+ showClose: false
+ }"
+ class="mt-2 mb-2 full-width padding-inline-0"
+ i18n></cds-inline-notification>
+<cds-tile>
+ <p class="cds--type-heading-compact-01"
+ i18n>Enable CephFS Mirroring</p>
+ <p class="cds--type-body-compact-01"
+ i18n>Turn on CephFS Mirroring to start creating mirror links and synchronizing data across clusters. After enabling, you can add mirror links.</p>
+ <button cdsButton="primary"
+ (click)="enableModule()"
+ i18n>Enable CephFS Mirroring</button>
+</cds-tile>
+<div cdsGrid
+ class="mt-5">
+ <div cdsRow>
+ <div cdsCol>
+ <img src="assets/empty-state.png"
+ alt="no-mirror-links" />
+ </div>
+ </div>
+ <div cdsRow>
+ <div cdsCol>
+ <p class="cds--body-compact-02">No CephFS mirror links available</p>
+ </div>
+ </div>
+</div>
--- /dev/null
+// Stack title above description when notification is full-width (Carbon lays them
+// in a row by default).
+cds-inline-notification.full-width {
+ max-inline-size: 100%;
+
+ [class*='inline-notification__text-wrapper'] {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ [class*='inline-notification__title'],
+ [class*='inline-notification__subtitle'] {
+ display: block;
+ }
+}
--- /dev/null
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { Router } from '@angular/router';
+import { of } from 'rxjs';
+
+import { CephfsMirroringErrorComponent } from './cephfs-mirroring-error.component';
+import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { RouterTestingModule } from '@angular/router/testing';
+
+describe('CephfsMirroringErrorComponent', () => {
+ let component: CephfsMirroringErrorComponent;
+ let fixture: ComponentFixture<CephfsMirroringErrorComponent>;
+
+ const routerMock = {
+ events: of({}),
+ onSameUrlNavigation: 'reload' as const,
+ navigate: jest.fn()
+ };
+
+ const mgrModuleServiceMock = {
+ updateModuleState: jest.fn(),
+ updateCompleted$: { subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }) }
+ };
+
+ beforeEach(async () => {
+ jest.clearAllMocks();
+
+ await TestBed.configureTestingModule({
+ declarations: [CephfsMirroringErrorComponent],
+ imports: [SharedModule, RouterTestingModule],
+ providers: [
+ { provide: Router, useValue: routerMock },
+ { provide: MgrModuleService, useValue: mgrModuleServiceMock }
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(CephfsMirroringErrorComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it('should call mgrModuleService.updateModuleState when enableModule is called', () => {
+ fixture.detectChanges();
+ component.enableModule();
+ expect(mgrModuleServiceMock.updateModuleState).toHaveBeenCalledWith(
+ 'mirroring',
+ false,
+ null,
+ 'cephfs/mirroring',
+ expect.any(String),
+ false,
+ expect.any(String)
+ );
+ });
+});
--- /dev/null
+import { Component, ViewEncapsulation } from '@angular/core';
+import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
+
+@Component({
+ selector: 'cd-cephfs-mirroring-error',
+ templateUrl: './cephfs-mirroring-error.component.html',
+ styleUrls: ['./cephfs-mirroring-error.component.scss'],
+ encapsulation: ViewEncapsulation.None,
+ standalone: false
+})
+export class CephfsMirroringErrorComponent {
+ constructor(private mgrModuleService: MgrModuleService) {}
+
+ enableModule(): void {
+ this.mgrModuleService.updateModuleState(
+ 'mirroring',
+ false,
+ null,
+ 'cephfs/mirroring',
+ $localize`CephFS Mirroring module enabled`,
+ false,
+ $localize`Enabling CephFS Mirroring. Reconnecting, please wait ...`
+ );
+ }
+}
-<cd-page-header
- i18n-title
- title="CephFS Mirroring"
- i18n-description
- description="Centralised view of all CephFS Mirroring relationships.">
-</cd-page-header>
-
-<ng-container *ngIf="daemonStatus$ | async as daemonStatus">
-
+@if (daemonStatus$ | async; as daemonStatus) {
<cd-table
#table
[data]="daemonStatus"
selectionType="single"
(updateSelection)="updateSelection($event)"
(fetchData)="loadDaemonStatus()">
- <cd-table-actions class="table-actions"
- [permission]="permission"
- [selection]="selection"
- [tableActions]="tableActions">
- </cd-table-actions>
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
</cd-table>
-</ng-container>
+}
import { CephfsSnapshotscheduleFormComponent } from './cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component';
import { CephfsMountDetailsComponent } from './cephfs-mount-details/cephfs-mount-details.component';
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 {
ButtonModule,
CheckboxModule,
PlaceholderModule,
SelectModule,
TimePickerModule,
+ TilesModule,
TreeviewModule,
TabsModule,
- RadioModule
+ RadioModule,
+ NotificationModule
} from 'carbon-components-angular';
import AddIcon from '@carbon/icons/es/add/32';
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 { CephfsMirroringListComponent } from './cephfs-mirroring-list/cephfs-mirroring-list.component';
import { CephfsMirroringWizardComponent } from './cephfs-mirroring-wizard/cephfs-mirroring-wizard.component';
import { CephfsFilesystemSelectorComponent } from './cephfs-filesystem-selector/cephfs-filesystem-selector.component';
IconModule,
BaseChartDirective,
TabsModule,
- RadioModule
+ RadioModule,
+ TilesModule,
+ NotificationModule
],
declarations: [
CephfsDetailComponent,
CephfsAuthModalComponent,
CephfsMirroringListComponent,
CephfsMirroringWizardComponent,
- CephfsFilesystemSelectorComponent
+ CephfsFilesystemSelectorComponent,
+ CephfsMirroringErrorComponent
],
providers: [provideCharts(withDefaultRegisterables())]
})
<div class="breadcrumbs--padding">
<cd-breadcrumbs></cd-breadcrumbs>
</div>
+ @if(pageHeaderTitle) {
+ <cd-page-header
+ [title]="pageHeaderTitle"
+ [description]="pageHeaderDescription">
+ </cd-page-header>
+ }
<router-outlet></router-outlet>
<cds-placeholder></cds-placeholder>
</div>
import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core';
-import { Router } from '@angular/router';
+import { ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
import { Subscription } from 'rxjs';
+import { filter } from 'rxjs/operators';
import { MultiClusterService } from '~/app/shared/api/multi-cluster.service';
import { Permissions } from '~/app/shared/models/permissions';
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
notifications: string[] = [];
private subs = new Subscription();
permissions: Permissions;
+ pageHeaderTitle: string | null = null;
+ pageHeaderDescription: string | null = null;
+
@HostBinding('class') get class(): string {
return 'top-notification-' + this.notifications.length;
}
})
);
this.faviconService.init();
+
+ this.updatePageHeaderFromRoute();
+ this.subs.add(
+ this.router.events
+ .pipe(filter((e) => e instanceof NavigationEnd))
+ .subscribe(() => this.updatePageHeaderFromRoute())
+ );
}
+
+ private updatePageHeaderFromRoute(): void {
+ let route: ActivatedRouteSnapshot | null = this.router.routerState.snapshot.root;
+ while (route?.firstChild) {
+ route = route.firstChild;
+ }
+ const pageHeader = route?.routeConfig?.data?.['pageHeader'] as
+ | { title?: string; description?: string }
+ | undefined;
+ this.pageHeaderTitle = pageHeader?.title ?? null;
+ this.pageHeaderDescription = pageHeader?.description ?? null;
+ }
+
showTopNotification(name: string, isDisplayed: boolean) {
if (isDisplayed) {
if (!this.notifications.includes(name)) {
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.`
+};
padding-inline: 0;
}
+.padding-inline-0 {
+ padding-inline: 0;
+}
+
/******************************************
Breadcrumbs
******************************************/