diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts
index 1d52e8156c..1f6fc3d021 100644
--- a/ui-ngx/src/app/modules/home/components/home-components.module.ts
+++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts
@@ -115,6 +115,7 @@ import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/
import { TbIotHubItemDetailDialogComponent } from '@home/components/iot-hub/iot-hub-item-detail-dialog.component';
import { TbIotHubBrowseComponent } from '@home/components/iot-hub/iot-hub-browse.component';
import { TbIotHubItemCardComponent } from '@home/components/iot-hub/iot-hub-item-card.component';
+import { TbIotHubAddItemDialogComponent } from '@home/components/iot-hub/iot-hub-add-item-dialog.component';
import { AddWidgetDialogComponent } from '@home/components/dashboard-page/add-widget-dialog.component';
import { ManageDashboardLayoutsDialogComponent } from '@home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component';
import {
@@ -327,6 +328,7 @@ import { SolutionInstallDialogComponent } from '@home/components/solution/soluti
TbIotHubItemDetailDialogComponent,
TbIotHubBrowseComponent,
TbIotHubItemCardComponent,
+ TbIotHubAddItemDialogComponent,
AddWidgetDialogComponent,
MoveWidgetsDialogComponent,
ManageDashboardLayoutsDialogComponent,
@@ -480,6 +482,7 @@ import { SolutionInstallDialogComponent } from '@home/components/solution/soluti
TbIotHubItemDetailDialogComponent,
TbIotHubBrowseComponent,
TbIotHubItemCardComponent,
+ TbIotHubAddItemDialogComponent,
AddWidgetDialogComponent,
MoveWidgetsDialogComponent,
ManageDashboardLayoutsDialogComponent,
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.html
new file mode 100644
index 0000000000..22a0206f7a
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.html
@@ -0,0 +1,39 @@
+
+
+
+
+ @if (isInstalling) {
+
+
+
+ }
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.scss
new file mode 100644
index 0000000000..6f8a6cfc2a
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.scss
@@ -0,0 +1,55 @@
+/**
+ * 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-add-item-dialog {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+}
+
+.tb-iot-hub-add-item-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 24px;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.12);
+ flex-shrink: 0;
+
+ h2 {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 500;
+ line-height: 28px;
+ color: rgba(0, 0, 0, 0.87);
+ }
+}
+
+.tb-iot-hub-add-item-content {
+ flex: 1;
+ overflow: hidden;
+ position: relative;
+}
+
+.tb-iot-hub-add-item-loading {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.7);
+ z-index: 10;
+}
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.ts
new file mode 100644
index 0000000000..157bd8b386
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.ts
@@ -0,0 +1,78 @@
+///
+/// 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';
+import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
+import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models';
+import { IotHubApiService } from '@core/http/iot-hub-api.service';
+import { TranslateService } from '@ngx-translate/core';
+
+export interface IotHubAddItemDialogData {
+ itemType: ItemType;
+ iotHubApiService: IotHubApiService;
+}
+
+export interface IotHubAddItemDialogResult {
+ item: MpItemVersionView;
+ descriptor: any;
+}
+
+@Component({
+ selector: 'tb-iot-hub-add-item-dialog',
+ standalone: false,
+ templateUrl: './iot-hub-add-item-dialog.component.html',
+ styleUrls: ['./iot-hub-add-item-dialog.component.scss']
+})
+export class TbIotHubAddItemDialogComponent {
+
+ itemType: ItemType;
+ isInstalling = false;
+
+ constructor(
+ @Inject(MAT_DIALOG_DATA) public data: IotHubAddItemDialogData,
+ private dialogRef: MatDialogRef,
+ private translate: TranslateService
+ ) {
+ this.itemType = data.itemType;
+ }
+
+ getTitle(): string {
+ const typeKey = itemTypeTranslations.get(this.itemType);
+ const typeLabel = typeKey ? this.translate.instant(typeKey) : '';
+ return this.translate.instant('iot-hub.add-item-from-iot-hub', { type: typeLabel });
+ }
+
+ onAddItem(item: MpItemVersionView): void {
+ this.isInstalling = true;
+ const versionId = item.id as string;
+ this.data.iotHubApiService.installItemVersion(versionId, { ignoreLoading: true }).subscribe({
+ next: (result) => {
+ this.isInstalling = false;
+ if (result.success) {
+ this.dialogRef.close({ item, descriptor: result.descriptor } as IotHubAddItemDialogResult);
+ }
+ },
+ error: () => {
+ this.isInstalling = false;
+ }
+ });
+ }
+
+ close(): void {
+ this.dialogRef.close();
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html
index 59557f53de..d510bfc887 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html
+++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html
@@ -210,11 +210,13 @@
[showTypeChip]="false"
[showSubtype]="true"
[showCreator]="!creatorId"
+ [mode]="mode"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
- (deleteClick)="deleteInstalledItem($event)">
+ (deleteClick)="deleteInstalledItem($event)"
+ (addClick)="onItemAdd($event)">
}
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 d9f66a23a5..ffac043baf 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
@@ -14,7 +14,7 @@
/// limitations under the License.
///
-import { Component, OnInit, OnDestroy, Input } from '@angular/core';
+import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
import { Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { PageLink } from '@shared/models/page/page-link';
@@ -66,6 +66,8 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
@Input() creatorId: string;
@Input() embedded = false;
@Input() hideTabs = false;
+ @Input() mode: 'default' | 'add' = 'default';
+ @Output() addItem = new EventEmitter();
@Input() set activeType(value: ItemType) {
if (value && value !== this._activeType) {
this._activeType = value;
@@ -423,16 +425,23 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
data: {
item,
iotHubApiService: this.iotHubApiService,
- installedItem: this.getInstalledItem(item)
+ installedItem: this.getInstalledItem(item),
+ mode: this.mode
} as IotHubItemDetailDialogData
});
dialogRef.afterClosed().subscribe(result => {
- if (result === 'installed' || result === 'updated' || result === 'deleted') {
+ if (result?.action === 'add') {
+ this.addItem.emit(result.item);
+ } else if (result === 'installed' || result === 'updated' || result === 'deleted') {
this.reloadInstalledItems();
}
});
}
+ onItemAdd(item: MpItemVersionView): void {
+ this.addItem.emit(item);
+ }
+
installItem(item: MpItemVersionView): void {
if (item.type === ItemType.DEVICE) {
this.installDevice(item);
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html
index 46a3b7fe76..91c42b897e 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html
+++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html
@@ -34,7 +34,9 @@
{{ item.totalInstallCount | shortNumber }}
- @if (isInstalled()) {
+ @if (mode === 'add') {
+
+ } @else if (isInstalled()) {
@if (isSameVersion()) {
check{{ 'iot-hub.installed' | translate }}