diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.html index 7db9a60aa3..c0923d1038 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.html @@ -21,197 +21,212 @@ } @else {
-

{{ packageInfo?.name || data.item.name }}

- @if (selectedConnectivity) { - {{ getConnectivityLabel(selectedConnectivity) }} +

{{ 'iot-hub.device-install-title' | translate:{ name: packageInfo?.name || data.item.name } }}

+ @if (selectedConnectivity && wizardStarted) { + {{ connectivityLabels.get(selectedConnectivity) }} }
-
- @switch (currentView) { - @case ('connectivity') { -
-

{{ 'iot-hub.device-install-select-connectivity' | translate }}

-
- @for (ct of availableConnectivityTypes; track ct) { - - } -
-
- } - @case ('instruction') { -
- - + + @if (!wizardStarted) { + +
+
+

{{ 'iot-hub.device-install-select-connectivity' | translate }}

+
+ @for (ct of availableConnectivityTypes; track ct) { + + }
- } - @case ('form') { -
- @if (currentFormGroup) { -
- @for (field of currentFormFields; track field.key) { - @switch (field.type) { - @case ('BOOLEAN') { - - {{ field.label }} - - } - @case ('SELECT') { - - {{ field.label }} - - @for (opt of field.options; track opt.value) { - {{ opt.label }} - } - - @if (currentFormGroup.controls[field.key]?.hasError('required')) { - {{ field.label + ' is required' }} - } - - } - @case ('PASSWORD') { - - {{ field.label }} - - - @if (currentFormGroup.controls[field.key]?.hasError('required')) { - {{ field.label + ' is required' }} - } - @if (currentFormGroup.controls[field.key]?.hasError('pattern')) { - {{ getPatternErrorMessage(field) }} - } - - } - @case ('INTEGER') { - - {{ field.label }} - - @if (currentFormGroup.controls[field.key]?.hasError('required')) { - {{ field.label + ' is required' }} - } - @if (currentFormGroup.controls[field.key]?.hasError('pattern')) { - {{ getPatternErrorMessage(field) }} - } - - } - @default { - - {{ field.label }} - - @if (currentFormGroup.controls[field.key]?.hasError('required')) { - {{ field.label + ' is required' }} - } - @if (currentFormGroup.controls[field.key]?.hasError('pattern')) { - {{ getPatternErrorMessage(field) }} - } - - } +
+
+ + + + + } @else { + +
+ + + done + + + @for (ws of wizardSteps; track ws.label; let i = $index; let last = $last) { + + {{ ws.label }} + +
+ @switch (ws.type) { + + @case ('instruction') { +
+ + +
} - @if (field.helpText) { -
{{ field.helpText }}
+ + @case ('form') { +
+ @if (ws.formGroup) { + + @for (field of ws.formFields; track field.key) { + @switch (field.type) { + @case ('BOOLEAN') { + + {{ field.label }} + + } + @case ('SELECT') { + + {{ field.label }} + + @for (opt of field.options; track opt.value) { + {{ opt.label }} + } + + @if (ws.formGroup.controls[field.key]?.hasError('required')) { + {{ field.label + ' is required' }} + } + + } + @case ('PASSWORD') { + + {{ field.label }} + + + @if (ws.formGroup.controls[field.key]?.hasError('required')) { + {{ field.label + ' is required' }} + } + @if (ws.formGroup.controls[field.key]?.hasError('pattern')) { + {{ getPatternErrorMessage(field) }} + } + + } + @case ('INTEGER') { + + {{ field.label }} + + @if (ws.formGroup.controls[field.key]?.hasError('required')) { + {{ field.label + ' is required' }} + } + @if (ws.formGroup.controls[field.key]?.hasError('pattern')) { + {{ getPatternErrorMessage(field) }} + } + + } + @default { + + {{ field.label }} + + @if (ws.formGroup.controls[field.key]?.hasError('required')) { + {{ field.label + ' is required' }} + } + @if (ws.formGroup.controls[field.key]?.hasError('pattern')) { + {{ getPatternErrorMessage(field) }} + } + + } + } + @if (field.helpText) { +
{{ field.helpText }}
+ } + @if (field.helpImage) { + + } + } + + } +
} - @if (field.helpImage) { - + + @case ('progress') { +
+ @for (ep of ws.entitySteps; track ep.step.name) { +
+
+ @switch (ep.status) { + @case ('pending') { + radio_button_unchecked + } + @case ('running') { + + } + @case ('success') { + check_circle + } + @case ('error') { + error + } + } +
+
+ {{ ep.resolvedName || ep.step.name }} + @if (ep.status === 'error' && ep.errorMessage) { + {{ ep.errorMessage }} + } +
+
+ } +
} } - - } -
- } - @case ('progress') { -
- @for (ep of entitySteps; track ep.step.name) { -
-
- @switch (ep.status) { - @case ('pending') { - radio_button_unchecked - } - @case ('running') { - - } - @case ('success') { - check_circle - } - @case ('error') { - error - } - } -
-
-
{{ ep.resolvedName || ep.step.name }}
- @if (ep.status === 'error' && ep.errorMessage) { -
{{ ep.errorMessage }}
- } -
- } -
- @if (progressError) { -
{{ progressError }}
+
} - } - @case ('done') { -
- @if (currentMarkdown) { - - - } @else { -

{{ 'iot-hub.install-success-desc' | translate:{ name: packageInfo?.name } }}

+ +
+ + + @if (currentWizardStep; as step) { + @switch (step.type) { + @case ('instruction') { + + + } + @case ('form') { + + + } + @case ('progress') { + @if (step.progressError) { + + + } @else if (step.progressDone && isLastWizardStep) { + + } } -
- } - } -
- - @switch (currentView) { - @case ('connectivity') { - - - } - @case ('instruction') { - - - } - @case ('form') { - - - } - @case ('progress') { - - @if (progressError) { - } } - @case ('done') { - - } - } - + + } } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.scss index 01f4428e85..2a764ccb12 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.scss @@ -17,9 +17,9 @@ :host { display: flex; flex-direction: column; - width: 640px; + width: 720px; max-width: 90vw; - max-height: 80vh; + max-height: 85vh; } .tb-device-install-header { @@ -54,13 +54,6 @@ } } -.tb-device-install-body { - flex: 1; - overflow-y: auto; - padding: 0 24px 24px; - min-height: 200px; -} - .tb-device-install-loading { display: flex; align-items: center; @@ -68,6 +61,14 @@ padding: 48px 0; } +// Connectivity selector (before stepper) +.tb-device-install-body { + flex: 1; + overflow-y: auto; + padding: 0 24px 24px; + min-height: 120px; +} + .tb-device-install-connectivity { display: flex; flex-direction: column; @@ -96,106 +97,133 @@ } } +// Stepper container +.tb-device-install-stepper-container { + flex: 1; + overflow-y: auto; + min-height: 200px; + + ::ng-deep { + .mat-horizontal-stepper-header-container { + padding: 0 16px; + } + + .mat-horizontal-content-container { + padding: 0 24px 16px; + } + + .mat-step-header { + padding: 0 12px; + } + } +} + +// Step content wrapper +.tb-wizard-step-content { + min-height: 120px; +} + +// Instruction view .tb-device-install-instruction { ::ng-deep tb-markdown { display: block; } + + ::ng-deep { + h1, h2, h3, h4 { + &:first-child { + margin-top: 0; + } + } + } } +// Form view .tb-device-install-form { display: flex; flex-direction: column; - gap: 16px; + gap: 8px; + + mat-form-field { + width: 100%; + } .tb-form-field-help { color: rgba(0, 0, 0, 0.54); font-size: 12px; line-height: 16px; - margin-top: 4px; + margin-top: -4px; + margin-bottom: 8px; } .tb-form-field-help-image { max-width: 100%; - margin-top: 8px; + margin-top: 4px; + margin-bottom: 8px; border-radius: 4px; border: 1px solid rgba(0, 0, 0, 0.12); } } +// Progress view .tb-device-install-progress { display: flex; flex-direction: column; gap: 12px; +} - .tb-progress-row { - display: flex; - align-items: center; - gap: 12px; - padding: 8px 0; - - .tb-progress-icon { - flex-shrink: 0; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - - mat-icon { - font-size: 20px; - width: 20px; - height: 20px; - } +.tb-progress-row { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 8px 0; +} - .tb-progress-pending { - color: rgba(0, 0, 0, 0.38); - } +.tb-progress-icon { + flex-shrink: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; - .tb-progress-success { - color: #2e7d32; - } + mat-icon { + font-size: 20px; + width: 20px; + height: 20px; + } - .tb-progress-error { - color: #c62828; - } - } + .tb-progress-pending { + color: rgba(0, 0, 0, 0.38); + } - .tb-progress-label { - flex: 1; - font-size: 14px; - line-height: 20px; - color: rgba(0, 0, 0, 0.87); - } + .tb-progress-success { + color: #2e7d32; + } - .tb-progress-error-msg { - font-size: 12px; - color: #c62828; - margin-top: 4px; - } + .tb-progress-error { + color: #c62828; } } -.tb-device-install-done { +.tb-progress-info { display: flex; flex-direction: column; - gap: 16px; + gap: 4px; + min-height: 24px; + justify-content: center; +} - .tb-done-message { - color: rgba(0, 0, 0, 0.87); - font-size: 14px; - line-height: 20px; - } +.tb-progress-label { + font-size: 14px; + line-height: 20px; + color: rgba(0, 0, 0, 0.87); } -.tb-device-install-error { - max-height: 200px; - overflow-y: auto; - padding: 12px; - background: #f5f5f5; - border-radius: 4px; - font-family: monospace; +.tb-progress-error-msg { font-size: 12px; - color: rgba(0, 0, 0, 0.7); + color: #c62828; + font-family: monospace; white-space: pre-wrap; word-break: break-word; } @@ -203,8 +231,3 @@ mat-dialog-actions { padding: 8px 24px 16px; } - -.tb-iot-hub-inline-spinner { - display: inline-block; - margin-right: 8px; -} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.ts index e5d69e2fbb..d2534aefb6 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.ts @@ -14,9 +14,10 @@ /// limitations under the License. /// -import { Component, Inject, OnInit } from '@angular/core'; +import { Component, Inject, OnInit, ViewChild } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; +import { MatStepper } from '@angular/material/stepper'; import { firstValueFrom } from 'rxjs'; import { PageLink } from '@shared/models/page/page-link'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; @@ -38,14 +39,31 @@ import { stepTypeAliasMap } from '@shared/models/iot-hub/device-package.models'; -export type DeviceInstallView = 'connectivity' | 'instruction' | 'form' | 'progress' | 'done'; - export interface DeviceInstallDialogData { item: MpItemVersionView; zipData: ArrayBuffer; iotHubApiService: IotHubApiService; } +export type WizardStepType = 'instruction' | 'form' | 'progress'; + +export interface WizardStep { + type: WizardStepType; + label: string; + rawSteps: DeviceInstallStep[]; + // Instruction + markdown?: string; + // Form + formFields?: FormFieldDefinition[]; + formGroup?: UntypedFormGroup; + // Progress + entitySteps?: EntityStepProgress[]; + progressError?: string; + progressDone?: boolean; +} + +const ENTITY_STEP_MIN_DELAY = 2000; + @Component({ selector: 'tb-device-install-dialog', standalone: false, @@ -54,29 +72,28 @@ export interface DeviceInstallDialogData { }) export class TbDeviceInstallDialogComponent implements OnInit { + @ViewChild('installStepper', {static: false}) stepper: MatStepper; + loading = true; packageInfo: DevicePackageInfo; zipFiles = new Map(); + // Connectivity + showConnectivitySelector = false; availableConnectivityTypes: string[] = []; selectedConnectivity: string | null = null; + connectivityLabels = connectivityTypeTranslations; - currentView: DeviceInstallView = 'connectivity'; - steps: DeviceInstallStep[] = []; - currentStepIndex = 0; - - currentMarkdown = ''; - currentFormFields: FormFieldDefinition[] = []; - currentFormGroup: UntypedFormGroup | null = null; + // Wizard + wizardSteps: WizardStep[] = []; + wizardStarted = false; passwordVisible: Record = {}; + // Variable resolution state formValues: Record = {}; entityOutputs = new Map(); positionalOutputs = new Map(); - entitySteps: EntityStepProgress[] = []; - progressError: string | null = null; - constructor( @Inject(MAT_DIALOG_DATA) public data: DeviceInstallDialogData, private dialogRef: MatDialogRef, @@ -102,9 +119,10 @@ export class TbDeviceInstallDialogComponent implements OnInit { ); if (this.availableConnectivityTypes.length === 1) { this.selectedConnectivity = this.availableConnectivityTypes[0]; - this.steps = this.packageInfo.installSteps[this.selectedConnectivity] || []; - this.currentStepIndex = 0; - this.advanceToCurrentStep(); + this.showConnectivitySelector = false; + this.startWizard(); + } else { + this.showConnectivitySelector = true; } } catch (e) { console.error('Failed to parse device package ZIP', e); @@ -112,85 +130,50 @@ export class TbDeviceInstallDialogComponent implements OnInit { this.loading = false; } - getConnectivityLabel(ct: string): string { - return connectivityTypeTranslations.get(ct) || ct; - } + // --- Connectivity --- selectConnectivity(ct: string): void { this.selectedConnectivity = ct; } - next(): void { - if (this.currentView === 'connectivity') { - if (!this.selectedConnectivity) { - return; - } - this.steps = this.packageInfo.installSteps[this.selectedConnectivity] || []; - this.currentStepIndex = 0; - this.formValues = {}; - this.entityOutputs.clear(); - this.positionalOutputs.clear(); - this.advanceToCurrentStep(); + confirmConnectivity(): void { + if (!this.selectedConnectivity) { return; } + this.startWizard(); + } - if (this.currentView === 'form') { - if (this.currentFormGroup && this.currentFormGroup.invalid) { - this.currentFormGroup.markAllAsTouched(); - return; - } - if (this.currentFormGroup) { - Object.assign(this.formValues, this.currentFormGroup.value); - } - } + // --- Wizard --- - if (this.currentView === 'instruction' && this.isLastStep()) { - this.done(); - return; - } + get isLastWizardStep(): boolean { + return this.stepper && this.stepper.selectedIndex === this.wizardSteps.length - 1; + } - this.currentStepIndex++; - this.advanceToCurrentStep(); + get currentWizardStep(): WizardStep | null { + if (!this.stepper || !this.wizardSteps.length) { + return null; + } + return this.wizardSteps[this.stepper.selectedIndex] ?? null; } - advanceToCurrentStep(): void { - if (this.currentStepIndex >= this.steps.length) { - this.currentView = 'done'; - this.currentMarkdown = ''; + nextStep(): void { + const step = this.currentWizardStep; + if (!step) { return; } - - const step = this.steps[this.currentStepIndex]; - - switch (step.type) { - case InstallStepType.SHOW_INSTRUCTION: { - this.currentView = 'instruction'; - const raw = this.zipFiles.get(step.file) || ''; - this.currentMarkdown = this.resolveVariables(raw); - break; - } - case InstallStepType.SHOW_FORM: { - this.currentView = 'form'; - const formJson = this.zipFiles.get(step.file) || '[]'; - this.currentFormFields = JSON.parse(formJson) as FormFieldDefinition[]; - this.buildFormGroup(); - break; - } - default: { - if (ENTITY_STEP_TYPES.has(step.type)) { - this.collectAndRunEntitySteps(); - } else { - // Skip unsupported step types (CONVERTER, INTEGRATION) - this.currentStepIndex++; - this.advanceToCurrentStep(); - } - break; + if (step.type === 'form') { + if (step.formGroup?.invalid) { + step.formGroup.markAllAsTouched(); + return; } + Object.assign(this.formValues, step.formGroup.getRawValue()); } - } - - isLastStep(): boolean { - return this.currentStepIndex >= this.steps.length - 1; + if (this.isLastWizardStep) { + this.done(); + return; + } + this.stepper.next(); + this.onStepActivated(); } done(): void { @@ -201,18 +184,17 @@ export class TbDeviceInstallDialogComponent implements OnInit { this.dialogRef.close(false); } - retryEntitySteps(): void { - this.progressError = null; - this.runEntitySteps(); + retryEntitySteps(step: WizardStep): void { + step.progressError = null; + this.runEntitySteps(step); } getPatternErrorMessage(field: FormFieldDefinition): string { - if (field.validators?.length > 0) { - return field.validators[0].message || 'Invalid format'; - } - return 'Invalid format'; + return field.validators?.[0]?.message || 'Invalid format'; } + // --- Variable Resolution --- + resolveVariables(content: string): string { return content.replace(/\$\{([^}]+)\}/g, (_match, key) => { if (key in this.formValues) { @@ -224,13 +206,13 @@ export class TbDeviceInstallDialogComponent implements OnInit { const prop = key.substring(dotIdx + 1); const output = this.entityOutputs.get(alias); if (output && prop in output) { - return String(output[prop]); + return String((output as any)[prop]); } const m = alias.match(/^step(\d+)$/); if (m) { const pos = this.positionalOutputs.get(parseInt(m[1], 10)); if (pos && prop in pos) { - return String(pos[prop]); + return String((pos as any)[prop]); } } } @@ -238,10 +220,70 @@ export class TbDeviceInstallDialogComponent implements OnInit { }); } - private buildFormGroup(): void { - const controls: Record = {}; + // --- Private --- + + private startWizard(): void { + this.formValues = {}; + this.entityOutputs.clear(); + this.positionalOutputs.clear(); this.passwordVisible = {}; - for (const field of this.currentFormFields) { + this.buildWizardSteps(); + this.wizardStarted = true; + + // Activate the first step after the stepper renders + setTimeout(() => this.onStepActivated(), 0); + } + + private buildWizardSteps(): void { + const rawSteps = this.packageInfo.installSteps[this.selectedConnectivity] || []; + this.wizardSteps = []; + let i = 0; + while (i < rawSteps.length) { + const step = rawSteps[i]; + if (step.type === InstallStepType.SHOW_INSTRUCTION) { + this.wizardSteps.push({ + type: 'instruction', + label: step.name, + rawSteps: [step] + }); + i++; + } else if (step.type === InstallStepType.SHOW_FORM) { + this.wizardSteps.push({ + type: 'form', + label: step.name, + rawSteps: [step] + }); + i++; + } else if (ENTITY_STEP_TYPES.has(step.type)) { + // Group consecutive entity steps + const group: DeviceInstallStep[] = []; + while (i < rawSteps.length && ENTITY_STEP_TYPES.has(rawSteps[i].type)) { + group.push(rawSteps[i]); + i++; + } + this.wizardSteps.push({ + type: 'progress', + label: 'Provisioning', + rawSteps: group + }); + } else { + // Skip unsupported steps (CONVERTER, INTEGRATION) + i++; + } + } + // Initialize form groups + for (const ws of this.wizardSteps) { + if (ws.type === 'form') { + this.initFormStep(ws); + } + } + } + + private initFormStep(ws: WizardStep): void { + const formJson = this.zipFiles.get(ws.rawSteps[0].file) || '[]'; + ws.formFields = JSON.parse(formJson) as FormFieldDefinition[]; + const controls: Record = {}; + for (const field of ws.formFields) { const validators = []; if (field.required) { validators.push(Validators.required); @@ -249,42 +291,54 @@ export class TbDeviceInstallDialogComponent implements OnInit { if (field.validators?.length > 0) { validators.push(Validators.pattern(field.validators[0].pattern)); } - const initialValue = field.key in this.formValues - ? this.formValues[field.key] - : (field.defaultValue ?? (field.type === FormFieldType.BOOLEAN ? false : '')); + const initialValue = field.defaultValue ?? (field.type === FormFieldType.BOOLEAN ? false : ''); controls[field.key] = new UntypedFormControl(initialValue, validators); if (field.type === FormFieldType.PASSWORD) { this.passwordVisible[field.key] = false; } } - this.currentFormGroup = new UntypedFormGroup(controls); + ws.formGroup = new UntypedFormGroup(controls); } - private collectAndRunEntitySteps(): void { - this.entitySteps = []; - let i = this.currentStepIndex; - while (i < this.steps.length && ENTITY_STEP_TYPES.has(this.steps[i].type)) { - this.entitySteps.push({ - step: this.steps[i], - status: 'pending', - resolvedName: this.resolveVariables(this.steps[i].name) - }); - i++; + private onStepActivated(): void { + const step = this.currentWizardStep; + if (!step) { + return; + } + if (step.type === 'instruction') { + const raw = this.zipFiles.get(step.rawSteps[0].file) || ''; + step.markdown = this.resolveVariables(raw); + } else if (step.type === 'progress' && !step.progressDone) { + this.initAndRunEntitySteps(step); } - this.currentView = 'progress'; - this.progressError = null; - this.runEntitySteps(); } - private async runEntitySteps(): Promise { - for (const ep of this.entitySteps) { + private initAndRunEntitySteps(ws: WizardStep): void { + ws.entitySteps = ws.rawSteps.map(s => ({ + step: s, + status: 'pending' as const, + resolvedName: this.resolveVariables(s.name) + })); + ws.progressError = null; + ws.progressDone = false; + this.runEntitySteps(ws); + } + + private async runEntitySteps(ws: WizardStep): Promise { + for (const ep of ws.entitySteps) { if (ep.status === 'success') { continue; } ep.status = 'running'; ep.errorMessage = null; try { + const startTime = Date.now(); const output = await this.createEntity(ep.step); + // Ensure minimum visible time per step + const elapsed = Date.now() - startTime; + if (elapsed < ENTITY_STEP_MIN_DELAY) { + await this.delay(ENTITY_STEP_MIN_DELAY - elapsed); + } ep.entityOutput = output; ep.status = 'success'; @@ -292,13 +346,14 @@ export class TbDeviceInstallDialogComponent implements OnInit { if (alias) { this.entityOutputs.set(alias, output); } - const stepIdx = this.steps.indexOf(ep.step); + const rawSteps = this.packageInfo.installSteps[this.selectedConnectivity] || []; + const stepIdx = rawSteps.indexOf(ep.step); if (stepIdx >= 0) { this.positionalOutputs.set(stepIdx + 1, output); } // Re-resolve names of remaining pending steps - for (const remaining of this.entitySteps) { + for (const remaining of ws.entitySteps) { if (remaining.status === 'pending') { remaining.resolvedName = this.resolveVariables(remaining.step.name); } @@ -306,26 +361,27 @@ export class TbDeviceInstallDialogComponent implements OnInit { } catch (err: any) { ep.status = 'error'; ep.errorMessage = err?.error?.message || err?.message || 'Unknown error'; - this.progressError = ep.errorMessage; + ws.progressError = ep.errorMessage; return; } } - // All entity steps succeeded — advance past them - const lastEntityIdx = this.currentStepIndex + this.entitySteps.length - 1; - this.currentStepIndex = lastEntityIdx + 1; - - // Report install + // All done — report install + ws.progressDone = true; try { await firstValueFrom( this.data.iotHubApiService.reportVersionInstalled(this.data.item.id as string, { ignoreLoading: true }) ); } catch (_e) { - // Non-critical — best effort + // Non-critical } - // Advance to next step (instruction after entities, or done) - this.advanceToCurrentStep(); + // Auto-advance to next step after a short pause + if (!this.isLastWizardStep) { + await this.delay(500); + this.stepper.next(); + this.onStepActivated(); + } } private async createEntity(step: DeviceInstallStep): Promise { @@ -384,4 +440,8 @@ export class TbDeviceInstallDialogComponent implements OnInit { const match = page.data.find(rc => rc.name === name); return match ? { id: match.id.id, name: match.name } : null; } + + private delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } }