Browse Source

Feature/iot hub alarm rules (#15539)

* feat(iot-hub): scaffold ALARM_RULE item type and CREATOR_VISIBLE_ITEM_TYPES

* feat(iot-hub): wire ALARM_RULE through browse, item card, detail dialog, installed items, and install dialog

* feat(iot-hub): reorder home cards, hide Dashboards, add Alarm Rules

* feat(iot-hub): scaffold ALARM_RULE descriptor and reject install with v4.3 upgrade hint

Registers AlarmRuleInstalledItemDescriptor in the Jackson polymorphism so
4.2 can browse Alarm Rule items from the marketplace without descriptor
deserialization failures.

The install handler explicitly rejects ALARM_RULE with a friendly message
asking the user to upgrade to v4.3+. The full install/update/delete
implementation depends on CalculatedFieldType.ALARM (added in v4.3) and
will land once master is merged into this branch.

* feat(iot-hub): hide redundant Type filter on Alarm Rules browse

* feat(iot-hub): show v4.3 update info dialog directly when installing an Alarm Rule
pull/15615/head
Andrew Shvayka 3 months ago
committed by GitHub
parent
commit
e62532dcc4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  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

@ -113,6 +113,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.entityId?.id;
case 'ALARM_RULE': return descriptor.entityId?.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 descriptor.entityId?.entityType as EntityType;
case 'ALARM_RULE': return descriptor.entityId?.entityType as EntityType;
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.entityId?.id;
entityType = descriptor.entityId?.entityType as EntityType;
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

@ -3691,6 +3691,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",
@ -3731,6 +3733,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",
@ -3738,6 +3741,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.",
@ -3746,6 +3752,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",
@ -3753,12 +3760,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