Browse Source

Merge branch 'feature/iot-hub' into feature/lts-4.3-iot-hub

Resolves descriptor unwrapping conflicts in iot-hub-installed-items-table
and iot-hub-item-detail-dialog: ALARM_RULE follows the lts-4.3 pattern of
navigating to the calculated-field entity itself (calculatedFieldId +
EntityType.CALCULATED_FIELD), matching CALCULATED_FIELD.
pull/15548/head
Andrii Shvaika 6 months ago
parent
commit
b7b8d572ac
  1. 2
      application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java
  2. 28
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/AlarmRuleInstalledItemDescriptor.java
  3. 1
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java
  4. 13
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts
  5. 2
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html
  6. 31
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts
  7. 2
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts
  8. 4
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts
  9. 9
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts
  10. 13
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts
  11. 1
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-deep-link.utils.ts
  12. 56
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html
  13. 4
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss
  14. 70
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts
  15. 7
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts
  16. 10
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts
  17. 7
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts
  18. 17
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts
  19. 9
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java

@ -114,6 +114,8 @@ public class DefaultIotHubService implements IotHubService {
case "WIDGET" -> installWidget(user, tenantId, fileData);
case "DASHBOARD" -> installDashboard(user, tenantId, fileData);
case "CALCULATED_FIELD" -> installCalculatedField(user, tenantId, fileData, data);
case "ALARM_RULE" -> throw new IllegalArgumentException(
"Alarm Rules require ThingsBoard 4.3 or later. Please update your platform instance to install Alarm Rule packages.");
case "RULE_CHAIN" -> installRuleChain(tenantId, fileData);
case "DEVICE" -> installDeviceProfile(user, tenantId, fileData);
case "SOLUTION_TEMPLATE" -> installSolution(user, tenantId, fileData, request);

28
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/AlarmRuleInstalledItemDescriptor.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.iot_hub;
import lombok.Data;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
@Data
public class AlarmRuleInstalledItemDescriptor implements IotHubInstalledItemDescriptor {
private CalculatedFieldId calculatedFieldId;
private EntityId entityId;
}

1
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java

@ -30,6 +30,7 @@ import java.io.Serializable;
@Type(name = "WIDGET", value = WidgetInstalledItemDescriptor.class),
@Type(name = "DASHBOARD", value = DashboardInstalledItemDescriptor.class),
@Type(name = "CALCULATED_FIELD", value = CalculatedFieldInstalledItemDescriptor.class),
@Type(name = "ALARM_RULE", value = AlarmRuleInstalledItemDescriptor.class),
@Type(name = "RULE_CHAIN", value = RuleChainInstalledItemDescriptor.class),
@Type(name = "DEVICE", value = DeviceInstalledItemDescriptor.class),
@Type(name = "SOLUTION_TEMPLATE", value = SolutionTemplateInstalledItemDescriptor.class)

13
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts

@ -18,6 +18,8 @@ import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Observable, of, EMPTY } from 'rxjs';
import { filter, mergeMap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { DialogService } from '@core/services/dialog.service';
import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models';
import { DeviceInstalledItemDescriptor, IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models';
@ -36,7 +38,9 @@ export class IotHubActionsService {
constructor(
private dialog: MatDialog,
private iotHubApiService: IotHubApiService
private iotHubApiService: IotHubApiService,
private dialogService: DialogService,
private translate: TranslateService
) {}
openItemDetail(item: MpItemVersionView, installedItem?: IotHubInstalledItem, installedItemsCount?: number,
@ -72,6 +76,13 @@ export class IotHubActionsService {
}
installItem(item: MpItemVersionView): Observable<string> {
if (item.type === ItemType.ALARM_RULE) {
this.dialogService.alert(
this.translate.instant('iot-hub.alarm-rule-install-update-required'),
this.translate.instant('iot-hub.alarm-rule-install-update-required-text')
);
return EMPTY;
}
if (item.type === ItemType.DEVICE) {
return this.installDevice(item);
}

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

@ -36,7 +36,7 @@
<!-- Filter panel template (reused in sidebar and drawer) -->
<ng-template #filterPanelContent>
<div class="tb-iot-hub-filters-inner">
@if (!fixedSubType && subtypeOptions.length > 0) {
@if (!fixedSubType && subtypeOptions.length > 0 && activeType !== ItemType.ALARM_RULE) {
<mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel">
<mat-expansion-panel-header>
<mat-panel-title>{{ 'iot-hub.type-filter-label' | translate }}</mat-panel-title>

31
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts

@ -21,7 +21,7 @@ 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, FilterParamInfo } from '@shared/models/iot-hub/iot-hub-item.models';
import { ItemType, FilterParamInfo, CREATOR_VISIBLE_ITEM_TYPES } 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';
@ -45,6 +45,18 @@ interface SortOption {
})
export class TbIotHubBrowseComponent implements OnInit, AfterViewInit, OnDestroy {
private static typeTabLabel(t: ItemType): string {
switch (t) {
case ItemType.WIDGET: return 'item.type-widget-plural';
case ItemType.DASHBOARD: return 'item.type-dashboard-plural';
case ItemType.SOLUTION_TEMPLATE: return 'item.type-solution-template-plural';
case ItemType.CALCULATED_FIELD: return 'item.type-calculated-field-plural';
case ItemType.ALARM_RULE: return 'item.type-alarm-rule-plural';
case ItemType.RULE_CHAIN: return 'item.type-rule-chain-plural';
case ItemType.DEVICE: return 'item.type-device-plural';
}
}
readonly ItemType = ItemType;
@Input() creatorId: string;
@ -76,7 +88,9 @@ export class TbIotHubBrowseComponent implements OnInit, AfterViewInit, OnDestroy
}
get isCompactType(): boolean {
return this._activeType === ItemType.CALCULATED_FIELD || this._activeType === ItemType.RULE_CHAIN;
return this._activeType === ItemType.CALCULATED_FIELD
|| this._activeType === ItemType.ALARM_RULE
|| this._activeType === ItemType.RULE_CHAIN;
}
items: MpItemVersionView[] = [];
@ -118,14 +132,10 @@ export class TbIotHubBrowseComponent implements OnInit, AfterViewInit, OnDestroy
{ value: 'name', label: 'iot-hub.sort-name', direction: Direction.ASC }
];
typeTabs: { type: ItemType; label: string }[] = [
{ type: ItemType.WIDGET, label: 'item.type-widget-plural' },
{ type: ItemType.DASHBOARD, label: 'item.type-dashboard-plural' },
{ type: ItemType.SOLUTION_TEMPLATE, label: 'item.type-solution-template-plural' },
{ type: ItemType.CALCULATED_FIELD, label: 'item.type-calculated-field-plural' },
{ type: ItemType.RULE_CHAIN, label: 'item.type-rule-chain-plural' },
{ type: ItemType.DEVICE, label: 'item.type-device-plural' }
];
typeTabs: { type: ItemType; label: string }[] = CREATOR_VISIBLE_ITEM_TYPES.map(t => ({
type: t,
label: TbIotHubBrowseComponent.typeTabLabel(t)
}));
selectedSortIndex = 0;
subtypeOptions: FilterParamInfo[] = [];
@ -512,6 +522,7 @@ export class TbIotHubBrowseComponent implements OnInit, AfterViewInit, OnDestroy
case ItemType.DASHBOARD: return 'iot-hub.title-dashboards';
case ItemType.SOLUTION_TEMPLATE: return 'iot-hub.title-solution-templates';
case ItemType.CALCULATED_FIELD: return 'iot-hub.title-calculated-fields';
case ItemType.ALARM_RULE: return 'iot-hub.title-alarm-rules';
case ItemType.RULE_CHAIN: return 'iot-hub.title-rule-chains';
case ItemType.DEVICE: return 'iot-hub.title-devices';
}

2
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts

@ -22,6 +22,7 @@ export const ITEM_TYPE_TO_ENTITY_TYPE: Record<string, EntityType> = {
'WIDGET': EntityType.WIDGET_TYPE,
'DASHBOARD': EntityType.DASHBOARD,
'CALCULATED_FIELD': EntityType.CALCULATED_FIELD,
'ALARM_RULE': EntityType.CALCULATED_FIELD,
'RULE_CHAIN': EntityType.RULE_CHAIN,
'DEVICE': EntityType.DEVICE_PROFILE
};
@ -39,6 +40,7 @@ export function resolveEntityDetailsUrl(descriptor: IotHubInstalledItemDescripto
case 'WIDGET': entityId = descriptor.widgetTypeId?.id; break;
case 'DASHBOARD': entityId = descriptor.dashboardId?.id; break;
case 'CALCULATED_FIELD': entityId = descriptor.calculatedFieldId?.id; break;
case 'ALARM_RULE': entityId = descriptor.calculatedFieldId?.id; break;
case 'RULE_CHAIN': entityId = descriptor.ruleChainId?.id; break;
}
if (!entityId) {

4
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts

@ -150,6 +150,7 @@ export class TbIotHubInstalledItemsTableComponent implements OnInit, OnChanges,
case 'DASHBOARD': return 'dashboard';
case 'SOLUTION_TEMPLATE': return 'integration_instructions';
case 'CALCULATED_FIELD': return 'functions';
case 'ALARM_RULE': return 'notification_important';
case 'RULE_CHAIN': return 'settings_ethernet';
case 'DEVICE': return 'memory';
default: return 'category';
@ -174,6 +175,7 @@ export class TbIotHubInstalledItemsTableComponent implements OnInit, OnChanges,
case 'WIDGET': return 'tb-type-widget';
case 'DASHBOARD': return 'tb-type-dashboard';
case 'CALCULATED_FIELD': return 'tb-type-calc-field';
case 'ALARM_RULE': return 'tb-type-alarm-rule';
case 'RULE_CHAIN': return 'tb-type-rule-chain';
case 'DEVICE': return 'tb-type-device';
case 'SOLUTION_TEMPLATE': return 'tb-type-solution-template';
@ -214,6 +216,7 @@ export class TbIotHubInstalledItemsTableComponent implements OnInit, OnChanges,
case 'WIDGET': return descriptor.widgetTypeId?.id;
case 'DASHBOARD': return descriptor.dashboardId?.id;
case 'CALCULATED_FIELD': return descriptor.calculatedFieldId?.id;
case 'ALARM_RULE': return descriptor.calculatedFieldId?.id;
case 'RULE_CHAIN': return descriptor.ruleChainId?.id;
case 'DEVICE': return descriptor.dashboardId?.id ?? null;
case 'SOLUTION_TEMPLATE': return descriptor.dashboardId?.id;
@ -227,6 +230,7 @@ export class TbIotHubInstalledItemsTableComponent implements OnInit, OnChanges,
case 'WIDGET': return EntityType.WIDGET_TYPE;
case 'DASHBOARD': return EntityType.DASHBOARD;
case 'CALCULATED_FIELD': return EntityType.CALCULATED_FIELD;
case 'ALARM_RULE': return EntityType.CALCULATED_FIELD;
case 'RULE_CHAIN': return EntityType.RULE_CHAIN;
case 'DEVICE': return descriptor.dashboardId ? EntityType.DASHBOARD : null;
case 'SOLUTION_TEMPLATE': return EntityType.DASHBOARD;

9
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts

@ -52,7 +52,9 @@ export class TbIotHubItemCardComponent {
) {}
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.ALARM_RULE
|| this.item.type === ItemType.RULE_CHAIN;
}
getPreviewUrl(): string | null {
@ -71,6 +73,8 @@ export class TbIotHubItemCardComponent {
case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions';
case ItemType.CALCULATED_FIELD:
return this.item.icon || cfTypeIcons.get(this.item.dataDescriptor?.cfType) || 'functions';
case ItemType.ALARM_RULE:
return this.item.icon || 'notification_important';
case ItemType.RULE_CHAIN:
return this.item.icon || (this.item.dataDescriptor?.ruleChainType === 'EDGE' ? 'router' : 'device_hub');
case ItemType.DEVICE: return 'memory';
@ -85,7 +89,8 @@ export class TbIotHubItemCardComponent {
const key = wt ? widgetTypeTranslations.get(wt) : null;
return key ? this.translate.instant(key) : wt || '';
}
case ItemType.CALCULATED_FIELD: {
case ItemType.CALCULATED_FIELD:
case ItemType.ALARM_RULE: {
const cfType = this.item.dataDescriptor?.cfType;
const key = cfType ? cfTypeTranslations.get(cfType) : null;
return key ? this.translate.instant(key) : cfType || '';

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

@ -85,7 +85,9 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
}
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.ALARM_RULE
|| this.item.type === ItemType.RULE_CHAIN;
}
getPreviewUrl(): string | null {
@ -107,6 +109,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
case ItemType.DASHBOARD: return 'dashboard';
case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions';
case ItemType.CALCULATED_FIELD: return 'functions';
case ItemType.ALARM_RULE: return 'notification_important';
case ItemType.RULE_CHAIN: return 'account_tree';
case ItemType.DEVICE: return 'memory';
default: return 'category';
@ -121,6 +124,9 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
const cfType = this.item.dataDescriptor?.cfType;
return cfTypeIcons.get(cfType) || 'functions';
}
if (this.item.type === ItemType.ALARM_RULE) {
return 'notification_important';
}
switch (this.item.dataDescriptor?.ruleChainType) {
case 'CORE': return 'device_hub';
case 'EDGE': return 'router';
@ -129,7 +135,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
}
getCompactSubtypeLabel(): string {
if (this.item.type === ItemType.CALCULATED_FIELD) {
if (this.item.type === ItemType.CALCULATED_FIELD || this.item.type === ItemType.ALARM_RULE) {
const cfType = this.item.dataDescriptor?.cfType;
const key = cfType ? cfTypeTranslations.get(cfType) : null;
return key ? this.translate.instant(key) : cfType || '';
@ -142,7 +148,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
getSubtypeLabel(): string {
switch (this.item.type) {
case ItemType.CALCULATED_FIELD:
return this.getCompactSubtypeLabel();
case ItemType.ALARM_RULE:
case ItemType.RULE_CHAIN:
return this.getCompactSubtypeLabel();
case ItemType.WIDGET: {
@ -213,6 +219,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
case 'WIDGET': entityId = descriptor.widgetTypeId?.id; entityType = EntityType.WIDGET_TYPE; break;
case 'DASHBOARD': entityId = descriptor.dashboardId?.id; entityType = EntityType.DASHBOARD; break;
case 'CALCULATED_FIELD':
case 'ALARM_RULE':
entityId = descriptor.calculatedFieldId?.id;
entityType = EntityType.CALCULATED_FIELD;
break;

1
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-deep-link.utils.ts

@ -29,6 +29,7 @@ export function typeSegment(t: ItemType): string | undefined {
case ItemType.DASHBOARD: return 'dashboards';
case ItemType.SOLUTION_TEMPLATE: return 'solution-templates';
case ItemType.CALCULATED_FIELD: return 'calculated-fields';
case ItemType.ALARM_RULE: return 'alarm-rules';
case ItemType.RULE_CHAIN: return 'rule-chains';
case ItemType.DEVICE: return 'devices';
default: return undefined;

56
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html

@ -56,7 +56,7 @@
(mouseenter)="onHeroTypeHover(ht)"
(mouseleave)="onHeroTypeLeave()"
(click)="navigateToBrowse(ht.type)"
(keyup.enter)="navigateToBrowse(ht.type)">{{ ht.labelKey | translate }}@if (!last) {,}</span>
(keyup.enter)="navigateToBrowse(ht.type)">@if (last) {& }{{ ht.labelKey | translate }}@if (!last) {,}</span>
@if (i === 2) {
<span class="tb-iot-hub-hero-subtitle-break"></span>
}
@ -178,47 +178,48 @@
</div>
}
<!-- Popular Dashboards -->
@if (popularDashboards.length) {
<!-- Popular Solution Templates -->
@if (popularSolutionTemplates.length) {
<div class="tb-iot-hub-section">
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.DASHBOARD)">
{{ 'iot-hub.popular-dashboards' | translate }}
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.SOLUTION_TEMPLATE)">
{{ 'iot-hub.popular-solution-templates' | translate }}
<mat-icon iconPositionEnd>chevron_right</mat-icon>
</button>
<div class="tb-iot-hub-big-cards-row">
@for (item of popularDashboards; track item.id) {
@for (item of popularSolutionTemplates; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledSolutionTemplate(item)"
[showTypeChip]="false"
[showCreator]="true"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)">
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)">
</tb-iot-hub-item-card>
}
</div>
</div>
}
<!-- Popular Solution Templates -->
@if (popularSolutionTemplates.length) {
<!-- Popular Devices -->
@if (popularDevices.length) {
<div class="tb-iot-hub-section">
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.SOLUTION_TEMPLATE)">
{{ 'iot-hub.popular-solution-templates' | translate }}
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.DEVICE)">
{{ 'iot-hub.popular-devices' | translate }}
<mat-icon iconPositionEnd>chevron_right</mat-icon>
</button>
<div class="tb-iot-hub-big-cards-row">
@for (item of popularSolutionTemplates; track item.id) {
@for (item of popularDevices; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledSolutionTemplate(item)"
[installedItemsCount]="findInstalledItemsCount(item)"
[showTypeChip]="false"
[showCreator]="true"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)">
(installClick)="installItem($event)">
</tb-iot-hub-item-card>
}
</div>
@ -247,15 +248,15 @@
</div>
}
<!-- Popular Rule Chains -->
@if (popularRuleChains.length) {
<!-- Popular Alarm Rules -->
@if (popularAlarmRules.length) {
<div class="tb-iot-hub-section">
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.RULE_CHAIN)">
{{ 'iot-hub.popular-rule-chains' | translate }}
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.ALARM_RULE)">
{{ 'iot-hub.popular-alarm-rules' | translate }}
<mat-icon iconPositionEnd>chevron_right</mat-icon>
</button>
<div class="tb-iot-hub-compact-cards-grid">
@for (item of popularRuleChains; track item.id) {
@for (item of popularAlarmRules; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[showTypeChip]="false"
@ -269,18 +270,17 @@
</div>
}
<!-- Popular Devices -->
@if (popularDevices.length) {
<!-- Popular Rule Chains -->
@if (popularRuleChains.length) {
<div class="tb-iot-hub-section">
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.DEVICE)">
{{ 'iot-hub.popular-devices' | translate }}
<button mat-button class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.RULE_CHAIN)">
{{ 'iot-hub.popular-rule-chains' | translate }}
<mat-icon iconPositionEnd>chevron_right</mat-icon>
</button>
<div class="tb-iot-hub-big-cards-row">
@for (item of popularDevices; track item.id) {
<div class="tb-iot-hub-compact-cards-grid">
@for (item of popularRuleChains; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItemsCount]="findInstalledItemsCount(item)"
[showTypeChip]="false"
[showCreator]="true"
(cardClick)="openItemDetail($event)"

4
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss

@ -499,6 +499,10 @@
background: linear-gradient(104.75deg, rgb(245, 252, 255) 0%, rgb(149, 222, 248) 100%);
}
.category-alarm-rules {
background: linear-gradient(104.75deg, rgb(245, 246, 255) 0%, rgb(189, 197, 255) 100%);
}
.category-rule-chains {
background: linear-gradient(104.75deg, rgb(255, 252, 245) 0%, rgb(255, 238, 194) 100%);
}

70
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts

@ -80,30 +80,30 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
gradientColor: 'rgba(44, 151, 85, 0.1)',
icons: ['assets/iot-hub/hero-widget-icon-1.svg', 'assets/iot-hub/hero-widget-icon-2.svg', 'assets/iot-hub/hero-widget-icon-3.svg', 'assets/iot-hub/hero-widget-icon-4.svg']
},
{
type: ItemType.DASHBOARD, labelKey: 'item.type-dashboard-plural', color: '#4d5fd0',
gradientColor: 'rgba(77, 95, 208, 0.1)',
icons: ['assets/iot-hub/hero-dashboard-icon-1.svg', 'assets/iot-hub/hero-dashboard-icon-2.svg', 'assets/iot-hub/hero-dashboard-icon-3.svg', 'assets/iot-hub/hero-dashboard-icon-4.svg']
},
{
type: ItemType.SOLUTION_TEMPLATE, labelKey: 'item.type-solution-template-plural', color: '#2666a9',
gradientColor: 'rgba(38, 102, 169, 0.1)',
icons: ['assets/iot-hub/hero-solution-template-icon-1.svg', 'assets/iot-hub/hero-solution-template-icon-2.svg', 'assets/iot-hub/hero-solution-template-icon-3.svg', 'assets/iot-hub/hero-solution-template-icon-4.svg']
},
{
type: ItemType.DEVICE, labelKey: 'item.type-device-plural', color: '#4b8a79',
gradientColor: 'rgba(75, 138, 121, 0.1)',
icons: ['assets/iot-hub/hero-device-icon-1.svg', 'assets/iot-hub/hero-device-icon-2.svg', 'assets/iot-hub/hero-device-icon-3.svg', 'assets/iot-hub/hero-device-icon-4.svg']
},
{
type: ItemType.CALCULATED_FIELD, labelKey: 'item.type-calculated-field-plural', color: '#006d92',
gradientColor: 'rgba(0, 109, 146, 0.1)',
icons: ['assets/iot-hub/hero-calculated-field-icon-1.svg', 'assets/iot-hub/hero-calculated-field-icon-2.svg', 'assets/iot-hub/hero-calculated-field-icon-3.svg', 'assets/iot-hub/hero-calculated-field-icon-4.svg']
},
{
type: ItemType.ALARM_RULE, labelKey: 'item.type-alarm-rule-plural', color: '#4d5fd0',
gradientColor: 'rgba(77, 95, 208, 0.1)',
icons: ['assets/iot-hub/hero-dashboard-icon-1.svg', 'assets/iot-hub/hero-dashboard-icon-2.svg', 'assets/iot-hub/hero-dashboard-icon-3.svg', 'assets/iot-hub/hero-dashboard-icon-4.svg']
},
{
type: ItemType.RULE_CHAIN, labelKey: 'item.type-rule-chain-plural', color: '#95694b',
gradientColor: 'rgba(149, 105, 75, 0.1)',
icons: ['assets/iot-hub/hero-rule-chain-icon-1.svg', 'assets/iot-hub/hero-rule-chain-icon-2.svg', 'assets/iot-hub/hero-rule-chain-icon-3.svg', 'assets/iot-hub/hero-rule-chain-icon-4.svg']
},
{
type: ItemType.DEVICE, labelKey: 'iot-hub.and-devices', color: '#4b8a79',
gradientColor: 'rgba(75, 138, 121, 0.1)',
icons: ['assets/iot-hub/hero-device-icon-1.svg', 'assets/iot-hub/hero-device-icon-2.svg', 'assets/iot-hub/hero-device-icon-3.svg', 'assets/iot-hub/hero-device-icon-4.svg']
}
];
@ -113,25 +113,25 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
categoryCards: CategoryCard[] = [
{ type: ItemType.WIDGET, titleKey: 'item.type-widget-plural', icon: 'widgets', cssClass: 'category-widgets', image: 'assets/iot-hub/category-widgets-img.svg' },
{ type: ItemType.DASHBOARD, titleKey: 'item.type-dashboard-plural', icon: 'dashboard', cssClass: 'category-dashboards', image: 'assets/iot-hub/category-dashboards-img.svg' },
{ type: ItemType.SOLUTION_TEMPLATE, titleKey: 'item.type-solution-template-plural', icon: 'integration_instructions', cssClass: 'category-solutions', image: 'assets/iot-hub/category-solution-templates-img.png' },
{ type: ItemType.DEVICE, titleKey: 'iot-hub.device-library', icon: 'memory', cssClass: 'category-devices', image: 'assets/iot-hub/category-device-library-img.svg' },
{ type: ItemType.CALCULATED_FIELD, titleKey: 'item.type-calculated-field-plural', icon: 'functions', cssClass: 'category-calc-fields', image: 'assets/iot-hub/category-calculated-fields-img.svg' },
{ type: ItemType.RULE_CHAIN, titleKey: 'item.type-rule-chain-plural', icon: 'account_tree', cssClass: 'category-rule-chains', image: 'assets/iot-hub/category-rule-chains-img.svg' },
{ type: ItemType.DEVICE, titleKey: 'iot-hub.device-library', icon: 'memory', cssClass: 'category-devices', image: 'assets/iot-hub/category-device-library-img.svg' }
{ type: ItemType.ALARM_RULE, titleKey: 'item.type-alarm-rule-plural', icon: 'notification_important', cssClass: 'category-alarm-rules', image: 'assets/iot-hub/category-dashboards-img.svg' },
{ type: ItemType.RULE_CHAIN, titleKey: 'item.type-rule-chain-plural', icon: 'account_tree', cssClass: 'category-rule-chains', image: 'assets/iot-hub/category-rule-chains-img.svg' }
];
popularWidgets: MpItemVersionView[] = [];
popularDashboards: MpItemVersionView[] = [];
popularSolutionTemplates: MpItemVersionView[] = [];
popularCalcFields: MpItemVersionView[] = [];
popularAlarmRules: MpItemVersionView[] = [];
popularRuleChains: MpItemVersionView[] = [];
popularDevices: MpItemVersionView[] = [];
installedWidgets: IotHubInstalledItem[] = [];
installedSolutionTemplates: IotHubInstalledItem[] = [];
installedDeviceCounts: Record<string, number> = {};
installedDashboardCounts: Record<string, number> = {};
installedCalcFieldCounts: Record<string, number> = {};
installedAlarmRuleCounts: Record<string, number> = {};
installedRuleChainCounts: Record<string, number> = {};
installedItemsCount = 0;
@ -260,11 +260,21 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
}
isCompactType(type: ItemType): boolean {
return type === ItemType.CALCULATED_FIELD || type === ItemType.RULE_CHAIN;
return type === ItemType.CALCULATED_FIELD
|| type === ItemType.ALARM_RULE
|| type === ItemType.RULE_CHAIN;
}
getCompactIcon(item: MpItemVersionView): string {
return item.icon || (item.type === ItemType.CALCULATED_FIELD ? 'functions' : 'settings_ethernet');
if (item.icon) {
return item.icon;
}
switch (item.type) {
case ItemType.CALCULATED_FIELD: return 'functions';
case ItemType.ALARM_RULE: return 'notification_important';
case ItemType.RULE_CHAIN: return 'settings_ethernet';
default: return 'category';
}
}
getItemImage(item: MpItemVersionView): string | null {
@ -276,6 +286,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
case ItemType.WIDGET: return 'widgets';
case ItemType.DASHBOARD: return 'dashboard';
case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions';
case ItemType.ALARM_RULE: return 'notification_important';
case ItemType.DEVICE: return 'memory';
default: return 'category';
}
@ -296,6 +307,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
case ItemType.DASHBOARD: return 'dashboards';
case ItemType.SOLUTION_TEMPLATE: return 'solution-templates';
case ItemType.CALCULATED_FIELD: return 'calculated-fields';
case ItemType.ALARM_RULE: return 'alarm-rules';
case ItemType.RULE_CHAIN: return 'rule-chains';
case ItemType.DEVICE: return 'devices';
default: return 'widgets';
@ -332,14 +344,14 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
this.iotHubApiService.getInstalledItemCounts(ItemType.DEVICE, config).subscribe(counts => {
this.installedDeviceCounts = counts;
});
} else if (type === ItemType.DASHBOARD) {
this.iotHubApiService.getInstalledItemCounts(ItemType.DASHBOARD, config).subscribe(counts => {
this.installedDashboardCounts = counts;
});
} else if (type === ItemType.CALCULATED_FIELD) {
this.iotHubApiService.getInstalledItemCounts(ItemType.CALCULATED_FIELD, config).subscribe(counts => {
this.installedCalcFieldCounts = counts;
});
} else if (type === ItemType.ALARM_RULE) {
this.iotHubApiService.getInstalledItemCounts(ItemType.ALARM_RULE, config).subscribe(counts => {
this.installedAlarmRuleCounts = counts;
});
} else if (type === ItemType.RULE_CHAIN) {
this.iotHubApiService.getInstalledItemCounts(ItemType.RULE_CHAIN, config).subscribe(counts => {
this.installedRuleChainCounts = counts;
@ -362,10 +374,10 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
switch (item.type) {
case ItemType.DEVICE:
return this.installedDeviceCounts[item.itemId] || 0;
case ItemType.DASHBOARD:
return this.installedDashboardCounts[item.itemId] || 0;
case ItemType.CALCULATED_FIELD:
return this.installedCalcFieldCounts[item.itemId] || 0;
case ItemType.ALARM_RULE:
return this.installedAlarmRuleCounts[item.itemId] || 0;
case ItemType.RULE_CHAIN:
return this.installedRuleChainCounts[item.itemId] || 0;
default:
@ -414,10 +426,10 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
this.installedSolutionTemplates = this.installedSolutionTemplates.filter(i => i.id.id !== installedItem.id.id);
} else if (item.type === ItemType.DEVICE && this.installedDeviceCounts[item.itemId]) {
this.installedDeviceCounts[item.itemId] = Math.max(0, this.installedDeviceCounts[item.itemId] - 1);
} else if (item.type === ItemType.DASHBOARD && this.installedDashboardCounts[item.itemId]) {
this.installedDashboardCounts[item.itemId] = Math.max(0, this.installedDashboardCounts[item.itemId] - 1);
} else if (item.type === ItemType.CALCULATED_FIELD && this.installedCalcFieldCounts[item.itemId]) {
this.installedCalcFieldCounts[item.itemId] = Math.max(0, this.installedCalcFieldCounts[item.itemId] - 1);
} else if (item.type === ItemType.ALARM_RULE && this.installedAlarmRuleCounts[item.itemId]) {
this.installedAlarmRuleCounts[item.itemId] = Math.max(0, this.installedAlarmRuleCounts[item.itemId] - 1);
} else if (item.type === ItemType.RULE_CHAIN && this.installedRuleChainCounts[item.itemId]) {
this.installedRuleChainCounts[item.itemId] = Math.max(0, this.installedRuleChainCounts[item.itemId] - 1);
}
@ -472,31 +484,31 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
forkJoin({
widgets: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.WIDGET, this.bigCardCount), config),
dashboards: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.DASHBOARD, this.bigCardCount), config),
solutionTemplates: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.SOLUTION_TEMPLATE, this.bigCardCount), config),
calcFields: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.CALCULATED_FIELD, this.compactCardCount), config),
alarmRules: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.ALARM_RULE, this.compactCardCount), config),
ruleChains: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.RULE_CHAIN, this.compactCardCount), config),
devices: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.DEVICE, this.bigCardCount), config),
installedWidgets: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.WIDGET, undefined, config),
installedSolutionTemplates: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.SOLUTION_TEMPLATE, undefined, config),
installedDeviceCounts: this.iotHubApiService.getInstalledItemCounts(ItemType.DEVICE, config),
installedDashboardCounts: this.iotHubApiService.getInstalledItemCounts(ItemType.DASHBOARD, config),
installedCalcFieldCounts: this.iotHubApiService.getInstalledItemCounts(ItemType.CALCULATED_FIELD, config),
installedAlarmRuleCounts: this.iotHubApiService.getInstalledItemCounts(ItemType.ALARM_RULE, config),
installedRuleChainCounts: this.iotHubApiService.getInstalledItemCounts(ItemType.RULE_CHAIN, config),
installedCount: this.iotHubApiService.getInstalledItemsCount(null, config)
}).subscribe({
next: (results) => {
this.popularWidgets = results.widgets.data;
this.popularDashboards = results.dashboards.data;
this.popularSolutionTemplates = results.solutionTemplates.data;
this.popularCalcFields = results.calcFields.data;
this.popularAlarmRules = results.alarmRules.data;
this.popularRuleChains = results.ruleChains.data;
this.popularDevices = results.devices.data;
this.installedWidgets = results.installedWidgets.data;
this.installedSolutionTemplates = results.installedSolutionTemplates.data;
this.installedDeviceCounts = results.installedDeviceCounts;
this.installedDashboardCounts = results.installedDashboardCounts;
this.installedCalcFieldCounts = results.installedCalcFieldCounts;
this.installedAlarmRuleCounts = results.installedAlarmRuleCounts;
this.installedRuleChainCounts = results.installedRuleChainCounts;
this.installedItemsCount = results.installedCount;
this.isLoading = false;

7
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts

@ -63,6 +63,13 @@ const PAGE_CONFIGS: Record<string, ItemTypePageConfig> = {
image: 'assets/iot-hub/items-page-calculated-fields-hero.svg',
routeSegment: 'calculated-fields'
},
ALARM_RULE: {
type: ItemType.ALARM_RULE,
titleKey: 'item.type-alarm-rule-plural',
descriptionKey: 'iot-hub.items-page-desc-alarm-rules',
image: 'assets/iot-hub/items-page-dashboards-hero.svg',
routeSegment: 'alarm-rules'
},
RULE_CHAIN: {
type: ItemType.RULE_CHAIN,
titleKey: 'item.type-rule-chain-plural',

10
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts

@ -84,6 +84,16 @@ const routes: Routes = [
breadcrumb: { label: 'item.type-calculated-field-plural', icon: 'functions' }
}
},
{
path: 'alarm-rules',
component: TbIotHubItemsPageComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'item.type-alarm-rule-plural',
itemType: 'ALARM_RULE',
breadcrumb: { label: 'item.type-alarm-rule-plural', icon: 'notification_important' }
}
},
{
path: 'rule-chains',
component: TbIotHubItemsPageComponent,

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

@ -32,6 +32,12 @@ export interface CalculatedFieldInstalledItemDescriptor {
entityId: { entityType: string; id: string };
}
export interface AlarmRuleInstalledItemDescriptor {
type: 'ALARM_RULE';
calculatedFieldId: { id: string };
entityId: { entityType: string; id: string };
}
export interface RuleChainInstalledItemDescriptor {
type: 'RULE_CHAIN';
ruleChainId: { id: string };
@ -58,6 +64,7 @@ export type IotHubInstalledItemDescriptor =
| WidgetInstalledItemDescriptor
| DashboardInstalledItemDescriptor
| CalculatedFieldInstalledItemDescriptor
| AlarmRuleInstalledItemDescriptor
| RuleChainInstalledItemDescriptor
| DeviceInstalledItemDescriptor
| SolutionTemplateInstalledItemDescriptor;

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

@ -19,6 +19,7 @@ export enum ItemType {
DASHBOARD = 'DASHBOARD',
SOLUTION_TEMPLATE = 'SOLUTION_TEMPLATE',
CALCULATED_FIELD = 'CALCULATED_FIELD',
ALARM_RULE = 'ALARM_RULE',
RULE_CHAIN = 'RULE_CHAIN',
DEVICE = 'DEVICE'
}
@ -29,11 +30,27 @@ export const itemTypeTranslations = new Map<ItemType, string>(
[ItemType.DASHBOARD, 'item.type-dashboard'],
[ItemType.SOLUTION_TEMPLATE, 'item.type-solution-template'],
[ItemType.CALCULATED_FIELD, 'item.type-calculated-field'],
[ItemType.ALARM_RULE, 'item.type-alarm-rule'],
[ItemType.RULE_CHAIN, 'item.type-rule-chain'],
[ItemType.DEVICE, 'item.type-device']
]
);
/**
* Item types discoverable to creators in the marketplace UI.
* DASHBOARD is intentionally absent (IoT Hub no longer accepts Dashboard contributions).
* Defensive code paths (item card, detail dialog descriptor switch, installed-items table,
* install handler, /iot-hub/dashboards route) remain functional for already-installed items.
*/
export const CREATOR_VISIBLE_ITEM_TYPES: ItemType[] = [
ItemType.WIDGET,
ItemType.SOLUTION_TEMPLATE,
ItemType.DEVICE,
ItemType.CALCULATED_FIELD,
ItemType.ALARM_RULE,
ItemType.RULE_CHAIN,
];
export interface FilterParamInfo {
key: string;
totalItems: number;

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

@ -4183,6 +4183,8 @@
"type-solution-template-plural": "Solution Templates",
"type-calculated-field": "Calculated Field",
"type-calculated-field-plural": "Calculated Fields",
"type-alarm-rule": "Alarm Rule",
"type-alarm-rule-plural": "Alarm Rules",
"type-rule-chain": "Rule Chain",
"type-rule-chain-plural": "Rule Chains",
"type-device": "Device",
@ -4223,6 +4225,7 @@
"items-page-desc-dashboards": "Explore dashboards to find pre-built visualization templates and interactive UI layouts you can deploy in minutes to bring your IoT data to life.",
"items-page-desc-solution-templates": "Complete IoT solution packages with dashboards, rule chains, and device configurations. Get started with proven architectures.",
"items-page-desc-calculated-fields": "Use pre-configured Calculated Fields to automate complex metrics like fuel efficiency or power factor. Skip the manual logic and keep your dashboards clean and actionable.",
"items-page-desc-alarm-rules": "Use pre-built Alarm Rule templates to detect critical conditions like low battery, threshold breaches, or devices going offline. Skip writing the rule logic and start reacting to incidents the moment they happen.",
"items-page-desc-rule-chains": "From sophisticated data processing to seamless API integrations, Rule Chain templates provide the architectural foundation you need to scale without building from scratch.",
"items-page-desc-devices": "Explore the device library to find pre-configured connectivity templates you can deploy in minutes to connect your hardware instantly.",
"device-library": "Device Library",
@ -4230,6 +4233,9 @@
"popular-dashboards": "Popular Dashboards",
"popular-solution-templates": "Popular Solution Templates",
"popular-calculated-fields": "Popular Calculated Fields",
"popular-alarm-rules": "Popular Alarm Rules",
"alarm-rule-install-update-required": "Update required",
"alarm-rule-install-update-required-text": "Alarm Rules require ThingsBoard 4.3 or later. Please update your platform instance to install Alarm Rule packages.",
"popular-rule-chains": "Popular Rule Chains",
"popular-devices": "Popular Devices",
"become-creator-text": "Submit your templates to the ThingsBoard IoT Hub to get featured and showcase your solutions to our global community.",
@ -4238,6 +4244,7 @@
"title-widgets": "Widgets",
"title-dashboards": "Dashboards",
"title-calculated-fields": "Calculated Fields",
"title-alarm-rules": "Alarm Rules",
"title-rule-chains": "Rule Chains",
"title-devices": "Devices",
"title-solution-templates": "Solution Templates",
@ -4245,12 +4252,14 @@
"subtitle-dashboards": "Browse ready-to-use dashboards",
"subtitle-solution-templates": "Browse complete IoT solution packages",
"subtitle-calculated-fields": "Browse calculated field templates",
"subtitle-alarm-rules": "Browse alarm rule templates",
"subtitle-rule-chains": "Browse rule chain templates",
"subtitle-devices": "Browse device configurations",
"search-widgets": "Search widgets...",
"search-dashboards": "Search dashboards...",
"search-solution-templates": "Search solution templates...",
"search-calculated-fields": "Search calculated fields...",
"search-alarm-rules": "Search alarm rules...",
"search-rule-chains": "Search rule chains...",
"search-devices": "Search devices...",
"sort-most-installed": "Most installed",

Loading…
Cancel
Save