</cd-setup-step-card>
</div>
</div>
-
- @if (isAllConfigured) {
- <div class="nvmeof-setup-cards__completion">
- <a
- cdsLink
- class="cds--link--disabled"
- aria-disabled="true"
- i18n
- >Configuration complete. View status →</a
- >
- </div>
- }
</cd-productive-card>
import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterModule } from '@angular/router';
import { NvmeofSetupCardsComponent } from './nvmeof-setup-cards.component';
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [NvmeofSetupCardsComponent, RouterModule.forRoot([])]
+ imports: [NvmeofSetupCardsComponent]
}).compileComponents();
fixture = TestBed.createComponent(NvmeofSetupCardsComponent);
expect(cards.length).toBe(3);
});
- it('should not show completion link when isAllConfigured is false', () => {
- component.isAllConfigured = false;
- fixture.detectChanges();
- const link = fixture.nativeElement.querySelector('.nvmeof-setup-cards__completion');
- expect(link).toBeNull();
- });
-
- it('should show completion link when isAllConfigured is true', () => {
- component.isAllConfigured = true;
- fixture.detectChanges();
- const link = fixture.nativeElement.querySelector('.nvmeof-setup-cards__completion');
- expect(link).toBeTruthy();
- });
-
- it('should not emit viewStatus from the disabled completion link', () => {
- component.isAllConfigured = true;
- fixture.detectChanges();
-
- const emitSpy = jest.spyOn(component.viewStatus, 'emit');
- const link = fixture.nativeElement.querySelector('.nvmeof-setup-cards__completion a');
- link.click();
-
- expect(emitSpy).not.toHaveBeenCalled();
- });
-
describe('setup state', () => {
const getStepCards = () =>
fixture.debugElement.queryAll((el) => el.name === 'cd-setup-step-card');
});
});
- it('should keep subsystem and namespace info messages when hasGatewayGroups is false', () => {
+ it('should show info messages for subsystem and namespace steps when hasGatewayGroups is false', () => {
component.hasGatewayGroups = false;
component.hasSubsystems = false;
component.hasNamespaces = false;
expect(namespaceCard.statusMessage).toBe('No namespace allocated or mapped yet.');
});
- it('should display original info messages when hasGatewayGroups is true', () => {
- component.hasGatewayGroups = true;
- component.hasSubsystems = false;
- component.hasNamespaces = false;
- fixture.detectChanges();
-
- const cardElements = fixture.debugElement.queryAll((el) => el.name === 'cd-setup-step-card');
- const subsystemCard = cardElements[1].componentInstance;
- const namespaceCard = cardElements[2].componentInstance;
-
- expect(subsystemCard.statusMessage).toBe('No subsystem configured for this cluster yet.');
- expect(namespaceCard.statusMessage).toBe('No namespace allocated or mapped yet.');
- });
-
it('should display gateway info message when hasGatewayGroups is false', () => {
component.hasGatewayGroups = false;
fixture.detectChanges();
import { CommonModule } from '@angular/common';
-import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
+import { Component, Input, ViewEncapsulation } from '@angular/core';
import { RouterModule } from '@angular/router';
-import { LayoutModule, LayerModule, LinkModule, TilesModule } from 'carbon-components-angular';
+import { LayoutModule, LayerModule, TilesModule } from 'carbon-components-angular';
import { ProductiveCardComponent } from '~/app/shared/components/productive-card/productive-card.component';
import { SetupStepCardComponent } from '~/app/shared/components/setup-step-card/setup-step-card.component';
LayoutModule,
LayerModule,
TilesModule,
- LinkModule,
ProductiveCardComponent,
SetupStepCardComponent
]
@Input() hasGatewayGroups = false;
@Input() hasSubsystems = false;
@Input() hasNamespaces = false;
- @Input() isAllConfigured = false;
- @Output() viewStatus = new EventEmitter<void>();
readonly cards = {
gateway: {
[hasGatewayGroups]="hasGatewayGroups"
[hasSubsystems]="hasSubsystems"
[hasNamespaces]="hasNamespaces"
- [isAllConfigured]="isAllConfigured"
>
</cd-nvmeof-setup-cards>
}
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ActivatedRoute, Event as RouterEvent, NavigationEnd, Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { NvmeofService } from '~/app/shared/api/nvmeof.service';
import { NvmeofStateService } from '../nvmeof-state.service';
-import { NvmeofTabsComponent } from './nvmeof-tabs.component';
+import { NvmeofTabsComponent, SETUP_CARDS_HIDE_DELAY_MS } from './nvmeof-tabs.component';
import { SharedModule } from '~/app/shared/shared.module';
import { NvmeofSetupCardsComponent } from '../nvmeof-setup-cards/nvmeof-setup-cards.component';
});
});
+ afterEach(() => {
+ fixture?.destroy();
+ });
+
it('should create', () => {
expect(component).toBeTruthy();
});
);
});
- it('should render success setup card messages before gateway groups are removed', () => {
+ it('should keep setup cards visible briefly when isAllConfigured becomes true', fakeAsync(() => {
setRouterUrl('/block/nvmeof/gateways');
+ setSetupState({ hasGatewayGroups: false, hasSubsystems: false, hasNamespaces: false });
component.ngOnInit();
+ fixture.detectChanges();
+
+ setSetupState({ hasGatewayGroups: true, hasSubsystems: true, hasNamespaces: true });
emitRefresh();
fixture.detectChanges();
- component.showSetupCards = true;
+
+ expect(component.isAllConfigured).toBe(true);
+ expect(component.showSetupCards).toBe(true);
+ expect(fixture.debugElement.query((el) => el.name === 'cd-nvmeof-setup-cards')).toBeTruthy();
+
+ tick(SETUP_CARDS_HIDE_DELAY_MS);
fixture.detectChanges();
- const cardElements = fixture.debugElement.queryAll((el) => el.name === 'cd-setup-step-card');
+ expect(component.showSetupCards).toBe(false);
+ expect(fixture.debugElement.query((el) => el.name === 'cd-nvmeof-setup-cards')).toBeNull();
+ }));
- expect(cardElements[0].componentInstance.statusMessage).toBe(
- 'Gateway group configured successfully.'
- );
- expect(cardElements[1].componentInstance.statusMessage).toBe(
- 'Subsystem configured successfully.'
- );
- expect(cardElements[2].componentInstance.statusMessage).toBe(
- 'Namespaces mapped successfully.'
- );
+ it('should hide setup cards after the success delay when already fully configured', fakeAsync(() => {
+ setRouterUrl('/block/nvmeof/gateways');
+ component.ngOnInit();
+ emitRefresh();
+ fixture.detectChanges();
+
+ expect(component.isAllConfigured).toBe(true);
+ expect(component.showSetupCards).toBe(true);
+
+ tick(SETUP_CARDS_HIDE_DELAY_MS);
+ fixture.detectChanges();
+
+ const setupCards = fixture.debugElement.query((el) => el.name === 'cd-nvmeof-setup-cards');
+ expect(setupCards).toBeNull();
+ }));
+
+ it('should show setup cards when isAllConfigured is false', () => {
+ setRouterUrl('/block/nvmeof/gateways');
+ setSetupState({ hasGatewayGroups: false, hasSubsystems: false, hasNamespaces: false });
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const setupCards = fixture.debugElement.query((el) => el.name === 'cd-nvmeof-setup-cards');
+ expect(setupCards).toBeTruthy();
});
+
+ it('should cancel the hide timer and keep setup cards visible when configuration becomes incomplete while timer is pending', fakeAsync(() => {
+ setRouterUrl('/block/nvmeof/gateways');
+ setSetupState({ hasGatewayGroups: false, hasSubsystems: false, hasNamespaces: false });
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ // Transition to fully configured — starts the 5-second hide timer
+ setSetupState({ hasGatewayGroups: true, hasSubsystems: true, hasNamespaces: true });
+ emitRefresh();
+ fixture.detectChanges();
+
+ expect(component.isAllConfigured).toBe(true);
+ expect(component.showSetupCards).toBe(true);
+
+ // Before the timer fires, configuration becomes incomplete again
+ setSetupState({ hasGatewayGroups: true, hasSubsystems: false, hasNamespaces: false });
+ emitRefresh();
+ fixture.detectChanges();
+
+ expect(component.isAllConfigured).toBe(false);
+ // Timer must have been cancelled: cards remain visible for the incomplete state
+ expect(component.showSetupCards).toBe(true);
+
+ // Advance past the original timer deadline — cards must still be visible
+ tick(SETUP_CARDS_HIDE_DELAY_MS);
+ fixture.detectChanges();
+
+ expect(component.showSetupCards).toBe(true);
+ }));
+
+ it('should show setup cards again if configuration becomes incomplete after dismiss', fakeAsync(() => {
+ setRouterUrl('/block/nvmeof/gateways');
+ component.ngOnInit();
+ tick(SETUP_CARDS_HIDE_DELAY_MS);
+ expect(component.showSetupCards).toBe(false);
+
+ setSetupState({ hasGatewayGroups: true, hasSubsystems: false, hasNamespaces: false });
+ emitRefresh();
+ fixture.detectChanges();
+
+ expect(component.showSetupCards).toBe(true);
+ expect(component.isAllConfigured).toBe(false);
+ }));
});
});
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
-import { Subscription, merge, of } from 'rxjs';
+import { Subscription, merge, of, timer } from 'rxjs';
import { filter, switchMap, tap } from 'rxjs/operators';
import { NvmeofService } from '~/app/shared/api/nvmeof.service';
const NVMEOF_PATH = 'block/nvmeof';
+/** How long to keep showing success cards after gateway/subsystem/namespace exist. */
+export const SETUP_CARDS_HIDE_DELAY_MS = 5000;
+
enum TABS {
gateways = 'gateways',
subsystems = 'subsystems',
hasNamespaces = false;
isAllConfigured = false;
private setupSubscription?: Subscription;
+ private hideSetupCardsSubscription?: Subscription;
+ private setupCardsDismissed = false;
constructor(
private router: Router,
this.showTabsShell = TAB_ROUTES.includes(primaryPath);
}
+ private scheduleHideSetupCards(): void {
+ if (this.hideSetupCardsSubscription || this.setupCardsDismissed) {
+ return;
+ }
+ this.hideSetupCardsSubscription = timer(SETUP_CARDS_HIDE_DELAY_MS).subscribe(() => {
+ this.showSetupCards = false;
+ this.setupCardsDismissed = true;
+ this.hideSetupCardsSubscription = undefined;
+ });
+ }
+
+ private cancelHideSetupCards(): void {
+ this.hideSetupCardsSubscription?.unsubscribe();
+ this.hideSetupCardsSubscription = undefined;
+ }
+
+ private updateSetupCardsVisibility(): void {
+ if (this.isAllConfigured) {
+ if (this.setupCardsDismissed) {
+ this.showSetupCards = false;
+ return;
+ }
+ // Keep success state visible briefly, then dismiss the banner.
+ this.showSetupCards = true;
+ this.scheduleHideSetupCards();
+ return;
+ }
+
+ this.setupCardsDismissed = false;
+ this.cancelHideSetupCards();
+ this.showSetupCards = true;
+ }
+
ngOnInit(): void {
this.updateActiveTab(this.router.url);
this.updateShellVisibility(this.router.url);
this.hasSubsystems = hasSubsystems;
this.hasNamespaces = hasNamespaces;
this.isAllConfigured = hasGatewayGroups && hasSubsystems && hasNamespaces;
- this.showSetupCards = true;
+ this.updateSetupCardsVisibility();
});
}
ngOnDestroy(): void {
+ this.cancelHideSetupCards();
this.setupSubscription?.unsubscribe();
}