Browse Source

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.
pull/15508/head
Andrii Shvaika 6 months ago
parent
commit
caf0c0062b
  1. 83
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html
  2. 13
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss
  3. 71
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts
  4. 82
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html
  5. 33
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts
  6. 14
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts
  7. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json

83
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html

@ -68,35 +68,70 @@
</mat-expansion-panel>
}
@if (categories.size > 0) {
@if (activeType === ItemType.DEVICE) {
@if (!fixedSubType && getSubtypeMap()) {
<div class="tb-iot-hub-filter-divider"></div>
}
<mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel">
<mat-expansion-panel-header>
<mat-panel-title>{{ 'iot-hub.category' | translate }}</mat-panel-title>
<mat-panel-title>{{ 'iot-hub.hardware-type' | translate }}</mat-panel-title>
</mat-expansion-panel-header>
@for (entry of categories | keyvalue; track entry.key) {
<mat-checkbox [checked]="isCategoryActive(entry.key)"
(change)="onCategoryToggle(entry.key)">
{{ entry.value | translate }}
@for (ht of hardwareTypes; track ht) {
<mat-checkbox [checked]="isHardwareTypeActive(ht)"
(change)="onHardwareTypeToggle(ht)">
{{ ht }}
</mat-checkbox>
}
</mat-expansion-panel>
<div class="tb-iot-hub-filter-divider"></div>
<mat-expansion-panel [expanded]="false" class="tb-iot-hub-filter-panel">
<mat-expansion-panel-header>
<mat-panel-title>{{ 'iot-hub.connectivity' | translate }}</mat-panel-title>
</mat-expansion-panel-header>
@for (group of connectivityGroups; track group.label) {
<div class="tb-iot-hub-filter-group-label">{{ group.label }}</div>
@for (val of group.values; track val) {
<mat-checkbox [checked]="isConnectivityActive(val)"
(change)="onConnectivityToggle(val)">
{{ val }}
</mat-checkbox>
}
}
</mat-expansion-panel>
} @else {
@if (categories.size > 0) {
@if (!fixedSubType && getSubtypeMap()) {
<div class="tb-iot-hub-filter-divider"></div>
}
<mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel">
<mat-expansion-panel-header>
<mat-panel-title>{{ 'iot-hub.category' | translate }}</mat-panel-title>
</mat-expansion-panel-header>
@for (entry of categories | keyvalue; track entry.key) {
<mat-checkbox [checked]="isCategoryActive(entry.key)"
(change)="onCategoryToggle(entry.key)">
{{ entry.value | translate }}
</mat-checkbox>
}
</mat-expansion-panel>
}
}
@if ((!fixedSubType && getSubtypeMap()) || categories.size > 0) {
@if (activeType === ItemType.DEVICE || (!fixedSubType && getSubtypeMap()) || categories.size > 0) {
<div class="tb-iot-hub-filter-divider"></div>
}
<mat-expansion-panel [expanded]="false" class="tb-iot-hub-filter-panel">
<mat-expansion-panel-header>
<mat-panel-title>{{ 'iot-hub.use-case' | translate }}</mat-panel-title>
</mat-expansion-panel-header>
@for (entry of useCases | keyvalue; track entry.key) {
<mat-checkbox [checked]="isUseCaseActive(entry.key)"
(change)="onUseCaseToggle(entry.key)">
{{ entry.value | translate }}
</mat-checkbox>
@for (group of useCaseGroups; track group.label) {
<div class="tb-iot-hub-filter-group-label">{{ group.label }}</div>
@for (uc of group.values; track uc) {
<mat-checkbox [checked]="isUseCaseActive(uc)"
(change)="onUseCaseToggle(uc)">
{{ uc }}
</mat-checkbox>
}
}
</mat-expansion-panel>
</div>
@ -129,6 +164,18 @@
<mat-icon matChipRemove>close</mat-icon>
</mat-chip>
}
@for (key of getActiveHardwareTypesArray(); track key) {
<mat-chip (removed)="onHardwareTypeToggle(key)">
{{ key }}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip>
}
@for (key of getActiveConnectivityArray(); track key) {
<mat-chip (removed)="onConnectivityToggle(key)">
{{ key }}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip>
}
@for (key of getActiveUseCasesArray(); track key) {
<mat-chip (removed)="onUseCaseToggle(key)">
{{ getUseCaseLabel(key) }}
@ -201,6 +248,18 @@
<mat-icon matChipRemove>close</mat-icon>
</mat-chip>
}
@for (key of getActiveHardwareTypesArray(); track key) {
<mat-chip (removed)="onHardwareTypeToggle(key)">
{{ key }}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip>
}
@for (key of getActiveConnectivityArray(); track key) {
<mat-chip (removed)="onConnectivityToggle(key)">
{{ key }}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip>
}
@for (key of getActiveUseCasesArray(); track key) {
<mat-chip (removed)="onUseCaseToggle(key)">
{{ getUseCaseLabel(key) }}

13
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

71
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<string>();
activeWidgetTypes = new Set<string>();
activeRuleChainTypes = new Set<string>();
activeConnectivity = new Set<string>();
activeHardwareTypes = new Set<string>();
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<string, string>();
useCases: Map<string, string> = useCaseTranslations as Map<string, string>;
useCaseGroups: UseCaseGroup[] = useCaseGroups;
connectivityGroups: ConnectivityGroup[] = connectivityGroups;
hardwareTypes: string[] = hardwareTypes;
cfTypes: Map<string, string> = cfTypeTranslations;
widgetTypes: Map<string, string> = widgetTypeTranslations;
ruleChainTypes: Map<string, string> = 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<string, string> | 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,

82
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html

@ -238,31 +238,67 @@
}
</div>
<!-- Categories & Use Cases -->
@if (item.categories?.length || item.useCases?.length) {
<div class="dlg-divider"></div>
<div class="dlg-meta">
@if (item.categories?.length) {
<div class="dlg-meta-group">
<span class="dlg-meta-label">{{ 'iot-hub.category' | translate }}</span>
<div class="dlg-meta-chips">
@for (cat of item.categories; track cat) {
<span class="dlg-chip dlg-chip-category">{{ getCategoryLabel(cat) }}</span>
}
<!-- Categories & Use Cases / Device metadata -->
@if (item.type === ItemType.DEVICE) {
@if (item.dataDescriptor?.hardwareType || item.dataDescriptor?.connectivity?.length || item.useCases?.length) {
<div class="dlg-divider"></div>
<div class="dlg-meta">
@if (item.dataDescriptor?.hardwareType) {
<div class="dlg-meta-group">
<span class="dlg-meta-label">{{ 'iot-hub.hardware-type' | translate }}</span>
<div class="dlg-meta-chips">
<span class="dlg-chip dlg-chip-category">{{ item.dataDescriptor.hardwareType }}</span>
</div>
</div>
</div>
}
@if (item.useCases?.length) {
<div class="dlg-meta-group">
<span class="dlg-meta-label">{{ 'iot-hub.use-cases' | translate }}</span>
<div class="dlg-meta-chips">
@for (uc of item.useCases; track uc) {
<span class="dlg-chip dlg-chip-usecase">{{ getUseCaseLabel(uc) }}</span>
}
}
@if (item.dataDescriptor?.connectivity?.length) {
<div class="dlg-meta-group">
<span class="dlg-meta-label">{{ 'iot-hub.connectivity' | translate }}</span>
<div class="dlg-meta-chips">
@for (conn of item.dataDescriptor.connectivity; track conn) {
<span class="dlg-chip dlg-chip-category">{{ conn }}</span>
}
</div>
</div>
</div>
}
</div>
}
@if (item.useCases?.length) {
<div class="dlg-meta-group">
<span class="dlg-meta-label">{{ 'iot-hub.use-cases' | translate }}</span>
<div class="dlg-meta-chips">
@for (uc of item.useCases; track uc) {
<span class="dlg-chip dlg-chip-usecase">{{ uc }}</span>
}
</div>
</div>
}
</div>
}
} @else {
@if (item.categories?.length || item.useCases?.length) {
<div class="dlg-divider"></div>
<div class="dlg-meta">
@if (item.categories?.length) {
<div class="dlg-meta-group">
<span class="dlg-meta-label">{{ 'iot-hub.category' | translate }}</span>
<div class="dlg-meta-chips">
@for (cat of item.categories; track cat) {
<span class="dlg-chip dlg-chip-category">{{ getCategoryLabel(cat) }}</span>
}
</div>
</div>
}
@if (item.useCases?.length) {
<div class="dlg-meta-group">
<span class="dlg-meta-label">{{ 'iot-hub.use-cases' | translate }}</span>
<div class="dlg-meta-chips">
@for (uc of item.useCases; track uc) {
<span class="dlg-chip dlg-chip-usecase">{{ getUseCaseLabel(uc) }}</span>
}
</div>
</div>
}
</div>
}
}
<!-- Rule Chain Nodes -->

33
ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts

@ -180,6 +180,39 @@ export const useCaseTranslations = new Map<UseCase, string>([
[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<string, string> {
switch (type) {
case ItemType.WIDGET:

14
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;
}
}

2
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",

Loading…
Cancel
Save