};
const mgrModuleServiceMock = {
- updateModuleState: jest.fn(),
- updateCompleted$: { subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }) }
+ updateModuleState: jest.fn()
};
beforeEach(async () => {
expect(component).toBeTruthy();
});
- it('should call mgrModuleService.updateModuleState when enableModule is called', () => {
+ it('should enable mirroring and snap_schedule modules when enableModule is called', () => {
fixture.detectChanges();
component.enableModule();
expect(mgrModuleServiceMock.updateModuleState).toHaveBeenCalledWith(
- 'mirroring',
+ ['mirroring', 'snap_schedule'],
false,
null,
'cephfs/mirroring',
enableModule(): void {
this.mgrModuleService.updateModuleState(
- 'mirroring',
+ ['mirroring', 'snap_schedule'],
false,
null,
'cephfs/mirroring',
- $localize`CephFS Mirroring module enabled`,
+ $localize`CephFS Mirroring and Snapshot Schedule modules enabled`,
false,
- $localize`Enabling CephFS Mirroring. Reconnecting, please wait ...`
+ $localize`Enabling CephFS Mirroring and Snapshot schedule modules. Reconnecting, please wait ...`
);
}
}
currentSyncSnapshotTpl!: TemplateRef<unknown>;
private cephfsService = inject(CephfsService);
+ private snapshotScheduleService = inject(CephfsSnapshotScheduleService);
private route = inject(ActivatedRoute);
private formatterService = inject(FormatterService);
private authStorageService = inject(AuthStorageService);
itemNames: [path],
actionDescription: 'remove',
submitActionObservable: () =>
- this.taskWrapper
- .wrapTaskAroundCall({
- task: new FinishedTask('cephfs/mirroring/path/remove', {
- fsName: this.fsName,
- path
- }),
- call: this.cephfsService.removeMirrorDirectory(this.fsName, path).pipe(
- tap(() => {
- if (this.selectedPath?.path === path) {
- this.closeSidePanel();
- }
- this.loadMirrorPaths();
- })
- )
- })
+ this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('cephfs/mirroring/path/remove', {
+ fsName: this.fsName,
+ path
+ }),
+ call: this.cephfsService.removeMirrorDirectory(this.fsName, path).pipe(
+ tap(() => {
+ if (this.selectedPath?.path === path) {
+ this.closeSidePanel();
+ }
+ this.loadMirrorPaths();
+ })
+ )
+ })
});
}
})
export class CephfsSnapshotscheduleFormComponent
extends CdForm
- implements OnInit, OnChanges, TearsheetStep
-{
+ implements OnInit, OnChanges, TearsheetStep {
@Input() embedded = false;
@Input() hideDirectory = false;
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
-import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import {
+ ComponentFixture,
+ discardPeriodicTasks,
+ fakeAsync,
+ flush,
+ TestBed,
+ tick
+} from '@angular/core/testing';
import { of as observableOf, throwError as observableThrowError } from 'rxjs';
import { configureTestBed } from '~/testing/unit-test-helper';
expect(blockUIService.stop).toHaveBeenCalled();
}));
+ it('should enable multiple modules sequentially', fakeAsync(() => {
+ const summaryService = TestBed.inject(SummaryService);
+ spyOn(service, 'enable').and.returnValues(
+ observableThrowError('mirroring reconnect'),
+ observableOf(null)
+ );
+ spyOn(service, 'list').and.returnValue(observableOf([]));
+ spyOn(notificationService, 'show');
+ spyOn(service.updateCompleted$, 'next');
+
+ service.updateModuleState(
+ ['mirroring', 'snap_schedule'],
+ false,
+ null,
+ '',
+ 'Enabled mirroring modules'
+ );
+ tick(service.REFRESH_INTERVAL);
+ flush();
+
+ expect(service.enable).toHaveBeenCalledWith('mirroring', false);
+ expect(service.enable).toHaveBeenCalledWith('snap_schedule', false);
+ expect(service.list).toHaveBeenCalledTimes(1);
+ expect(notificationService.show).toHaveBeenCalledWith(
+ jasmine.any(Number),
+ jasmine.any(String)
+ );
+ expect(service.updateCompleted$.next).toHaveBeenCalled();
+ expect(summaryService.startPolling).toHaveBeenCalled();
+ discardPeriodicTasks();
+ }));
+
it('should not disable module without selecting one', () => {
expect(component.getTableActionDisabledDesc()).toBeTruthy();
});
import { Injectable } from '@angular/core';
import { BlockUIService } from 'ng-block-ui';
-import { Observable, Subject, timer } from 'rxjs';
+import { from, Observable, Subject, timer } from 'rxjs';
import { NotificationService } from '../services/notification.service';
import { TableComponent } from '../datatable/table/table.component';
import { Router } from '@angular/router';
import { MgrModuleInfo } from '../models/mgr-modules.interface';
import { NotificationType } from '../enum/notification-type.enum';
-import { delay, retryWhen, switchMap, tap } from 'rxjs/operators';
+import { catchError, concatMap, delay, map, retryWhen, switchMap, tap } from 'rxjs/operators';
import { SummaryService } from '../services/summary.service';
const GLOBAL = 'global';
/**
* Update the Ceph Mgr module state to enabled or disabled.
+ * @param modules One module name or a list of module names to enable/disable sequentially.
*/
updateModuleState(
- module: string,
+ modules: string | string[],
enabled: boolean = false,
table: TableComponent = null,
navigateTo: string = '',
reconnectingMessage: string = $localize`Reconnecting, please wait ...`,
force: boolean = false
): void {
+ const moduleList = Array.isArray(modules) ? modules : [modules];
+
+ from(moduleList)
+ .pipe(
+ concatMap((module) =>
+ this.toggleModuleWithReconnect(module, enabled, force, reconnectingMessage)
+ )
+ )
+ .subscribe({
+ complete: () => {
+ this.completeModuleStateUpdate(table, navigateTo, notificationText, navigateByUrl);
+ }
+ });
+ }
+
+ private toggleModuleWithReconnect(
+ module: string,
+ enabled: boolean,
+ force: boolean,
+ reconnectingMessage: string
+ ): Observable<void> {
const moduleToggle$ = enabled ? this.disable(module) : this.enable(module, force);
- moduleToggle$.subscribe({
- next: () => {
- // Module toggle succeeded
- this.updateCompleted$.next();
- },
- error: () => {
- // Module toggle failed, trigger reconnect flow
- this.notificationService.suspendToasties(true);
- this.blockUI.start(GLOBAL, reconnectingMessage);
-
- timer(this.REFRESH_INTERVAL)
- .pipe(
- switchMap(() => this.list()),
- retryWhen((errors) =>
- errors.pipe(
- tap(() => {
- // Keep retrying until list() succeeds
- }),
- delay(this.REFRESH_INTERVAL)
- )
- )
- )
- .subscribe({
- next: () => {
- // Reconnection successful
- this.notificationService.suspendToasties(false);
- this.blockUI.stop(GLOBAL);
-
- if (table) {
- table.refreshBtn();
- }
-
- if (notificationText) {
- this.notificationService.show(
- NotificationType.success,
- $localize`${notificationText}`
- );
- }
-
- if (navigateTo) {
- const navigate = () => this.router.navigate([navigateTo]);
- if (navigateByUrl) {
- this.router.navigateByUrl('/', { skipLocationChange: true }).then(navigate);
- } else {
- navigate();
- }
- }
-
- this.updateCompleted$.next();
- this.summaryService.startPolling();
- }
- });
+ return moduleToggle$.pipe(
+ map(() => undefined),
+ catchError(() => this.reconnectAfterModuleToggle(reconnectingMessage))
+ );
+ }
+
+ private reconnectAfterModuleToggle(reconnectingMessage: string): Observable<void> {
+ this.notificationService.suspendToasties(true);
+ this.blockUI.start(GLOBAL, reconnectingMessage);
+
+ return timer(this.REFRESH_INTERVAL).pipe(
+ switchMap(() => this.list()),
+ retryWhen((errors) =>
+ errors.pipe(
+ tap(() => {
+ // Keep retrying until list() succeeds
+ }),
+ delay(this.REFRESH_INTERVAL)
+ )
+ ),
+ tap(() => {
+ this.notificationService.suspendToasties(false);
+ this.blockUI.stop(GLOBAL);
+ }),
+ map(() => undefined)
+ );
+ }
+
+ private completeModuleStateUpdate(
+ table: TableComponent,
+ navigateTo: string,
+ notificationText?: string,
+ navigateByUrl?: boolean
+ ): void {
+ if (table) {
+ table.refreshBtn();
+ }
+
+ if (notificationText) {
+ this.notificationService.show(NotificationType.success, $localize`${notificationText}`);
+ }
+
+ if (navigateTo) {
+ const navigate = () => this.router.navigate([navigateTo]);
+ if (navigateByUrl) {
+ this.router.navigateByUrl('/', { skipLocationChange: true }).then(navigate);
+ } else {
+ navigate();
}
- });
+ }
+
+ this.updateCompleted$.next();
+ this.summaryService.startPolling();
}
}
it('should show empty permissions', () => {
expect(new Permissions({})).toEqual({
cephfs: { create: false, delete: false, read: false, update: false },
+ cephfsMirror: { create: false, delete: false, read: false, update: false },
configOpt: { create: false, delete: false, read: false, update: false },
grafana: { create: false, delete: false, read: false, update: false },
hosts: { create: false, delete: false, read: false, update: false },
it('should show full permissions', () => {
const fullyGranted = {
cephfs: ['create', 'read', 'update', 'delete'],
+ 'cephfs-mirror': ['create', 'read', 'update', 'delete'],
'config-opt': ['create', 'read', 'update', 'delete'],
grafana: ['create', 'read', 'update', 'delete'],
hosts: ['create', 'read', 'update', 'delete'],
};
expect(new Permissions(fullyGranted)).toEqual({
cephfs: { create: true, delete: true, read: true, update: true },
+ cephfsMirror: { create: true, delete: true, read: true, update: true },
configOpt: { create: true, delete: true, read: true, update: true },
grafana: { create: true, delete: true, read: true, update: true },
hosts: { create: true, delete: true, read: true, update: true },