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 e88d812f5b..f7cc08391a 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 @@ -64,6 +64,7 @@ import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService; import org.thingsboard.server.service.entitiy.device.TbDeviceService; import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileService; import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService; +import org.thingsboard.server.service.install.ProjectInfo; import org.thingsboard.server.service.rule.TbRuleChainService; import org.thingsboard.server.service.security.model.SecurityUser; @@ -112,6 +113,7 @@ public class DefaultIotHubService implements IotHubService { private final DeviceService deviceService; private final TbDeviceService tbDeviceService; private final SolutionService solutionService; + private final ProjectInfo projectInfo; // Field names of the marketplace version JSON payload. Both the install path and the // install-plan resolver parse the same shape, so the contract lives here in one place. @@ -196,10 +198,11 @@ public class DefaultIotHubService implements IotHubService { } try { - iotHubRestClient.reportVersionInstalled(versionId); + InstallReport report = buildInstallReport(tenantId.getId(), user.getId().getId(), + projectInfo.getProjectVersion(), projectInfo.getProductType()); + iotHubRestClient.reportVersionInstalled(versionId, report); } catch (Exception e) { - // Counter ping is best-effort — do not fail the install if it errors. - log.warn("[{}] Failed to report install counter for version {}: {}", tenantId, versionId, e.getMessage()); + log.warn("[{}] Failed to report install for version {}: {}", tenantId, versionId, e.getMessage()); } log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType); return installedItem; @@ -611,6 +614,13 @@ public class DefaultIotHubService implements IotHubService { } } + static InstallReport buildInstallReport(UUID tenantId, UUID userId, String tbVersion, String edition) { + String salt = tenantId.toString(); + String tenantHash = sha256(salt + tenantId); + String userHash = sha256(salt + userId); + return new InstallReport(tenantHash, userHash, tbVersion, edition); + } + @Override public InstallItemVersionResult registerDeviceInstall(SecurityUser user, String versionId, DeviceInstalledItemDescriptor descriptor) { TenantId tenantId = user.getTenantId(); @@ -650,10 +660,11 @@ public class DefaultIotHubService implements IotHubService { } try { - iotHubRestClient.reportVersionInstalled(versionId); + InstallReport report = buildInstallReport(tenantId.getId(), user.getId().getId(), + projectInfo.getProjectVersion(), projectInfo.getProductType()); + iotHubRestClient.reportVersionInstalled(versionId, report); } catch (Exception e) { - // Counter ping is best-effort — do not fail the install if it errors. - log.warn("[{}] Failed to report install counter for version {}: {}", tenantId, versionId, e.getMessage()); + log.warn("[{}] Failed to report install for version {}: {}", tenantId, versionId, e.getMessage()); } log.info("[{}] Registered device package install: {} (version {})", tenantId, itemName, version); diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java new file mode 100644 index 0000000000..3b253e426c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java @@ -0,0 +1,19 @@ +/** + * 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.service.iot_hub; + +public record InstallReport(String tenantHash, String userHash, String tbVersion, String edition) { +} diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java index c67fef55a8..ae3cf5b07e 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java @@ -120,9 +120,9 @@ public class IotHubRestClient { }); } - public void reportVersionInstalled(String versionId) { + public void reportVersionInstalled(String versionId, InstallReport report) { String url = baseUrl + "/api/versions/" + versionId + "/install"; log.debug("Reporting IoT Hub version installed: {}", url); - restTemplate.postForObject(url, null, Void.class); + restTemplate.postForObject(url, report, Void.class); } } diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java index 71020ae47e..34e56275e2 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java @@ -393,12 +393,12 @@ public class DefaultSolutionService implements SolutionService { provisionAlarmRules(ctx); + provisionCalculatedFields(ctx); + Set> telemetryLoading = launchEmulators(ctx, devices, assets); waitForTelemetryCompletion(telemetryLoading); - provisionCalculatedFields(ctx); - ctx.getSolutionInstructions().setDetails(prepareInstructions(ctx, request)); List ruleChainDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {}); @@ -1297,27 +1297,7 @@ public class DefaultSolutionService implements SolutionService { List cfs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "calculated_fields.json", new TypeReference<>() { }); cfs.addAll(loadListOfEntitiesFromDirectory(ctx.getTempDir(), "calculated_fields", CalculatedFieldDefinition.class)); - - List createOnly = new ArrayList<>(); - TreeMap> ordered = new TreeMap<>(); - - for (CalculatedFieldDefinition cf : cfs) { - if (cf.getReprocessingOrder() == null || cf.getReprocessingOrder() < 0) { - createOnly.add(cf); - } else { - ordered.computeIfAbsent(cf.getReprocessingOrder(), integer -> new ArrayList<>()).add(cf); - } - } - - createOnly.forEach(cf -> ctx.register(createCalculatedField(cf, ctx))); - - for (Map.Entry> entry : ordered.entrySet()) { - List cfDefs = entry.getValue(); - for (CalculatedFieldDefinition cfDef : cfDefs) { - CalculatedField calculatedField = createCalculatedField(cfDef, ctx); - ctx.register(calculatedField); - } - } + cfs.forEach(cf -> ctx.register(createCalculatedField(cf, ctx))); } private CalculatedField createCalculatedField(CalculatedField cf, SolutionInstallContext ctx) { diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CalculatedFieldDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CalculatedFieldDefinition.java index 1dcc7a6bbd..536d3ce279 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CalculatedFieldDefinition.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CalculatedFieldDefinition.java @@ -23,6 +23,4 @@ import org.thingsboard.server.common.data.cf.CalculatedField; @EqualsAndHashCode(callSuper = true) public class CalculatedFieldDefinition extends CalculatedField { - private Integer reprocessingOrder; - } diff --git a/application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java b/application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java new file mode 100644 index 0000000000..73f968fc01 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java @@ -0,0 +1,46 @@ +/** + * 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.service.iot_hub; + +import org.junit.jupiter.api.Test; + +import java.security.MessageDigest; +import java.nio.charset.StandardCharsets; +import java.util.HexFormat; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class InstallReportTest { + + private static String sha256(String s) throws Exception { + MessageDigest d = MessageDigest.getInstance("SHA-256"); + return HexFormat.of().formatHex(d.digest(s.getBytes(StandardCharsets.UTF_8))); + } + + @Test + void buildInstallReport_hashesMatchSaltedFormula() throws Exception { + UUID tenantId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + + InstallReport r = DefaultIotHubService.buildInstallReport(tenantId, userId, "4.2.0", "CE"); + + assertEquals(sha256(tenantId.toString() + tenantId), r.tenantHash()); + assertEquals(sha256(tenantId.toString() + userId), r.userHash()); + assertEquals("4.2.0", r.tbVersion()); + assertEquals("CE", r.edition()); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html index 25db54641d..0e5b9977ca 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html @@ -69,7 +69,7 @@
@@ -169,7 +169,7 @@ [disabled]="ruleChainInstallForm.invalid || resolvingPlan" (click)="onRuleChainInstall()"> @if (resolvingPlan) { - + } {{ 'iot-hub.install' | translate }} @@ -178,7 +178,7 @@ [disabled]="(activeSelectEntityConfig.required && !selectedEntityId) || resolvingPlan" (click)="onEntitySelectInstall()"> @if (resolvingPlan) { - + } {{ 'iot-hub.install' | translate }} @@ -188,7 +188,7 @@ @@ -204,7 +204,7 @@ @case ('installing') { } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html index a64f9ab73e..11143dc0bf 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html @@ -135,14 +135,8 @@ - @if (isLoading) { -
- -
- } - @if (!isLoading && dataSource.length === 0) { -
+

{{ 'iot-hub.no-installed-items' | translate }}

{{ 'iot-hub.no-installed-items-text' | translate }}

@@ -166,4 +160,13 @@ (page)="onPageChange($event)" [showFirstLastButtons]="true"> + + @if (isLoading) { +
+
+ +
+
+ } +
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss index 400bb5778b..a908470ae8 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss @@ -25,6 +25,7 @@ // Table + paginator container — border .tb-installed-table-container { + position: relative; border: 1px solid rgba(0, 0, 0, 0.12); } 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 bb5e1ecd7a..4435c82736 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 @@ -156,8 +156,8 @@ export class TbIotHubInstalledItemsTableComponent implements OnInit, OnChanges, } deleteItem(item: IotHubInstalledItem): void { - this.iotHubActions.deleteItem(item).subscribe(confirmed => { - if (confirmed) { + this.iotHubActions.deleteItem(item).subscribe(deleted => { + if (deleted) { this.loadData(); } }); diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html index e42f1521aa..30d9322dd9 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html @@ -96,7 +96,7 @@
} @else { -
+
@if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) { 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 04d49b7f86..bc72b7a94c 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 @@ -220,8 +220,8 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent { - if (confirmed) { + this.iotHubActions.deleteItem(this.installedItem).subscribe(deleted => { + if (deleted) { this.dialogRef.close('deleted'); } }); diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts index 64ff558723..790baf05f7 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts @@ -247,7 +247,6 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { updateItem(item: MpItemVersionView): void { const installedItem = this.getInstalledItem(item); - if (!installedItem) { return; } this.iotHubActions.updateItem(installedItem, item.version, item.id as string).subscribe(result => { if (result === 'updated') { this.reloadInstalledItems(); @@ -257,9 +256,10 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { deleteInstalledItem(item: MpItemVersionView): void { const installedItem = this.getInstalledItem(item); - if (!installedItem) { return; } - this.iotHubActions.deleteItem(installedItem).subscribe(() => { - this.reloadInstalledItems(); + this.iotHubActions.deleteItem(installedItem).subscribe((deleted) => { + if (deleted) { + this.reloadInstalledItems(); + } }); } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.html index 78e8fc3b66..48396089ca 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.html @@ -51,7 +51,7 @@ @case ('updating') { } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts index 6962073104..67ef8c14f9 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts @@ -46,7 +46,7 @@ export type UpdateState = 'confirm' | 'updating' | 'success' | 'error'; templateUrl: './iot-hub-update-dialog.component.html', styleUrls: ['./iot-hub-install-dialog.component.scss'] }) -export class TbIotHubUpdateDialogComponent extends DialogComponent { +export class TbIotHubUpdateDialogComponent extends DialogComponent { ItemType = ItemType; @@ -58,7 +58,7 @@ export class TbIotHubUpdateDialogComponent extends DialogComponent, protected router: Router, - protected dialogRef: MatDialogRef, + protected dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: IotHubUpdateDialogData, private dialog: MatDialog, private dialogService: DialogService, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index fc7ad9f64b..058a178825 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -683,6 +683,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } private reInitImpl() { + this.displayNoData = false; this.onDestroy(); if (!this.typeParameters.useCustomDatasources) { this.createDefaultSubscription().subscribe({ @@ -726,7 +727,6 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds); } if (subscriptionChanged && !this.typeParameters.useCustomDatasources) { - this.displayNoData = false; this.reInit(); } } @@ -740,7 +740,6 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, subscriptionChanged = subscriptionChanged || subscription.onFiltersChanged(filterIds); } if (subscriptionChanged && !this.typeParameters.useCustomDatasources) { - this.displayNoData = false; this.reInit(); } } diff --git a/ui-ngx/src/app/modules/home/home.component.html b/ui-ngx/src/app/modules/home/home.component.html index 10c15080f6..03ce291f18 100644 --- a/ui-ngx/src/app/modules/home/home.component.html +++ b/ui-ngx/src/app/modules/home/home.component.html @@ -79,7 +79,7 @@ -
+
diff --git a/ui-ngx/src/app/modules/home/home.component.ts b/ui-ngx/src/app/modules/home/home.component.ts index 6191280933..1bd9b73386 100644 --- a/ui-ngx/src/app/modules/home/home.component.ts +++ b/ui-ngx/src/app/modules/home/home.component.ts @@ -58,6 +58,8 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni @ViewChild('sidenav') sidenav: MatSidenav; + @ViewChild('mainContent', { static: true }) mainContent: ElementRef; + @ViewChild('searchInput') searchInputField: ElementRef; fullscreenEnabled = screenfull.isEnabled; @@ -136,6 +138,7 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni activeComponentChanged(activeComponent: any) { this.activeComponentService.setCurrentActiveComponent(activeComponent); + this.mainContent?.nativeElement?.scrollTo({ top: 0, left: 0 }); if (!this.activeComponent) { setTimeout(() => { this.updateActiveComponent(activeComponent); diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss index f0768bf330..c433bad1a9 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss @@ -757,6 +757,14 @@ padding: 140px 16px 40px; } + .tb-iot-hub-hero-cluster { + top: 15%; + } + + .tb-iot-hub-hero-title { + font-size: 28px; + } + .tb-iot-hub-categories { grid-template-columns: 1fr; } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts index 87f8eee0b2..07c3b701eb 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts @@ -381,7 +381,6 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { updateItem(item: MpItemVersionView): void { const installedItem = this.findInstalledItem(item); - if (!installedItem) { return; } this.iotHubActions.updateItem(installedItem, item.version, item.id as string).subscribe(result => { if (result === 'updated') { this.reloadInstalledItems(item.type); @@ -404,20 +403,22 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { deleteInstalledItem(item: MpItemVersionView): void { const installedItem = this.findInstalledItem(item); if (!installedItem) { return; } - this.iotHubActions.deleteItem(installedItem).subscribe(() => { - this.installedItemsCount = Math.max(0, this.installedItemsCount - 1); - if (item.type === ItemType.WIDGET) { - this.installedWidgets = this.installedWidgets.filter(i => i.id.id !== installedItem.id.id); - } else if (item.type === ItemType.SOLUTION_TEMPLATE) { - 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.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); + this.iotHubActions.deleteItem(installedItem).subscribe((deleted) => { + if (deleted) { + this.installedItemsCount = Math.max(0, this.installedItemsCount - 1); + if (item.type === ItemType.WIDGET) { + this.installedWidgets = this.installedWidgets.filter(i => i.id.id !== installedItem.id.id); + } else if (item.type === ItemType.SOLUTION_TEMPLATE) { + 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.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); + } } }); } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html index be6793f766..1061eb07a9 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html @@ -28,9 +28,9 @@ @if (!updatesChecked) { 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 b2320453bb..b2bb0e7ad9 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 @@ -140,8 +140,16 @@ export const getInstalledItemUrl = (descriptor?: IotHubInstalledItemDescriptor): let query: string | null = null; switch (descriptor.type) { case 'DEVICE': - entityId = descriptor.dashboardId?.id; - entityType = EntityType.DASHBOARD; + if (descriptor.dashboardId) { + entityId = descriptor.dashboardId?.id; + entityType = EntityType.DASHBOARD; + } else if (descriptor.createdEntityIds) { + const found = descriptor.createdEntityIds.find(id => id.entityType === EntityType.DEVICE); + if (found) { + entityId = found.id; + entityType = EntityType.DEVICE; + } + } break; case 'WIDGET': entityId = descriptor.widgetTypeId?.id; diff --git a/ui-ngx/src/app/shared/models/units/volume-flow.ts b/ui-ngx/src/app/shared/models/units/volume-flow.ts index fb0b85005d..c6a0018151 100644 --- a/ui-ngx/src/app/shared/models/units/volume-flow.ts +++ b/ui-ngx/src/app/shared/models/units/volume-flow.ts @@ -25,6 +25,7 @@ export type VolumeFlowMetricUnits = | 'L/min' | 'L/hr' | 'm³/s' + | 'm³/min' | 'm³/hr'; export type VolumeFlowImperialUnits = @@ -67,6 +68,11 @@ const METRIC: TbMeasureUnits = { tags: ['airflow', 'ventilation', 'HVAC', 'gas flow rate'], to_anchor: 1000, }, + 'm³/min': { + name: 'unit.cubic-meters-per-minute', + tags: ['airflow', 'ventilation', 'HVAC', 'gas flow rate'], + to_anchor: 1000 / 60, + }, 'm³/hr': { name: 'unit.cubic-meters-per-hour', tags: ['airflow', 'ventilation', 'HVAC', 'gas flow rate'], 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 acfe5c344a..b7a2d72e33 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6743,6 +6743,7 @@ "cubic-foot-per-minute": "Cubic foot per minute", "cubic-meters-per-hour": "Cubic meters per hour", "cubic-meters-per-second": "Cubic meters per second", + "cubic-meters-per-minute": "Cubic meters per minute", "liter-per-second": "Liter per second", "liter-per-minute": "Liter per minute", "gallons-per-minute": "Gallons per minute",