Browse Source

Use dynamic filter info API for IoT Hub browse filters

Replace hardcoded category/use-case/subtype enums and translation maps
with dynamic FilterParamInfo data from /api/item-listing/filterInfo/{itemType}.
Add inline search input for filter panels with >= 10 items, "Most popular"
grouping for use cases and vendors with >= 11 items, scrollable filter
items container with hover-visible scrollbar, and multi-vendor support
in MpItemVersionQuery. Remove redundant enums, maps, and locale translations.
Update dashboard widget select filter panel to use dynamic data.
pull/15508/head
Igor Kulikov 6 months ago
parent
commit
4ecfa03935
  1. 7
      ui-ngx/src/app/core/http/iot-hub-api.service.ts
  2. 24
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html
  3. 26
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts
  4. 259
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html
  5. 154
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss
  6. 127
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts
  7. 11
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts
  8. 4
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html
  9. 16
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts
  10. 202
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts
  11. 6
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts
  12. 47
      ui-ngx/src/assets/locale/locale.constant-en_US.json

7
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 { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import { CreatorView } from '@shared/models/iot-hub/iot-hub-creator.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 { 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 { InterceptorHttpParams } from '@core/interceptors/interceptor-http-params';
import { InterceptorConfig } from '@core/interceptors/interceptor-config'; import { InterceptorConfig } from '@core/interceptors/interceptor-config';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -84,9 +85,9 @@ export class IotHubApiService {
); );
} }
public getPublishedDeviceVendors(config?: IotHubRequestConfig): Observable<string[]> { public getFilterInfo(itemType: ItemType, config?: IotHubRequestConfig): Observable<ItemTypeFilterInfo> {
return this.http.get<string[]>( return this.http.get<ItemTypeFilterInfo>(
`${this.baseUrl}/api/versions/published/vendors`, `${this.baseUrl}/api/item-listing/filterInfo/${itemType}`,
{ params: this.buildParams(config) } { params: this.buildParams(config) }
); );
} }

24
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html

@ -208,10 +208,10 @@
<span *ngIf="iotHubPendingWidgetTypes.size" class="tb-iot-hub-filter-badge">{{ iotHubPendingWidgetTypes.size }}</span> <span *ngIf="iotHubPendingWidgetTypes.size" class="tb-iot-hub-filter-badge">{{ iotHubPendingWidgetTypes.size }}</span>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div *ngFor="let entry of iotHubWidgetTypesMap | keyvalue" class="tb-iot-hub-filter-option"> <div *ngFor="let opt of iotHubWidgetTypeOptions" class="tb-iot-hub-filter-option">
<mat-checkbox [checked]="iotHubPendingWidgetTypes.has(entry.key)" <mat-checkbox [checked]="iotHubPendingWidgetTypes.has(opt.key)"
(change)="toggleIotHubWidgetType(entry.key)"> (change)="toggleIotHubWidgetType(opt.key)">
{{ entry.value | translate }} {{ getIotHubWidgetTypeLabel(opt.key) }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
@ -223,10 +223,10 @@
<span *ngIf="iotHubPendingCategories.size" class="tb-iot-hub-filter-badge">{{ iotHubPendingCategories.size }}</span> <span *ngIf="iotHubPendingCategories.size" class="tb-iot-hub-filter-badge">{{ iotHubPendingCategories.size }}</span>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div *ngFor="let entry of iotHubCategoriesMap | keyvalue" class="tb-iot-hub-filter-option"> <div *ngFor="let opt of iotHubCategoryOptions" class="tb-iot-hub-filter-option">
<mat-checkbox [checked]="iotHubPendingCategories.has(entry.key)" <mat-checkbox [checked]="iotHubPendingCategories.has(opt.key)"
(change)="toggleIotHubCategory(entry.key)"> (change)="toggleIotHubCategory(opt.key)">
{{ entry.value | translate }} {{ opt.key }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
@ -238,10 +238,10 @@
<span *ngIf="iotHubPendingUseCases.size" class="tb-iot-hub-filter-badge">{{ iotHubPendingUseCases.size }}</span> <span *ngIf="iotHubPendingUseCases.size" class="tb-iot-hub-filter-badge">{{ iotHubPendingUseCases.size }}</span>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div *ngFor="let entry of iotHubUseCasesMap | keyvalue" class="tb-iot-hub-filter-option"> <div *ngFor="let opt of iotHubUseCaseOptions" class="tb-iot-hub-filter-option">
<mat-checkbox [checked]="iotHubPendingUseCases.has(entry.key)" <mat-checkbox [checked]="iotHubPendingUseCases.has(opt.key)"
(change)="toggleIotHubUseCase(entry.key)"> (change)="toggleIotHubUseCase(opt.key)">
{{ entry.value | translate }} {{ opt.key }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>

26
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 { ItemSizeStrategy } from '@shared/components/grid/scroll-grid.component';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MpItemVersionQuery, MpItemVersionView, widgetTypeTranslations as iotHubWidgetTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models'; import { TranslateService } from '@ngx-translate/core';
import { ItemType, getCategoriesForType, useCaseTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; 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 { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { import {
@ -117,6 +118,7 @@ export class DashboardWidgetSelectComponent implements OnInit {
this.selectWidgetMode$.next(mode); this.selectWidgetMode$.next(mode);
if (mode === 'iotHub') { if (mode === 'iotHub') {
this.loadInstalledWidgets(); this.loadInstalledWidgets();
this.loadIotHubFilterInfo();
} else { } else {
this.iotHubInstalledMode = 'all'; this.iotHubInstalledMode = 'all';
this.installedWidgetVersions = null; this.installedWidgetVersions = null;
@ -207,13 +209,14 @@ export class DashboardWidgetSelectComponent implements OnInit {
iotHubPendingUseCases = new Set<string>(); iotHubPendingUseCases = new Set<string>();
iotHubFilterDirty = false; iotHubFilterDirty = false;
iotHubWidgetTypesMap: Map<string, string> = iotHubWidgetTypeTranslations; iotHubWidgetTypeOptions: FilterParamInfo[] = [];
iotHubCategoriesMap: Map<string, string> = getCategoriesForType(ItemType.WIDGET); iotHubCategoryOptions: FilterParamInfo[] = [];
iotHubUseCasesMap: Map<string, string> = useCaseTranslations as Map<string, string>; iotHubUseCaseOptions: FilterParamInfo[] = [];
iotHubFilterCount = 0; iotHubFilterCount = 0;
constructor(private widgetsService: WidgetService, constructor(private widgetsService: WidgetService,
private iotHubApiService: IotHubApiService, private iotHubApiService: IotHubApiService,
private translate: TranslateService,
private dialog: MatDialog, private dialog: MatDialog,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef) {
@ -398,6 +401,11 @@ export class DashboardWidgetSelectComponent implements OnInit {
this.iotHubFilterDirty = false; this.iotHubFilterDirty = false;
} }
getIotHubWidgetTypeLabel(key: string): string {
const translationKey = widgetTypeTranslations.get(key);
return translationKey ? this.translate.instant(translationKey) : key;
}
toggleIotHubWidgetType(key: string): void { toggleIotHubWidgetType(key: string): void {
if (this.iotHubPendingWidgetTypes.has(key)) { if (this.iotHubPendingWidgetTypes.has(key)) {
this.iotHubPendingWidgetTypes.delete(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 { private loadInstalledWidgets(): void {
if (this.installedWidgets === null) { if (this.installedWidgets === null) {
this.installedWidgets = []; this.installedWidgets = [];

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

@ -54,95 +54,216 @@
<!-- Filter panel template (reused in sidebar and drawer) --> <!-- Filter panel template (reused in sidebar and drawer) -->
<ng-template #filterPanelContent> <ng-template #filterPanelContent>
<div class="tb-iot-hub-filters-inner"> <div class="tb-iot-hub-filters-inner">
@if (!fixedSubType && getSubtypeMap(); as subtypeMap) { @if (!fixedSubType && subtypeOptions.length > 0) {
<mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel"> <mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title>{{ 'iot-hub.type-filter-label' | translate }}</mat-panel-title> <mat-panel-title>{{ 'iot-hub.type-filter-label' | translate }}</mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
@for (entry of subtypeMap | keyvalue; track entry.key) { @if (subtypeOptions.length >= 10) {
<mat-checkbox [checked]="isSubtypeActive(entry.key)" <div class="tb-iot-hub-filter-search">
(change)="onSubtypeToggle(entry.key)"> <mat-icon>search</mat-icon>
{{ entry.value | translate }} <input [placeholder]="'iot-hub.search' | translate"
</mat-checkbox> [ngModel]="filterSearch['subtype']"
(ngModelChange)="filterSearch['subtype'] = $event">
@if (filterSearch['subtype']) {
<button mat-icon-button class="tb-iot-hub-filter-search-clear" (click)="filterSearch['subtype'] = ''">
<mat-icon>close</mat-icon>
</button>
}
</div>
} }
<div class="tb-iot-hub-filter-items" [class.scrollable]="getFilteredItems(subtypeOptions, 'subtype').length > 8"
(mouseenter)="getFilteredItems(subtypeOptions, 'subtype').length > 8 && (filterItemsHovered = true)"
(mouseleave)="filterItemsHovered = false">
@for (opt of getFilteredItems(subtypeOptions, 'subtype'); track opt.key) {
<mat-checkbox [checked]="isSubtypeActive(opt.key)"
(change)="onSubtypeToggle(opt.key)">
{{ getSubtypeLabel(opt.key) }}
</mat-checkbox>
}
</div>
</mat-expansion-panel> </mat-expansion-panel>
} }
@if (activeType === ItemType.DEVICE) { @if (activeType === ItemType.DEVICE) {
@if (!fixedSubType && getSubtypeMap()) { @if (!fixedSubType && subtypeOptions.length > 0) {
<div class="tb-iot-hub-filter-divider"></div> <div class="tb-iot-hub-filter-divider"></div>
} }
<mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel"> @if (vendorOptions.length > 0) {
<mat-expansion-panel-header> <mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel">
<mat-panel-title>{{ 'iot-hub.vendor' | translate }}</mat-panel-title> <mat-expansion-panel-header>
</mat-expansion-panel-header> <mat-panel-title>{{ 'iot-hub.vendor' | translate }}</mat-panel-title>
@for (v of availableVendors; track v) { </mat-expansion-panel-header>
<mat-checkbox [checked]="isVendorActive(v)" @if (vendorOptions.length >= 10) {
(change)="onVendorToggle(v)"> <div class="tb-iot-hub-filter-search">
{{ v }} <mat-icon>search</mat-icon>
</mat-checkbox> <input [placeholder]="'iot-hub.search' | translate"
} [ngModel]="filterSearch['vendor']"
</mat-expansion-panel> (ngModelChange)="filterSearch['vendor'] = $event">
<div class="tb-iot-hub-filter-divider"></div> @if (filterSearch['vendor']) {
<mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel"> <button mat-icon-button class="tb-iot-hub-filter-search-clear" (click)="filterSearch['vendor'] = ''">
<mat-expansion-panel-header> <mat-icon>close</mat-icon>
<mat-panel-title>{{ 'iot-hub.hardware-type' | translate }}</mat-panel-title> </button>
</mat-expansion-panel-header> }
@for (ht of hardwareTypes; track ht) { </div>
<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>
} }
} <div class="tb-iot-hub-filter-items" [class.scrollable]="getFilteredItems(vendorOptions, 'vendor').length > 8"
</mat-expansion-panel> (mouseenter)="getFilteredItems(vendorOptions, 'vendor').length > 8 && (filterItemsHovered = true)"
(mouseleave)="filterItemsHovered = false">
@for (group of getGroupedFilterItems(vendorOptions, 'vendor'); track group.label) {
@if (group.label) {
<div class="tb-iot-hub-filter-group-label">{{ group.label | translate }}</div>
}
@for (v of group.items; track v.key) {
<mat-checkbox [checked]="isVendorActive(v.key)"
(change)="onVendorToggle(v.key)">
{{ v.key }}
</mat-checkbox>
}
}
</div>
</mat-expansion-panel>
<div class="tb-iot-hub-filter-divider"></div>
}
@if (hardwareTypeOptions.length > 0) {
<mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel">
<mat-expansion-panel-header>
<mat-panel-title>{{ 'iot-hub.hardware-type' | translate }}</mat-panel-title>
</mat-expansion-panel-header>
@if (hardwareTypeOptions.length >= 10) {
<div class="tb-iot-hub-filter-search">
<mat-icon>search</mat-icon>
<input [placeholder]="'iot-hub.search' | translate"
[ngModel]="filterSearch['hardwareType']"
(ngModelChange)="filterSearch['hardwareType'] = $event">
@if (filterSearch['hardwareType']) {
<button mat-icon-button class="tb-iot-hub-filter-search-clear" (click)="filterSearch['hardwareType'] = ''">
<mat-icon>close</mat-icon>
</button>
}
</div>
}
<div class="tb-iot-hub-filter-items" [class.scrollable]="getFilteredItems(hardwareTypeOptions, 'hardwareType').length > 8"
(mouseenter)="getFilteredItems(hardwareTypeOptions, 'hardwareType').length > 8 && (filterItemsHovered = true)"
(mouseleave)="filterItemsHovered = false">
@for (ht of getFilteredItems(hardwareTypeOptions, 'hardwareType'); track ht.key) {
<mat-checkbox [checked]="isHardwareTypeActive(ht.key)"
(change)="onHardwareTypeToggle(ht.key)">
{{ ht.key }}
</mat-checkbox>
}
</div>
</mat-expansion-panel>
<div class="tb-iot-hub-filter-divider"></div>
}
@if (connectivityGroups.length > 0) {
<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>
@if (allConnectivityCount >= 10) {
<div class="tb-iot-hub-filter-search">
<mat-icon>search</mat-icon>
<input [placeholder]="'iot-hub.search' | translate"
[ngModel]="filterSearch['connectivity']"
(ngModelChange)="filterSearch['connectivity'] = $event">
@if (filterSearch['connectivity']) {
<button mat-icon-button class="tb-iot-hub-filter-search-clear" (click)="filterSearch['connectivity'] = ''">
<mat-icon>close</mat-icon>
</button>
}
</div>
}
<div class="tb-iot-hub-filter-items" [class.scrollable]="allConnectivityCount > 8"
(mouseenter)="allConnectivityCount > 8 && (filterItemsHovered = true)"
(mouseleave)="filterItemsHovered = false">
@for (group of getFilteredConnectivityGroups('connectivity'); track group.group) {
<div class="tb-iot-hub-filter-group-label">{{ group.group }}</div>
@for (val of group.values; track val.key) {
<mat-checkbox [checked]="isConnectivityActive(val.key)"
(change)="onConnectivityToggle(val.key)">
{{ val.key }}
</mat-checkbox>
}
}
</div>
</mat-expansion-panel>
}
} @else { } @else {
@if (categories.size > 0) { @if (categoryOptions.length > 0) {
@if (!fixedSubType && getSubtypeMap()) { @if (!fixedSubType && subtypeOptions.length > 0) {
<div class="tb-iot-hub-filter-divider"></div> <div class="tb-iot-hub-filter-divider"></div>
} }
<mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel"> <mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title>{{ 'iot-hub.category' | translate }}</mat-panel-title> <mat-panel-title>{{ 'iot-hub.category' | translate }}</mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
@for (entry of categories | keyvalue; track entry.key) { @if (categoryOptions.length >= 10) {
<mat-checkbox [checked]="isCategoryActive(entry.key)" <div class="tb-iot-hub-filter-search">
(change)="onCategoryToggle(entry.key)"> <mat-icon>search</mat-icon>
{{ entry.value | translate }} <input [placeholder]="'iot-hub.search' | translate"
</mat-checkbox> [ngModel]="filterSearch['category']"
(ngModelChange)="filterSearch['category'] = $event">
@if (filterSearch['category']) {
<button mat-icon-button class="tb-iot-hub-filter-search-clear" (click)="filterSearch['category'] = ''">
<mat-icon>close</mat-icon>
</button>
}
</div>
} }
<div class="tb-iot-hub-filter-items" [class.scrollable]="getFilteredItems(categoryOptions, 'category').length > 8"
(mouseenter)="getFilteredItems(categoryOptions, 'category').length > 8 && (filterItemsHovered = true)"
(mouseleave)="filterItemsHovered = false">
@for (cat of getFilteredItems(categoryOptions, 'category'); track cat.key) {
<mat-checkbox [checked]="isCategoryActive(cat.key)"
(change)="onCategoryToggle(cat.key)">
{{ cat.key }}
</mat-checkbox>
}
</div>
</mat-expansion-panel> </mat-expansion-panel>
} }
} }
@if (activeType === ItemType.DEVICE || (!fixedSubType && getSubtypeMap()) || categories.size > 0) { @if (useCaseOptions.length > 0) {
@if (activeType === ItemType.DEVICE || (!fixedSubType && subtypeOptions.length > 0) || categoryOptions.length > 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>
@if (useCaseOptions.length >= 10) {
<div class="tb-iot-hub-filter-search">
<mat-icon>search</mat-icon>
<input [placeholder]="'iot-hub.search' | translate"
[ngModel]="filterSearch['useCase']"
(ngModelChange)="filterSearch['useCase'] = $event">
@if (filterSearch['useCase']) {
<button mat-icon-button class="tb-iot-hub-filter-search-clear" (click)="filterSearch['useCase'] = ''">
<mat-icon>close</mat-icon>
</button>
}
</div>
}
<div class="tb-iot-hub-filter-items" [class.scrollable]="getFilteredItems(useCaseOptions, 'useCase').length > 8"
(mouseenter)="getFilteredItems(useCaseOptions, 'useCase').length > 8 && (filterItemsHovered = true)"
(mouseleave)="filterItemsHovered = false">
@for (group of getGroupedFilterItems(useCaseOptions, 'useCase'); track group.label) {
@if (group.label) {
<div class="tb-iot-hub-filter-group-label">{{ group.label | translate }}</div>
}
@for (uc of group.items; track uc.key) {
<mat-checkbox [checked]="isUseCaseActive(uc.key)"
(change)="onUseCaseToggle(uc.key)">
{{ uc.key }}
</mat-checkbox>
}
}
</div>
</mat-expansion-panel>
<div class="tb-iot-hub-filter-divider"></div> <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 (uc of useCaseValues; track uc) {
<mat-checkbox [checked]="isUseCaseActive(uc)"
(change)="onUseCaseToggle(uc)">
{{ uc }}
</mat-checkbox>
}
</mat-expansion-panel>
</div> </div>
</ng-template> </ng-template>
@ -169,7 +290,7 @@
} }
@for (key of getActiveCategoriesArray(); track key) { @for (key of getActiveCategoriesArray(); track key) {
<mat-chip (removed)="onCategoryToggle(key)"> <mat-chip (removed)="onCategoryToggle(key)">
{{ getCategoryLabel(key) }} {{ key }}
<mat-icon matChipRemove>close</mat-icon> <mat-icon matChipRemove>close</mat-icon>
</mat-chip> </mat-chip>
} }
@ -193,7 +314,7 @@
} }
@for (key of getActiveUseCasesArray(); track key) { @for (key of getActiveUseCasesArray(); track key) {
<mat-chip (removed)="onUseCaseToggle(key)"> <mat-chip (removed)="onUseCaseToggle(key)">
{{ getUseCaseLabel(key) }} {{ key }}
<mat-icon matChipRemove>close</mat-icon> <mat-icon matChipRemove>close</mat-icon>
</mat-chip> </mat-chip>
} }
@ -204,7 +325,7 @@
</div> </div>
} }
<div class="tb-iot-hub-filter-divider"></div> <div class="tb-iot-hub-filter-divider"></div>
<div class="tb-iot-hub-filter-drawer-body"> <div class="tb-iot-hub-filter-drawer-body" [class.filter-items-hovered]="filterItemsHovered">
<ng-container *ngTemplateOutlet="filterPanelContent"></ng-container> <ng-container *ngTemplateOutlet="filterPanelContent"></ng-container>
</div> </div>
</mat-drawer> </mat-drawer>
@ -212,7 +333,7 @@
<mat-drawer-content class="tb-iot-hub-drawer-content"> <mat-drawer-content class="tb-iot-hub-drawer-content">
<!-- Left sidebar filters (visible on large screens) --> <!-- Left sidebar filters (visible on large screens) -->
<div class="tb-iot-hub-filters tb-iot-hub-filters-desktop"> <div class="tb-iot-hub-filters tb-iot-hub-filters-desktop">
<div class="tb-iot-hub-filters-wrapper"> <div class="tb-iot-hub-filters-wrapper" [class.filter-items-hovered]="filterItemsHovered">
<ng-container *ngTemplateOutlet="filterPanelContent"></ng-container> <ng-container *ngTemplateOutlet="filterPanelContent"></ng-container>
</div> </div>
</div> </div>
@ -259,7 +380,7 @@
} }
@for (key of getActiveCategoriesArray(); track key) { @for (key of getActiveCategoriesArray(); track key) {
<mat-chip (removed)="onCategoryToggle(key)"> <mat-chip (removed)="onCategoryToggle(key)">
{{ getCategoryLabel(key) }} {{ key }}
<mat-icon matChipRemove>close</mat-icon> <mat-icon matChipRemove>close</mat-icon>
</mat-chip> </mat-chip>
} }
@ -283,7 +404,7 @@
} }
@for (key of getActiveUseCasesArray(); track key) { @for (key of getActiveUseCasesArray(); track key) {
<mat-chip (removed)="onUseCaseToggle(key)"> <mat-chip (removed)="onUseCaseToggle(key)">
{{ getUseCaseLabel(key) }} {{ key }}
<mat-icon matChipRemove>close</mat-icon> <mat-icon matChipRemove>close</mat-icon>
</mat-chip> </mat-chip>
} }

154
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss

@ -50,6 +50,7 @@
.tb-iot-hub-content { .tb-iot-hub-content {
display: flex; display: flex;
flex: 1; flex: 1;
height: 100%;
background: transparent; background: transparent;
overflow: visible; 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 { .tb-iot-hub-filters-wrapper {
position: absolute; position: absolute;
inset: 0; inset: 0;
@ -141,15 +172,25 @@
padding: 8px 0 0; padding: 8px 0 0;
.tb-iot-hub-filters-inner { .tb-iot-hub-filters-inner {
padding: 12px 0; padding: 0;
} }
.tb-iot-hub-filter-panel.mat-expansion-panel .mat-expansion-panel-header { .tb-iot-hub-filter-panel.mat-expansion-panel {
padding-left: 24px; .mat-expansion-panel-header {
} padding-left: 24px;
}
.tb-iot-hub-filter-panel.mat-expansion-panel mat-checkbox { mat-checkbox {
padding-left: 12px; 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 { ::ng-deep .mat-expansion-panel-body {
padding: 0; padding: 0 8px 0 0;
} }
mat-checkbox { mat-checkbox {
display: block; display: block;
padding: 0 0 0 44px; overflow: hidden;
&:hover { &:hover {
background: rgba(0, 0, 0, 0.04); background: rgba(0, 0, 0, 0.04);
@ -202,17 +243,98 @@
margin-top: 4px; 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 { .tb-iot-hub-filter-group-label {
font-size: 12px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: rgba(0, 0, 0, 0.54); color: rgba(0, 0, 0, 0.54);
text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
padding: 12px 0 4px 44px; padding: 12px 0 4px 12px;
&:first-child {
padding-top: 4px;
}
} }
} }
@ -235,7 +357,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 16px 0 0; padding: 24px 0 0;
flex-shrink: 0; flex-shrink: 0;
} }

127
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 { Direction, SortOrder } from '@shared/models/page/sort-order';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import { import { ItemType, FilterParamInfo } from '@shared/models/iot-hub/iot-hub-item.models';
ItemType, import { widgetTypeTranslations, cfTypeTranslations, ruleChainTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models';
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 { IotHubInstalledItem, DeviceInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { IotHubInstalledItem, DeviceInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { TbDeviceInstallDialogComponent, DeviceInstallDialogData } from './device-install-dialog/device-install-dialog.component'; 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), // When set after init (e.g., parent resolves config in its ngOnInit),
// trigger type-specific loading since our ngOnInit already ran // trigger type-specific loading since our ngOnInit already ran
if (wasInit) { if (wasInit) {
this.updateCategories(); this.loadFilterInfo();
if (value === ItemType.DEVICE) { if (value === ItemType.DEVICE) {
this.loadVendors();
this.loadInstalledDevices(); this.loadInstalledDevices();
} else if (value === ItemType.WIDGET) { } else if (value === ItemType.WIDGET) {
this.loadInstalledWidgets(); this.loadInstalledWidgets();
@ -113,7 +108,6 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
activeConnectivity = new Set<string>(); activeConnectivity = new Set<string>();
activeHardwareTypes = new Set<string>(); activeHardwareTypes = new Set<string>();
activeVendors = new Set<string>(); activeVendors = new Set<string>();
availableVendors: string[] = [];
sortOptions: SortOption[] = [ sortOptions: SortOption[] = [
{ value: 'totalInstallCount', label: 'iot-hub.sort-most-installed', direction: Direction.DESC }, { value: 'totalInstallCount', label: 'iot-hub.sort-most-installed', direction: Direction.DESC },
@ -122,14 +116,15 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
]; ];
selectedSortIndex = 0; selectedSortIndex = 0;
categories = new Map<string, string>(); subtypeOptions: FilterParamInfo[] = [];
useCases: Map<string, string> = useCaseTranslations as Map<string, string>; categoryOptions: FilterParamInfo[] = [];
useCaseValues: string[] = useCaseValues; useCaseOptions: FilterParamInfo[] = [];
connectivityGroups: ConnectivityGroup[] = connectivityGroups; vendorOptions: FilterParamInfo[] = [];
hardwareTypes: string[] = hardwareTypes; hardwareTypeOptions: FilterParamInfo[] = [];
cfTypes: Map<string, string> = cfTypeTranslations; connectivityGroups: { group: string; values: FilterParamInfo[] }[] = [];
widgetTypes: Map<string, string> = widgetTypeTranslations;
ruleChainTypes: Map<string, string> = ruleChainTypeTranslations; filterSearch: Record<string, string> = {};
filterItemsHovered = false;
installedWidgets: IotHubInstalledItem[] = null; installedWidgets: IotHubInstalledItem[] = null;
installedSolutionTemplates: IotHubInstalledItem[] = null; installedSolutionTemplates: IotHubInstalledItem[] = null;
@ -165,13 +160,12 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
subtypes.add(this.fixedSubType); subtypes.add(this.fixedSubType);
} }
} }
this.updateCategories(); this.loadFilterInfo();
if (this.activeType === ItemType.WIDGET) { if (this.activeType === ItemType.WIDGET) {
this.loadInstalledWidgets(); this.loadInstalledWidgets();
} else if (this.activeType === ItemType.SOLUTION_TEMPLATE) { } else if (this.activeType === ItemType.SOLUTION_TEMPLATE) {
this.loadInstalledSolutionTemplates(); this.loadInstalledSolutionTemplates();
} else if (this.activeType === ItemType.DEVICE) { } else if (this.activeType === ItemType.DEVICE) {
this.loadVendors();
this.loadInstalledDevices(); this.loadInstalledDevices();
} }
this.loadItems(); this.loadItems();
@ -205,14 +199,14 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
this.activeConnectivity.clear(); this.activeConnectivity.clear();
this.activeHardwareTypes.clear(); this.activeHardwareTypes.clear();
this.activeVendors.clear(); this.activeVendors.clear();
this.updateCategories(); this.filterSearch = {};
this.loadFilterInfo();
this.pageIndex = 0; this.pageIndex = 0;
if (type === ItemType.WIDGET) { if (type === ItemType.WIDGET) {
this.loadInstalledWidgets(); this.loadInstalledWidgets();
} else if (type === ItemType.SOLUTION_TEMPLATE) { } else if (type === ItemType.SOLUTION_TEMPLATE) {
this.loadInstalledSolutionTemplates(); this.loadInstalledSolutionTemplates();
} else if (type === ItemType.DEVICE) { } else if (type === ItemType.DEVICE) {
this.loadVendors();
this.loadInstalledDevices(); this.loadInstalledDevices();
} }
this.loadItems(); this.loadItems();
@ -372,15 +366,6 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
return Array.from(this.activeHardwareTypes); return Array.from(this.activeHardwareTypes);
} }
getSubtypeMap(): Map<string, string> | 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<string> { getActiveSubtypes(): Set<string> {
switch (this.activeType) { switch (this.activeType) {
case ItemType.WIDGET: return this.activeWidgetTypes; case ItemType.WIDGET: return this.activeWidgetTypes;
@ -444,19 +429,52 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
this.loadItems(); this.loadItems();
} }
getSubtypeLabel(key: string): string { getFilteredItems(items: FilterParamInfo[], searchKey: string): FilterParamInfo[] {
const map = this.getSubtypeMap(); const search = (this.filterSearch[searchKey] || '').toLowerCase();
const translationKey = map?.get(key); if (!search) { return items; }
return translationKey ? this.translate.instant(translationKey) : key; return items.filter(item => item.key.toLowerCase().includes(search));
} }
getCategoryLabel(key: string): string { getGroupedFilterItems(items: FilterParamInfo[], searchKey: string): { label: string; items: FilterParamInfo[] }[] {
const translationKey = this.categories.get(key); const filtered = this.getFilteredItems(items, searchKey);
return translationKey ? this.translate.instant(translationKey) : key; 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 { getFilteredConnectivityGroups(searchKey: string): { group: string; values: FilterParamInfo[] }[] {
const translationKey = this.useCases.get(key); 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; return translationKey ? this.translate.instant(translationKey) : key;
} }
@ -473,7 +491,8 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
this.getActiveSubtypes()?.add(this.fixedSubType); this.getActiveSubtypes()?.add(this.fixedSubType);
} }
this.textSearch = ''; this.textSearch = '';
this.updateCategories(); this.filterSearch = {};
this.loadFilterInfo();
this.pageIndex = 0; this.pageIndex = 0;
this.loadItems(); 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 { private loadInstalledDevices(): void {
const pageLink = new PageLink(10000, 0); const pageLink = new PageLink(10000, 0);
this.iotHubApiService.getInstalledItems(pageLink, 'DEVICE', {ignoreLoading: true, ignoreErrors: true}).subscribe({ this.iotHubApiService.getInstalledItems(pageLink, 'DEVICE', {ignoreLoading: true, ignoreErrors: true}).subscribe({
@ -672,8 +683,22 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
} }
} }
private updateCategories(): void { private loadFilterInfo(): void {
this.categories = getCategoriesForType(this.activeType); 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 { loadItems(): void {
@ -695,7 +720,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
undefined, undefined,
this.activeHardwareTypes.size > 0 ? Array.from(this.activeHardwareTypes) : undefined, this.activeHardwareTypes.size > 0 ? Array.from(this.activeHardwareTypes) : undefined,
this.activeConnectivity.size > 0 ? Array.from(this.activeConnectivity) : 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( this.iotHubApiService.getPublishedVersions(
query, query,

11
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 { 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 { 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 { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { IotHubApiService } from '@core/http/iot-hub-api.service';
@ -148,19 +148,14 @@ export class TbIotHubItemCardComponent {
if (!this.item.categories?.length) { if (!this.item.categories?.length) {
return []; return [];
} }
const categoryMap = getCategoriesForType(this.item.type); return this.item.categories;
return this.item.categories.map(c => {
const key = categoryMap.get(c);
return key ? this.translate.instant(key) : c;
});
} }
getFirstUseCaseLabel(): string | null { getFirstUseCaseLabel(): string | null {
if (!this.item.useCases?.length) { if (!this.item.useCases?.length) {
return null; return null;
} }
const key = useCaseTranslations.get(this.item.useCases[0] as any); return this.item.useCases[0];
return key ? this.translate.instant(key) : this.item.useCases[0];
} }
getNodes(): NodeInfo[] { getNodes(): NodeInfo[] {

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

@ -294,7 +294,7 @@
<span class="dlg-meta-label">{{ 'iot-hub.category' | translate }}</span> <span class="dlg-meta-label">{{ 'iot-hub.category' | translate }}</span>
<div class="dlg-meta-chips"> <div class="dlg-meta-chips">
@for (cat of item.categories; track cat) { @for (cat of item.categories; track cat) {
<span class="dlg-chip dlg-chip-category">{{ getCategoryLabel(cat) }}</span> <span class="dlg-chip dlg-chip-category">{{ cat }}</span>
} }
</div> </div>
</div> </div>
@ -304,7 +304,7 @@
<span class="dlg-meta-label">{{ 'iot-hub.use-cases' | translate }}</span> <span class="dlg-meta-label">{{ 'iot-hub.use-cases' | translate }}</span>
<div class="dlg-meta-chips"> <div class="dlg-meta-chips">
@for (uc of item.useCases; track uc) { @for (uc of item.useCases; track uc) {
<span class="dlg-chip dlg-chip-usecase">{{ getUseCaseLabel(uc) }}</span> <span class="dlg-chip dlg-chip-usecase">{{ uc }}</span>
} }
</div> </div>
</div> </div>

16
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 { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component'; 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 { 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 { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -61,9 +61,6 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
carouselImages: string[] = []; carouselImages: string[] = [];
carouselIndex = 0; carouselIndex = 0;
private categoryMap: Map<string, string>;
private useCaseMap = useCaseTranslations;
constructor( constructor(
protected store: Store<AppState>, protected store: Store<AppState>,
protected router: Router, protected router: Router,
@ -78,21 +75,10 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
this.mode = data.mode || 'default'; this.mode = data.mode || 'default';
this.showCreator = data.showCreator !== false; this.showCreator = data.showCreator !== false;
this.installedItem = data.installedItem; this.installedItem = data.installedItem;
this.categoryMap = getCategoriesForType(this.item.type);
this.buildCarouselImages(); this.buildCarouselImages();
this.loadReadme(); this.loadReadme();
} }
getCategoryLabel(key: string): string {
const translationKey = this.categoryMap.get(key);
return translationKey ? this.translate.instant(translationKey) : key;
}
getUseCaseLabel(key: string): string {
const translationKey = this.useCaseMap.get(key as any);
return translationKey ? this.translate.instant(translationKey) : key;
}
isCompactLayout(): boolean { isCompactLayout(): boolean {
return this.item.type === ItemType.CALCULATED_FIELD || this.item.type === ItemType.RULE_CHAIN; return this.item.type === ItemType.CALCULATED_FIELD || this.item.type === ItemType.RULE_CHAIN;
} }

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

@ -34,193 +34,17 @@ export const itemTypeTranslations = new Map<ItemType, string>(
] ]
); );
export enum WidgetCategory { export interface FilterParamInfo {
CHARTS_GRAPHS = 'CHARTS_GRAPHS', key: string;
GAUGES_INDICATORS = 'GAUGES_INDICATORS', totalItems: number;
CONTROLS = 'CONTROLS', totalInstallCount: number;
MAPS_LOCATION = 'MAPS_LOCATION', }
TABLES_LISTS = 'TABLES_LISTS',
CARDS_INFO = 'CARDS_INFO', export interface ItemTypeFilterInfo {
SCADA = 'SCADA', types: FilterParamInfo[];
INPUT_FORMS = 'INPUT_FORMS' categories: FilterParamInfo[];
} useCases: FilterParamInfo[];
vendors: FilterParamInfo[];
export const widgetCategoryTranslations = new Map<WidgetCategory, string>([ hardwareTypes: FilterParamInfo[];
[WidgetCategory.CHARTS_GRAPHS, 'iot-hub.category.charts-graphs'], connectivities: Record<string, FilterParamInfo[]>;
[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, string>([
[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, string>([
[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, string>([
[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, string>([
[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, string>([
[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, string>([
[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<string, string> {
switch (type) {
case ItemType.WIDGET:
return widgetCategoryTranslations as Map<string, string>;
case ItemType.DASHBOARD:
return dashboardCategoryTranslations as Map<string, string>;
case ItemType.SOLUTION_TEMPLATE:
return solutionTemplateCategoryTranslations as Map<string, string>;
case ItemType.CALCULATED_FIELD:
return calcFieldCategoryTranslations as Map<string, string>;
case ItemType.RULE_CHAIN:
return ruleChainCategoryTranslations as Map<string, string>;
case ItemType.DEVICE:
return deviceCategoryTranslations as Map<string, string>;
default:
return new Map();
}
} }

6
ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts

@ -125,7 +125,7 @@ export class MpItemVersionQuery {
public tbVersion?: number, public tbVersion?: number,
public hardwareTypes?: string[], public hardwareTypes?: string[],
public connectivity?: string[], public connectivity?: string[],
public vendor?: string public vendors?: string[]
) {} ) {}
public toQuery(): string { public toQuery(): string {
@ -163,8 +163,8 @@ export class MpItemVersionQuery {
if (this.connectivity?.length) { if (this.connectivity?.length) {
query += this.connectivity.map(c => `&connectivity=${encodeURIComponent(c)}`).join(''); query += this.connectivity.map(c => `&connectivity=${encodeURIComponent(c)}`).join('');
} }
if (this.vendor) { if (this.vendors?.length) {
query += `&vendor=${encodeURIComponent(this.vendor)}`; query += this.vendors.map(v => `&vendors=${encodeURIComponent(v)}`).join('');
} }
return query; return query;
} }

47
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3827,51 +3827,8 @@
"update-error-title": "Update Failed", "update-error-title": "Update Failed",
"update-error-message": "Failed to update '{{name}}'.", "update-error-message": "Failed to update '{{name}}'.",
"update-error": "Failed to update {{name}}", "update-error": "Failed to update {{name}}",
"category.charts-graphs": "Charts & Graphs", "most-popular": "Most popular",
"category.gauges-indicators": "Gauges & Indicators", "all": "All",
"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",
"installed-items": "Installed Items", "installed-items": "Installed Items",
"search-installed-items": "Search installed items...", "search-installed-items": "Search installed items...",
"item-name": "Name", "item-name": "Name",

Loading…
Cancel
Save