From 9212c0240a813dcbcf8e9eb4fde1d9b06acca01e Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Thu, 30 Apr 2026 13:29:38 +0300 Subject: [PATCH] LTS 4.3 IoT hub alarm rules (#15548) * 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 * feat(iot-hub): implement ALARM_RULE install / update / delete handlers Replaces the temporary v4.3-upgrade-required rejection with the actual backend handlers. Alarm rules install as CalculatedField entities with type=ALARM, mirroring the CALCULATED_FIELD path: - installAlarmRule parses the alarm-rule JSON, validates type==ALARM, saves via tbCalculatedFieldService, and returns AlarmRuleInstalledItemDescriptor with calculatedFieldId + entityId. - updateAlarmRule re-saves the existing CF with new name, type, and configuration. - Delete branch removes the underlying calculated field on installed-item delete. - Checksum dispatch unifies CF and ALARM_RULE through a shared calculateCalculatedFieldChecksum(CalculatedFieldId) helper. Frontend: drops the v4.3 info-dialog interception in IotHubActionsService (install now actually runs); restores the select-entity branch for ALARM_RULE in the install dialog so users can pick a parent device. Removes the now-unused alarm-rule-install-update-required* locale keys. --- .../service/iot_hub/DefaultIotHubService.java | 62 +++++++++++++++- .../AlarmRuleInstalledItemDescriptor.java | 28 ++++++++ .../IotHubInstalledItemDescriptor.java | 1 + .../iot-hub/iot-hub-browse.component.html | 2 +- .../iot-hub/iot-hub-browse.component.ts | 31 +++++--- .../iot-hub/iot-hub-components.models.ts | 2 + .../iot-hub-install-dialog.component.ts | 2 +- ...iot-hub-installed-items-table.component.ts | 4 ++ .../iot-hub/iot-hub-item-card.component.ts | 9 ++- .../iot-hub-item-detail-dialog.component.ts | 13 +++- .../pages/iot-hub/iot-hub-deep-link.utils.ts | 1 + .../pages/iot-hub/iot-hub-home.component.html | 56 +++++++-------- .../pages/iot-hub/iot-hub-home.component.scss | 4 ++ .../pages/iot-hub/iot-hub-home.component.ts | 70 +++++++++++-------- .../iot-hub/iot-hub-items-page.component.ts | 7 ++ .../pages/iot-hub/iot-hub-routing.module.ts | 10 +++ .../iot-hub/iot-hub-installed-item.models.ts | 7 ++ .../models/iot-hub/iot-hub-item.models.ts | 17 +++++ .../assets/locale/locale.constant-en_US.json | 7 ++ 19 files changed, 256 insertions(+), 77 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/AlarmRuleInstalledItemDescriptor.java diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java index 2fb80edc49..b4a50e9086 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java @@ -24,6 +24,8 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; @@ -32,6 +34,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.IotHubInstalledItemId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.iot_hub.AlarmRuleInstalledItemDescriptor; import org.thingsboard.server.common.data.iot_hub.CalculatedFieldInstalledItemDescriptor; import org.thingsboard.server.common.data.iot_hub.DashboardInstalledItemDescriptor; import org.thingsboard.server.common.data.iot_hub.DeviceInstalledItemDescriptor; @@ -114,6 +117,7 @@ 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" -> installAlarmRule(user, tenantId, fileData, data); case "RULE_CHAIN" -> installRuleChain(tenantId, fileData); case "DEVICE" -> installDeviceProfile(user, tenantId, fileData); case "SOLUTION_TEMPLATE" -> installSolution(user, tenantId, fileData, request); @@ -196,6 +200,30 @@ public class DefaultIotHubService implements IotHubService { return descriptor; } + private AlarmRuleInstalledItemDescriptor installAlarmRule(SecurityUser user, TenantId tenantId, byte[] fileData, JsonNode data) throws Exception { + CalculatedField alarmRule; + try { + alarmRule = JacksonUtil.fromString(new String(fileData), CalculatedField.class, true); + } catch (Exception e) { + throw new Exception("Failed to parse alarm rule data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + } + if (alarmRule.getType() != CalculatedFieldType.ALARM) { + throw new Exception("Expected ALARM-type calculated field for ALARM_RULE install; got: " + alarmRule.getType()); + } + alarmRule.setId(null); + alarmRule.setTenantId(tenantId); + if (data != null && data.has("entityId")) { + EntityId entityId = JacksonUtil.treeToValue(data.get("entityId"), EntityId.class); + alarmRule.setEntityId(entityId); + } + CalculatedField saved = tbCalculatedFieldService.save(alarmRule, user); + log.debug("[{}] Alarm rule installed: {}", tenantId, saved.getName()); + AlarmRuleInstalledItemDescriptor descriptor = new AlarmRuleInstalledItemDescriptor(); + descriptor.setCalculatedFieldId(saved.getId()); + descriptor.setEntityId(saved.getEntityId()); + return descriptor; + } + private RuleChainInstalledItemDescriptor installRuleChain(TenantId tenantId, byte[] fileData) throws Exception { JsonNode json = JacksonUtil.toJsonNode(new String(fileData)); @@ -299,6 +327,7 @@ public class DefaultIotHubService implements IotHubService { case "WIDGET" -> updateWidget(user, tenantId, (WidgetInstalledItemDescriptor) descriptor, fileData); case "DASHBOARD" -> updateDashboard(user, tenantId, (DashboardInstalledItemDescriptor) descriptor, fileData); case "CALCULATED_FIELD" -> updateCalculatedField(user, tenantId, (CalculatedFieldInstalledItemDescriptor) descriptor, fileData); + case "ALARM_RULE" -> updateAlarmRule(user, tenantId, (AlarmRuleInstalledItemDescriptor) descriptor, fileData); case "RULE_CHAIN" -> updateRuleChain(tenantId, (RuleChainInstalledItemDescriptor) descriptor, fileData); case "DEVICE" -> { DeviceInstalledItemDescriptor dd = (DeviceInstalledItemDescriptor) descriptor; @@ -385,6 +414,26 @@ public class DefaultIotHubService implements IotHubService { tbCalculatedFieldService.save(existing, user); } + private void updateAlarmRule(SecurityUser user, TenantId tenantId, AlarmRuleInstalledItemDescriptor descriptor, byte[] fileData) throws Exception { + CalculatedField newCf; + try { + newCf = JacksonUtil.fromString(new String(fileData), CalculatedField.class, true); + } catch (Exception e) { + throw new Exception("Failed to parse alarm rule data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e); + } + if (newCf.getType() != CalculatedFieldType.ALARM) { + throw new Exception("Expected ALARM-type calculated field for ALARM_RULE update; got: " + newCf.getType()); + } + CalculatedField existing = calculatedFieldService.findById(tenantId, descriptor.getCalculatedFieldId()); + if (existing == null) { + throw new Exception("Alarm rule not found for update"); + } + existing.setName(newCf.getName()); + existing.setType(newCf.getType()); + existing.setConfiguration(newCf.getConfiguration()); + tbCalculatedFieldService.save(existing, user); + } + private void updateRuleChain(TenantId tenantId, RuleChainInstalledItemDescriptor descriptor, byte[] fileData) throws Exception { JsonNode json = JacksonUtil.toJsonNode(new String(fileData)); RuleChainMetaData metadata; @@ -421,15 +470,17 @@ public class DefaultIotHubService implements IotHubService { } else if (descriptor instanceof DashboardInstalledItemDescriptor dd) { return calculateDashboardChecksum(tenantId, dd); } else if (descriptor instanceof CalculatedFieldInstalledItemDescriptor cd) { - return calculateCalculatedFieldChecksum(tenantId, cd); + return calculateCalculatedFieldChecksum(tenantId, cd.getCalculatedFieldId()); + } else if (descriptor instanceof AlarmRuleInstalledItemDescriptor ad) { + return calculateCalculatedFieldChecksum(tenantId, ad.getCalculatedFieldId()); } else if (descriptor instanceof RuleChainInstalledItemDescriptor rd) { return calculateRuleChainChecksum(tenantId, rd); } return null; } - private String calculateCalculatedFieldChecksum(TenantId tenantId, CalculatedFieldInstalledItemDescriptor descriptor) { - CalculatedField cf = calculatedFieldService.findById(tenantId, descriptor.getCalculatedFieldId()); + private String calculateCalculatedFieldChecksum(TenantId tenantId, CalculatedFieldId calculatedFieldId) { + CalculatedField cf = calculatedFieldService.findById(tenantId, calculatedFieldId); if (cf == null) { return null; } @@ -588,6 +639,11 @@ public class DefaultIotHubService implements IotHubService { if (calculatedField != null) { tbCalculatedFieldService.delete(calculatedField, user); } + } else if (descriptor instanceof AlarmRuleInstalledItemDescriptor ad) { + CalculatedField alarmRule = calculatedFieldService.findById(tenantId, ad.getCalculatedFieldId()); + if (alarmRule != null) { + tbCalculatedFieldService.delete(alarmRule, user); + } } else if (descriptor instanceof RuleChainInstalledItemDescriptor rd) { if (rd.getRuleChainId() != null) { RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, rd.getRuleChainId()); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/AlarmRuleInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/AlarmRuleInstalledItemDescriptor.java new file mode 100644 index 0000000000..2a7be1f5d3 --- /dev/null +++ b/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; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java index 544f44cbc2..6a0359ceb3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java +++ b/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) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html index 254438e2e6..a653685882 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html @@ -36,7 +36,7 @@
- @if (!fixedSubType && subtypeOptions.length > 0) { + @if (!fixedSubType && subtypeOptions.length > 0 && activeType !== ItemType.ALARM_RULE) { {{ 'iot-hub.type-filter-label' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts index 554e158e12..a6daaf8772 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts @@ -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'; } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts index ccefb3069b..1233dfa89c 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.models.ts +++ b/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 = { '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) { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts index 44e4d1a601..4b56427f06 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts @@ -75,7 +75,7 @@ export class TbIotHubInstallDialogComponent extends DialogComponent{{ ht.labelKey | translate }}@if (!last) {,} + (keyup.enter)="navigateToBrowse(ht.type)">@if (last) {& }{{ ht.labelKey | translate }}@if (!last) {,} @if (i === 2) { } @@ -178,47 +178,48 @@
} - - @if (popularDashboards.length) { + + @if (popularSolutionTemplates.length) {
-
- @for (item of popularDashboards; track item.id) { + @for (item of popularSolutionTemplates; track item.id) { + (installClick)="installItem($event)" + (updateClick)="updateItem($event)" + (deleteClick)="deleteInstalledItem($event)"> }
} - - @if (popularSolutionTemplates.length) { + + @if (popularDevices.length) {
-
- @for (item of popularSolutionTemplates; track item.id) { + @for (item of popularDevices; track item.id) { + (installClick)="installItem($event)"> }
@@ -247,15 +248,15 @@
} - - @if (popularRuleChains.length) { + + @if (popularAlarmRules.length) {
-
- @for (item of popularRuleChains; track item.id) { + @for (item of popularAlarmRules; track item.id) { } - - @if (popularDevices.length) { + + @if (popularRuleChains.length) {
- -
- @for (item of popularDevices; track item.id) { +
+ @for (item of popularRuleChains; track item.id) { = {}; - installedDashboardCounts: Record = {}; installedCalcFieldCounts: Record = {}; + installedAlarmRuleCounts: Record = {}; installedRuleChainCounts: Record = {}; 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; diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts index 0e8c731ce6..6be857519e 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.ts @@ -63,6 +63,13 @@ const PAGE_CONFIGS: Record = { 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', diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts index 118256edff..9fd8cd7d3d 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts +++ b/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, diff --git a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts index 9b30377ceb..25ac09d161 100644 --- a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts +++ b/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; diff --git a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts index 88f350bc3b..6451bc5d52 100644 --- a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts +++ b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts @@ -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.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; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 639fcc9194..46a1ad5ea4 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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,7 @@ "popular-dashboards": "Popular Dashboards", "popular-solution-templates": "Popular Solution Templates", "popular-calculated-fields": "Popular Calculated Fields", + "popular-alarm-rules": "Popular Alarm Rules", "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 +4242,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 +4250,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",