Browse Source

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
pull/15347/head
Igor Kulikov 6 months ago
parent
commit
a503ee60b2
  1. 83
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-delete-dialog.component.ts
  2. 6
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html
  3. 55
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts
  4. 80
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.html
  5. 57
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.scss
  6. 115
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts
  7. 4
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html
  8. 9
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss
  9. 6
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts
  10. 129
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html
  11. 22
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.scss
  12. 19
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.ts
  13. 4
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts
  14. 7
      ui-ngx/src/assets/locale/locale.constant-en_US.json

83
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: `
<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>
</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>
</div>
`,
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<TbIotHubDeleteDialogComponent>
) {}
confirm(): void {
this.dialogRef.close(true);
}
cancel(): void {
this.dialogRef.close(false);
}
}

6
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)">
</tb-iot-hub-item-card>
}
</div>
@ -133,7 +134,8 @@
[showCreator]="true"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)">
(installClick)="installItem($event)"
(deleteClick)="deleteInstalledItem($event)">
</tb-iot-hub-item-card>
}
</div>

55
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');
}

80
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.html

@ -0,0 +1,80 @@
<!--
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.
-->
<div class="tb-iot-hub-install-content">
@switch (state) {
@case ('confirm') {
<h2 class="tb-iot-hub-install-title">{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}</h2>
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.install-desc' | translate }}</p>
}
@case ('select-entity') {
<h2 class="tb-iot-hub-install-title">{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}</h2>
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.select-entity-for-cf' | translate:{ name: item.name } }}</p>
<tb-entity-select
[(ngModel)]="selectedEntityId"
[allowedEntityTypes]="cfEntityTypes"
[defaultEntityType]="defaultCfEntityType"
[filterAllowedEntityTypes]="false"
appearance="outline"
required>
</tb-entity-select>
}
@case ('installing') {
<h2 class="tb-iot-hub-install-title">{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}</h2>
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.install-desc' | translate }}</p>
}
@case ('success') {
<h2 class="tb-iot-hub-install-title">{{ 'iot-hub.install-success-title' | translate }}</h2>
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.install-success-desc' | translate:{ name: item.name } }}</p>
}
@case ('error') {
<h2 class="tb-iot-hub-install-title">{{ 'iot-hub.install-error-title' | translate }}</h2>
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.install-error-message' | translate:{ name: item.name } }}</p>
<div class="tb-iot-hub-error-details">{{ errorMessage }}</div>
}
}
</div>
<mat-dialog-actions align="end">
@switch (state) {
@case ('confirm') {
<button mat-button (click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-flat-button color="primary" (click)="install()">{{ 'iot-hub.install' | translate }}</button>
}
@case ('select-entity') {
<button mat-button (click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-flat-button color="primary" [disabled]="!selectedEntityId" (click)="doInstall()">{{ 'iot-hub.install' | translate }}</button>
}
@case ('installing') {
<button mat-button disabled>{{ 'action.cancel' | translate }}</button>
<button mat-flat-button color="primary" disabled>
<mat-spinner diameter="18" class="tb-iot-hub-inline-spinner"></mat-spinner>
{{ 'iot-hub.installing' | translate }}
</button>
}
@case ('success') {
<button mat-button (click)="close()">{{ (entityDetailsUrl ? 'action.cancel' : 'action.close') | translate }}</button>
@if (entityDetailsUrl) {
<button mat-flat-button color="primary" (click)="openEntityDetails()">
{{ 'iot-hub.open-details' | translate }}
</button>
}
}
@case ('error') {
<button mat-button (click)="close()">{{ 'action.close' | translate }}</button>
}
}
</mat-dialog-actions>

57
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;
}

115
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') {
<h2 mat-dialog-title>{{ 'iot-hub.install-item-title' | translate }}</h2>
<mat-dialog-content>
<p>{{ 'iot-hub.install-confirm' | translate:{ name: item.name, version: item.version } }}</p>
<p class="tb-iot-hub-install-meta">{{ 'iot-hub.install-type' | translate:{ type: getTypeLabel() } }}</p>
<p class="tb-iot-hub-install-meta">{{ 'iot-hub.install-creator' | translate:{ creator: item.creatorDisplayName } }}</p>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-raised-button color="primary" (click)="install()">{{ 'iot-hub.install' | translate }}</button>
</mat-dialog-actions>
}
@case ('select-entity') {
<h2 mat-dialog-title>{{ 'iot-hub.install-item-title' | translate }}</h2>
<mat-dialog-content>
<p>{{ 'iot-hub.select-entity-for-cf' | translate:{ name: item.name } }}</p>
<tb-entity-select
[(ngModel)]="selectedEntityId"
[allowedEntityTypes]="cfEntityTypes"
[defaultEntityType]="defaultCfEntityType"
[filterAllowedEntityTypes]="false"
appearance="outline"
required>
</tb-entity-select>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-raised-button color="primary" [disabled]="!selectedEntityId" (click)="doInstall()">{{ 'iot-hub.install' | translate }}</button>
</mat-dialog-actions>
}
@case ('installing') {
<h2 mat-dialog-title>{{ 'iot-hub.install-item-title' | translate }}</h2>
<mat-dialog-content>
<p>{{ 'iot-hub.install-confirm' | translate:{ name: item.name, version: item.version } }}</p>
<p class="tb-iot-hub-install-meta">{{ 'iot-hub.install-type' | translate:{ type: getTypeLabel() } }}</p>
<p class="tb-iot-hub-install-meta">{{ 'iot-hub.install-creator' | translate:{ creator: item.creatorDisplayName } }}</p>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button disabled>{{ 'action.cancel' | translate }}</button>
<button mat-raised-button color="primary" disabled>
<mat-spinner diameter="18" class="tb-iot-hub-inline-spinner"></mat-spinner>
{{ 'iot-hub.installing' | translate }}
</button>
</mat-dialog-actions>
}
@case ('success') {
<h2 mat-dialog-title>
<mat-icon class="tb-iot-hub-result-icon tb-iot-hub-success-icon">check_circle</mat-icon>
{{ 'iot-hub.install-success-title' | translate }}
</h2>
<mat-dialog-content>
<p>{{ 'iot-hub.install-success-message' | translate:{ name: item.name, version: item.version } }}</p>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="close()">{{ 'action.close' | translate }}</button>
@if (entityDetailsUrl) {
<button mat-raised-button color="primary" (click)="openEntityDetails()">
{{ 'iot-hub.open-item-type-details' | translate:{ type: getTypeLabel() } }}
</button>
}
</mat-dialog-actions>
}
@case ('error') {
<h2 mat-dialog-title>
<mat-icon class="tb-iot-hub-result-icon tb-iot-hub-error-icon">error</mat-icon>
{{ 'iot-hub.install-error-title' | translate }}
</h2>
<mat-dialog-content>
<p>{{ 'iot-hub.install-error-message' | translate:{ name: item.name } }}</p>
<div class="tb-iot-hub-error-details">{{ errorMessage }}</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="close()">{{ 'action.close' | translate }}</button>
</mat-dialog-actions>
}
}
`,
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 {

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

@ -33,7 +33,7 @@
</div>
@if (isInstalled()) {
@if (isSameVersion()) {
<span class="tb-iot-hub-card-installed-label"><tb-icon>check</tb-icon>{{ 'iot-hub.installed' | translate }}</span>
<button class="tb-iot-hub-card-installed-label" (click)="onDeleteClick($event)"><tb-icon>check</tb-icon>{{ 'iot-hub.installed' | translate }}</button>
} @else {
<button class="tb-iot-hub-card-update-btn" (click)="onUpdateClick($event)">{{ 'iot-hub.update' | translate }}</button>
}
@ -66,7 +66,7 @@
<span class="tb-iot-hub-card-stats">
@if (isInstalled()) {
@if (isSameVersion()) {
<span class="tb-iot-hub-card-installed-label"><tb-icon>check</tb-icon>{{ 'iot-hub.installed' | translate }}</span>
<button class="tb-iot-hub-card-installed-label" (click)="onDeleteClick($event)"><tb-icon>check</tb-icon>{{ 'iot-hub.installed' | translate }}</button>
} @else {
<button class="tb-iot-hub-card-update-btn" (click)="onUpdateClick($event)">{{ 'iot-hub.update' | translate }}</button>
}

9
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;

6
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<string>();
@Output() installClick = new EventEmitter<MpItemVersionView>();
@Output() updateClick = new EventEmitter<MpItemVersionView>();
@Output() deleteClick = new EventEmitter<MpItemVersionView>();
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);

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

@ -18,6 +18,11 @@
<div class="tb-iot-hub-detail-dialog">
<!-- Header -->
<div class="dlg-header">
@if (item.type === ItemType.CALCULATED_FIELD || item.type === ItemType.RULE_CHAIN) {
<div class="dlg-header-icon" [style.background]="item.color || '#048ad3'">
<tb-icon>{{ getCompactIcon() }}</tb-icon>
</div>
}
<div class="dlg-header-content">
<span class="dlg-title">{{ item.name }}</span>
<div class="dlg-subtitle">
@ -26,8 +31,10 @@
<span class="dlg-dot"></span>
<tb-icon class="dlg-subtitle-icon">download</tb-icon>
<span>{{ item.totalInstallCount | shortNumber }} {{ 'iot-hub.installs' | translate }}</span>
<span class="dlg-dot"></span>
<span>Latest</span>
@if (getSubtypeLabel(); as subtypeLabel) {
<span class="dlg-dot"></span>
<span>{{ subtypeLabel }}</span>
}
<span class="dlg-dot"></span>
<span>v {{ item.version }}</span>
@if (item.publishedTime) {
@ -43,54 +50,10 @@
<!-- Scrollable content -->
<div class="dlg-body">
<!-- Row 1: Image + Description + Author -->
<!-- Main content -->
<div class="dlg-main">
<div class="dlg-row-1">
<div class="dlg-preview">
@if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) {
<div class="dlg-hero-carousel">
<ngx-hm-carousel
[(ngModel)]="carouselIndex"
[data]="carouselImages"
[infinite]="true"
[autoplay-speed]="3000"
[between-delay]="0"
[autoplay]="true"
[mouse-enable]="true"
class="dlg-carousel">
<section ngx-hm-carousel-container class="dlg-carousel-container">
<article class="dlg-carousel-item"
ngx-hm-carousel-item
*ngFor="let imageUrl of carouselImages">
<img [src]="imageUrl" alt="">
</article>
</section>
<ng-template #carouselPrev>
<button mat-icon-button class="dlg-carousel-nav">
<mat-icon>chevron_left</mat-icon>
</button>
</ng-template>
<ng-template #carouselNext>
<button mat-icon-button class="dlg-carousel-nav">
<mat-icon>chevron_right</mat-icon>
</button>
</ng-template>
<ng-template #carouselDot let-model>
<div class="dlg-carousel-dot"
[class.active]="model.index === model.currentIndex"></div>
</ng-template>
</ngx-hm-carousel>
</div>
} @else {
@if (getPreviewUrl(); as url) {
<img [src]="url" alt="" class="dlg-preview-img">
} @else {
<div class="dlg-preview-placeholder">
<tb-icon>{{ getTypeIcon() }}</tb-icon>
</div>
}
}
</div>
@if (item.type === ItemType.CALCULATED_FIELD || item.type === ItemType.RULE_CHAIN) {
<!-- CF/RC: description + author only (no image) -->
<div class="dlg-info">
@if (item.description) {
<div class="dlg-description">
@ -102,7 +65,67 @@
<span>{{ item.creatorDisplayName }}</span>
</div>
</div>
</div>
} @else {
<!-- Widget/Dashboard/Solution: image + description side by side -->
<div class="dlg-row-1">
<div class="dlg-preview">
@if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) {
<div class="dlg-hero-carousel">
<ngx-hm-carousel
[(ngModel)]="carouselIndex"
[data]="carouselImages"
[infinite]="true"
[autoplay-speed]="3000"
[between-delay]="0"
[autoplay]="true"
[mouse-enable]="true"
class="dlg-carousel">
<section ngx-hm-carousel-container class="dlg-carousel-container">
<article class="dlg-carousel-item"
ngx-hm-carousel-item
*ngFor="let imageUrl of carouselImages">
<img [src]="imageUrl" alt="">
</article>
</section>
<ng-template #carouselPrev>
<button mat-icon-button class="dlg-carousel-nav">
<mat-icon>chevron_left</mat-icon>
</button>
</ng-template>
<ng-template #carouselNext>
<button mat-icon-button class="dlg-carousel-nav">
<mat-icon>chevron_right</mat-icon>
</button>
</ng-template>
<ng-template #carouselDot let-model>
<div class="dlg-carousel-dot"
[class.active]="model.index === model.currentIndex"></div>
</ng-template>
</ngx-hm-carousel>
</div>
} @else {
@if (getPreviewUrl(); as url) {
<img [src]="url" alt="" class="dlg-preview-img">
} @else {
<div class="dlg-preview-placeholder">
<tb-icon>{{ getTypeIcon() }}</tb-icon>
</div>
}
}
</div>
<div class="dlg-info">
@if (item.description) {
<div class="dlg-description">
<tb-markdown [data]="item.description"></tb-markdown>
</div>
}
<div class="dlg-author" (click)="navigateToCreator()">
<tb-icon>person</tb-icon>
<span>{{ item.creatorDisplayName }}</span>
</div>
</div>
</div>
}
</div>
<!-- Categories & Use Cases -->
@ -149,6 +172,12 @@
<!-- Footer -->
<div class="dlg-footer">
@if (isInstalled()) {
<button mat-stroked-button color="warn" (click)="deleteItem()">
<mat-icon>delete</mat-icon>
{{ 'action.delete' | translate }}
</button>
}
@if (isSameVersion() && item.type === ItemType.SOLUTION_TEMPLATE) {
<button mat-button (click)="openSolutionInstructions()">
{{ 'iot-hub.solution-instructions' | translate }}

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

@ -27,15 +27,33 @@
width: 900px;
}
// Header — Design: pl-24, pr-12, py-16, gap-8
// Header — Design: pl-24, pr-12, py-16, gap between icon+content=16, content+close=8
.dlg-header {
display: flex;
align-items: flex-start;
gap: 8px;
gap: 16px;
padding: 16px 12px 16px 24px;
flex-shrink: 0;
}
// Header icon for CF/RC — Design: 64px, rounded-8, colored bg, white icon 36px
.dlg-header-icon {
width: 64px;
height: 64px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
tb-icon {
font-size: 36px;
width: 36px;
height: 36px;
color: #fff;
}
}
.dlg-header-content {
flex: 1;
min-width: 0;

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

@ -26,6 +26,7 @@ import { EntityType } from '@shared/models/entity-type.models';
import { getEntityDetailsPageURL } from '@core/utils';
import { TbIotHubInstallDialogComponent, IotHubInstallDialogData } from './iot-hub-install-dialog.component';
import { TbIotHubUpdateDialogComponent, IotHubUpdateDialogData } from './iot-hub-update-dialog.component';
import { TbIotHubDeleteDialogComponent, IotHubDeleteDialogData } from './iot-hub-delete-dialog.component';
import { SolutionInstallDialogComponent } from '@home/components/solution/solution-install-dialog.component';
import { SolutionTemplateInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models';
@ -320,6 +321,24 @@ export class TbIotHubItemDetailDialogComponent {
}
}
deleteItem(): void {
if (!this.installedItem) {
return;
}
const deleteDialogRef = this.dialog.open(TbIotHubDeleteDialogComponent, {
panelClass: ['tb-dialog'],
autoFocus: false,
data: { itemName: this.item.name } as IotHubDeleteDialogData
});
deleteDialogRef.afterClosed().subscribe(confirmed => {
if (confirmed) {
this.data.iotHubApiService.deleteInstalledItem(this.installedItem.id.id).subscribe(() => {
this.dialogRef.close('deleted');
});
}
});
}
close(): void {
this.dialogRef.close();
}

4
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts

@ -26,6 +26,7 @@ import { TbIotHubInstallDialogComponent } from './iot-hub-install-dialog.compone
import { TbIotHubItemCardComponent } from './iot-hub-item-card.component';
import { TbIotHubInstalledItemsComponent } from './iot-hub-installed-items.component';
import { TbIotHubUpdateDialogComponent } from './iot-hub-update-dialog.component';
import { TbIotHubDeleteDialogComponent } from './iot-hub-delete-dialog.component';
@NgModule({
declarations: [
@ -36,7 +37,8 @@ import { TbIotHubUpdateDialogComponent } from './iot-hub-update-dialog.component
TbIotHubInstallDialogComponent,
TbIotHubItemCardComponent,
TbIotHubInstalledItemsComponent,
TbIotHubUpdateDialogComponent
TbIotHubUpdateDialogComponent,
TbIotHubDeleteDialogComponent
],
imports: [
CommonModule,

7
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3756,6 +3756,13 @@
"created-by": "Created by {{name}}",
"install-item-title": "Install Item",
"install-confirm": "Install {{name}} v{{version}}?",
"install-confirm-title": "Install '{{name}}'?",
"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",
"select-entity-for-cf": "Select entity for calculated field '{{name}}'",
"install-type": "Type: {{type}}",
"install-creator": "Creator: {{creator}}",

Loading…
Cancel
Save