From 6d2f652c893d1be9fee0cd1ca8001fa65d52504b Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 16 Apr 2026 18:21:57 +0300 Subject: [PATCH] feat(iot-hub): redesign device install dialog progress, conflict, and error states Redesign progress view to match design: success (green check + Done), running (spinner + In progress + gray bg), pending (numbered dashed circle), conflict (orange bg + warning icon + inline action buttons), error (orange bg + Retry button). Add Back button, Next with label, Close when all progress done. Use tb-fullscreen-dialog-lt-md panel class. Add markdown view overrides matching detail dialog readme styles. --- .../device-install-dialog.component.html | 175 +++++---- .../device-install-dialog.component.scss | 365 +++++++++++++----- .../device-install-dialog.component.ts | 27 ++ .../iot-hub/iot-hub-actions.service.ts | 2 +- .../iot-hub/iot-hub-browse.component.ts | 2 +- .../iot-hub-item-detail-dialog.component.ts | 2 +- .../iot-hub-installed-items.component.ts | 2 +- .../assets/locale/locale.constant-en_US.json | 5 +- ui-ngx/src/styles.scss | 19 + 9 files changed, 423 insertions(+), 176 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html index 8d1aad37bc..e078c63cf5 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html @@ -24,7 +24,6 @@ @case ('instruction') {
@@ -110,59 +109,86 @@ @case ('progress') {
- @for (ep of ws.entitySteps; track ep.step.name) { -
-
- @switch (ep.status) { - @case ('pending') { - radio_button_unchecked + @for (ep of ws.entitySteps; track ep.step.name; let i = $index) { + @if (i > 0) { +
+ } + @if (ep.status === 'conflict' || ep.status === 'error') { +
+
+
+ warning +
+
+ + {{ ('iot-hub.device-install-step-type-' + ep.step.type | translate) + ' — ' + (ep.resolvedName || ep.step.name) }} + + @if (ep.status === 'conflict') { + + {{ 'iot-hub.device-install-conflict-exists' | translate:{ type: ('iot-hub.device-install-step-type-' + ep.step.type | translate) } }} + + } + @if (ep.status === 'error' && ep.errorMessage) { + {{ ep.errorMessage }} + } +
+
+
+ @if (ep.status === 'conflict') { + @if (ep.conflictType === 'use-or-overwrite') { + + + } @else { + + + } } - @case ('running') { - + @if (ep.status === 'error') { + } - @case ('success') { - check_circle +
+
+ } @else { +
+
+ @switch (ep.status) { + @case ('pending') { +
{{ i + 1 }}
+ } + @case ('running') { + + } + @case ('success') { + check_circle + } } - @case ('error') { - error +
+ + {{ ('iot-hub.device-install-step-type-' + ep.step.type | translate) + ' — ' + (ep.resolvedName || ep.step.name) }} + + + @if (ep.status === 'success') { + {{ 'iot-hub.done' | translate }} } - @case ('conflict') { - warning + @if (ep.status === 'running') { + {{ 'iot-hub.in-progress' | translate }} } - } -
-
- {{ ('iot-hub.device-install-step-type-' + ep.step.type | translate) + ' — ' + (ep.resolvedName || ep.step.name) }} - @if (ep.status === 'error' && ep.errorMessage) { - {{ ep.errorMessage }} - } - @if (ep.status === 'conflict') { - {{ 'iot-hub.device-install-conflict-exists' | translate:{ type: ('iot-hub.device-install-step-type-' + ep.step.type | translate) } }} - } +
- @if (ep.status === 'success' && ep.resolution) { - {{ 'iot-hub.device-install-resolution-' + ep.resolution | translate }} - } - @if (ep.status === 'conflict') { -
- @if (ep.conflictType === 'use-or-overwrite') { - - - } @else { - - - } -
- } -
+ } }
} @@ -178,9 +204,6 @@ } @else {

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

- @if (selectedInstallMethod && wizardStarted) { - {{ installMethodLabels.get(selectedInstallMethod) }} - } @@ -189,7 +212,7 @@ @if (!wizardStarted) { -
+

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

@@ -206,7 +229,7 @@
- + } @else { + + } + + + + @if (!isFirstWizardStep) { + + } + @if (currentWizardStep; as step) { @switch (step.type) { @case ('instruction') { @@ -270,22 +305,21 @@ } } @else { - - } } @case ('form') { - - } @case ('progress') { @if (step.progressError) { - - + @@ -296,6 +330,11 @@ {{ action.label }} } + } @else if (!step.progressError) { + + } } } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss index 8083e6a44a..86e2889e7f 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss @@ -14,18 +14,60 @@ * limitations under the License. */ +@import "../../../../../../scss/constants"; + +:host-context(.tb-default .tb-dialog .mat-mdc-dialog-component-host), +:host-context(.tb-default .tb-fullscreen-dialog-lt-md .mat-mdc-dialog-component-host) { + .mat-mdc-dialog-content { + padding: 0; + max-height: 100%; + overflow: hidden; + } + @media #{$mat-gt-sm} { + width: 800px; + height: 1000px; + max-width: 100%; + } +} + :host { display: flex; flex-direction: column; - width: 720px; - max-width: 90vw; - max-height: 85vh; + max-height: 90vh; + + @media #{$mat-lt-md} { + max-height: 100%; + height: 100%; + } + + ::ng-deep { + .mat-stepper-horizontal { + height: 100%; + } + .mat-horizontal-stepper-wrapper { + height: 100%; + } + .mat-horizontal-stepper-header-container { + padding: 24px 16px 16px; + } + .mat-horizontal-content-container { + overflow: auto; + flex: 1; + padding: 0; + } + .mat-horizontal-stepper-content.mat-horizontal-stepper-content-current { + height: 100%; + } + .mat-horizontal-stepper-header.mat-step-header { + padding: 0 12px; + } + } } .tb-device-install-header { display: flex; align-items: center; - padding: 16px 24px; + padding: 8px 8px 8px 24px; gap: 12px; .tb-device-install-title { @@ -40,18 +82,6 @@ text-overflow: ellipsis; white-space: nowrap; } - - .tb-device-install-badge { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: 12px; - background: #e3f2fd; - color: #1565c0; - font-size: 12px; - font-weight: 500; - white-space: nowrap; - } } .tb-device-install-loading { @@ -61,14 +91,6 @@ 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; @@ -97,56 +119,97 @@ } } -// Tabs container (review mode) -.tb-device-install-tabs-container { - flex: 1; - overflow-y: auto; - min-height: 200px; - - .tb-tab-content { - padding: 16px 24px; - } +.tb-tab-content { + padding: 16px 24px; } -// Stepper container -.tb-device-install-stepper-container { - flex: 1; - overflow-y: auto; - min-height: 200px; +// Instruction view +.tb-device-install-instruction { + font-size: 14px; + line-height: 24px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + + ::ng-deep tb-markdown .tb-markdown-view { + padding: 16px 24px 24px; + + h1, h2, h3, h4, h5, h6 { + font-size: 20px; + font-weight: 600; + line-height: 24px; + letter-spacing: 0.1px; + color: rgba(0, 0, 0, 0.76); + margin: 0; + padding: 0; + } - ::ng-deep { - .mat-horizontal-stepper-header-container { - padding: 0 16px; + > h1, > h2, > h3, > h4, > h5, > h6 { + padding: 0; + margin-top: 20px; } - .mat-horizontal-content-container { - padding: 0 24px 16px; + > :first-child { + margin-top: 0; } - .mat-step-header { - padding: 0 12px; + h1 { + font-size: 24px; + line-height: 32px; + padding-right: 0; } - } -} -// Step content wrapper -.tb-wizard-step-content { - min-height: 120px; -} + p { + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + margin: 0; + } -// Instruction view -.tb-device-install-instruction { - ::ng-deep tb-markdown { - display: block; - } + > p, > div { + padding-left: 0; + padding-right: 0; + } - ::ng-deep { - h1, h2, h3, h4 { - &:first-child { - margin-top: 0; + p + p { + margin-top: 8px; + } + + h1 + p, h2 + p, h3 + p, + h1 + ul, h2 + ul, h3 + ul, + h1 + ol, h2 + ol, h3 + ol { + margin-top: 8px; + } + + ul, ol { + padding-left: 21px; + padding-right: 0; + margin: 0; + font-size: 14px; + line-height: 24px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + + + h1, + h2, + h3, + h4, + h5, + h6 { + padding-top: 0; + margin-top: 20px; } } + li { + padding-bottom: 0; + margin-bottom: 0; + line-height: 24px; + } + + code:not([class*=language-]) { + font-size: 14px; + } + } + + ::ng-deep { + .tb-download-btn { display: inline-flex; align-items: center; @@ -232,6 +295,7 @@ display: flex; flex-direction: column; gap: 8px; + padding: 24px; mat-form-field { width: 100%; @@ -258,19 +322,32 @@ .tb-device-install-progress { display: flex; flex-direction: column; - gap: 12px; + padding: 24px; +} + +.tb-progress-divider { + width: 1px; + height: 16px; + background-color: rgba(0, 0, 0, 0.12); + margin-left: 24px; } .tb-progress-row { display: flex; - align-items: flex-start; - gap: 12px; + align-items: center; + gap: 16px; padding: 8px 12px; - border-radius: 8px; + border-radius: 4px; + + &.tb-progress-row-active { + background: rgba(0, 0, 0, 0.03); + } - &.tb-progress-conflict-row { - background: #fff8e1; - border: 1px solid #ffcc02; + &.tb-progress-row-alert { + background: rgba(246, 103, 22, 0.06); + align-items: flex-start; + flex-wrap: wrap; + gap: 12px; } } @@ -283,70 +360,152 @@ justify-content: center; mat-icon { - font-size: 20px; - width: 20px; - height: 20px; - } - - .tb-progress-pending { - color: rgba(0, 0, 0, 0.38); + font-size: 24px; + width: 24px; + height: 24px; } .tb-progress-success { - color: #2e7d32; - } - - .tb-progress-error { - color: #c62828; + color: #198038; } - .tb-progress-conflict { - color: #f57f17; + .tb-progress-warning { + color: #f66716; } } -.tb-progress-info { +.tb-progress-pending-circle { + width: 24px; + height: 24px; display: flex; - flex-direction: column; - gap: 4px; - min-height: 24px; + align-items: center; justify-content: center; + font-size: 12px; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.4px; + color: rgba(0, 0, 0, 0.54); + position: relative; + + &::after { + content: ''; + position: absolute; + width: 20px; + height: 20px; + border: 1px dashed rgba(0, 0, 0, 0.12); + border-radius: 999px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } } .tb-progress-label { + flex: 1; + font-size: 16px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.76); + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &.tb-progress-label-active { + font-weight: 500; + letter-spacing: 0.25px; + } + + &.tb-progress-label-pending { + color: rgba(0, 0, 0, 0.54); + } +} + +.tb-progress-status { + flex-shrink: 0; font-size: 14px; + font-weight: 500; line-height: 20px; - color: rgba(0, 0, 0, 0.87); + letter-spacing: 0.25px; + padding: 6px 12px; } -.tb-progress-error-msg { - font-size: 12px; - color: #c62828; - font-family: monospace; - white-space: pre-wrap; - word-break: break-word; +.tb-progress-status-done { + color: #198038; } -.tb-progress-conflict-msg { - font-size: 12px; - color: #f57f17; +.tb-progress-status-progress { + color: rgba(0, 0, 0, 0.54); } -.tb-progress-resolution { - margin-left: auto; - flex-shrink: 0; - font-size: 13px; - color: #2e7d32; - font-weight: 500; +.tb-progress-alert-content { + display: flex; + gap: 16px; + align-items: flex-start; + flex: 1; + min-width: 0; } -.tb-progress-conflict-actions { +.tb-progress-alert-text { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; +} + +.tb-progress-alert-msg { + font-size: 12px; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.4px; + + &.tb-progress-alert-msg-warning { + color: #f66716; + } + + &.tb-progress-alert-msg-error { + color: #dd2c00; + } +} + +.tb-progress-alert-actions { display: flex; gap: 8px; - margin-left: auto; flex-shrink: 0; } +.tb-progress-action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 12px; + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.12); + background: white; + font-size: 14px; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.25px; + white-space: nowrap; + cursor: pointer; + + &.tb-progress-action-primary { + color: #00695c; + } + + &.tb-progress-action-warning { + color: #f66716; + } + + &:hover { + background: rgba(0, 0, 0, 0.04); + } +} + mat-dialog-actions { padding: 8px 24px 16px; + border-top-style: solid; + border-top-color: var(--mat-divider-color, var(--mat-app-outline)); + border-top-width: var(--mat-divider-width); } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts index b3e4e25cc1..78e31f192a 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts @@ -210,6 +210,14 @@ export class TbDeviceInstallDialogComponent extends DialogComponent s.type === 'progress').every(s => s.progressDone); + } + get isLastWizardStep(): boolean { return this.stepper && this.stepper.selectedIndex === this.wizardSteps.length - 1; } @@ -221,6 +229,25 @@ export class TbDeviceInstallDialogComponent extends DialogComponent= this.wizardSteps.length - 1) { + return ''; + } + return this.wizardSteps[this.stepper.selectedIndex + 1]?.label || ''; + } + + previousStep(): void { + if (!this.stepper || this.stepper.selectedIndex <= 0) { + return; + } + const step = this.currentWizardStep; + if (step?.type === 'progress' && step.progressError) { + this.goBackToForm(); + } else { + this.stepper.previous(); + } + } + nextStep(): void { const step = this.currentWizardStep; if (!step) { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts index 79b9d42562..59246aae18 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts @@ -108,7 +108,7 @@ export class IotHubActionsService { mergeMap((blob: Blob) => blob.arrayBuffer()), mergeMap((zipData: ArrayBuffer) => this.dialog.open(TbDeviceInstallDialogComponent, { - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + panelClass: ['tb-dialog', 'tb-fullscreen-dialog-lt-md'], disableClose: true, autoFocus: false, data: { item, zipData } as DeviceInstallDialogData diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts index 7a04e1b284..fb2edb5b64 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts @@ -609,7 +609,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { next: async (blob: Blob) => { const zipData = await blob.arrayBuffer(); this.dialog.open(TbDeviceInstallDialogComponent, { - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + panelClass: ['tb-dialog', 'tb-fullscreen-dialog-lt-md'], disableClose: false, autoFocus: false, data: { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts index 08dd33dbf2..05d72cc093 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts @@ -266,7 +266,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent { const zipData = await blob.arrayBuffer(); const dialogRef = this.dialog.open(TbDeviceInstallDialogComponent, { - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + panelClass: ['tb-dialog', 'tb-fullscreen-dialog-lt-md'], disableClose: true, autoFocus: false, data: { diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts index 3942e03449..86250ed163 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts @@ -270,7 +270,7 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit, O const zipData = await blob.arrayBuffer(); this.iotHubApiService.getVersionInfo(item.itemVersionId, {ignoreLoading: true, ignoreErrors: true}).subscribe(versionView => { this.dialog.open(TbDeviceInstallDialogComponent, { - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + panelClass: ['tb-dialog', 'tb-fullscreen-dialog-lt-md'], disableClose: false, autoFocus: false, data: { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index c3851b33fa..f683d7cac8 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3870,7 +3870,10 @@ "device-install-gateway-docker-compose": "Download docker-compose.yml for your gateway", "device-install-title": "Install \"{{name}}\"", "device-install-complete-title": "Install \"{{name}}\" — Complete", - "device-installing-title": "Installing \"{{name}}\"" + "device-installing-title": "Installing \"{{name}}\"", + "done": "Done", + "in-progress": "In progress", + "error": "Error" }, "item-data": { "widget-type-timeseries": "Timeseries", diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 9058ce4960..00e051c497 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -1078,6 +1078,25 @@ pre.tb-highlight { } } + .tb-fullscreen-dialog-lt-md { + @media #{$mat-lt-md} { + min-height: 100%; + min-width: 100%; + max-width: none !important; + position: absolute !important; + inset: 0; + .mat-mdc-dialog-container { + > *:first-child, form { + min-width: 100% !important; + height: 100%; + } + .mat-mdc-dialog-content { + max-height: 100%; + } + } + } + } + .tb-fullscreen-dialog { @media #{$mat-lt-sm} { min-height: 100%;