diff --git a/ui-ngx/src/app/core/http/iot-hub-api.service.ts b/ui-ngx/src/app/core/http/iot-hub-api.service.ts index ae3951fca8..dbe6e98706 100644 --- a/ui-ngx/src/app/core/http/iot-hub-api.service.ts +++ b/ui-ngx/src/app/core/http/iot-hub-api.service.ts @@ -23,6 +23,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { CreatorView } from '@shared/models/iot-hub/iot-hub-creator.models'; import { IotHubInstalledItem, InstallItemVersionResult, UpdateItemVersionResult, ItemPublishedVersionInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models'; +import { ItemType, ItemTypeFilterInfo } from '@shared/models/iot-hub/iot-hub-item.models'; import { InterceptorHttpParams } from '@core/interceptors/interceptor-http-params'; import { InterceptorConfig } from '@core/interceptors/interceptor-config'; import { AppState } from '@core/core.state'; @@ -84,9 +85,9 @@ export class IotHubApiService { ); } - public getPublishedDeviceVendors(config?: IotHubRequestConfig): Observable { - return this.http.get( - `${this.baseUrl}/api/versions/published/vendors`, + public getFilterInfo(itemType: ItemType, config?: IotHubRequestConfig): Observable { + return this.http.get( + `${this.baseUrl}/api/item-listing/filterInfo/${itemType}`, { params: this.buildParams(config) } ); } 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 65242b012a..087d320f83 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 @@ -208,10 +208,10 @@ {{ iotHubPendingWidgetTypes.size }} -
- - {{ entry.value | translate }} +
+ + {{ getIotHubWidgetTypeLabel(opt.key) }}
@@ -223,10 +223,10 @@ {{ iotHubPendingCategories.size }} -
- - {{ entry.value | translate }} +
+ + {{ opt.key }}
@@ -238,10 +238,10 @@ {{ iotHubPendingUseCases.size }} -
- - {{ entry.value | translate }} +
+ + {{ opt.key }}
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 d4a22603a4..f45b18b24a 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 @@ -35,8 +35,9 @@ import { GridEntitiesFetchFunction, ScrollGridColumns } from '@shared/components import { ItemSizeStrategy } from '@shared/components/grid/scroll-grid.component'; import { coerceBoolean } from '@shared/decorators/coercion'; import { MatDialog } from '@angular/material/dialog'; -import { MpItemVersionQuery, MpItemVersionView, widgetTypeTranslations as iotHubWidgetTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models'; -import { ItemType, getCategoriesForType, useCaseTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; +import { TranslateService } from '@ngx-translate/core'; +import { MpItemVersionQuery, MpItemVersionView, widgetTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models'; +import { ItemType, FilterParamInfo } 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 { @@ -117,6 +118,7 @@ export class DashboardWidgetSelectComponent implements OnInit { this.selectWidgetMode$.next(mode); if (mode === 'iotHub') { this.loadInstalledWidgets(); + this.loadIotHubFilterInfo(); } else { this.iotHubInstalledMode = 'all'; this.installedWidgetVersions = null; @@ -207,13 +209,14 @@ export class DashboardWidgetSelectComponent implements OnInit { iotHubPendingUseCases = new Set(); iotHubFilterDirty = false; - iotHubWidgetTypesMap: Map = iotHubWidgetTypeTranslations; - iotHubCategoriesMap: Map = getCategoriesForType(ItemType.WIDGET); - iotHubUseCasesMap: Map = useCaseTranslations as Map; + iotHubWidgetTypeOptions: FilterParamInfo[] = []; + iotHubCategoryOptions: FilterParamInfo[] = []; + iotHubUseCaseOptions: FilterParamInfo[] = []; iotHubFilterCount = 0; constructor(private widgetsService: WidgetService, private iotHubApiService: IotHubApiService, + private translate: TranslateService, private dialog: MatDialog, private cd: ChangeDetectorRef) { @@ -398,6 +401,11 @@ export class DashboardWidgetSelectComponent implements OnInit { this.iotHubFilterDirty = false; } + getIotHubWidgetTypeLabel(key: string): string { + const translationKey = widgetTypeTranslations.get(key); + return translationKey ? this.translate.instant(translationKey) : key; + } + toggleIotHubWidgetType(key: string): void { if (this.iotHubPendingWidgetTypes.has(key)) { this.iotHubPendingWidgetTypes.delete(key); @@ -489,6 +497,14 @@ export class DashboardWidgetSelectComponent implements OnInit { }); } + private loadIotHubFilterInfo(): void { + this.iotHubApiService.getFilterInfo(ItemType.WIDGET, { ignoreLoading: true }).subscribe(info => { + this.iotHubWidgetTypeOptions = info.types || []; + this.iotHubCategoryOptions = info.categories || []; + this.iotHubUseCaseOptions = info.useCases || []; + }); + } + private loadInstalledWidgets(): void { if (this.installedWidgets === null) { this.installedWidgets = []; 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 63ccc11289..9d3a04791e 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 @@ -54,95 +54,216 @@
- @if (!fixedSubType && getSubtypeMap(); as subtypeMap) { + @if (!fixedSubType && subtypeOptions.length > 0) { {{ 'iot-hub.type-filter-label' | translate }} - @for (entry of subtypeMap | keyvalue; track entry.key) { - - {{ entry.value | translate }} - + @if (subtypeOptions.length >= 10) { + } +
+ @for (opt of getFilteredItems(subtypeOptions, 'subtype'); track opt.key) { + + {{ getSubtypeLabel(opt.key) }} + + } +
} @if (activeType === ItemType.DEVICE) { - @if (!fixedSubType && getSubtypeMap()) { + @if (!fixedSubType && subtypeOptions.length > 0) {
} - - - {{ 'iot-hub.vendor' | translate }} - - @for (v of availableVendors; track v) { - - {{ v }} - - } - -
- - - {{ 'iot-hub.hardware-type' | translate }} - - @for (ht of hardwareTypes; track ht) { - - {{ ht }} - - } - -
- - - {{ 'iot-hub.connectivity' | translate }} - - @for (group of connectivityGroups; track group.label) { -
{{ group.label }}
- @for (val of group.values; track val) { - - {{ val }} - + @if (vendorOptions.length > 0) { + + + {{ 'iot-hub.vendor' | translate }} + + @if (vendorOptions.length >= 10) { + } - } - +
+ @for (group of getGroupedFilterItems(vendorOptions, 'vendor'); track group.label) { + @if (group.label) { +
{{ group.label | translate }}
+ } + @for (v of group.items; track v.key) { + + {{ v.key }} + + } + } +
+
+
+ } + @if (hardwareTypeOptions.length > 0) { + + + {{ 'iot-hub.hardware-type' | translate }} + + @if (hardwareTypeOptions.length >= 10) { + + } +
+ @for (ht of getFilteredItems(hardwareTypeOptions, 'hardwareType'); track ht.key) { + + {{ ht.key }} + + } +
+
+
+ } + @if (connectivityGroups.length > 0) { + + + {{ 'iot-hub.connectivity' | translate }} + + @if (allConnectivityCount >= 10) { + + } +
+ @for (group of getFilteredConnectivityGroups('connectivity'); track group.group) { +
{{ group.group }}
+ @for (val of group.values; track val.key) { + + {{ val.key }} + + } + } +
+
+ } } @else { - @if (categories.size > 0) { - @if (!fixedSubType && getSubtypeMap()) { + @if (categoryOptions.length > 0) { + @if (!fixedSubType && subtypeOptions.length > 0) {
} {{ 'iot-hub.category' | translate }} - @for (entry of categories | keyvalue; track entry.key) { - - {{ entry.value | translate }} - + @if (categoryOptions.length >= 10) { + } +
+ @for (cat of getFilteredItems(categoryOptions, 'category'); track cat.key) { + + {{ cat.key }} + + } +
} } - @if (activeType === ItemType.DEVICE || (!fixedSubType && getSubtypeMap()) || categories.size > 0) { + @if (useCaseOptions.length > 0) { + @if (activeType === ItemType.DEVICE || (!fixedSubType && subtypeOptions.length > 0) || categoryOptions.length > 0) { +
+ } + + + {{ 'iot-hub.use-case' | translate }} + + @if (useCaseOptions.length >= 10) { + + } +
+ @for (group of getGroupedFilterItems(useCaseOptions, 'useCase'); track group.label) { + @if (group.label) { +
{{ group.label | translate }}
+ } + @for (uc of group.items; track uc.key) { + + {{ uc.key }} + + } + } +
+
} - - - {{ 'iot-hub.use-case' | translate }} - - @for (uc of useCaseValues; track uc) { - - {{ uc }} - - } -
@@ -169,7 +290,7 @@ } @for (key of getActiveCategoriesArray(); track key) { - {{ getCategoryLabel(key) }} + {{ key }} close } @@ -193,7 +314,7 @@ } @for (key of getActiveUseCasesArray(); track key) { - {{ getUseCaseLabel(key) }} + {{ key }} close } @@ -204,7 +325,7 @@
}
-
+
@@ -212,7 +333,7 @@
-
+
@@ -259,7 +380,7 @@ } @for (key of getActiveCategoriesArray(); track key) { - {{ getCategoryLabel(key) }} + {{ key }} close } @@ -283,7 +404,7 @@ } @for (key of getActiveUseCasesArray(); track key) { - {{ getUseCaseLabel(key) }} + {{ key }} close } 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 b9b1076f72..73345849cd 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 @@ -50,6 +50,7 @@ .tb-iot-hub-content { display: flex; flex: 1; + height: 100%; background: transparent; overflow: visible; @@ -81,6 +82,36 @@ } } +.tb-iot-hub-filters-wrapper, +.tb-iot-hub-filter-drawer-body { + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background-color: rgba(0, 0, 0, 0.04); + } + + &::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + border-radius: 3px; + + &:hover { + background-color: rgba(0, 0, 0, 0.35); + } + } + + &.filter-items-hovered { + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: transparent; + } + } +} + .tb-iot-hub-filters-wrapper { position: absolute; inset: 0; @@ -141,15 +172,25 @@ padding: 8px 0 0; .tb-iot-hub-filters-inner { - padding: 12px 0; + padding: 0; } - .tb-iot-hub-filter-panel.mat-expansion-panel .mat-expansion-panel-header { - padding-left: 24px; - } + .tb-iot-hub-filter-panel.mat-expansion-panel { + .mat-expansion-panel-header { + padding-left: 24px; + } - .tb-iot-hub-filter-panel.mat-expansion-panel mat-checkbox { - padding-left: 12px; + mat-checkbox { + padding-left: 0; + } + + .tb-iot-hub-filter-items { + padding-left: 16px; + } + + .tb-iot-hub-filter-search { + margin-left: 24px; + } } } @@ -185,12 +226,12 @@ } ::ng-deep .mat-expansion-panel-body { - padding: 0; + padding: 0 8px 0 0; } mat-checkbox { display: block; - padding: 0 0 0 44px; + overflow: hidden; &:hover { background: rgba(0, 0, 0, 0.04); @@ -202,17 +243,98 @@ margin-top: 4px; } + .tb-iot-hub-filter-items { + padding-left: 44px; + max-height: 340px; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: transparent; + border-radius: 3px; + } + + &:hover { + &::-webkit-scrollbar-track { + background-color: rgba(0, 0, 0, 0.04); + } + + &::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + + &:hover { + background-color: rgba(0, 0, 0, 0.35); + } + } + } + } + + .tb-iot-hub-filter-search { + display: flex; + align-items: center; + gap: 8px; + height: 40px; + 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; + } + + 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); + } + } + + .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; + } + } + } + .tb-iot-hub-filter-group-label { - font-size: 12px; + font-size: 14px; font-weight: 500; color: rgba(0, 0, 0, 0.54); - text-transform: uppercase; letter-spacing: 0.5px; - padding: 12px 0 4px 44px; - - &:first-child { - padding-top: 4px; - } + padding: 12px 0 4px 12px; } } @@ -235,7 +357,7 @@ display: flex; align-items: center; justify-content: space-between; - padding: 16px 0 0; + padding: 24px 0 0; flex-shrink: 0; } 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 9e61475550..7a04e1b284 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 @@ -21,12 +21,8 @@ import { PageLink } from '@shared/models/page/page-link'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { PageData } from '@shared/models/page/page-data'; import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; -import { - ItemType, - getCategoriesForType, useCaseTranslations, useCaseValues, - connectivityGroups, ConnectivityGroup, hardwareTypes -} from '@shared/models/iot-hub/iot-hub-item.models'; -import { cfTypeTranslations, widgetTypeTranslations, ruleChainTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models'; +import { ItemType, FilterParamInfo } from '@shared/models/iot-hub/iot-hub-item.models'; +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'; @@ -74,9 +70,8 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { // When set after init (e.g., parent resolves config in its ngOnInit), // trigger type-specific loading since our ngOnInit already ran if (wasInit) { - this.updateCategories(); + this.loadFilterInfo(); if (value === ItemType.DEVICE) { - this.loadVendors(); this.loadInstalledDevices(); } else if (value === ItemType.WIDGET) { this.loadInstalledWidgets(); @@ -113,7 +108,6 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { activeConnectivity = new Set(); activeHardwareTypes = new Set(); activeVendors = new Set(); - availableVendors: string[] = []; sortOptions: SortOption[] = [ { value: 'totalInstallCount', label: 'iot-hub.sort-most-installed', direction: Direction.DESC }, @@ -122,14 +116,15 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { ]; selectedSortIndex = 0; - categories = new Map(); - useCases: Map = useCaseTranslations as Map; - useCaseValues: string[] = useCaseValues; - connectivityGroups: ConnectivityGroup[] = connectivityGroups; - hardwareTypes: string[] = hardwareTypes; - cfTypes: Map = cfTypeTranslations; - widgetTypes: Map = widgetTypeTranslations; - ruleChainTypes: Map = ruleChainTypeTranslations; + subtypeOptions: FilterParamInfo[] = []; + categoryOptions: FilterParamInfo[] = []; + useCaseOptions: FilterParamInfo[] = []; + vendorOptions: FilterParamInfo[] = []; + hardwareTypeOptions: FilterParamInfo[] = []; + connectivityGroups: { group: string; values: FilterParamInfo[] }[] = []; + + filterSearch: Record = {}; + filterItemsHovered = false; installedWidgets: IotHubInstalledItem[] = null; installedSolutionTemplates: IotHubInstalledItem[] = null; @@ -165,13 +160,12 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { subtypes.add(this.fixedSubType); } } - this.updateCategories(); + this.loadFilterInfo(); if (this.activeType === ItemType.WIDGET) { this.loadInstalledWidgets(); } else if (this.activeType === ItemType.SOLUTION_TEMPLATE) { this.loadInstalledSolutionTemplates(); } else if (this.activeType === ItemType.DEVICE) { - this.loadVendors(); this.loadInstalledDevices(); } this.loadItems(); @@ -205,14 +199,14 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.activeConnectivity.clear(); this.activeHardwareTypes.clear(); this.activeVendors.clear(); - this.updateCategories(); + this.filterSearch = {}; + this.loadFilterInfo(); this.pageIndex = 0; if (type === ItemType.WIDGET) { this.loadInstalledWidgets(); } else if (type === ItemType.SOLUTION_TEMPLATE) { this.loadInstalledSolutionTemplates(); } else if (type === ItemType.DEVICE) { - this.loadVendors(); this.loadInstalledDevices(); } this.loadItems(); @@ -372,15 +366,6 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { return Array.from(this.activeHardwareTypes); } - getSubtypeMap(): Map | null { - switch (this.activeType) { - case ItemType.WIDGET: return this.widgetTypes; - case ItemType.CALCULATED_FIELD: return this.cfTypes; - case ItemType.RULE_CHAIN: return this.ruleChainTypes; - default: return null; - } - } - getActiveSubtypes(): Set { switch (this.activeType) { case ItemType.WIDGET: return this.activeWidgetTypes; @@ -444,19 +429,52 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.loadItems(); } - getSubtypeLabel(key: string): string { - const map = this.getSubtypeMap(); - const translationKey = map?.get(key); - return translationKey ? this.translate.instant(translationKey) : key; + 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)); } - getCategoryLabel(key: string): string { - const translationKey = this.categories.get(key); - return translationKey ? this.translate.instant(translationKey) : key; + 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; } - getUseCaseLabel(key: string): string { - const translationKey = this.useCases.get(key); + getFilteredConnectivityGroups(searchKey: string): { group: string; values: FilterParamInfo[] }[] { + const search = (this.filterSearch[searchKey] || '').toLowerCase(); + if (!search) { return this.connectivityGroups; } + return this.connectivityGroups + .map(g => ({ group: g.group, values: g.values.filter(v => v.key.toLowerCase().includes(search)) })) + .filter(g => g.values.length > 0); + } + + get allConnectivityCount(): number { + return this.connectivityGroups.reduce((sum, g) => sum + g.values.length, 0); + } + + getSubtypeLabel(key: string): string { + let translationKey: string; + switch (this.activeType) { + case ItemType.WIDGET: translationKey = widgetTypeTranslations.get(key); break; + case ItemType.CALCULATED_FIELD: translationKey = cfTypeTranslations.get(key); break; + case ItemType.RULE_CHAIN: translationKey = ruleChainTypeTranslations.get(key); break; + default: return key; + } return translationKey ? this.translate.instant(translationKey) : key; } @@ -473,7 +491,8 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.getActiveSubtypes()?.add(this.fixedSubType); } this.textSearch = ''; - this.updateCategories(); + this.filterSearch = {}; + this.loadFilterInfo(); this.pageIndex = 0; this.loadItems(); } @@ -625,14 +644,6 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { }); } - private loadVendors(): void { - this.iotHubApiService.getPublishedDeviceVendors({ignoreLoading: true, ignoreErrors: true}).subscribe({ - next: (vendors) => { - this.availableVendors = vendors?.sort() || []; - } - }); - } - private loadInstalledDevices(): void { const pageLink = new PageLink(10000, 0); this.iotHubApiService.getInstalledItems(pageLink, 'DEVICE', {ignoreLoading: true, ignoreErrors: true}).subscribe({ @@ -672,8 +683,22 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { } } - private updateCategories(): void { - this.categories = getCategoriesForType(this.activeType); + private loadFilterInfo(): void { + const hasItems = (i: FilterParamInfo) => i.totalItems > 0; + this.iotHubApiService.getFilterInfo(this.activeType, { ignoreLoading: true }).subscribe(info => { + this.subtypeOptions = (info.types || []).filter(hasItems); + this.categoryOptions = (info.categories || []).filter(hasItems); + this.useCaseOptions = (info.useCases || []).filter(hasItems); + this.vendorOptions = (info.vendors || []).filter(hasItems); + this.hardwareTypeOptions = (info.hardwareTypes || []).filter(hasItems); + if (info.connectivities) { + this.connectivityGroups = Object.entries(info.connectivities) + .map(([group, values]) => ({ group, values: values.filter(hasItems) })) + .filter(g => g.values.length > 0); + } else { + this.connectivityGroups = []; + } + }); } loadItems(): void { @@ -695,7 +720,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { undefined, this.activeHardwareTypes.size > 0 ? Array.from(this.activeHardwareTypes) : undefined, this.activeConnectivity.size > 0 ? Array.from(this.activeConnectivity) : undefined, - this.activeVendors.size === 1 ? Array.from(this.activeVendors)[0] : undefined + this.activeVendors.size > 0 ? Array.from(this.activeVendors) : undefined ); this.iotHubApiService.getPublishedVersions( query, diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts index 6c1d5e4e83..ca9fd11af6 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts @@ -16,7 +16,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTranslations, widgetTypeTranslations, NodeInfo, nodeComponentTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models'; -import { ItemType, itemTypeTranslations, getCategoriesForType, useCaseTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; +import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { TranslateService } from '@ngx-translate/core'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; @@ -148,19 +148,14 @@ export class TbIotHubItemCardComponent { if (!this.item.categories?.length) { return []; } - const categoryMap = getCategoriesForType(this.item.type); - return this.item.categories.map(c => { - const key = categoryMap.get(c); - return key ? this.translate.instant(key) : c; - }); + return this.item.categories; } getFirstUseCaseLabel(): string | null { if (!this.item.useCases?.length) { return null; } - const key = useCaseTranslations.get(this.item.useCases[0] as any); - return key ? this.translate.instant(key) : this.item.useCases[0]; + return this.item.useCases[0]; } getNodes(): NodeInfo[] { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html index 909fa37bd3..969431c462 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html @@ -294,7 +294,7 @@ {{ 'iot-hub.category' | translate }}
@for (cat of item.categories; track cat) { - {{ getCategoryLabel(cat) }} + {{ cat }} }
@@ -304,7 +304,7 @@ {{ 'iot-hub.use-cases' | translate }}
@for (uc of item.useCases; track uc) { - {{ getUseCaseLabel(uc) }} + {{ uc }} }
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts index afaf5cb39a..65bcd0e645 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts @@ -21,7 +21,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { DialogComponent } from '@shared/components/dialog.component'; import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTranslations, widgetTypeTranslations, nodeComponentTypeTranslations, NodeInfo } from '@shared/models/iot-hub/iot-hub-version.models'; -import { ItemType, itemTypeTranslations, getCategoriesForType, useCaseTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; +import { ItemType, itemTypeTranslations } 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 { TranslateService } from '@ngx-translate/core'; @@ -61,9 +61,6 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent; - private useCaseMap = useCaseTranslations; - constructor( protected store: Store, protected router: Router, @@ -78,21 +75,10 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent( ] ); -export enum WidgetCategory { - CHARTS_GRAPHS = 'CHARTS_GRAPHS', - GAUGES_INDICATORS = 'GAUGES_INDICATORS', - CONTROLS = 'CONTROLS', - MAPS_LOCATION = 'MAPS_LOCATION', - TABLES_LISTS = 'TABLES_LISTS', - CARDS_INFO = 'CARDS_INFO', - SCADA = 'SCADA', - INPUT_FORMS = 'INPUT_FORMS' -} - -export const widgetCategoryTranslations = new Map([ - [WidgetCategory.CHARTS_GRAPHS, 'iot-hub.category.charts-graphs'], - [WidgetCategory.GAUGES_INDICATORS, 'iot-hub.category.gauges-indicators'], - [WidgetCategory.CONTROLS, 'iot-hub.category.controls'], - [WidgetCategory.MAPS_LOCATION, 'iot-hub.category.maps-location'], - [WidgetCategory.TABLES_LISTS, 'iot-hub.category.tables-lists'], - [WidgetCategory.CARDS_INFO, 'iot-hub.category.cards-info'], - [WidgetCategory.SCADA, 'iot-hub.category.scada'], - [WidgetCategory.INPUT_FORMS, 'iot-hub.category.input-forms'] -]); - -export enum DashboardCategory { - MONITORING = 'MONITORING', - ANALYTICS = 'ANALYTICS', - DEVICE_MANAGEMENT = 'DEVICE_MANAGEMENT', - USER_MANAGEMENT = 'USER_MANAGEMENT', - ASSET_MANAGEMENT = 'ASSET_MANAGEMENT', - SCADA = 'SCADA', - REPORTING = 'REPORTING', - OVERVIEW = 'OVERVIEW', - OPERATIONS = 'OPERATIONS' -} - -export const dashboardCategoryTranslations = new Map([ - [DashboardCategory.MONITORING, 'iot-hub.category.monitoring'], - [DashboardCategory.ANALYTICS, 'iot-hub.category.analytics'], - [DashboardCategory.DEVICE_MANAGEMENT, 'iot-hub.category.device-management'], - [DashboardCategory.USER_MANAGEMENT, 'iot-hub.category.user-management'], - [DashboardCategory.ASSET_MANAGEMENT, 'iot-hub.category.asset-management'], - [DashboardCategory.SCADA, 'iot-hub.category.scada'], - [DashboardCategory.REPORTING, 'iot-hub.category.reporting'], - [DashboardCategory.OVERVIEW, 'iot-hub.category.overview'], - [DashboardCategory.OPERATIONS, 'iot-hub.category.operations'] -]); - -export enum CalcFieldCategory { - AGGREGATION = 'AGGREGATION', - GEOSPATIAL = 'GEOSPATIAL', - STATISTICAL = 'STATISTICAL', - ENERGY = 'ENERGY', - ENVIRONMENTAL = 'ENVIRONMENTAL', - PREDICTIVE = 'PREDICTIVE', - CUSTOM_FORMULA = 'CUSTOM_FORMULA' -} - -export const calcFieldCategoryTranslations = new Map([ - [CalcFieldCategory.AGGREGATION, 'iot-hub.category.aggregation'], - [CalcFieldCategory.GEOSPATIAL, 'iot-hub.category.geospatial'], - [CalcFieldCategory.STATISTICAL, 'iot-hub.category.statistical'], - [CalcFieldCategory.ENERGY, 'iot-hub.category.energy'], - [CalcFieldCategory.ENVIRONMENTAL, 'iot-hub.category.environmental'], - [CalcFieldCategory.PREDICTIVE, 'iot-hub.category.predictive'], - [CalcFieldCategory.CUSTOM_FORMULA, 'iot-hub.category.custom-formula'] -]); - -export enum RuleChainCategory { - DATA_PROCESSING = 'DATA_PROCESSING', - ALERTING = 'ALERTING', - DEVICE_CONNECTIVITY = 'DEVICE_CONNECTIVITY', - INTEGRATION = 'INTEGRATION', - ANALYTICS = 'ANALYTICS' -} - -export const ruleChainCategoryTranslations = new Map([ - [RuleChainCategory.DATA_PROCESSING, 'iot-hub.category.data-processing'], - [RuleChainCategory.ALERTING, 'iot-hub.category.alerting'], - [RuleChainCategory.DEVICE_CONNECTIVITY, 'iot-hub.category.device-connectivity'], - [RuleChainCategory.INTEGRATION, 'iot-hub.category.integration'], - [RuleChainCategory.ANALYTICS, 'iot-hub.category.analytics'] -]); - -export enum SolutionTemplateCategory { - MONITORING = 'MONITORING', - ANALYTICS = 'ANALYTICS', - DEVICE_MANAGEMENT = 'DEVICE_MANAGEMENT', - ASSET_MANAGEMENT = 'ASSET_MANAGEMENT', - ENERGY = 'ENERGY', - SMART_CITY = 'SMART_CITY', - AGRICULTURE = 'AGRICULTURE', - INDUSTRIAL = 'INDUSTRIAL' -} - -export const solutionTemplateCategoryTranslations = new Map([ - [SolutionTemplateCategory.MONITORING, 'iot-hub.category.monitoring'], - [SolutionTemplateCategory.ANALYTICS, 'iot-hub.category.analytics'], - [SolutionTemplateCategory.DEVICE_MANAGEMENT, 'iot-hub.category.device-management'], - [SolutionTemplateCategory.ASSET_MANAGEMENT, 'iot-hub.category.asset-management'], - [SolutionTemplateCategory.ENERGY, 'iot-hub.category.energy'], - [SolutionTemplateCategory.SMART_CITY, 'iot-hub.category.smart-city'], - [SolutionTemplateCategory.AGRICULTURE, 'iot-hub.category.agriculture'], - [SolutionTemplateCategory.INDUSTRIAL, 'iot-hub.category.industrial'] -]); - -export enum DeviceCategory { - SENSORS = 'SENSORS', - GATEWAYS = 'GATEWAYS', - CONTROLLERS = 'CONTROLLERS', - ACTUATORS = 'ACTUATORS', - TRACKERS = 'TRACKERS', - METERS = 'METERS' -} - -export const deviceCategoryTranslations = new Map([ - [DeviceCategory.SENSORS, 'iot-hub.category.sensors'], - [DeviceCategory.GATEWAYS, 'iot-hub.category.gateways'], - [DeviceCategory.CONTROLLERS, 'iot-hub.category.controllers'], - [DeviceCategory.ACTUATORS, 'iot-hub.category.actuators'], - [DeviceCategory.TRACKERS, 'iot-hub.category.trackers'], - [DeviceCategory.METERS, 'iot-hub.category.meters'] -]); - -export enum UseCase { - SMART_HOME = 'SMART_HOME', - INDUSTRIAL_IOT = 'INDUSTRIAL_IOT', - ENERGY_MANAGEMENT = 'ENERGY_MANAGEMENT', - FLEET_MANAGEMENT = 'FLEET_MANAGEMENT', - AGRICULTURE = 'AGRICULTURE', - SMART_CITY = 'SMART_CITY', - HEALTHCARE = 'HEALTHCARE', - RETAIL = 'RETAIL', - WATER_UTILITIES = 'WATER_UTILITIES' -} - -export const useCaseTranslations = new Map([ - [UseCase.SMART_HOME, 'iot-hub.use-case.smart-home'], - [UseCase.INDUSTRIAL_IOT, 'iot-hub.use-case.industrial-iot'], - [UseCase.ENERGY_MANAGEMENT, 'iot-hub.use-case.energy-management'], - [UseCase.FLEET_MANAGEMENT, 'iot-hub.use-case.fleet-management'], - [UseCase.AGRICULTURE, 'iot-hub.use-case.agriculture'], - [UseCase.SMART_CITY, 'iot-hub.use-case.smart-city'], - [UseCase.HEALTHCARE, 'iot-hub.use-case.healthcare'], - [UseCase.RETAIL, 'iot-hub.use-case.retail'], - [UseCase.WATER_UTILITIES, 'iot-hub.use-case.water-utilities'] -]); - -export const useCaseValues: string[] = [ - 'Air Quality Monitoring', 'Asset Tracking', 'Cold Chain', 'Drones', - 'Environment Monitoring', 'Fleet Tracking', 'Health Care', 'Industrial Automation', - 'Predictive Maintenance', 'Robotics', 'SCADA', 'Smart Building', - 'Smart City', 'Smart Energy', 'Smart Farming', 'Smart Home', - 'Smart Metering', 'Smart Office', 'Smart Retail', 'Solar Monitoring', - 'Tank Level Monitoring', 'Waste Management' -]; - -export interface ConnectivityGroup { - label: string; - values: string[]; -} - -export const connectivityGroups: ConnectivityGroup[] = [ - { label: 'Wireless', values: ['Wi-Fi', 'Bluetooth', 'LoRaWAN', 'Zigbee', 'Z-Wave', 'Thread', 'NB-IoT', 'LTE-M', 'Sigfox', 'NFC', '2G', '3G', '4G', '5G', '6LoWPAN', 'DigiMesh', 'UWB'] }, - { label: 'Wired', values: ['Ethernet', 'RS485', 'RS232', 'USB', 'UART', 'SPI', 'I2C', 'CAN', '1-Wire', '4-20mA', '0-10V', 'M-Bus', 'IO-Link', 'IrDA', 'Power Line Communication', 'SDI-12'] }, - { label: 'Protocols', values: ['MQTT', 'HTTP', 'HTTPS', 'CoAP', 'REST', 'SNMP', 'Modbus RTU', 'Modbus TCP', 'OPC-UA', 'BACnet', 'KNX', 'WebSocket', 'TCP/IP', 'UDP', 'CANopen', 'DNP3', 'EtherCAT', 'Foundation Fieldbus', 'HART', 'IEC 61850', 'PROFINET', 'Wireless M-Bus'] } -]; - -export const hardwareTypes: string[] = [ - 'Actuator', 'AI Accelerator', 'Analyzer', 'Camera', 'Controller', 'Data Logger', - 'Development Board', 'Display', 'Gateway', 'Meter', 'PLC', 'Relay', - 'Sensor', 'Single Board Computer', 'Tracker' -]; - -export function getCategoriesForType(type: ItemType): Map { - switch (type) { - case ItemType.WIDGET: - return widgetCategoryTranslations as Map; - case ItemType.DASHBOARD: - return dashboardCategoryTranslations as Map; - case ItemType.SOLUTION_TEMPLATE: - return solutionTemplateCategoryTranslations as Map; - case ItemType.CALCULATED_FIELD: - return calcFieldCategoryTranslations as Map; - case ItemType.RULE_CHAIN: - return ruleChainCategoryTranslations as Map; - case ItemType.DEVICE: - return deviceCategoryTranslations as Map; - default: - return new Map(); - } +export interface FilterParamInfo { + key: string; + totalItems: number; + totalInstallCount: number; +} + +export interface ItemTypeFilterInfo { + types: FilterParamInfo[]; + categories: FilterParamInfo[]; + useCases: FilterParamInfo[]; + vendors: FilterParamInfo[]; + hardwareTypes: FilterParamInfo[]; + connectivities: Record; } diff --git a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts index dfacd44e92..d87cb41599 100644 --- a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts +++ b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts @@ -125,7 +125,7 @@ export class MpItemVersionQuery { public tbVersion?: number, public hardwareTypes?: string[], public connectivity?: string[], - public vendor?: string + public vendors?: string[] ) {} public toQuery(): string { @@ -163,8 +163,8 @@ export class MpItemVersionQuery { if (this.connectivity?.length) { query += this.connectivity.map(c => `&connectivity=${encodeURIComponent(c)}`).join(''); } - if (this.vendor) { - query += `&vendor=${encodeURIComponent(this.vendor)}`; + if (this.vendors?.length) { + query += this.vendors.map(v => `&vendors=${encodeURIComponent(v)}`).join(''); } return query; } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 819bc69125..c3851b33fa 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3827,51 +3827,8 @@ "update-error-title": "Update Failed", "update-error-message": "Failed to update '{{name}}'.", "update-error": "Failed to update {{name}}", - "category.charts-graphs": "Charts & Graphs", - "category.gauges-indicators": "Gauges & Indicators", - "category.controls": "Controls", - "category.maps-location": "Maps & Location", - "category.tables-lists": "Tables & Lists", - "category.cards-info": "Cards & Info", - "category.scada": "SCADA", - "category.input-forms": "Input Forms", - "category.monitoring": "Monitoring", - "category.analytics": "Analytics", - "category.device-management": "Device Management", - "category.user-management": "User Management", - "category.asset-management": "Asset Management", - "category.reporting": "Reporting", - "category.overview": "Overview", - "category.operations": "Operations", - "category.aggregation": "Aggregation", - "category.geospatial": "Geospatial", - "category.statistical": "Statistical", - "category.energy": "Energy", - "category.environmental": "Environmental", - "category.predictive": "Predictive", - "category.custom-formula": "Custom Formula", - "category.data-processing": "Data Processing", - "category.alerting": "Alerting", - "category.device-connectivity": "Device Connectivity", - "category.integration": "Integration", - "category.smart-city": "Smart City", - "category.agriculture": "Agriculture", - "category.industrial": "Industrial", - "category.sensors": "Sensors", - "category.gateways": "Gateways", - "category.controllers": "Controllers", - "category.actuators": "Actuators", - "category.trackers": "Trackers", - "category.meters": "Meters", - "use-case.smart-home": "Smart Home", - "use-case.industrial-iot": "Industrial IoT", - "use-case.energy-management": "Energy Management", - "use-case.fleet-management": "Fleet Management", - "use-case.agriculture": "Agriculture", - "use-case.smart-city": "Smart City", - "use-case.healthcare": "Healthcare", - "use-case.retail": "Retail", - "use-case.water-utilities": "Water & Utilities", + "most-popular": "Most popular", + "all": "All", "installed-items": "Installed Items", "search-installed-items": "Search installed items...", "item-name": "Name",