From 348177bff3a42f943699064118e4e46c34cf9da9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 7 Apr 2026 12:26:21 +0300 Subject: [PATCH] Add "Add from IoT Hub" to dashboards page and reusable add item dialog - Add 'add' mode to item card: shows "Add" button, emits addClick - Add 'add' mode and addItem output to browse component - Browse passes mode to cards and detail dialog, propagates add results - New TbIotHubAddItemDialogComponent: fullscreen dialog with embedded browse in add mode, installs item via API and returns descriptor - Add "Add from IoT Hub" menu item to dashboards table config - On add: installs dashboard, navigates to newly created dashboard - Add HomeComponentsModule import to IotHubModule --- .../home/components/home-components.module.ts | 3 + .../iot-hub-add-item-dialog.component.html | 39 ++++++++++ .../iot-hub-add-item-dialog.component.scss | 55 +++++++++++++ .../iot-hub-add-item-dialog.component.ts | 78 +++++++++++++++++++ .../iot-hub/iot-hub-browse.component.html | 4 +- .../iot-hub/iot-hub-browse.component.ts | 15 +++- .../iot-hub/iot-hub-item-card.component.html | 8 +- .../iot-hub/iot-hub-item-card.component.ts | 7 ++ .../dashboards-table-config.resolver.ts | 34 ++++++++ .../home/pages/iot-hub/iot-hub.module.ts | 2 + .../assets/locale/locale.constant-en_US.json | 2 + 11 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-add-item-dialog.component.ts 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 @@ + +
+
+

{{ getTitle() }}

+ +
+
+ @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 }} + } @else if (isInstalled()) { @if (isSameVersion()) { check{{ 'iot-hub.installed' | translate }}