]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Allow adding authentication to subsystem flow
authorAfreen Misbah <afreen@ibm.com>
Tue, 24 Feb 2026 01:46:26 +0000 (07:16 +0530)
committerAfreen Misbah <afreen@ibm.com>
Fri, 27 Feb 2026 09:16:12 +0000 (14:46 +0530)
- integrates third step with overall tearsheet

Signed-off-by: Afreen Misbah <afreen@ibm.com>
14 files changed:
src/pybind/mgr/dashboard/controllers/nvmeof.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts

index 7655e989477b48fb0f55950a016e30566bd8d6ee..54854ee3989066e64321600eed21e61ce56e3cb7 100644 (file)
@@ -1533,7 +1533,8 @@ else:
         @EndpointDoc("Add one or more initiator hosts to an NVMeoF subsystem",
                      parameters={
                          'subsystem_nqn': (str, 'Subsystem NQN'),
-                         "host_nqn": Param(str, 'Comma separated list of NVMeoF host NQNs'),
+                         "allow_all": Param(bool, 'Allow all hosts. Default is True.'),
+                         "hosts": Param(List, 'List containg host nqn and dhchap key'),
                          "gw_group": Param(str, "NVMeoF gateway group")
                      })
         @empty_response
@@ -1541,19 +1542,36 @@ else:
         @CreatePermission
         def add(self, subsystem_nqn: str,
                 gw_group: str,
-                host_nqn: str = "",
+                hosts: Optional[list[dict]] = None,
+                allow_all: bool = True,
                 server_address: Optional[str] = None):
             response = None
-            all_host_nqns = host_nqn.split(',')
 
-            for nqn in all_host_nqns:
+            if allow_all:
+                return NVMeoFClient(gw_group=gw_group,
+                                    server_address=server_address).stub.add_host(
+                    NVMeoFClient.pb2.add_host_req(
+                        subsystem_nqn=subsystem_nqn,
+                        host_nqn="*",
+                        dhchap_key=None
+                    ))
+
+            for h in (hosts or []):
+                nqn = h["host_nqn"]
+                key = h.get("dhchap_key")
+
                 response = NVMeoFClient(gw_group=gw_group,
                                         server_address=server_address).stub.add_host(
-                    NVMeoFClient.pb2.add_host_req(subsystem_nqn=subsystem_nqn, host_nqn=nqn)
+                    NVMeoFClient.pb2.add_host_req(
+                        subsystem_nqn=subsystem_nqn,
+                        host_nqn=nqn,
+                        dhchap_key=key
+                    )
                 )
                 if response.status != 0:
                     return response
             return response
+
         # UI API for deleting one or more than one hosts to subsystem
 
         @Endpoint(method='DELETE', path="/subsystem/{subsystem_nqn}/host/{host_nqn}")
index e01943f65335db19090236fc5e671ad6c7e6825a..89d7d6a794b6586f87ec847e3e0a733dd582dd9c 100644 (file)
@@ -21,6 +21,7 @@ describe('NvmeofInitiatorsFormComponent', () => {
   let fixture: ComponentFixture<NvmeofInitiatorsFormComponent>;
   let nvmeofService: NvmeofService;
   const mockTimestamp = 1720693470789;
+  const mockGroupName = 'default';
 
   beforeEach(async () => {
     spyOn(Date, 'now').and.returnValue(mockTimestamp);
@@ -54,6 +55,7 @@ describe('NvmeofInitiatorsFormComponent', () => {
     component = fixture.componentInstance;
     component.ngOnInit();
     fixture.detectChanges();
+    component.group = mockGroupName;
   });
 
   it('should create', () => {
index 9cee05cd5c4b7a52595bc9e3788cdaced3893c5f..3eec171ba32e95cac0cc0aa08a1a3377dc89daad 100644 (file)
@@ -78,7 +78,8 @@
       </div>
       }
       <div cdsRow
-           class="form-item step-3-form-item">
+           class="form-item step-3-form-item"
+           formArrayName="hostDchapKeyList">
         <div class="step-3-heading">
           <h1
             class="cds--type-heading-compact-01"
           <p class="cds--type-label-01 text-helper"
              i18n>{{formGroup.get('authType').value === AUTHENTICATION.Bidirectional ? 'All fields are required.' : 'Optional fields.'}}</p>
         </div>
-        @for(hostDchapKeyItem of hostDchapKeyList.controls; track hostDchapKeyItem; let i = $index) {
-        <cds-text-label
-          [formGroupName]="i"
-          i18n>
-          DHCHAP Key | {{hostNQN}}
-          <input cdsPassword
-                 formControlName="key"
-                 type="password"
-                 placeholder="Enter DHCHAP key"
-                 class="step-3-form-item"
-                 i18n-placeholder
-                 autocomplete>
-        </cds-text-label>
+        @for (hostDchapKeyItem of hostDchapKeyList.controls; track hostDchapKeyItem.get('host_nqn')?.value; let i = $index) {
+        <div [formGroupName]="i">
+          <cds-text-label
+            class="cds-mb-3">
+            <span
+              class="cds-mb-3"
+              i18n>DHCHAP Key | {{hostDchapKeyItem.get('host_nqn')?.value }}</span>
+            <input cdsPassword
+                   formControlName="dhchap_key"
+                   type="password"
+                   placeholder="Enter DHCHAP key"
+                   class="step-3-form-item"
+                   i18n-placeholder
+                   autocomplete>
+          </cds-text-label>
+        </div>
         }
       </div>
     </div>
index 1b004adeadaef64672d515bf7f66dd1dd9c21fcb..96316d726189809351ac67e4f763fb235f65b6a0 100644 (file)
@@ -11,7 +11,7 @@ import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
 import { SharedModule } from '~/app/shared/shared.module';
 import { NvmeofSubsystemsStepThreeComponent } from './nvmeof-subsystem-step-3.component';
 import { NvmeofService } from '~/app/shared/api/nvmeof.service';
-import { GridModule, RadioModule, TagModule } from 'carbon-components-angular';
+import { GridModule, InputModule, RadioModule, TagModule } from 'carbon-components-angular';
 import { AUTHENTICATION } from '~/app/shared/models/nvmeof';
 
 describe('NvmeofSubsystemsStepThreeComponent', () => {
@@ -34,6 +34,7 @@ describe('NvmeofSubsystemsStepThreeComponent', () => {
         GridModule,
         RadioModule,
         TagModule,
+        InputModule,
         ToastrModule.forRoot()
       ]
     }).compileComponents();
index a1ecc509441f269eaaf3a1c2d92de9e3e4631e81..4cd4cc110719579bc5f3f724f997f014057b5ebc 100644 (file)
@@ -1,10 +1,9 @@
 import { Component, Input, OnInit } from '@angular/core';
 import { FormArray, UntypedFormControl } from '@angular/forms';
-import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 
 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
-import { AUTHENTICATION } from '~/app/shared/models/nvmeof';
+import { AUTHENTICATION, StepTwoType } from '~/app/shared/models/nvmeof';
 import { TearsheetStep } from '~/app/shared/models/tearsheet-step';
 
 @Component({
@@ -15,6 +14,13 @@ import { TearsheetStep } from '~/app/shared/models/tearsheet-step';
 })
 export class NvmeofSubsystemsStepThreeComponent implements OnInit, TearsheetStep {
   @Input() group!: string;
+  @Input() set stepTwoValue(value: StepTwoType | null) {
+    this._addedHosts = value?.addedHosts ?? [];
+    if (this.formGroup) {
+      this.syncHostList();
+    }
+  }
+
   formGroup: CdFormGroup;
   action: string;
   pageURL: string;
@@ -23,28 +29,54 @@ export class NvmeofSubsystemsStepThreeComponent implements OnInit, TearsheetStep
   };
   AUTHENTICATION = AUTHENTICATION;
 
-  constructor(public actionLabels: ActionLabelsI18n, public activeModal: NgbActiveModal) {}
+  _addedHosts: Array<string> = [];
 
-  ngOnInit() {
-    this.createForm();
+  constructor(public actionLabels: ActionLabelsI18n) {}
+
+  private syncHostList() {
+    const currentList = this.hostDchapKeyList;
+
+    // save existing dhchap keys by host_nqn
+    const existing = new Map<string, string | null>();
+    currentList.getRawValue().forEach((x: any) => existing.set(x.host_nqn, x.dhchap_key));
+
+    currentList.clear();
+
+    const hosts = this._addedHosts;
+
+    if (hosts.length) {
+      hosts.forEach((nqn) => {
+        currentList.push(this.createHostDhchapKeyFormGroup(nqn, existing.get(nqn) ?? null));
+      });
+    } else {
+      currentList.push(this.createHostDhchapKeyFormGroup('', null));
+    }
   }
 
-  createForm() {
+  private createForm() {
     this.formGroup = new CdFormGroup({
       authType: new UntypedFormControl(AUTHENTICATION.Unidirectional),
       subsystemDchapKey: new UntypedFormControl(null),
-      hostDchapKeyList: new FormArray([this.createHostDchapKeyItem()])
+      hostDchapKeyList: new FormArray([])
     });
+
+    this.syncHostList();
   }
 
-  createHostDchapKeyItem() {
+  private createHostDhchapKeyFormGroup(hostNQN: string = '', key: string | null = null) {
     return new CdFormGroup({
-      key: new UntypedFormControl(null),
-      hostNQN: new UntypedFormControl('')
+      dhchap_key: new UntypedFormControl(key),
+      host_nqn: new UntypedFormControl(hostNQN)
     });
   }
 
+  ngOnInit() {
+    this.createForm();
+  }
+
   get hostDchapKeyList() {
     return this.formGroup.get('hostDchapKeyList') as FormArray;
   }
+
+  trackByIndex = (i: number) => i;
 }
index 7b7aba13de0643098ef88b3ef6c0d2331aadd9c1..6a48e878694f4055bc8432d797c4606b6e775f51 100644 (file)
@@ -4,7 +4,11 @@
  [description]="description"
  [isSubmitLoading]="isSubmitLoading"
  (submitRequested)="onSubmit($event)"
+<<<<<<< HEAD
  (stepChanged)="populateReviewData()"
+=======
+ (stepChanged)="onStepChanged($event)"
+>>>>>>> 6d877ea7101 (mgr/dashboard: Allow adding authentication to subsystem flow)
  >
   <cd-tearsheet-step>
     <cd-nvmeof-subsystem-step-one
       #tearsheetStep
       [group]="group"></cd-nvmeof-subsystem-step-two>
   </cd-tearsheet-step>
+  @if(showAuthStep) {
   <cd-tearsheet-step>
     <cd-nvmeof-subsystem-step-three
      #tearsheetStep
+     [stepTwoValue]="stepTwoValue"
      [group]="group"></cd-nvmeof-subsystem-step-three>
   </cd-tearsheet-step>
+<<<<<<< HEAD
   <cd-tearsheet-step>
     <cd-nvmeof-subsystem-step-four
      #tearsheetStep
@@ -33,5 +40,8 @@
      [subsystemDchapKey]="reviewSubsystemDchapKey"
      [hostDchapKeyCount]="reviewHostDchapKeyCount"></cd-nvmeof-subsystem-step-four>
   </cd-tearsheet-step>
+=======
+  }
+>>>>>>> 6d877ea7101 (mgr/dashboard: Allow adding authentication to subsystem flow)
 </cd-tearsheet>
 
index 52e3aa48a5368863539d09fd1fb90af49b5c44f0..5b1f36f472284f47f004eebff3d822cdee3e385c 100644 (file)
@@ -23,7 +23,7 @@ import {
   TagModule
 } from 'carbon-components-angular';
 import { NvmeofSubsystemsStepThreeComponent } from './nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component';
-import { HOST_TYPE } from '~/app/shared/models/nvmeof';
+import { AUTHENTICATION, HOST_TYPE } from '~/app/shared/models/nvmeof';
 import { NvmeofSubsystemsStepTwoComponent } from './nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component';
 import { NvmeofSubsystemsStepFourComponent } from './nvmeof-subsystem-step-4/nvmeof-subsystem-step-4.component';
 import { of } from 'rxjs';
@@ -40,7 +40,9 @@ describe('NvmeofSubsystemsFormComponent', () => {
     subsystemDchapKey: 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY=',
     addedHosts: [],
     hostType: HOST_TYPE.ALL,
-    listeners: []
+    listeners: [],
+    hostDchapKeyList: [],
+    authType: AUTHENTICATION.Bidirectional
   };
 
   beforeEach(async () => {
@@ -113,14 +115,25 @@ describe('NvmeofSubsystemsFormComponent', () => {
         addedHosts: [],
         hostType: HOST_TYPE.ALL,
         subsystemDchapKey: 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY=',
+<<<<<<< HEAD
         listeners: []
+=======
+        authType: AUTHENTICATION.Bidirectional,
+        hostDchapKeyList: []
+>>>>>>> 6d877ea7101 (mgr/dashboard: Allow adding authentication to subsystem flow)
       };
 
       component.group = mockGroupName;
       component.onSubmit(payload);
 
+<<<<<<< HEAD
       expect(nvmeofService.addInitiators).toHaveBeenCalledWith('test-nqn.default', {
         host_nqn: '*',
+=======
+      expect(nvmeofService.addSubsystemInitiators).toHaveBeenCalledWith('test-nqn.default', {
+        allow_all: true,
+        hosts: [],
+>>>>>>> 6d877ea7101 (mgr/dashboard: Allow adding authentication to subsystem flow)
         gw_group: mockGroupName
       });
     });
index 027eaa66397af64fc2a223c010c714a5a75b7147..9e3a32fac1980a1ac922be273060295cb0f0342f 100644 (file)
@@ -5,9 +5,10 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { ActivatedRoute, Router } from '@angular/router';
 import { Step } from 'carbon-components-angular';
-import { InitiatorRequest, NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { NvmeofService, SubsystemInitiatorRequest } from '~/app/shared/api/nvmeof.service';
 import { TearsheetComponent } from '~/app/shared/components/tearsheet/tearsheet.component';
 import { HOST_TYPE, ListenerItem, AUTHENTICATION } from '~/app/shared/models/nvmeof';
+import { AUTHENTICATION, HOST_TYPE, StepTwoType } from '~/app/shared/models/nvmeof';
 import { from, Observable, of } from 'rxjs';
 import { NotificationService } from '~/app/shared/services/notification.service';
 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
@@ -21,6 +22,8 @@ export type SubsystemPayload = {
   addedHosts: string[];
   hostType: string;
   listeners: ListenerItem[];
+  authType: AUTHENTICATION.Bidirectional | AUTHENTICATION.Unidirectional;
+  hostDchapKeyList: Array<{ dhchap_key: string; host_nqn: string }>;
 };
 
 type StepResult = { step: string; success: boolean; error?: string };
@@ -34,29 +37,13 @@ type StepResult = { step: string; success: boolean; error?: string };
 export class NvmeofSubsystemsFormComponent implements OnInit {
   action: string;
   group: string;
-  steps: Step[] = [
-    {
-      label: $localize`Subsystem details`,
-      complete: false,
-      invalid: false
-    },
-    {
-      label: $localize`Host access control`,
-      invalid: false
-    },
-    {
-      label: $localize`Authentication`,
-      complete: false
-    },
-    {
-      label: $localize`Review`,
-      complete: false
-    }
-  ];
+  steps: Step[] = [];
   title: string = $localize`Create Subsystem`;
   description: string = $localize`Subsytems define how hosts connect to NVMe namespaces and ensure secure access to storage.`;
   isSubmitLoading: boolean = false;
   private lastCreatedNqn: string;
+  stepTwoValue: StepTwoType = null;
+  showAuthStep = true;
 
   @ViewChild(TearsheetComponent) tearsheet!: TearsheetComponent;
 
@@ -84,6 +71,7 @@ export class NvmeofSubsystemsFormComponent implements OnInit {
     this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
       this.group = params?.['group'];
     });
+    this.rebuildSteps();
   }
 
   populateReviewData() {
@@ -111,6 +99,33 @@ export class NvmeofSubsystemsFormComponent implements OnInit {
       this.reviewSubsystemDchapKey = step3Form.get('subsystemDchapKey')?.value || '';
       const hostKeys = step3Form.get('hostDchapKeyList')?.value || [];
       this.reviewHostDchapKeyCount = hostKeys.filter((k: any) => k?.key).length;
+  
+      }  }
+  onStepChanged(_e: { current: number }) {
+    const stepTwo = this.tearsheet?.getStepValue(1);
+    this.stepTwoValue = stepTwo;
+
+    this.showAuthStep = stepTwo?.hostType !== HOST_TYPE.ALL;
+
+    this.rebuildSteps();
+  }
+
+  rebuildSteps() {
+    const steps: Step[] = [
+      { label: 'Subsystem details', invalid: false },
+      { label: 'Host access control', invalid: false }
+    ];
+
+    if (this.showAuthStep) {
+      steps.push({ label: 'Authentication', invalid: false });
+    }
+
+    steps.push({ label: 'Review', invalid: false });
+
+    this.steps = steps;
+
+    if (this.tearsheet?.currentStep >= steps.length) {
+      this.tearsheet.currentStep = steps.length - 1;
     }
   }
 
@@ -118,8 +133,9 @@ export class NvmeofSubsystemsFormComponent implements OnInit {
     this.isSubmitLoading = true;
     this.lastCreatedNqn = payload.nqn;
     const stepResults: StepResult[] = [];
-    const initiatorRequest: InitiatorRequest = {
-      host_nqn: payload.hostType === HOST_TYPE.ALL ? '*' : payload.addedHosts.join(','),
+    const initiatorRequest: SubsystemInitiatorRequest = {
+      allow_all: payload.hostType === HOST_TYPE.ALL,
+      hosts: payload.hostType === HOST_TYPE.SPECIFIC ? payload.hostDchapKeyList : [],
       gw_group: this.group
     };
     this.nvmeofService
index aefdf0739d51f7186d69a601eb8dc6894168fd7f..9ce80fcd448f2714a24e7af196480bcb7f21352b 100755 (executable)
@@ -191,6 +191,7 @@ describe('NvmeofService', () => {
 
   describe('test initiators APIs', () => {
     let request = { host_nqn: '', gw_group: mockGroupName };
+    let addRequest = { hosts: [], allow_all: true, gw_group: mockGroupName };
     it('should call getInitiators', () => {
       service.getInitiators(mockNQN, mockGroupName).subscribe();
       const req = httpTesting.expectOne(
@@ -199,7 +200,7 @@ describe('NvmeofService', () => {
       expect(req.request.method).toBe('GET');
     });
     it('should call addInitiators', () => {
-      service.addInitiators(mockNQN, request).subscribe();
+      service.addSubsystemInitiators(mockNQN, addRequest).subscribe();
       const req = httpTesting.expectOne(`${UI_API_PATH}/subsystem/${mockNQN}/host`);
       expect(req.request.method).toBe('POST');
     });
index f5833f3ca3ebfffa1797348d16a12454123dcc1c..8fc6b43eba8385811711af0ac9e7a1535fca7b98 100644 (file)
@@ -50,6 +50,11 @@ export type InitiatorRequest = NvmeofRequest & {
   dhchap_key?: string;
 };
 
+export type SubsystemInitiatorRequest = NvmeofRequest & {
+  hosts: Array<{ dhchap_key: string; host_nqn: string }>;
+  allow_all: boolean;
+};
+
 export type NamespaceInitiatorRequest = InitiatorRequest & {
   subsystem_nqn: string;
 };
@@ -211,7 +216,7 @@ export class NvmeofService {
     return this.http.get(`${API_PATH}/subsystem/${subsystemNQN}/host?gw_group=${group}`);
   }
 
-  addInitiators(subsystemNQN: string, request: InitiatorRequest) {
+  addSubsystemInitiators(subsystemNQN: string, request: SubsystemInitiatorRequest) {
     return this.http.post(`${UI_API_PATH}/subsystem/${subsystemNQN}/host`, request, {
       observe: 'response'
     });
index b2157fe3b63bb408a4aeb40a9bebb463c8d71e12..dd4fed0fb39311803174946016cf2bdb2cff47cb 100644 (file)
 }
 
 .tearsheet-content {
+  max-block-size: 39.7rem;
   background-color: var(--cds-layer-01);
   margin: 0;
   padding: var(--cds-spacing-06) var(--cds-spacing-07);
index 769a0d1515efedd0b85e2ce44e0cd171d0e1c163..c3806ea0920700edef1fd17fd8379326459b4546 100644 (file)
@@ -68,7 +68,7 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
 
   @Output() submitRequested = new EventEmitter<void>();
   @Output() closeRequested = new EventEmitter<void>();
-  @Output() stepChanged = new EventEmitter<number>();
+  @Output() stepChanged = new EventEmitter<{ current: number }>();
 
   @ContentChildren(TearsheetStepComponent)
   stepContents!: QueryList<TearsheetStepComponent>;
@@ -85,6 +85,11 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
     return this.stepContents?.toArray()[this.currentStep]?.showRightInfluencer;
   }
 
+  getStepValue<T = any>(index: number): T | null {
+    const wrapper = this.stepContents?.toArray()?.[index];
+    return wrapper?.stepComponent?.formGroup?.value ?? null;
+  }
+
   currentStep: number = 0;
   lastStep: number = null;
   isOpen: boolean = true;
@@ -110,7 +115,7 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
 
   onStepSelect(event: { step: Step; index: number }) {
     this.currentStep = event.index;
-    this.stepChanged.emit(this.currentStep);
+    this.stepChanged.emit({ current: this.currentStep });
   }
 
   closeTearsheet() {
@@ -134,7 +139,7 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
   onPrevious() {
     if (this.currentStep !== 0) {
       this.currentStep = this.currentStep - 1;
-      this.stepChanged.emit(this.currentStep);
+      this.stepChanged.emit({ current: this.currentStep });
     }
   }
 
@@ -143,7 +148,7 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
     formEl?.dispatchEvent(new Event('submit', { bubbles: true }));
     if (this.currentStep !== this.lastStep && !this.steps[this.currentStep].invalid) {
       this.currentStep = this.currentStep + 1;
-      this.stepChanged.emit(this.currentStep);
+      this.stepChanged.emit({ current: this.currentStep });
     }
   }
 
@@ -179,19 +184,29 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
   }
 
   ngAfterViewInit() {
-    if (!this.stepContents?.length) return;
-
-    this.stepContents.forEach((wrapper, index) => {
-      const form = wrapper.stepComponent?.formGroup;
-      if (!form) return;
+    const setup = () => {
+      // keep lastStep in sync with steps input
+      this.lastStep = this.steps.length - 1;
+
+      // clamp currentStep so template lookup never goes out of range
+      if (this.currentStep > this.lastStep) {
+        this.currentStep = this.lastStep;
+      }
+
+      // subscribe to each form statusChanges
+      this.stepContents.forEach((wrapper, index) => {
+        const form = wrapper.stepComponent?.formGroup;
+        if (!form) return;
+
+        form.statusChanges
+          .pipe(takeUntilDestroyed(this.destroyRef))
+          .subscribe(() => this._updateStepInvalid(index, form.invalid));
+      });
+    };
 
-      // initial state
-      this._updateStepInvalid(index, form.invalid);
+    setup();
 
-      form.statusChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
-        this._updateStepInvalid(index, form.invalid);
-      });
-    });
+    this.stepContents.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => setup());
   }
 
   ngOnDestroy() {
index 36061c628f8050d3f3c9b07537d540246e3b0607..5107b671578e56900e3b5d413b23f09eb0e41e76 100644 (file)
@@ -148,3 +148,9 @@ export type NvmeofInitiatorCandidate = {
   content: string;
   selected: boolean;
 };
+
+export type StepTwoType = {
+  addedHosts: Array<string>;
+  hostname: string;
+  hostType: string;
+};