From: Afreen Misbah Date: Tue, 24 Feb 2026 01:46:26 +0000 (+0530) Subject: mgr/dashboard: Allow adding authentication to subsystem flow X-Git-Tag: v21.0.0~180^2~1 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=3b1aac6e18cd74299a51ecdf46516dac377b206e;p=ceph.git mgr/dashboard: Allow adding authentication to subsystem flow - integrates third step with overall tearsheet Signed-off-by: Afreen Misbah --- diff --git a/src/pybind/mgr/dashboard/controllers/nvmeof.py b/src/pybind/mgr/dashboard/controllers/nvmeof.py index 7655e989477b..54854ee39890 100644 --- a/src/pybind/mgr/dashboard/controllers/nvmeof.py +++ b/src/pybind/mgr/dashboard/controllers/nvmeof.py @@ -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}") diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts index e01943f65335..89d7d6a794b6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts @@ -21,6 +21,7 @@ describe('NvmeofInitiatorsFormComponent', () => { let fixture: ComponentFixture; 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', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.html index 9cee05cd5c4b..3eec171ba32e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.html @@ -78,7 +78,8 @@ }
+ class="form-item step-3-form-item" + formArrayName="hostDchapKeyList">

{{formGroup.get('authType').value === AUTHENTICATION.Bidirectional ? 'All fields are required.' : 'Optional fields.'}}

- @for(hostDchapKeyItem of hostDchapKeyList.controls; track hostDchapKeyItem; let i = $index) { - - DHCHAP Key | {{hostNQN}} - - + @for (hostDchapKeyItem of hostDchapKeyList.controls; track hostDchapKeyItem.get('host_nqn')?.value; let i = $index) { +
+ + DHCHAP Key | {{hostDchapKeyItem.get('host_nqn')?.value }} + + +
}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.scss index 1b140bda80a7..a8945d41ba38 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.scss @@ -14,5 +14,5 @@ } .step-3-form-item { - max-inline-size: 23rem; + max-inline-size: 30rem; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.spec.ts index 1b004adeadae..96316d726189 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.spec.ts @@ -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(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.ts index a1ecc509441f..4cd4cc110719 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.ts @@ -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 = []; - ngOnInit() { - this.createForm(); + constructor(public actionLabels: ActionLabelsI18n) {} + + private syncHostList() { + const currentList = this.hostDchapKeyList; + + // save existing dhchap keys by host_nqn + const existing = new Map(); + 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; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.html index 7b7aba13de06..6a48e878694f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.html @@ -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) > + @if(showAuthStep) { +<<<<<<< HEAD +======= + } +>>>>>>> 6d877ea7101 (mgr/dashboard: Allow adding authentication to subsystem flow) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts index 52e3aa48a536..5b1f36f47228 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts @@ -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 }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts index 027eaa66397a..9e3a32fac198 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts @@ -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 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts index aefdf0739d51..9ce80fcd448f 100755 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts @@ -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'); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts index f5833f3ca3eb..8fc6b43eba83 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts @@ -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' }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.scss index b2157fe3b63b..dd4fed0fb393 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.scss @@ -100,6 +100,7 @@ } .tearsheet-content { + max-block-size: 39.7rem; background-color: var(--cds-layer-01); margin: 0; padding: var(--cds-spacing-06) var(--cds-spacing-07); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts index 769a0d1515ef..c3806ea09207 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts @@ -68,7 +68,7 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy { @Output() submitRequested = new EventEmitter(); @Output() closeRequested = new EventEmitter(); - @Output() stepChanged = new EventEmitter(); + @Output() stepChanged = new EventEmitter<{ current: number }>(); @ContentChildren(TearsheetStepComponent) stepContents!: QueryList; @@ -85,6 +85,11 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy { return this.stepContents?.toArray()[this.currentStep]?.showRightInfluencer; } + getStepValue(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() { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts index 36061c628f80..5107b671578e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts @@ -148,3 +148,9 @@ export type NvmeofInitiatorCandidate = { content: string; selected: boolean; }; + +export type StepTwoType = { + addedHosts: Array; + hostname: string; + hostType: string; +};