From c770bf290f3bd2fab610f34ef895f621f15b6478 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 3 Apr 2026 16:03:57 +0300 Subject: [PATCH] Update card installed state, remove labels, detail dialog info, and API changes - Item card: installed chip (non-clickable pill, green) + separate remove icon button (mdi:trash-can-outline) for both same-version and update-available states - Detail dialog: info section matches design exactly (badge, links, actions layout), solution template links on separate row, remove button with warn outline matching label text color, matButtonIcon + tb-mat-24 on remove icon - Rename all Delete labels/keys to Remove across locale and components - Solution template remove dialog: specific description about multiple entities - getInstalledItems API: change itemType to List itemTypes through full stack (controller, service, DAO, repository JPQL uses IN clause, UI accepts string|string[]) --- .../server/controller/IotHubController.java | 4 +- .../dao/iot_hub/IotHubInstalledItemDao.java | 2 +- .../iot_hub/IotHubInstalledItemService.java | 2 +- .../IotHubInstalledItemServiceImpl.java | 4 +- .../IotHubInstalledItemRepository.java | 4 +- .../iot_hub/JpaIotHubInstalledItemDao.java | 4 +- .../src/app/core/http/iot-hub-api.service.ts | 7 +-- .../pages/iot-hub/iot-hub-browse.component.ts | 2 +- .../iot-hub-delete-dialog.component.ts | 7 +-- .../pages/iot-hub/iot-hub-home.component.ts | 2 +- .../iot-hub-installed-items.component.ts | 8 ++-- .../iot-hub/iot-hub-item-card.component.html | 16 ++++++- .../iot-hub/iot-hub-item-card.component.scss | 44 +++++++++++++------ .../iot-hub-item-detail-dialog.component.html | 44 +++++++++++-------- .../iot-hub-item-detail-dialog.component.scss | 25 ++++++++--- .../iot-hub-item-detail-dialog.component.ts | 2 +- .../assets/locale/locale.constant-en_US.json | 15 ++++--- 17 files changed, 122 insertions(+), 70 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java index 9271839004..06de253471 100644 --- a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java +++ b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java @@ -94,9 +94,9 @@ public class IotHubController extends BaseController { @RequestParam(required = false) String textSearch, @RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortOrder, - @RequestParam(required = false) String itemType) throws ThingsboardException { + @RequestParam(required = false) List itemTypes) throws ThingsboardException { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return iotHubInstalledItemService.findByTenantId(getTenantId(), itemType, pageLink); + return iotHubInstalledItemService.findByTenantId(getTenantId(), itemTypes, pageLink); } @PreAuthorize("hasAuthority('TENANT_ADMIN')") diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java index 2a5b88804a..27af668bbe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java @@ -26,7 +26,7 @@ import java.util.UUID; public interface IotHubInstalledItemDao extends Dao { - PageData findByTenantId(TenantId tenantId, String itemType, PageLink pageLink); + PageData findByTenantId(TenantId tenantId, List itemTypes, PageLink pageLink); List findInstalledItemIdsByTenantId(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java index 5a9d434c08..71176f09e0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java @@ -30,7 +30,7 @@ public interface IotHubInstalledItemService { IotHubInstalledItem findById(TenantId tenantId, IotHubInstalledItemId id); - PageData findByTenantId(TenantId tenantId, String itemType, PageLink pageLink); + PageData findByTenantId(TenantId tenantId, List itemTypes, PageLink pageLink); List findInstalledItemIdsByTenantId(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java index 28197668da..4d49e20584 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java @@ -46,8 +46,8 @@ class IotHubInstalledItemServiceImpl implements IotHubInstalledItemService { } @Override - public PageData findByTenantId(TenantId tenantId, String itemType, PageLink pageLink) { - return iotHubInstalledItemDao.findByTenantId(tenantId, itemType, pageLink); + public PageData findByTenantId(TenantId tenantId, List itemTypes, PageLink pageLink) { + return iotHubInstalledItemDao.findByTenantId(tenantId, itemTypes, pageLink); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java index 3dde0d3b09..62a562c537 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java @@ -35,13 +35,13 @@ interface IotHubInstalledItemRepository extends JpaRepository findByTenantId(@Param("tenantId") UUID tenantId, @Param("itemType") String itemType, @Param("textSearch") String textSearch, Pageable pageable); + Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("itemTypes") List itemTypes, @Param("textSearch") String textSearch, Pageable pageable); @Query(""" SELECT COUNT(item) FROM IotHubInstalledItemEntity item diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java index ff2664127b..9eb9a068b0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java @@ -43,10 +43,10 @@ class JpaIotHubInstalledItemDao extends JpaAbstractDao findByTenantId(TenantId tenantId, String itemType, PageLink pageLink) { + public PageData findByTenantId(TenantId tenantId, List itemTypes, PageLink pageLink) { return DaoUtil.toPageData(repository.findByTenantId( tenantId.getId(), - itemType, + itemTypes == null || itemTypes.isEmpty() ? null : itemTypes, StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), toPageRequest(pageLink)) ); diff --git a/ui-ngx/src/app/core/http/iot-hub-api.service.ts b/ui-ngx/src/app/core/http/iot-hub-api.service.ts index 1b902fb26d..bce32a4cd8 100644 --- a/ui-ngx/src/app/core/http/iot-hub-api.service.ts +++ b/ui-ngx/src/app/core/http/iot-hub-api.service.ts @@ -151,10 +151,11 @@ export class IotHubApiService { ); } - public getInstalledItems(pageLink: PageLink, itemType?: string, config?: IotHubRequestConfig): Observable> { + public getInstalledItems(pageLink: PageLink, itemTypes?: string | string[], config?: IotHubRequestConfig): Observable> { let query = pageLink.toQuery(); - if (itemType) { - query += `&itemType=${itemType}`; + if (itemTypes) { + const types = Array.isArray(itemTypes) ? itemTypes : [itemTypes]; + types.forEach(t => query += `&itemTypes=${t}`); } return this.http.get>( `/api/iot-hub/installedItems${query}`, diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts index 0c96e89eb3..854481cd1c 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts @@ -500,7 +500,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { const dialogRef = this.dialog.open(TbIotHubDeleteDialogComponent, { panelClass: ['tb-dialog'], autoFocus: false, - data: { itemName: item.name } as IotHubDeleteDialogData + data: { itemName: item.name, itemType: item.type } as IotHubDeleteDialogData }); dialogRef.afterClosed().subscribe(confirmed => { if (confirmed) { 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 index 75eb44dddc..5ca9d623b4 100644 --- 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 @@ -19,6 +19,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; export interface IotHubDeleteDialogData { itemName: string; + itemType?: string; } @Component({ @@ -26,12 +27,12 @@ export interface IotHubDeleteDialogData { standalone: false, template: `
-

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

-

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

+

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

+

{{ (data.itemType === 'SOLUTION_TEMPLATE' ? 'iot-hub.remove-desc-solution-template' : 'iot-hub.remove-desc') | translate }}

- +
`, styles: [` 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 2eef023df1..d00897c7df 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 @@ -315,7 +315,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { const dialogRef = this.dialog.open(TbIotHubDeleteDialogComponent, { panelClass: ['tb-dialog'], autoFocus: false, - data: { itemName: item.name } as IotHubDeleteDialogData + data: { itemName: item.name, itemType: item.type } as IotHubDeleteDialogData }); dialogRef.afterClosed().subscribe(confirmed => { if (confirmed) { 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 e8a875acea..940bfa5eee 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 @@ -106,8 +106,8 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { deleteItem(item: IotHubInstalledItem): void { this.dialogService.confirm( - this.translate.instant('iot-hub.delete-installed-item-title'), - this.translate.instant('iot-hub.delete-installed-item-text', {name: item.itemName}), + this.translate.instant('iot-hub.remove-installed-item-title'), + this.translate.instant('iot-hub.remove-installed-item-text', {name: item.itemName}), this.translate.instant('action.no'), this.translate.instant('action.yes') ).subscribe(result => { @@ -115,7 +115,7 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { this.iotHubApiService.deleteInstalledItem(item.id.id).subscribe({ next: () => { this.store.dispatch(new ActionNotificationShow({ - message: this.translate.instant('iot-hub.installed-item-deleted', {name: item.itemName}), + message: this.translate.instant('iot-hub.installed-item-removed', {name: item.itemName}), type: 'success', duration: 3000 })); @@ -123,7 +123,7 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { }, error: () => { this.store.dispatch(new ActionNotificationShow({ - message: this.translate.instant('iot-hub.installed-item-delete-error', {name: item.itemName}), + message: this.translate.instant('iot-hub.installed-item-remove-error', {name: item.itemName}), type: 'error', duration: 5000 })); 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 9d3abff394..46a3b7fe76 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 @@ -36,9 +36,15 @@ @if (isInstalled()) { @if (isSameVersion()) { - + check{{ 'iot-hub.installed' | translate }} + } @else { + } } @else { @@ -72,9 +78,15 @@ @if (isInstalled()) { @if (isSameVersion()) { - + check{{ 'iot-hub.installed' | translate }} + } @else { + } } @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 ca294f2846..3e7d038e44 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 @@ -320,35 +320,53 @@ } } -// Installed label — Design: check icon + text, no bg, $tb-primary-color, h-32, pl-8 pr-12 py-6, rounded-4, gap-4 -.tb-iot-hub-card-installed-label { +// Installed chip — Design: pill, bg rgba(25,128,56,0.06), check 18px + "Installed" 14px Regular, #198038 +.tb-iot-hub-card-installed-chip { display: inline-flex; align-items: center; gap: 4px; height: 32px; padding: 6px 12px 6px 8px; - border-radius: 4px; - border: none; - background: none; - color: $tb-primary-color; + border-radius: 16px; + background: rgba(25, 128, 56, 0.06); + color: #198038; font-size: 14px; - font-weight: 500; + font-weight: 400; line-height: 20px; - 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: 18px; + width: 18px; + height: 18px; } +} + +// Remove icon button — Design: trash-can-outline 24px, rgba(0,0,0,0.54) +.tb-iot-hub-card-remove-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 4px; + border: none; + background: none; + cursor: pointer; + flex-shrink: 0; + border-radius: 50%; + transition: background 0.15s; tb-icon { font-size: 24px; width: 24px; height: 24px; + color: rgba(0, 0, 0, 0.54); + } + + &:hover { + background: rgba(0, 0, 0, 0.04); } } 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 3497c5b79a..bd256be12e 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 @@ -54,7 +54,7 @@
@if (item.type === ItemType.CALCULATED_FIELD || item.type === ItemType.RULE_CHAIN || item.type === ItemType.DEVICE) { -
+
@if (isInstalled()) {
@@ -146,33 +146,39 @@
- + @if (isInstalled()) { -
+ @if (item.type === ItemType.SOLUTION_TEMPLATE) { + check {{ 'iot-hub.installed' | translate }} v{{ installedItem.version }} - @if (item.type === ItemType.SOLUTION_TEMPLATE) { - + } @else { + +
+ + check + {{ 'iot-hub.installed' | translate }} v{{ installedItem.version }} + open_in_new {{ 'iot-hub.open-item-type-details' | translate:{ type: getTypeLabel() } }} - } -
+
+ } } @if (item.description) {
@@ -197,7 +203,7 @@ } @if (isInstalled()) { } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.scss index 3f38254efd..0037388209 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.scss @@ -115,11 +115,10 @@ padding: 0 24px 16px; } -// Row 1 — Design: flex row, gap-24, items-center +// Row 1 — Design: flex row, gap-24 .dlg-row-1 { display: flex; gap: 24px; - align-items: center; } // Preview image — Design: flex-1, aspect 305:172, bg rgba(0,0,0,0.06), rounded-8, p-16, dot pattern @@ -462,15 +461,20 @@ &.active { background: rgba(0, 0, 0, 0.6); } } -// Info panel — Design: flex-col, gap-32 between text-container and buttons, py-16 +// Info panel — Design: flex-col, space-between, py-16, gap-32 .dlg-info { flex: 1; min-width: 0; display: flex; flex-direction: column; + justify-content: space-between; gap: 32px; - justify-content: center; padding: 16px 0; + + &.dlg-info-compact { + justify-content: flex-start; + padding: 0; + } } // Text container — Design: flex-col, gap-16 @@ -492,6 +496,7 @@ .dlg-installed-badge { display: inline-flex; align-items: center; + align-self: flex-start; gap: 4px; padding: 4px 8px; border-radius: 16px; @@ -521,6 +526,8 @@ color: $tb-primary-color; cursor: pointer; text-decoration: none; + border: none; + border-bottom: none; mat-icon { font-size: 16px; @@ -530,6 +537,8 @@ &:hover { text-decoration: underline; + border: none; + border-bottom: none; } } @@ -540,12 +549,16 @@ gap: 16px; } -// Action buttons row — Design: flex, justify-between, full width +// Action buttons row — Design: flex, full width, Remove always right-aligned .dlg-info-actions { display: flex; align-items: center; - justify-content: space-between; width: 100%; + + [color="warn"] { + margin-left: auto; + --mat-button-outlined-outline-color: var(--mat-button-outlined-label-text-color); + } } // Footer — only Close button diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.ts index 5cfe0b3992..f2de08de2d 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.ts @@ -357,7 +357,7 @@ export class TbIotHubItemDetailDialogComponent { const deleteDialogRef = this.dialog.open(TbIotHubDeleteDialogComponent, { panelClass: ['tb-dialog'], autoFocus: false, - data: { itemName: this.item.name } as IotHubDeleteDialogData + data: { itemName: this.item.name, itemType: this.item.type } as IotHubDeleteDialogData }); deleteDialogRef.afterClosed().subscribe(confirmed => { if (confirmed) { 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 520ea96fe7..7119089cdd 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3770,10 +3770,11 @@ "install-desc": "Installing this will automatically create and configure the necessary resources within your tenant", "install-success-desc": "'{{name}}' has been installed successfully", "open-details": "Open details", - "delete-confirm-title": "Delete '{{name}}'?", - "delete-desc": "Deleting this will also remove the installed entity and its associated resources within your tenant", - "delete-anyway": "Delete anyway", "remove": "Remove", + "remove-confirm-title": "Remove '{{name}}'?", + "remove-desc": "Removing this will also remove the installed entity and its associated resources within your tenant", + "remove-desc-solution-template": "Removing this solution template will also remove all created entities including dashboards, rule chains, devices, and other associated resources within your tenant", + "remove-anyway": "Remove anyway", "select-entity-for-cf": "Select entity for calculated field '{{name}}'", "install-type": "Type: {{type}}", "install-creator": "Creator: {{creator}}", @@ -3859,10 +3860,10 @@ "installed-date": "Installed", "actions": "Actions", "no-installed-items": "No installed items found", - "delete-installed-item-title": "Delete Installed Item", - "delete-installed-item-text": "Are you sure you want to delete '{{name}}'? The installed entity will also be removed.", - "installed-item-deleted": "'{{name}}' has been deleted", - "installed-item-delete-error": "Failed to delete '{{name}}'", + "remove-installed-item-title": "Remove Installed Item", + "remove-installed-item-text": "Are you sure you want to remove '{{name}}'? The installed entity will also be removed.", + "installed-item-removed": "'{{name}}' has been removed", + "installed-item-remove-error": "Failed to remove '{{name}}'", "open-installed-entity": "Open installed entity", "view-item-details": "View item details", "open-item-type-details": "Open {{type}} details",