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..2654fd7f59 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 @@ -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); 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-actions.service.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts index ee32245eb1..4fd1565c07 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts +++ b/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 { + 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); } 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-installed-items-table.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts index 564c1e9b93..2ad2946fd4 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts +++ b/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; diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts index a84d81ef56..49e3574188 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts +++ b/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 || ''; diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts index fae83e1ef5..9aaffbd3a6 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts +++ b/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{{ 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..4816fbc844 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,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",