From a503ee60b243299ce1f94b1eae793b32305183c4 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 1 Apr 2026 14:35:26 +0300 Subject: [PATCH] Add install/delete dialogs, detail dialog updates, and installed items handling - Create TbIotHubDeleteDialogComponent with warn-colored "Delete anyway" button - Extract install dialog template and styles to separate HTML/SCSS files - Install dialog: simplified confirm/success layout matching design, "Close" label when only button shown on success - Item card: installed label is now a clickable button emitting deleteClick - Detail dialog: 64px colored icon header for CF/RC types, description-only layout (no image) for CF/RC, delete button (stroked warn) in footer when installed, closes with 'deleted' result after confirmed deletion - Home component: handle install/delete dialog results by reloading installed items, pass installedItem to detail dialog --- .../iot-hub-delete-dialog.component.ts | 83 +++++++++++ .../pages/iot-hub/iot-hub-home.component.html | 6 +- .../pages/iot-hub/iot-hub-home.component.ts | 55 +++++++- .../iot-hub-install-dialog.component.html | 80 +++++++++++ .../iot-hub-install-dialog.component.scss | 57 ++++++++ .../iot-hub-install-dialog.component.ts | 115 +--------------- .../iot-hub/iot-hub-item-card.component.html | 4 +- .../iot-hub/iot-hub-item-card.component.scss | 9 ++ .../iot-hub/iot-hub-item-card.component.ts | 6 + .../iot-hub-item-detail-dialog.component.html | 129 +++++++++++------- .../iot-hub-item-detail-dialog.component.scss | 22 ++- .../iot-hub-item-detail-dialog.component.ts | 19 +++ .../home/pages/iot-hub/iot-hub.module.ts | 4 +- .../assets/locale/locale.constant-en_US.json | 7 + 14 files changed, 423 insertions(+), 173 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-delete-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.scss diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-delete-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-delete-dialog.component.ts new file mode 100644 index 0000000000..75eb44dddc --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-delete-dialog.component.ts @@ -0,0 +1,83 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +export interface IotHubDeleteDialogData { + itemName: string; +} + +@Component({ + selector: 'tb-iot-hub-delete-dialog', + standalone: false, + template: ` +
+

{{ 'iot-hub.delete-confirm-title' | translate:{ name: data.itemName } }}

+

{{ 'iot-hub.delete-desc' | translate }}

+
+
+ + +
+ `, + styles: [` + .tb-iot-hub-delete-content { + display: flex; + flex-direction: column; + gap: 20px; + padding: 24px; + } + .tb-iot-hub-delete-title { + font-size: 20px; + font-weight: 600; + line-height: 24px; + letter-spacing: 0.1px; + color: rgba(0, 0, 0, 0.87); + margin: 0; + } + .tb-iot-hub-delete-desc { + font-size: 14px; + font-weight: 400; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.54); + margin: 0; + } + .tb-iot-hub-delete-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + padding: 8px; + } + `] +}) +export class TbIotHubDeleteDialogComponent { + + constructor( + @Inject(MAT_DIALOG_DATA) public data: IotHubDeleteDialogData, + private dialogRef: MatDialogRef + ) {} + + confirm(): void { + this.dialogRef.close(true); + } + + cancel(): void { + this.dialogRef.close(false); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html index 44ef219800..301d9c27fb 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html @@ -88,7 +88,8 @@ [showCreator]="true" (cardClick)="openItemDetail($event)" (creatorClick)="navigateToCreator($event)" - (installClick)="installItem($event)"> + (installClick)="installItem($event)" + (deleteClick)="deleteInstalledItem($event)"> } @@ -133,7 +134,8 @@ [showCreator]="true" (cardClick)="openItemDetail($event)" (creatorClick)="navigateToCreator($event)" - (installClick)="installItem($event)"> + (installClick)="installItem($event)" + (deleteClick)="deleteInstalledItem($event)"> } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts index 060585266a..7f8e5a77a7 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts @@ -26,6 +26,7 @@ import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-it import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { TbIotHubItemDetailDialogComponent, IotHubItemDetailDialogData } from './iot-hub-item-detail-dialog.component'; import { TbIotHubInstallDialogComponent, IotHubInstallDialogData } from './iot-hub-install-dialog.component'; +import { TbIotHubDeleteDialogComponent, IotHubDeleteDialogData } from './iot-hub-delete-dialog.component'; interface CategoryCard { type: ItemType; @@ -167,7 +168,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { openItemDetail(item: MpItemVersionView): void { const installedItem = this.findInstalledItem(item); - this.dialog.open(TbIotHubItemDetailDialogComponent, { + const dialogRef = this.dialog.open(TbIotHubItemDetailDialogComponent, { panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], autoFocus: false, data: { @@ -176,6 +177,25 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { installedItem } as IotHubItemDetailDialogData }); + dialogRef.afterClosed().subscribe(result => { + if (result === 'installed' || result === 'deleted') { + this.reloadInstalledItems(item.type); + } + }); + } + + private reloadInstalledItems(type: ItemType): void { + const config = { ignoreLoading: true }; + const pageLink = new PageLink(10000, 0); + if (type === ItemType.WIDGET) { + this.iotHubApiService.getInstalledItems(pageLink, ItemType.WIDGET, config).subscribe(data => { + this.installedWidgets = data.data; + }); + } else if (type === ItemType.SOLUTION_TEMPLATE) { + this.iotHubApiService.getInstalledItems(pageLink, ItemType.SOLUTION_TEMPLATE, config).subscribe(data => { + this.installedSolutionTemplates = data.data; + }); + } } private findInstalledItem(item: MpItemVersionView): IotHubInstalledItem | undefined { @@ -190,13 +210,19 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { } installItem(item: MpItemVersionView): void { - this.dialog.open(TbIotHubInstallDialogComponent, { - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + const dialogRef = this.dialog.open(TbIotHubInstallDialogComponent, { + panelClass: ['tb-dialog'], + autoFocus: false, data: { item, iotHubApiService: this.iotHubApiService } as IotHubInstallDialogData }); + dialogRef.afterClosed().subscribe(result => { + if (result === 'installed') { + this.reloadInstalledItems(item.type); + } + }); } navigateToCreator(creatorId: string): void { @@ -211,6 +237,29 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { return this.installedSolutionTemplates.find(i => i.itemId === item.itemId); } + deleteInstalledItem(item: MpItemVersionView): void { + const installedItem = this.findInstalledItem(item); + if (!installedItem) { + return; + } + const dialogRef = this.dialog.open(TbIotHubDeleteDialogComponent, { + panelClass: ['tb-dialog'], + autoFocus: false, + data: { itemName: item.name } as IotHubDeleteDialogData + }); + dialogRef.afterClosed().subscribe(confirmed => { + if (confirmed) { + this.iotHubApiService.deleteInstalledItem(installedItem.id.id).subscribe(() => { + if (item.type === ItemType.WIDGET) { + this.installedWidgets = this.installedWidgets.filter(i => i.id.id !== installedItem.id.id); + } else if (item.type === ItemType.SOLUTION_TEMPLATE) { + this.installedSolutionTemplates = this.installedSolutionTemplates.filter(i => i.id.id !== installedItem.id.id); + } + }); + } + }); + } + openSignup(): void { window.open('https://iothub.thingsboard.io/signup', '_blank'); } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.html new file mode 100644 index 0000000000..f101a75cbb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.html @@ -0,0 +1,80 @@ + +
+ @switch (state) { + @case ('confirm') { +

{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}

+

{{ 'iot-hub.install-desc' | translate }}

+ } + @case ('select-entity') { +

{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}

+

{{ 'iot-hub.select-entity-for-cf' | translate:{ name: item.name } }}

+ + + } + @case ('installing') { +

{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}

+

{{ 'iot-hub.install-desc' | translate }}

+ } + @case ('success') { +

{{ 'iot-hub.install-success-title' | translate }}

+

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

+ } + @case ('error') { +

{{ 'iot-hub.install-error-title' | translate }}

+

{{ 'iot-hub.install-error-message' | translate:{ name: item.name } }}

+
{{ errorMessage }}
+ } + } +
+ + @switch (state) { + @case ('confirm') { + + + } + @case ('select-entity') { + + + } + @case ('installing') { + + + } + @case ('success') { + + @if (entityDetailsUrl) { + + } + } + @case ('error') { + + } + } + diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.scss new file mode 100644 index 0000000000..a7ad4a2d3b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.scss @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-iot-hub-install-content { + display: flex; + flex-direction: column; + gap: 20px; + padding: 24px; +} + +.tb-iot-hub-install-title { + font-size: 20px; + font-weight: 600; + line-height: 24px; + letter-spacing: 0.1px; + color: rgba(0, 0, 0, 0.87); + margin: 0; +} + +.tb-iot-hub-install-desc { + color: rgba(0, 0, 0, 0.54); + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + margin: 0; +} + +.tb-iot-hub-inline-spinner { + display: inline-block; + margin-right: 8px; +} + +.tb-iot-hub-error-details { + max-height: 200px; + overflow-y: auto; + padding: 12px; + background: #f5f5f5; + border-radius: 4px; + font-family: monospace; + font-size: 12px; + color: rgba(0, 0, 0, 0.7); + white-space: pre-wrap; + word-break: break-word; +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts index ecb623103d..3b66341e08 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts @@ -40,119 +40,8 @@ export type InstallState = 'select-entity' | 'confirm' | 'installing' | 'success @Component({ selector: 'tb-iot-hub-install-dialog', standalone: false, - template: ` - @switch (state) { - @case ('confirm') { -

{{ 'iot-hub.install-item-title' | translate }}

- -

{{ 'iot-hub.install-confirm' | translate:{ name: item.name, version: item.version } }}

-

{{ 'iot-hub.install-type' | translate:{ type: getTypeLabel() } }}

-

{{ 'iot-hub.install-creator' | translate:{ creator: item.creatorDisplayName } }}

-
- - - - - } - @case ('select-entity') { -

{{ 'iot-hub.install-item-title' | translate }}

- -

{{ 'iot-hub.select-entity-for-cf' | translate:{ name: item.name } }}

- - -
- - - - - } - @case ('installing') { -

{{ 'iot-hub.install-item-title' | translate }}

- -

{{ 'iot-hub.install-confirm' | translate:{ name: item.name, version: item.version } }}

-

{{ 'iot-hub.install-type' | translate:{ type: getTypeLabel() } }}

-

{{ 'iot-hub.install-creator' | translate:{ creator: item.creatorDisplayName } }}

-
- - - - - } - @case ('success') { -

- check_circle - {{ 'iot-hub.install-success-title' | translate }} -

- -

{{ 'iot-hub.install-success-message' | translate:{ name: item.name, version: item.version } }}

-
- - - @if (entityDetailsUrl) { - - } - - } - @case ('error') { -

- error - {{ 'iot-hub.install-error-title' | translate }} -

- -

{{ 'iot-hub.install-error-message' | translate:{ name: item.name } }}

-
{{ errorMessage }}
-
- - - - } - } - `, - styles: [` - .tb-iot-hub-install-meta { - margin: 4px 0; - color: rgba(0, 0, 0, 0.54); - font-size: 14px; - } - .tb-iot-hub-inline-spinner { - display: inline-block; - margin-right: 8px; - } - .tb-iot-hub-result-icon { - vertical-align: middle; - margin-right: 8px; - } - .tb-iot-hub-success-icon { - color: #2e7d32; - } - .tb-iot-hub-error-icon { - color: #c62828; - } - .tb-iot-hub-error-details { - max-height: 200px; - overflow-y: auto; - padding: 12px; - background: #f5f5f5; - border-radius: 4px; - font-family: monospace; - font-size: 12px; - color: rgba(0, 0, 0, 0.7); - white-space: pre-wrap; - word-break: break-word; - margin-top: 8px; - } - `] + templateUrl: './iot-hub-install-dialog.component.html', + styleUrls: ['./iot-hub-install-dialog.component.scss'] }) export class TbIotHubInstallDialogComponent { diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html index e2b2813ff3..cbebb07387 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html @@ -33,7 +33,7 @@ @if (isInstalled()) { @if (isSameVersion()) { - check{{ 'iot-hub.installed' | translate }} + } @else { } @@ -66,7 +66,7 @@ @if (isInstalled()) { @if (isSameVersion()) { - check{{ 'iot-hub.installed' | translate }} + } @else { } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss index 7cabcd8fa1..79ed432451 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss @@ -318,6 +318,8 @@ height: 32px; padding: 6px 12px 6px 8px; border-radius: 4px; + border: none; + background: none; color: $tb-primary-color; font-size: 14px; font-weight: 500; @@ -325,6 +327,13 @@ letter-spacing: 0.25px; white-space: nowrap; flex-shrink: 0; + cursor: pointer; + font-family: inherit; + transition: background 0.15s; + + &:hover { + background: rgba($tb-primary-color, 0.04); + } tb-icon { font-size: 24px; diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts index c43ba55e03..1e7f685808 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts @@ -41,6 +41,7 @@ export class TbIotHubItemCardComponent { @Output() creatorClick = new EventEmitter(); @Output() installClick = new EventEmitter(); @Output() updateClick = new EventEmitter(); + @Output() deleteClick = new EventEmitter(); typeTranslations = itemTypeTranslations; @@ -209,6 +210,11 @@ export class TbIotHubItemCardComponent { this.updateClick.emit(this.item); } + onDeleteClick(event: MouseEvent): void { + event.stopPropagation(); + this.deleteClick.emit(this.item); + } + onCreatorClick(event: MouseEvent): void { event.stopPropagation(); this.creatorClick.emit(this.item.creatorId); diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html index 5feb405405..de9c3f1f3b 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html @@ -18,6 +18,11 @@
+ @if (item.type === ItemType.CALCULATED_FIELD || item.type === ItemType.RULE_CHAIN) { +
+ {{ getCompactIcon() }} +
+ }
{{ item.name }}
@@ -26,8 +31,10 @@ download {{ item.totalInstallCount | shortNumber }} {{ 'iot-hub.installs' | translate }} - - Latest + @if (getSubtypeLabel(); as subtypeLabel) { + + {{ subtypeLabel }} + } v {{ item.version }} @if (item.publishedTime) { @@ -43,54 +50,10 @@
- +
-
-
- @if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) { - - } @else { - @if (getPreviewUrl(); as url) { - - } @else { -
- {{ getTypeIcon() }} -
- } - } -
+ @if (item.type === ItemType.CALCULATED_FIELD || item.type === ItemType.RULE_CHAIN) { +
@if (item.description) {
@@ -102,7 +65,67 @@ {{ item.creatorDisplayName }}
-
+ } @else { + +
+
+ @if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) { + + } @else { + @if (getPreviewUrl(); as url) { + + } @else { +
+ {{ getTypeIcon() }} +
+ } + } +
+
+ @if (item.description) { +
+ +
+ } +
+ person + {{ item.creatorDisplayName }} +
+
+
+ }
@@ -149,6 +172,12 @@