Browse Source

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<String> itemTypes through full stack
  (controller, service, DAO, repository JPQL uses IN clause, UI accepts string|string[])
pull/15508/head
Igor Kulikov 6 months ago
parent
commit
c770bf290f
  1. 4
      application/src/main/java/org/thingsboard/server/controller/IotHubController.java
  2. 2
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java
  3. 2
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java
  4. 4
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java
  5. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java
  6. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java
  7. 7
      ui-ngx/src/app/core/http/iot-hub-api.service.ts
  8. 2
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts
  9. 7
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-delete-dialog.component.ts
  10. 2
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts
  11. 8
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts
  12. 16
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html
  13. 44
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss
  14. 44
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html
  15. 25
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.scss
  16. 2
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.ts
  17. 15
      ui-ngx/src/assets/locale/locale.constant-en_US.json

4
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<String> 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')")

2
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<IotHubInstalledItem> {
PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, String itemType, PageLink pageLink);
PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, List<String> itemTypes, PageLink pageLink);
List<UUID> findInstalledItemIdsByTenantId(TenantId tenantId);

2
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<IotHubInstalledItem> findByTenantId(TenantId tenantId, String itemType, PageLink pageLink);
PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, List<String> itemTypes, PageLink pageLink);
List<UUID> findInstalledItemIdsByTenantId(TenantId tenantId);

4
dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java

@ -46,8 +46,8 @@ class IotHubInstalledItemServiceImpl implements IotHubInstalledItemService {
}
@Override
public PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, String itemType, PageLink pageLink) {
return iotHubInstalledItemDao.findByTenantId(tenantId, itemType, pageLink);
public PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, List<String> itemTypes, PageLink pageLink) {
return iotHubInstalledItemDao.findByTenantId(tenantId, itemTypes, pageLink);
}
@Override

4
dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java

@ -35,13 +35,13 @@ interface IotHubInstalledItemRepository extends JpaRepository<IotHubInstalledIte
@Query("""
SELECT item FROM IotHubInstalledItemEntity item
WHERE item.tenantId = :tenantId
AND (:itemType IS NULL OR item.itemType = :itemType)
AND (:itemTypes IS NULL OR item.itemType IN :itemTypes)
AND (:textSearch IS NULL
OR ilike(item.itemName, CONCAT('%', :textSearch, '%')) = true
OR ilike(item.itemType, CONCAT('%', :textSearch, '%')) = true
OR ilike(item.version, CONCAT('%', :textSearch, '%')) = true)
""")
Page<IotHubInstalledItemEntity> findByTenantId(@Param("tenantId") UUID tenantId, @Param("itemType") String itemType, @Param("textSearch") String textSearch, Pageable pageable);
Page<IotHubInstalledItemEntity> findByTenantId(@Param("tenantId") UUID tenantId, @Param("itemTypes") List<String> itemTypes, @Param("textSearch") String textSearch, Pageable pageable);
@Query("""
SELECT COUNT(item) FROM IotHubInstalledItemEntity item

4
dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java

@ -43,10 +43,10 @@ class JpaIotHubInstalledItemDao extends JpaAbstractDao<IotHubInstalledItemEntity
private final IotHubInstalledItemRepository repository;
@Override
public PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, String itemType, PageLink pageLink) {
public PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, List<String> itemTypes, PageLink pageLink) {
return DaoUtil.toPageData(repository.findByTenantId(
tenantId.getId(),
itemType,
itemTypes == null || itemTypes.isEmpty() ? null : itemTypes,
StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null),
toPageRequest(pageLink))
);

7
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<PageData<IotHubInstalledItem>> {
public getInstalledItems(pageLink: PageLink, itemTypes?: string | string[], config?: IotHubRequestConfig): Observable<PageData<IotHubInstalledItem>> {
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<PageData<IotHubInstalledItem>>(
`/api/iot-hub/installedItems${query}`,

2
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) {

7
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: `
<div class="tb-iot-hub-delete-content">
<h2 class="tb-iot-hub-delete-title">{{ 'iot-hub.delete-confirm-title' | translate:{ name: data.itemName } }}</h2>
<p class="tb-iot-hub-delete-desc">{{ 'iot-hub.delete-desc' | translate }}</p>
<h2 class="tb-iot-hub-delete-title">{{ 'iot-hub.remove-confirm-title' | translate:{ name: data.itemName } }}</h2>
<p class="tb-iot-hub-delete-desc">{{ (data.itemType === 'SOLUTION_TEMPLATE' ? 'iot-hub.remove-desc-solution-template' : 'iot-hub.remove-desc') | translate }}</p>
</div>
<div class="tb-iot-hub-delete-actions">
<button mat-button (click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-flat-button color="warn" (click)="confirm()">{{ 'iot-hub.delete-anyway' | translate }}</button>
<button mat-flat-button color="warn" (click)="confirm()">{{ 'iot-hub.remove-anyway' | translate }}</button>
</div>
`,
styles: [`

2
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) {

8
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
}));

16
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html

@ -36,9 +36,15 @@
</div>
@if (isInstalled()) {
@if (isSameVersion()) {
<button class="tb-iot-hub-card-installed-label" (click)="onDeleteClick($event)"><tb-icon>check</tb-icon>{{ 'iot-hub.installed' | translate }}</button>
<span class="tb-iot-hub-card-installed-chip"><tb-icon>check</tb-icon>{{ 'iot-hub.installed' | translate }}</span>
<button class="tb-iot-hub-card-remove-btn" (click)="onDeleteClick($event)">
<tb-icon>mdi:trash-can-outline</tb-icon>
</button>
} @else {
<button class="tb-iot-hub-card-update-btn" (click)="onUpdateClick($event)">{{ 'iot-hub.update' | translate }}</button>
<button class="tb-iot-hub-card-remove-btn" (click)="onDeleteClick($event)">
<tb-icon>mdi:trash-can-outline</tb-icon>
</button>
}
} @else {
<button class="tb-iot-hub-card-install-btn" (click)="onInstallClick($event)">{{ 'iot-hub.install' | translate }}</button>
@ -72,9 +78,15 @@
<span class="tb-iot-hub-card-stats">
@if (isInstalled()) {
@if (isSameVersion()) {
<button class="tb-iot-hub-card-installed-label" (click)="onDeleteClick($event)"><tb-icon>check</tb-icon>{{ 'iot-hub.installed' | translate }}</button>
<span class="tb-iot-hub-card-installed-chip"><tb-icon>check</tb-icon>{{ 'iot-hub.installed' | translate }}</span>
<button class="tb-iot-hub-card-remove-btn" (click)="onDeleteClick($event)">
<tb-icon>mdi:trash-can-outline</tb-icon>
</button>
} @else {
<button class="tb-iot-hub-card-update-btn" (click)="onUpdateClick($event)">{{ 'iot-hub.update' | translate }}</button>
<button class="tb-iot-hub-card-remove-btn" (click)="onDeleteClick($event)">
<tb-icon>mdi:trash-can-outline</tb-icon>
</button>
}
} @else {
<button class="tb-iot-hub-card-install-btn" (click)="onInstallClick($event)">{{ 'iot-hub.install' | translate }}</button>

44
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);
}
}

44
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html

@ -54,7 +54,7 @@
<div class="dlg-main">
@if (item.type === ItemType.CALCULATED_FIELD || item.type === ItemType.RULE_CHAIN || item.type === ItemType.DEVICE) {
<!-- CF/RC/Device: description + author (no image) -->
<div class="dlg-info">
<div class="dlg-info dlg-info-compact">
<div class="dlg-info-text">
@if (isInstalled()) {
<div class="dlg-info-status-row">
@ -146,33 +146,39 @@
</div>
<div class="dlg-info">
<div class="dlg-info-text">
<!-- Installed status + links row -->
<!-- Installed status + links -->
@if (isInstalled()) {
<div class="dlg-info-status-row">
@if (item.type === ItemType.SOLUTION_TEMPLATE) {
<!-- Solution template: badge on own row, links on separate row -->
<span class="dlg-installed-badge">
<mat-icon>check</mat-icon>
{{ 'iot-hub.installed' | translate }} v{{ installedItem.version }}
</span>
@if (item.type === ItemType.SOLUTION_TEMPLATE) {
<div class="dlg-info-links-row">
@if (isSameVersion()) {
<a class="dlg-info-link" (click)="openSolutionInstructions()">
<mat-icon>info_outline</mat-icon>
{{ 'iot-hub.solution-instructions' | translate }}
</a>
}
<a class="dlg-info-link" (click)="openEntityDetails()">
<mat-icon>open_in_new</mat-icon>
{{ 'iot-hub.goto-main-dashboard' | translate }}
<div class="dlg-info-status-row">
@if (isSameVersion()) {
<a class="dlg-info-link" (click)="openSolutionInstructions()">
<mat-icon>info_outline</mat-icon>
{{ 'iot-hub.solution-instructions' | translate }}
</a>
</div>
} @else {
}
<a class="dlg-info-link" (click)="openEntityDetails()">
<mat-icon>open_in_new</mat-icon>
{{ 'iot-hub.goto-main-dashboard' | translate }}
</a>
</div>
} @else {
<!-- Other types: badge + link on same row -->
<div class="dlg-info-status-row">
<span class="dlg-installed-badge">
<mat-icon>check</mat-icon>
{{ 'iot-hub.installed' | translate }} v{{ installedItem.version }}
</span>
<a class="dlg-info-link" (click)="openEntityDetails()">
<mat-icon>open_in_new</mat-icon>
{{ 'iot-hub.open-item-type-details' | translate:{ type: getTypeLabel() } }}
</a>
}
</div>
</div>
}
}
@if (item.description) {
<div class="dlg-description">
@ -197,7 +203,7 @@
}
@if (isInstalled()) {
<button mat-stroked-button color="warn" (click)="deleteItem()">
<mat-icon>delete</mat-icon>
<tb-icon matButtonIcon class="tb-mat-24">mdi:trash-can-outline</tb-icon>
{{ 'iot-hub.remove' | translate }}
</button>
}

25
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

2
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) {

15
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",

Loading…
Cancel
Save