diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html index 53b557a12e..6719d02eb7 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html @@ -242,7 +242,7 @@ [noData]="noData"> - +
{{item.name}} @@ -344,7 +344,9 @@ }
@if (selectWidgetMode === 'installed' && installedSubMode === 'allWidgets') { -
+
storefront

{{ 'iot-hub.browse-tip-prefix' | translate }} {{ 'iot-hub.browse-tip-link' | translate }} {{ 'iot-hub.browse-tip-suffix' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss index 0a0d700053..f3d589c8a8 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss @@ -14,6 +14,7 @@ * limitations under the License. */ @import "../../../../../scss/constants"; +@import "../iot-hub/iot-hub-common"; tb-dashboard-widget-select { display: flex; @@ -501,55 +502,19 @@ tb-dashboard-widget-select { } .tb-iot-hub-filter-search { - display: flex; - align-items: center; - gap: 8px; - height: 40px; + @include tb-iot-hub-search-container; margin: 4px 8px; - padding: 8px 12px; - border: 1px solid rgba(0, 0, 0, 0.12); - border-radius: 8px; - background: white; mat-icon { - font-size: 24px; - width: 24px; - height: 24px; - color: rgba(0, 0, 0, 0.54); - flex-shrink: 0; + @include tb-iot-hub-search-icon(rgba(0, 0, 0, 0.54)); } input { - flex: 1; - border: none; - outline: none; - font-size: 14px; - font-weight: 400; - line-height: 20px; - letter-spacing: 0.2px; - color: rgba(0, 0, 0, 0.87); - background: transparent; - padding: 0; - min-width: 0; - - &::placeholder { - color: rgba(0, 0, 0, 0.38); - } + @include tb-iot-hub-search-input-text(0.2px); } .tb-iot-hub-filter-search-clear.mat-mdc-icon-button { - width: 24px; - height: 24px; - padding: 0; - flex-shrink: 0; - color: rgba(0, 0, 0, 0.38); - --mdc-icon-button-state-layer-size: 24px; - - mat-icon { - font-size: 18px; - width: 18px; - height: 18px; - } + @include tb-iot-hub-search-clear; } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts index 4b744bb762..8671057520 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts @@ -28,22 +28,18 @@ import { } from '@shared/models/widget.models'; import { debounceTime, distinctUntilChanged, map, skip, switchMap } from 'rxjs/operators'; import { BehaviorSubject, combineLatest, forkJoin, of } from 'rxjs'; -import { isObject } from '@core/utils'; import { PageLink } from '@shared/models/page/page-link'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { GridEntitiesFetchFunction, ScrollGridColumns } from '@shared/components/grid/scroll-grid-datasource'; import { ItemSizeStrategy } from '@shared/components/grid/scroll-grid.component'; import { coerceBoolean } from '@shared/decorators/coercion'; -import { MatDialog } from '@angular/material/dialog'; import { TranslateService } from '@ngx-translate/core'; import { MpItemVersionQuery, MpItemVersionView, widgetTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models'; import { ItemType, FilterParamInfo, WidgetCategory } from '@shared/models/iot-hub/iot-hub-item.models'; import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; -import { - TbIotHubItemDetailDialogComponent, - IotHubItemDetailDialogData -} from '@home/components/iot-hub/iot-hub-item-detail-dialog.component'; +import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; +import { filterIotHubItemsBySearch, groupIotHubFilterItems, IotHubFilterGroup, resolveIotHubItemImageUrl } from '@home/components/iot-hub/iot-hub-utils'; type selectWidgetMode = 'installed' | 'iotHub'; type installedSubMode = 'default' | 'allWidgets'; @@ -281,8 +277,8 @@ export class DashboardWidgetSelectComponent { constructor(private widgetsService: WidgetService, private iotHubApiService: IotHubApiService, + private iotHubActions: IotHubActionsService, private translate: TranslateService, - private dialog: MatDialog, private cd: ChangeDetectorRef) { this.widgetBundlesFetchFunction = (pageSize, page, filter) => { @@ -437,7 +433,7 @@ export class DashboardWidgetSelectComponent { } } - onWidgetClicked($event: Event, widget: WidgetTypeInfo): void { + onWidgetClicked(widget: WidgetTypeInfo): void { this.widgetSelected.emit(this.toWidgetInfo(widget)); } @@ -498,15 +494,7 @@ export class DashboardWidgetSelectComponent { } return; } - const dialogRef = this.dialog.open(TbIotHubItemDetailDialogComponent, { - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - autoFocus: false, - data: { - item, - mode: 'add' - } as IotHubItemDetailDialogData - }); - dialogRef.afterClosed().subscribe(result => { + this.iotHubActions.openItemDetail(item, undefined, undefined, 'add').subscribe(result => { if (result?.action === 'add') { this.installAndAddWidget(result.item); } @@ -518,14 +506,7 @@ export class DashboardWidgetSelectComponent { } getIotHubItemImage(item: MpItemVersionView): string | null { - if (item.image) { - return this.iotHubApiService.resolveResourceUrl(item.image); - } - const resource = item.resources?.find(r => r.type === 'SCREENSHOT') || item.resources?.find(r => r.type === 'ICON'); - if (resource) { - return this.iotHubApiService.resolveResourceUrl(`/api/resources/${resource.id}`); - } - return null; + return resolveIotHubItemImageUrl(item, this.iotHubApiService); } getIotHubWidgetTypeLabel(key: string): string { @@ -534,29 +515,11 @@ export class DashboardWidgetSelectComponent { } getFilteredIotHubItems(items: FilterParamInfo[], searchKey: string): FilterParamInfo[] { - const search = (this.iotHubFilterSearch[searchKey] || '').toLowerCase(); - if (!search) { return items; } - return items.filter(item => item.key.toLowerCase().includes(search)); + return filterIotHubItemsBySearch(items, this.iotHubFilterSearch[searchKey]); } - getGroupedIotHubFilterItems(items: FilterParamInfo[], searchKey: string): { label: string; items: FilterParamInfo[] }[] { - const filtered = this.getFilteredIotHubItems(items, searchKey); - if (items.length < 11) { - return [{ label: null, items: filtered }]; - } - const topKeys = new Set( - [...items].sort((a, b) => b.totalInstallCount - a.totalInstallCount).slice(0, 10).map(i => i.key) - ); - const popular = filtered.filter(i => topKeys.has(i.key)); - const rest = filtered.filter(i => !topKeys.has(i.key)); - const groups: { label: string; items: FilterParamInfo[] }[] = []; - if (popular.length) { - groups.push({ label: 'iot-hub.most-popular', items: popular }); - } - if (rest.length) { - groups.push({ label: 'iot-hub.all', items: rest }); - } - return groups; + getGroupedIotHubFilterItems(items: FilterParamInfo[], searchKey: string): IotHubFilterGroup[] { + return groupIotHubFilterItems(items, this.iotHubFilterSearch[searchKey]); } toggleIotHubWidgetType(key: string): void { @@ -774,10 +737,6 @@ export class DashboardWidgetSelectComponent { this.cd.markForCheck(); } - isObject(value: any): boolean { - return isObject(value); - } - private installAndAddWidget(item: MpItemVersionView): void { const versionId = item.id as string; this.iotHubApiService.installItemVersion(versionId, { ignoreLoading: true }).subscribe({ diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/_iot-hub-common.scss b/ui-ngx/src/app/modules/home/components/iot-hub/_iot-hub-common.scss new file mode 100644 index 0000000000..4cbe30650d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/iot-hub/_iot-hub-common.scss @@ -0,0 +1,72 @@ +/** + * 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. + */ + +// Shared IoT Hub visual building blocks — used across +// iot-hub-browse, iot-hub-search, and dashboard-widget-select +// to keep filter / search controls in sync. + +@mixin tb-iot-hub-search-container { + display: flex; + align-items: center; + gap: 8px; + height: 40px; + padding: 8px 12px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 8px; + background: white; +} + +@mixin tb-iot-hub-search-icon($color: rgba(0, 0, 0, 0.38)) { + font-size: 24px; + width: 24px; + height: 24px; + color: $color; + flex-shrink: 0; +} + +@mixin tb-iot-hub-search-input-text($letter-spacing: normal) { + flex: 1; + min-width: 0; + border: none; + outline: none; + padding: 0; + font-family: inherit; + font-size: 14px; + font-weight: 400; + line-height: 20px; + letter-spacing: $letter-spacing; + color: rgba(0, 0, 0, 0.87); + background: transparent; + + &::placeholder { + color: rgba(0, 0, 0, 0.38); + } +} + +@mixin tb-iot-hub-search-clear { + width: 24px; + height: 24px; + padding: 0; + flex-shrink: 0; + color: rgba(0, 0, 0, 0.38); + --mdc-icon-button-state-layer-size: 24px; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html index e078c63cf5..27571c6cac 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html @@ -30,7 +30,7 @@ } @case ('form') { -

+
@if (ws.formGroup) {
@for (field of ws.formFields; track field.key) { @@ -108,18 +108,18 @@ } @case ('progress') { -
+
@for (ep of ws.entitySteps; track ep.step.name; let i = $index) { @if (i > 0) {
} @if (ep.status === 'conflict' || ep.status === 'error') {
-
+
warning
-
+
{{ ('iot-hub.device-install-step-type-' + ep.step.type | translate) + ' — ' + (ep.resolvedName || ep.step.name) }} @@ -133,7 +133,7 @@ }
-
+
@if (ep.status === 'conflict') { @if (ep.conflictType === 'use-or-overwrite') {
@if (hasActiveDropdownFilters()) {
-
+
@for (key of getActiveSubtypesArray(); track key) { @@ -330,7 +309,7 @@
- +
@@ -341,8 +320,8 @@
-
-
+
+
search -
+
{{ 'iot-hub.results-count' | translate:{ count: totalElements } }} @@ -409,7 +388,7 @@ {{ 'iot-hub.clear-all' | translate }} } - + diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss index edccc18463..9d886ec6d0 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss @@ -15,16 +15,13 @@ */ @import "../../../../../scss/constants"; +@import "./iot-hub-common"; :host { display: block; } .tb-iot-hub-browse { - display: flex; - flex-direction: column; - height: 100%; - &.tabs-hidden:not(.embedded) { border-top: 1px solid rgba(0, 0, 0, 0.12); } @@ -67,6 +64,8 @@ } } +// tb-iot-hub-drawer-content layout handled via Tailwind (flex flex-1); overflow stays default visible + // Left sidebar filters — Design: 280px wide, right border, own scroll .tb-iot-hub-filters { width: 300px; @@ -118,13 +117,6 @@ overflow-y: auto; } -// Drawer content — holds desktop filters + results side by side -.tb-iot-hub-drawer-content { - display: flex; - flex: 1; - overflow: visible; -} - // Filter toggle button — hidden on large screens, shown on lt-lg .tb-iot-hub-filter-toggle-btn { display: none; @@ -162,9 +154,6 @@ } .tb-iot-hub-filter-drawer-header { - display: flex; - align-items: center; - justify-content: space-between; padding: 12px 12px 12px 24px; font-size: 18px; font-weight: 500; @@ -177,10 +166,6 @@ } .tb-iot-hub-filter-drawer-chips { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; padding: 4px 24px 12px; } @@ -256,11 +241,6 @@ } } - .tb-vendor-filter-field { - width: 100%; - margin-top: 4px; - } - .tb-iot-hub-filter-items { padding-left: 44px; max-height: 340px; @@ -295,55 +275,19 @@ } .tb-iot-hub-filter-search { - display: flex; - align-items: center; - gap: 8px; - height: 40px; + @include tb-iot-hub-search-container; margin: 4px 8px 4px 56px; - padding: 8px 12px; - border: 1px solid rgba(0, 0, 0, 0.12); - border-radius: 8px; - background: white; mat-icon { - font-size: 24px; - width: 24px; - height: 24px; - color: rgba(0, 0, 0, 0.54); - flex-shrink: 0; + @include tb-iot-hub-search-icon(rgba(0, 0, 0, 0.54)); } input { - flex: 1; - border: none; - outline: none; - font-size: 14px; - font-weight: 400; - line-height: 20px; - letter-spacing: 0.2px; - color: rgba(0, 0, 0, 0.87); - background: transparent; - padding: 0; - min-width: 0; - - &::placeholder { - color: rgba(0, 0, 0, 0.38); - } + @include tb-iot-hub-search-input-text(0.2px); } .tb-iot-hub-filter-search-clear.mat-mdc-icon-button { - width: 24px; - height: 24px; - padding: 0; - flex-shrink: 0; - color: rgba(0, 0, 0, 0.38); - --mdc-icon-button-state-layer-size: 24px; - - mat-icon { - font-size: 18px; - width: 18px; - height: 18px; - } + @include tb-iot-hub-search-clear; } } @@ -370,14 +314,6 @@ } } -// Top bar: results count + search -.tb-iot-hub-results-top { - display: flex; - align-items: center; - justify-content: space-between; - padding: 24px 0 0; - flex-shrink: 0; -} // Results count — Design: 14px Medium, rgba(0,0,0,0.76), tracking 0.25 .tb-iot-hub-results-count { @@ -388,71 +324,21 @@ color: rgba(0, 0, 0, 0.76); } -// Search field — Design: 320px, h-40, rounded-8, border rgba(0,0,0,0.12), white bg .tb-iot-hub-search-field { + @include tb-iot-hub-search-container; width: 320px; - height: 40px; - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - border-radius: 8px; - border: 1px solid rgba(0, 0, 0, 0.12); - background: white; } .tb-iot-hub-search-icon { - font-size: 24px; - width: 24px; - height: 24px; - color: rgba(0, 0, 0, 0.38); - flex-shrink: 0; + @include tb-iot-hub-search-icon; } .tb-iot-hub-search-input { - flex: 1; - min-width: 0; - border: none; - outline: none; - font-size: 14px; - font-weight: 400; - line-height: 20px; - color: rgba(0, 0, 0, 0.87); - background: transparent; - font-family: inherit; - - &::placeholder { - color: rgba(0, 0, 0, 0.38); - } + @include tb-iot-hub-search-input-text; } .tb-iot-hub-search-clear.mat-mdc-icon-button { - width: 24px; - height: 24px; - padding: 0; - flex-shrink: 0; - color: rgba(0, 0, 0, 0.38); - --mdc-icon-button-state-layer-size: 24px; - - mat-icon { - font-size: 18px; - width: 18px; - height: 18px; - } -} - -// Sort row + active filters -.tb-iot-hub-sort-row { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 0; - flex-shrink: 0; - flex-wrap: wrap; -} - -.tb-iot-hub-sort-spacer { - flex: 1; + @include tb-iot-hub-search-clear; } // Sort button — Design: 14px Medium, primary color, with filter_list icon @@ -499,20 +385,8 @@ } } -// Loading / Empty states -.tb-iot-hub-loading { - display: flex; - justify-content: center; - padding: 80px 0; -} - +// Empty state text + no-data image .tb-iot-hub-empty-state { - display: flex; - flex-direction: column; - align-items: center; - padding: 80px 0; - text-align: center; - .tb-no-data-bg { flex: auto; height: 100px; @@ -532,20 +406,13 @@ } } -// Pagination — Design: centered page numbers, items per page on the right +// Pagination — centered page numbers via absolute positioning .tb-iot-hub-pagination { - display: flex; - align-items: center; - padding: 24px 0 0; - flex-shrink: 0; position: relative; } -// Page buttons — Design: h-40, px-16 py-8, rounded-4, gap-1 +// Page buttons — centered via absolute positioning .tb-iot-hub-pagination-pages { - display: flex; - align-items: center; - gap: 1px; position: absolute; left: 50%; transform: translateX(-50%); 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 38f52ce97c..9b67f4b57e 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 @@ -25,11 +25,10 @@ import { ItemType, FilterParamInfo } from '@shared/models/iot-hub/iot-hub-item.m import { widgetTypeTranslations, cfTypeTranslations, ruleChainTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models'; import { IotHubInstalledItem, DeviceInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; -import { TbDeviceInstallDialogComponent, DeviceInstallDialogData } from './device-install-dialog/device-install-dialog.component'; -import { MatDialog } from '@angular/material/dialog'; import { TranslateService } from '@ngx-translate/core'; import { ActivatedRoute, Router } from '@angular/router'; import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; +import { filterIotHubItemsBySearch, groupIotHubFilterItems, IotHubFilterGroup } from '@home/components/iot-hub/iot-hub-utils'; interface SortOption { value: string; @@ -37,15 +36,6 @@ interface SortOption { direction: Direction; } -const TYPE_TABS: ItemType[] = [ - ItemType.WIDGET, - ItemType.DASHBOARD, - ItemType.SOLUTION_TEMPLATE, - ItemType.CALCULATED_FIELD, - ItemType.RULE_CHAIN, - ItemType.DEVICE -]; - @Component({ selector: 'tb-iot-hub-browse', standalone: false, @@ -114,6 +104,15 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { { value: 'publishedTime', label: 'iot-hub.sort-newest', direction: Direction.DESC }, { value: 'name', label: 'iot-hub.sort-name', direction: Direction.ASC } ]; + + typeTabs: { type: ItemType; label: string }[] = [ + { type: ItemType.WIDGET, label: 'item.type-widget-plural' }, + { type: ItemType.DASHBOARD, label: 'item.type-dashboard-plural' }, + { type: ItemType.SOLUTION_TEMPLATE, label: 'item.type-solution-template-plural' }, + { type: ItemType.CALCULATED_FIELD, label: 'item.type-calculated-field-plural' }, + { type: ItemType.RULE_CHAIN, label: 'item.type-rule-chain-plural' }, + { type: ItemType.DEVICE, label: 'item.type-device-plural' } + ]; selectedSortIndex = 0; subtypeOptions: FilterParamInfo[] = []; @@ -138,8 +137,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { private iotHubActions: IotHubActionsService, private translate: TranslateService, private router: Router, - private route: ActivatedRoute, - private dialog: MatDialog + private route: ActivatedRoute ) {} ngOnInit(): void { @@ -181,24 +179,9 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.searchSubject.next(value); } - getTypeTabIndex(): number { - return TYPE_TABS.indexOf(this.activeType); - } - - onTypeTabIndexChange(index: number): void { - this.onTypeTabChange(TYPE_TABS[index]); - } - onTypeTabChange(type: ItemType): void { this.activeType = type; - this.activeCategories.clear(); - this.activeUseCases.clear(); - this.activeCfTypes.clear(); - this.activeWidgetTypes.clear(); - this.activeRuleChainTypes.clear(); - this.activeConnectivity.clear(); - this.activeHardwareTypes.clear(); - this.activeVendors.clear(); + this.clearActiveFilterSets(); this.filterSearch = {}; this.loadFilterInfo(); this.pageIndex = 0; @@ -255,7 +238,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { const pages: number[] = []; const maxVisible = 5; let start = Math.max(0, this.pageIndex - Math.floor(maxVisible / 2)); - let end = Math.min(total, start + maxVisible); + const end = Math.min(total, start + maxVisible); if (end - start < maxVisible) { start = Math.max(0, end - maxVisible); } @@ -278,12 +261,6 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.loadItems(); } - clearCategories(): void { - this.activeCategories.clear(); - this.pageIndex = 0; - this.loadItems(); - } - onUseCaseToggle(useCase: string): void { if (this.activeUseCases.has(useCase)) { this.activeUseCases.delete(useCase); @@ -294,12 +271,6 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.loadItems(); } - clearUseCases(): void { - this.activeUseCases.clear(); - this.pageIndex = 0; - this.loadItems(); - } - onConnectivityToggle(value: string): void { if (this.activeConnectivity.has(value)) { this.activeConnectivity.delete(value); @@ -310,12 +281,6 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.loadItems(); } - clearConnectivity(): void { - this.activeConnectivity.clear(); - this.pageIndex = 0; - this.loadItems(); - } - isConnectivityActive(value: string): boolean { return this.activeConnectivity.has(value); } @@ -330,12 +295,6 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.loadItems(); } - clearHardwareTypes(): void { - this.activeHardwareTypes.clear(); - this.pageIndex = 0; - this.loadItems(); - } - isHardwareTypeActive(value: string): boolean { return this.activeHardwareTypes.has(value); } @@ -386,12 +345,6 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.loadItems(); } - clearSubtypes(): void { - this.getActiveSubtypes().clear(); - this.pageIndex = 0; - this.loadItems(); - } - getActiveSubtypesArray(): string[] { if (this.fixedSubType) { return Array.from(this.getActiveSubtypes()).filter(s => s !== this.fixedSubType); @@ -407,52 +360,12 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { return Array.from(this.activeUseCases); } - onSubtypesChange(values: string[]): void { - const active = this.getActiveSubtypes(); - active.clear(); - values.forEach(v => active.add(v)); - this.pageIndex = 0; - this.loadItems(); - } - - onCategoriesChange(values: string[]): void { - this.activeCategories.clear(); - values.forEach(v => this.activeCategories.add(v)); - this.pageIndex = 0; - this.loadItems(); - } - - onUseCasesChange(values: string[]): void { - this.activeUseCases.clear(); - values.forEach(v => this.activeUseCases.add(v)); - this.pageIndex = 0; - this.loadItems(); - } - getFilteredItems(items: FilterParamInfo[], searchKey: string): FilterParamInfo[] { - const search = (this.filterSearch[searchKey] || '').toLowerCase(); - if (!search) { return items; } - return items.filter(item => item.key.toLowerCase().includes(search)); + return filterIotHubItemsBySearch(items, this.filterSearch[searchKey]); } - getGroupedFilterItems(items: FilterParamInfo[], searchKey: string): { label: string; items: FilterParamInfo[] }[] { - const filtered = this.getFilteredItems(items, searchKey); - if (items.length < 11) { - return [{ label: null, items: filtered }]; - } - const topKeys = new Set( - [...items].sort((a, b) => b.totalInstallCount - a.totalInstallCount).slice(0, 10).map(i => i.key) - ); - const popular = filtered.filter(i => topKeys.has(i.key)); - const rest = filtered.filter(i => !topKeys.has(i.key)); - const groups: { label: string; items: FilterParamInfo[] }[] = []; - if (popular.length) { - groups.push({ label: 'iot-hub.most-popular', items: popular }); - } - if (rest.length) { - groups.push({ label: 'iot-hub.all', items: rest }); - } - return groups; + getGroupedFilterItems(items: FilterParamInfo[], searchKey: string): IotHubFilterGroup[] { + return groupIotHubFilterItems(items, this.filterSearch[searchKey]); } getFilteredConnectivityGroups(searchKey: string): { group: string; values: FilterParamInfo[] }[] { @@ -479,14 +392,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { } clearAllFilters(): void { - this.activeCategories.clear(); - this.activeUseCases.clear(); - this.activeCfTypes.clear(); - this.activeWidgetTypes.clear(); - this.activeRuleChainTypes.clear(); - this.activeConnectivity.clear(); - this.activeHardwareTypes.clear(); - this.activeVendors.clear(); + this.clearActiveFilterSets(); if (this.fixedSubType) { this.getActiveSubtypes()?.add(this.fixedSubType); } @@ -497,6 +403,17 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.loadItems(); } + private clearActiveFilterSets(): void { + this.activeCategories.clear(); + this.activeUseCases.clear(); + this.activeCfTypes.clear(); + this.activeWidgetTypes.clear(); + this.activeRuleChainTypes.clear(); + this.activeConnectivity.clear(); + this.activeHardwareTypes.clear(); + this.activeVendors.clear(); + } + get activeFilterCount(): number { const subtypeCount = this.fixedSubType ? this.getActiveSubtypesArray().length : (this.getActiveSubtypes()?.size || 0); return subtypeCount + this.activeCategories.size + this.activeUseCases.size + @@ -526,28 +443,6 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { } } - getSubtitle(): string { - switch (this.activeType) { - case ItemType.WIDGET: return 'iot-hub.subtitle-widgets'; - case ItemType.DASHBOARD: return 'iot-hub.subtitle-dashboards'; - case ItemType.SOLUTION_TEMPLATE: return 'iot-hub.subtitle-solution-templates'; - case ItemType.CALCULATED_FIELD: return 'iot-hub.subtitle-calculated-fields'; - case ItemType.RULE_CHAIN: return 'iot-hub.subtitle-rule-chains'; - case ItemType.DEVICE: return 'iot-hub.subtitle-devices'; - } - } - - getSearchPlaceholder(): string { - switch (this.activeType) { - case ItemType.WIDGET: return 'iot-hub.search-widgets'; - case ItemType.DASHBOARD: return 'iot-hub.search-dashboards'; - case ItemType.SOLUTION_TEMPLATE: return 'iot-hub.search-solution-templates'; - case ItemType.CALCULATED_FIELD: return 'iot-hub.search-calculated-fields'; - case ItemType.RULE_CHAIN: return 'iot-hub.search-rule-chains'; - case ItemType.DEVICE: return 'iot-hub.search-devices'; - } - } - getInstalledItem(item: MpItemVersionView): IotHubInstalledItem | undefined { if (this.activeType === ItemType.WIDGET && this.installedWidgets) { return this.installedWidgets.find(i => i.itemId === item.itemId); @@ -587,7 +482,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { updateItem(item: MpItemVersionView): void { const installedItem = this.getInstalledItem(item); if (!installedItem) { return; } - this.iotHubActions.updateItem(item, installedItem).subscribe(result => { + this.iotHubActions.updateItem(installedItem, item.version, item.id as string).subscribe(result => { if (result === 'updated') { this.reloadInstalledItems(); } @@ -597,7 +492,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { deleteInstalledItem(item: MpItemVersionView): void { const installedItem = this.getInstalledItem(item); if (!installedItem) { return; } - this.iotHubActions.deleteItem(item, installedItem).subscribe(() => { + this.iotHubActions.deleteItem(installedItem).subscribe(() => { this.reloadInstalledItems(); }); } @@ -606,31 +501,13 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { const installedItem = this.getInstalledItem(item); if (!installedItem) { return; } const descriptor = installedItem.descriptor as DeviceInstalledItemDescriptor; - this.iotHubApiService.getVersionFileData(installedItem.itemVersionId, { ignoreLoading: true, ignoreErrors: true }).subscribe({ - next: async (blob: Blob) => { - const zipData = await blob.arrayBuffer(); - this.dialog.open(TbDeviceInstallDialogComponent, { - panelClass: ['tb-dialog', 'tb-fullscreen-dialog-lt-md'], - disableClose: false, - autoFocus: false, - data: { - item, - zipData, - reviewMode: true, - selectedInstallMethod: descriptor?.selectedInstallMethod, - installState: descriptor?.installState - } as DeviceInstallDialogData - }); - } + this.iotHubApiService.getVersionInfo(installedItem.itemVersionId, {ignoreLoading: true, ignoreErrors: true}).subscribe(versionView => { + this.iotHubActions.reviewDevice(versionView, descriptor).subscribe(); }); } navigateToCreator(creatorId: string): void { - this.router.navigate(['/iot-hub/creator', creatorId]); - } - - navigateToInstalledItems(): void { - this.router.navigate(['/iot-hub/installed']); + void this.router.navigate(['/iot-hub/creator', creatorId]); } private loadInstalledWidgets(): void { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-delete-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-delete-dialog.component.html index 01e3a24c10..dfbbbf5d79 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-delete-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-delete-dialog.component.html @@ -15,11 +15,11 @@ limitations under the License. --> -
+

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

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

-
+
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-delete-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-delete-dialog.component.scss index 731f4c7654..bb7b7fe893 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-delete-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-delete-dialog.component.scss @@ -14,13 +14,6 @@ * limitations under the License. */ -.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; @@ -38,11 +31,3 @@ 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; -} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html index 213b469541..d76e36233f 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
@switch (state) { @case ('confirm') {

{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}

@@ -61,7 +61,7 @@ @case ('installing') { } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.scss index e7a4592437..e3deca2ca6 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.scss @@ -14,13 +14,6 @@ * 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; @@ -38,12 +31,6 @@ margin: 0; } -.tb-iot-hub-inline-spinner { - display: inline-block; - margin-right: 8px; - vertical-align: middle; -} - .tb-iot-hub-error-details { max-height: 200px; overflow-y: auto; diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts index b9391e2240..65d7eed406 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts @@ -22,10 +22,7 @@ import { AppState } from '@core/core.state'; import { DialogComponent } from '@shared/components/dialog.component'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; -import { - IotHubInstalledItemDescriptor, - SolutionTemplateInstalledItemDescriptor -} from '@shared/models/iot-hub/iot-hub-installed-item.models'; +import { SolutionTemplateInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; @@ -125,7 +122,7 @@ export class TbIotHubInstallDialogComponent extends DialogComponent -
-
-

+
+
+

{{ 'iot-hub.installed-items-for' | translate:{ name: item.name } }}

@@ -82,20 +82,10 @@
}

-
- @if (mode !== 'add' && isInstalled()) { - @if (item.type !== ItemType.WIDGET && item.type !== ItemType.SOLUTION_TEMPLATE) { - - } - } -
} @else { -
+
@if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) {
}
- -
- @if (mode !== 'add' && isInstalled()) { - @if (item.type !== ItemType.WIDGET && item.type !== ItemType.SOLUTION_TEMPLATE) { - - } - } -
} @@ -177,7 +157,10 @@ @if (showCreator) {
{{ 'iot-hub.creator' | translate }} -
+
@if (getCreatorAvatarUrl(); as avatarUrl) { @@ -278,7 +261,7 @@
{{ 'iot-hub.details' | translate }} @@ -95,14 +95,14 @@ @if (isLoading) { -
+
} @if (!isLoading && totalElements === 0 && searchText?.trim()) { -
+

{{ 'iot-hub.no-items-found' | translate }}

{{ 'iot-hub.try-adjusting-search' | translate }}

diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.scss index 8beb544c23..14ebf0dd60 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.scss @@ -15,6 +15,7 @@ */ @import "../../../../../scss/constants"; +@import "./iot-hub-common"; :host { display: flex; @@ -22,76 +23,27 @@ gap: 40px; } -// Title row -.tb-search-title-row { - display: flex; - align-items: baseline; - gap: 16px; -} - .tb-search-results-count-mobile { display: none; margin-left: auto; } -// Search + Sort toolbar -.tb-search-toolbar { - display: flex; - align-items: center; - gap: 16px; -} - .tb-search-input-field { + @include tb-iot-hub-search-container; width: 320px; min-width: 0; - height: 40px; - display: flex; - align-items: center; - gap: 8px; - padding: 0 12px; - border: 1px solid rgba(0, 0, 0, 0.12); - border-radius: 8px; - background: white; } .tb-search-input-icon { - font-size: 24px; - width: 24px; - height: 24px; - color: rgba(0, 0, 0, 0.38); - flex-shrink: 0; + @include tb-iot-hub-search-icon; } .tb-search-input { - flex: 1; - min-width: 0; - border: none; - outline: none; - font-size: 14px; - font-weight: 400; - line-height: 20px; - color: rgba(0, 0, 0, 0.87); - background: transparent; - font-family: inherit; - - &::placeholder { - color: rgba(0, 0, 0, 0.38); - } + @include tb-iot-hub-search-input-text; } .tb-search-input-clear.mat-mdc-icon-button { - width: 24px; - height: 24px; - padding: 0; - flex-shrink: 0; - color: rgba(0, 0, 0, 0.38); - --mdc-icon-button-state-layer-size: 24px; - - mat-icon { - font-size: 18px; - width: 18px; - height: 18px; - } + @include tb-iot-hub-search-clear; } .tb-search-sort-btn { @@ -128,12 +80,6 @@ gap: 24px; } -.tb-search-pagination-pages { - display: flex; - align-items: center; - gap: 1px; -} - .tb-search-page-btn { min-width: 40px; height: 40px; @@ -181,44 +127,36 @@ } } -// Loading -.tb-search-loading { - display: flex; - justify-content: center; - padding: 48px 0; +// Loading / Empty negative margin +.tb-search-loading, +.tb-search-empty { margin-top: -20px; } -// Empty state +// Empty state no-data-bg + text .tb-search-empty { - display: flex; - flex-direction: column; - align-items: center; - padding: 40px 0; - margin-top: -20px; - .tb-no-data-bg { flex: auto; height: 100px; } -} -.tb-search-empty h3 { - font-size: 20px; - font-weight: 600; - line-height: 24px; - letter-spacing: 0.1px; - color: rgba(0, 0, 0, 0.76); - margin: 0 0 8px; -} + h3 { + font-size: 20px; + font-weight: 600; + line-height: 24px; + letter-spacing: 0.1px; + color: rgba(0, 0, 0, 0.76); + margin: 0 0 8px; + } -.tb-search-empty p { - font-size: 14px; - font-weight: 400; - line-height: 20px; - letter-spacing: 0.2px; - color: rgba(0, 0, 0, 0.54); - margin: 0; + p { + font-size: 14px; + font-weight: 400; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.54); + margin: 0; + } } // Divider before categories @@ -324,10 +262,6 @@ } @media #{$mat-lt-md} { - .tb-search-title-row { - margin-bottom: -24px; - } - .tb-search-results-count-mobile { display: inline; } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts index 6f33084ed8..0d92d87bee 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts @@ -194,7 +194,7 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { navigateToType(type: ItemType): void { const search = this.searchText?.trim() || undefined; - this.router.navigate(['/iot-hub', this.getTypeRoute(type)], { queryParams: { search } }); + void this.router.navigate(['/iot-hub', this.getTypeRoute(type)], { queryParams: { search } }); } // Installed items @@ -244,7 +244,7 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { updateItem(item: MpItemVersionView): void { const installedItem = this.getInstalledItem(item); if (!installedItem) { return; } - this.iotHubActions.updateItem(item, installedItem).subscribe(result => { + this.iotHubActions.updateItem(installedItem, item.version, item.id as string).subscribe(result => { if (result === 'updated') { this.reloadInstalledItems(); } @@ -254,13 +254,13 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { deleteInstalledItem(item: MpItemVersionView): void { const installedItem = this.getInstalledItem(item); if (!installedItem) { return; } - this.iotHubActions.deleteItem(item, installedItem).subscribe(() => { + this.iotHubActions.deleteItem(installedItem).subscribe(() => { this.reloadInstalledItems(); }); } navigateToCreator(creatorId: string): void { - this.router.navigate(['/iot-hub/creator', creatorId]); + void this.router.navigate(['/iot-hub/creator', creatorId]); } // Data loading diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.html index cc86d800c7..65ec2f4635 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
@switch (state) { @case ('confirm') {

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

@@ -45,7 +45,7 @@ @case ('updating') { } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.scss deleted file mode 100644 index e7a4592437..0000000000 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.scss +++ /dev/null @@ -1,58 +0,0 @@ -/** - * 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; - vertical-align: middle; -} - -.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; -} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts index acb78672a0..d430242400 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts @@ -21,10 +21,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { DialogComponent } from '@shared/components/dialog.component'; import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; -import { - IotHubInstalledItemDescriptor, - SolutionTemplateInstalledItemDescriptor -} from '@shared/models/iot-hub/iot-hub-installed-item.models'; +import { SolutionTemplateInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; @@ -45,7 +42,7 @@ export type UpdateState = 'confirm' | 'updating' | 'success' | 'error'; selector: 'tb-iot-hub-update-dialog', standalone: false, templateUrl: './iot-hub-update-dialog.component.html', - styleUrls: ['./iot-hub-update-dialog.component.scss'] + styleUrls: ['./iot-hub-install-dialog.component.scss'] }) export class TbIotHubUpdateDialogComponent extends DialogComponent { @@ -116,7 +113,7 @@ export class TbIotHubUpdateDialogComponent extends DialogComponent item.key.toLowerCase().includes(normalized)); +} + +export function groupIotHubFilterItems(items: FilterParamInfo[], search: string): IotHubFilterGroup[] { + const filtered = filterIotHubItemsBySearch(items, search); + if (items.length < IOT_HUB_FILTER_GROUPING_THRESHOLD) { + return [{ label: null, items: filtered }]; + } + const topKeys = new Set( + [...items] + .sort((a, b) => b.totalInstallCount - a.totalInstallCount) + .slice(0, IOT_HUB_FILTER_POPULAR_LIMIT) + .map(i => i.key) + ); + const popular = filtered.filter(i => topKeys.has(i.key)); + const rest = filtered.filter(i => !topKeys.has(i.key)); + const groups: IotHubFilterGroup[] = []; + if (popular.length) { + groups.push({ label: 'iot-hub.most-popular', items: popular }); + } + if (rest.length) { + groups.push({ label: 'iot-hub.all', items: rest }); + } + return groups; +} + +export function resolveIotHubItemImageUrl(item: MpItemVersionView, api: IotHubApiService): string | null { + if (item.image) { + return api.resolveResourceUrl(item.image); + } + const resource = item.resources?.find(r => r.type === 'SCREENSHOT') || item.resources?.find(r => r.type === 'ICON'); + if (resource) { + return api.resolveResourceUrl(`/api/resources/${resource.id}`); + } + return null; +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.html index cd0c4a8a7d..3e597f61ac 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.html @@ -20,11 +20,11 @@
- -
+
@if (getAvatarUrl(); as avatarUrl) { @@ -32,7 +32,7 @@ person }
-
+

{{ creator.displayName }}

@if (creator.verified) { {{ creator.description }}

} -