From caf0c0062b12b36469f26a817a4b5fe1ae5c42b2 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Thu, 9 Apr 2026 17:26:22 +0300 Subject: [PATCH] feat(iot-hub): update browse filters and use cases for device library - Use Cases: add 27 grouped values (8 industry groups) with sub-headers in the filter panel. Applied to all item types. - Device filters: replace Category with Connectivity (55 values, 3 groups) and Hardware Type (19 values) filters. Category hidden for DEVICE type. - Query model: add hardwareTypes, connectivity, vendor params to MpItemVersionQuery (IoT Hub API already supports these). - Detail dialog: DEVICE type shows Hardware Type + Connectivity + Use Cases instead of Category + Use Cases. - Filter chips updated for new filter types. --- .../iot-hub/iot-hub-browse.component.html | 83 ++++++++++++++++--- .../iot-hub/iot-hub-browse.component.scss | 13 +++ .../iot-hub/iot-hub-browse.component.ts | 71 +++++++++++++++- .../iot-hub-item-detail-dialog.component.html | 82 +++++++++++++----- .../models/iot-hub/iot-hub-item.models.ts | 33 ++++++++ .../models/iot-hub/iot-hub-version.models.ts | 14 +++- .../assets/locale/locale.constant-en_US.json | 2 + 7 files changed, 258 insertions(+), 40 deletions(-) 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 c20acedecb..d8c67197f4 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 @@ -68,35 +68,70 @@ } - @if (categories.size > 0) { + @if (activeType === ItemType.DEVICE) { @if (!fixedSubType && getSubtypeMap()) {
} - {{ 'iot-hub.category' | translate }} + {{ 'iot-hub.hardware-type' | translate }} - @for (entry of categories | keyvalue; track entry.key) { - - {{ entry.value | 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 }} + + } + } +
+ } @else { + @if (categories.size > 0) { + @if (!fixedSubType && getSubtypeMap()) { +
+ } + + + {{ 'iot-hub.category' | translate }} + + @for (entry of categories | keyvalue; track entry.key) { + + {{ entry.value | translate }} + + } + + } } - @if ((!fixedSubType && getSubtypeMap()) || categories.size > 0) { + @if (activeType === ItemType.DEVICE || (!fixedSubType && getSubtypeMap()) || categories.size > 0) {
} {{ 'iot-hub.use-case' | translate }} - @for (entry of useCases | keyvalue; track entry.key) { - - {{ entry.value | translate }} - + @for (group of useCaseGroups; track group.label) { +
{{ group.label }}
+ @for (uc of group.values; track uc) { + + {{ uc }} + + } }
@@ -129,6 +164,18 @@ close } + @for (key of getActiveHardwareTypesArray(); track key) { + + {{ key }} + close + + } + @for (key of getActiveConnectivityArray(); track key) { + + {{ key }} + close + + } @for (key of getActiveUseCasesArray(); track key) { {{ getUseCaseLabel(key) }} @@ -201,6 +248,18 @@ close } + @for (key of getActiveHardwareTypesArray(); track key) { + + {{ key }} + close + + } + @for (key of getActiveConnectivityArray(); track key) { + + {{ key }} + close + + } @for (key of getActiveUseCasesArray(); track key) { {{ getUseCaseLabel(key) }} 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 955233e880..56730ec3a6 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 @@ -196,6 +196,19 @@ background: rgba(0, 0, 0, 0.04); } } + + .tb-iot-hub-filter-group-label { + font-size: 12px; + 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; + } + } } // Right content area — Design: flex-1, pr-40, scrollable 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 e0d94228b3..6a2aba27cc 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 @@ -23,7 +23,8 @@ import { PageData } from '@shared/models/page/page-data'; import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { ItemType, - getCategoriesForType, useCaseTranslations + getCategoriesForType, useCaseTranslations, useCaseGroups, UseCaseGroup, + 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 { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models'; @@ -93,6 +94,8 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { activeCfTypes = new Set(); activeWidgetTypes = new Set(); activeRuleChainTypes = new Set(); + activeConnectivity = new Set(); + activeHardwareTypes = new Set(); sortOptions: SortOption[] = [ { value: 'totalInstallCount', label: 'iot-hub.sort-most-installed', direction: Direction.DESC }, @@ -103,6 +106,9 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { categories = new Map(); useCases: Map = useCaseTranslations as Map; + useCaseGroups: UseCaseGroup[] = useCaseGroups; + connectivityGroups: ConnectivityGroup[] = connectivityGroups; + hardwareTypes: string[] = hardwareTypes; cfTypes: Map = cfTypeTranslations; widgetTypes: Map = widgetTypeTranslations; ruleChainTypes: Map = ruleChainTypeTranslations; @@ -173,6 +179,8 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.activeCfTypes.clear(); this.activeWidgetTypes.clear(); this.activeRuleChainTypes.clear(); + this.activeConnectivity.clear(); + this.activeHardwareTypes.clear(); this.updateCategories(); this.pageIndex = 0; if (type === ItemType.WIDGET) { @@ -271,6 +279,54 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.loadItems(); } + onConnectivityToggle(value: string): void { + if (this.activeConnectivity.has(value)) { + this.activeConnectivity.delete(value); + } else { + this.activeConnectivity.add(value); + } + this.pageIndex = 0; + this.loadItems(); + } + + clearConnectivity(): void { + this.activeConnectivity.clear(); + this.pageIndex = 0; + this.loadItems(); + } + + isConnectivityActive(value: string): boolean { + return this.activeConnectivity.has(value); + } + + onHardwareTypeToggle(value: string): void { + if (this.activeHardwareTypes.has(value)) { + this.activeHardwareTypes.delete(value); + } else { + this.activeHardwareTypes.add(value); + } + this.pageIndex = 0; + this.loadItems(); + } + + clearHardwareTypes(): void { + this.activeHardwareTypes.clear(); + this.pageIndex = 0; + this.loadItems(); + } + + isHardwareTypeActive(value: string): boolean { + return this.activeHardwareTypes.has(value); + } + + getActiveConnectivityArray(): string[] { + return Array.from(this.activeConnectivity); + } + + getActiveHardwareTypesArray(): string[] { + return Array.from(this.activeHardwareTypes); + } + getSubtypeMap(): Map | null { switch (this.activeType) { case ItemType.WIDGET: return this.widgetTypes; @@ -365,6 +421,8 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.activeCfTypes.clear(); this.activeWidgetTypes.clear(); this.activeRuleChainTypes.clear(); + this.activeConnectivity.clear(); + this.activeHardwareTypes.clear(); if (this.fixedSubType) { this.getActiveSubtypes()?.add(this.fixedSubType); } @@ -376,13 +434,15 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { get activeFilterCount(): number { const subtypeCount = this.fixedSubType ? this.getActiveSubtypesArray().length : (this.getActiveSubtypes()?.size || 0); - return subtypeCount + this.activeCategories.size + this.activeUseCases.size; + return subtypeCount + this.activeCategories.size + this.activeUseCases.size + + this.activeConnectivity.size + this.activeHardwareTypes.size; } hasActiveDropdownFilters(): boolean { const subtypeCount = this.fixedSubType ? this.getActiveSubtypesArray().length : (this.getActiveSubtypes()?.size || 0); return this.activeCategories.size > 0 || - this.activeUseCases.size > 0 || subtypeCount > 0; + this.activeUseCases.size > 0 || subtypeCount > 0 || + this.activeConnectivity.size > 0 || this.activeHardwareTypes.size > 0; } hasActiveFilters(): boolean { @@ -537,7 +597,10 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy { this.activeUseCases.size > 0 ? Array.from(this.activeUseCases) : undefined, this.activeCfTypes.size > 0 ? Array.from(this.activeCfTypes) : undefined, this.activeWidgetTypes.size > 0 ? Array.from(this.activeWidgetTypes) : undefined, - this.activeRuleChainTypes.size > 0 ? Array.from(this.activeRuleChainTypes) : undefined + this.activeRuleChainTypes.size > 0 ? Array.from(this.activeRuleChainTypes) : undefined, + undefined, + this.activeHardwareTypes.size > 0 ? Array.from(this.activeHardwareTypes) : undefined, + this.activeConnectivity.size > 0 ? Array.from(this.activeConnectivity) : undefined ); this.iotHubApiService.getPublishedVersions( query, 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 3b4ac5e57a..b149a86498 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 @@ -238,31 +238,67 @@ } - - @if (item.categories?.length || item.useCases?.length) { -
-
- @if (item.categories?.length) { -
- {{ 'iot-hub.category' | translate }} -
- @for (cat of item.categories; track cat) { - {{ getCategoryLabel(cat) }} - } + + @if (item.type === ItemType.DEVICE) { + @if (item.dataDescriptor?.hardwareType || item.dataDescriptor?.connectivity?.length || item.useCases?.length) { +
+
+ @if (item.dataDescriptor?.hardwareType) { +
+ {{ 'iot-hub.hardware-type' | translate }} +
+ {{ item.dataDescriptor.hardwareType }} +
-
- } - @if (item.useCases?.length) { -
- {{ 'iot-hub.use-cases' | translate }} -
- @for (uc of item.useCases; track uc) { - {{ getUseCaseLabel(uc) }} - } + } + @if (item.dataDescriptor?.connectivity?.length) { +
+ {{ 'iot-hub.connectivity' | translate }} +
+ @for (conn of item.dataDescriptor.connectivity; track conn) { + {{ conn }} + } +
-
- } -
+ } + @if (item.useCases?.length) { +
+ {{ 'iot-hub.use-cases' | translate }} +
+ @for (uc of item.useCases; track uc) { + {{ uc }} + } +
+
+ } +
+ } + } @else { + @if (item.categories?.length || item.useCases?.length) { +
+
+ @if (item.categories?.length) { +
+ {{ 'iot-hub.category' | translate }} +
+ @for (cat of item.categories; track cat) { + {{ getCategoryLabel(cat) }} + } +
+
+ } + @if (item.useCases?.length) { +
+ {{ 'iot-hub.use-cases' | translate }} +
+ @for (uc of item.useCases; track uc) { + {{ getUseCaseLabel(uc) }} + } +
+
+ } +
+ } } diff --git a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts index d5fd6c57ab..e096ded646 100644 --- a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts +++ b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts @@ -180,6 +180,39 @@ export const useCaseTranslations = new Map([ [UseCase.WATER_UTILITIES, 'iot-hub.use-case.water-utilities'] ]); +export interface UseCaseGroup { + label: string; + values: string[]; +} + +export const useCaseGroups: UseCaseGroup[] = [ + { label: 'Energy & Utilities', values: ['Smart Energy', 'Smart Grid', 'Tank Level Monitoring', 'Waste Management'] }, + { label: 'Buildings & Facilities', values: ['Smart Office', 'Smart Buildings', 'HVAC Monitoring', 'Access Control'] }, + { label: 'Environment & Agriculture', values: ['Smart Farming', 'Air Quality Monitoring', 'Environment Monitoring', 'Water Quality'] }, + { label: 'Manufacturing', values: ['Predictive Maintenance', 'Asset Tracking', 'Production Monitoring'] }, + { label: 'Transportation', values: ['Fleet Tracking', 'Cold Chain', 'Connected Vehicles'] }, + { label: 'Health & Safety', values: ['Health Care', 'Safety Monitoring'] }, + { label: 'Retail', values: ['Smart Retail', 'Inventory Tracking'] }, + { label: 'Smart Cities', values: ['Street Lighting', 'Parking', 'Traffic 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[] = [ + 'Accelerometer', 'Actuator', 'Analyzer', 'Controller', 'Data Logger', 'Detector', + 'Energy Meter', 'Gateway', 'Microcontroller', 'Module', 'Monitor', 'Probe', 'Relay', + 'Sensor', 'Station', 'Switch', 'System', 'Tracker', 'Transmitter' +]; + export function getCategoriesForType(type: ItemType): Map { switch (type) { case ItemType.WIDGET: 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 1d5637e2d0..dfacd44e92 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 @@ -122,7 +122,10 @@ export class MpItemVersionQuery { public cfTypes?: string[], public widgetTypes?: string[], public ruleChainTypes?: string[], - public tbVersion?: number + public tbVersion?: number, + public hardwareTypes?: string[], + public connectivity?: string[], + public vendor?: string ) {} public toQuery(): string { @@ -154,6 +157,15 @@ export class MpItemVersionQuery { if (this.tbVersion != null) { query += `&tbVersion=${this.tbVersion}`; } + if (this.hardwareTypes?.length) { + query += this.hardwareTypes.map(ht => `&hardwareTypes=${encodeURIComponent(ht)}`).join(''); + } + if (this.connectivity?.length) { + query += this.connectivity.map(c => `&connectivity=${encodeURIComponent(c)}`).join(''); + } + if (this.vendor) { + query += `&vendor=${encodeURIComponent(this.vendor)}`; + } 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 6560a11c97..3282836af5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3758,6 +3758,8 @@ "type-filter-label": "Type", "category": "Category", "use-case": "Use case", + "hardware-type": "Hardware Type", + "connectivity": "Connectivity", "clear-all-filters": "Clear all filters", "clear-all": "Clear all", "results-count": "{{count}} results",