]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Recommended first-time setup" banner should disappear once the NVMEoF... 70725/head
authorpujashahu <pshahu@redhat.com>
Thu, 30 Jul 2026 16:54:07 +0000 (22:24 +0530)
committerpujashahu <pshahu@redhat.com>
Mon, 3 Aug 2026 06:53:35 +0000 (12:23 +0530)
Fixes: https://tracker.ceph.com/issues/78888
Signed-off-by: pujaoshahu <pshahu@redhat.com>
 Conflicts:
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.html
- Kept heading="Gateway groups"

Signed-off-by: pujashahu <pshahu@redhat.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-setup-cards/nvmeof-setup-cards.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-setup-cards/nvmeof-setup-cards.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-setup-cards/nvmeof-setup-cards.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-tabs/nvmeof-tabs.component.ts

index f544a4989a0a902200ce21398f2d7111a3c20dc0..975810d9fc417dd776c279c563b87fdd926fd9f6 100644 (file)
       </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>
index b0e7044403df515546355ef32e12689c67e78e8a..67185b581c2de3d5269d9eb3bf7bb3ba4accbfaf 100644 (file)
@@ -1,5 +1,4 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterModule } from '@angular/router';
 
 import { NvmeofSetupCardsComponent } from './nvmeof-setup-cards.component';
 
@@ -9,7 +8,7 @@ describe('NvmeofSetupCardsComponent', () => {
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
-      imports: [NvmeofSetupCardsComponent, RouterModule.forRoot([])]
+      imports: [NvmeofSetupCardsComponent]
     }).compileComponents();
 
     fixture = TestBed.createComponent(NvmeofSetupCardsComponent);
@@ -26,31 +25,6 @@ describe('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');
@@ -90,7 +64,7 @@ describe('NvmeofSetupCardsComponent', () => {
     });
   });
 
-  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;
@@ -104,20 +78,6 @@ describe('NvmeofSetupCardsComponent', () => {
     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();
index dda0027a22b15334e7d278fa989348534a66e588..e09009c12451805bdf0c9833f41c80fc9b666469 100644 (file)
@@ -1,7 +1,7 @@
 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';
 
@@ -17,7 +17,6 @@ import { SetupStepCardComponent } from '~/app/shared/components/setup-step-card/
     LayoutModule,
     LayerModule,
     TilesModule,
-    LinkModule,
     ProductiveCardComponent,
     SetupStepCardComponent
   ]
@@ -26,8 +25,6 @@ export class NvmeofSetupCardsComponent {
   @Input() hasGatewayGroups = false;
   @Input() hasSubsystems = false;
   @Input() hasNamespaces = false;
-  @Input() isAllConfigured = false;
-  @Output() viewStatus = new EventEmitter<void>();
 
   readonly cards = {
     gateway: {
index 6c65b23b337f73eb6e5e832474fa2f2d5e194ca4..b68f1367137dff41a83a597e34c629a48a4a7f85 100644 (file)
@@ -16,7 +16,6 @@
       [hasGatewayGroups]="hasGatewayGroups"
       [hasSubsystems]="hasSubsystems"
       [hasNamespaces]="hasNamespaces"
-      [isAllConfigured]="isAllConfigured"
     >
     </cd-nvmeof-setup-cards>
   }
index 44e148377218593f4d8dd0420bfb94844d8f65fd..40f0d0514b5ce5e9531a6a7fc4d1ebec2b70d0fd 100644 (file)
@@ -1,4 +1,4 @@
-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';
@@ -8,7 +8,7 @@ import { TabsModule } from 'carbon-components-angular';
 
 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';
 
@@ -95,6 +95,10 @@ describe('NvmeofTabsComponent', () => {
     });
   });
 
+  afterEach(() => {
+    fixture?.destroy();
+  });
+
   it('should create', () => {
     expect(component).toBeTruthy();
   });
@@ -394,25 +398,95 @@ describe('NvmeofTabsComponent', () => {
       );
     });
 
-    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);
+    }));
   });
 });
index e8c933fc2fe1524863de8a79a7a6f12d69dbe075..4d89029db579f6b0a97a646e76282a610768d29b 100644 (file)
@@ -1,6 +1,6 @@
 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';
@@ -8,6 +8,9 @@ import { NvmeofStateService } from '../nvmeof-state.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',
@@ -36,6 +39,8 @@ export class NvmeofTabsComponent implements OnInit, OnDestroy {
   hasNamespaces = false;
   isAllConfigured = false;
   private setupSubscription?: Subscription;
+  private hideSetupCardsSubscription?: Subscription;
+  private setupCardsDismissed = false;
 
   constructor(
     private router: Router,
@@ -57,6 +62,39 @@ export class NvmeofTabsComponent implements OnInit, OnDestroy {
     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);
@@ -80,11 +118,12 @@ export class NvmeofTabsComponent implements OnInit, OnDestroy {
         this.hasSubsystems = hasSubsystems;
         this.hasNamespaces = hasNamespaces;
         this.isAllConfigured = hasGatewayGroups && hasSubsystems && hasNamespaces;
-        this.showSetupCards = true;
+        this.updateSetupCardsVisibility();
       });
   }
 
   ngOnDestroy(): void {
+    this.cancelHideSetupCards();
     this.setupSubscription?.unsubscribe();
   }