Browse Source

Merge remote-tracking branch 'thingsboard-ce/lts-4.3' into fix/alarm-rule-condition-validator-dirty

pull/15868/head
Maksym Tsymbarov 4 months ago
parent
commit
9601046aab
  1. 23
      application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java
  2. 19
      application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java
  3. 4
      application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java
  4. 26
      application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java
  5. 2
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CalculatedFieldDefinition.java
  6. 46
      application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java
  7. 2
      pom.xml
  8. 4
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html
  9. 16
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts
  10. 6
      ui-ngx/src/app/shared/models/units/volume-flow.ts
  11. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -68,6 +68,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;
@ -117,6 +118,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.
@ -203,10 +205,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;
@ -665,6 +668,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();
@ -704,10 +714,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);

19
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) {
}

4
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);
}
}

26
application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java

@ -395,12 +395,12 @@ public class DefaultSolutionService implements SolutionService {
provisionAlarmRules(ctx);
provisionCalculatedFields(ctx);
Set<CompletableFuture<Void>> telemetryLoading = launchEmulators(ctx, devices, assets);
waitForTelemetryCompletion(telemetryLoading);
provisionCalculatedFields(ctx);
ctx.getSolutionInstructions().setDetails(prepareInstructions(ctx, request));
List<ReferenceableEntityDefinition> ruleChainDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {});
@ -1302,27 +1302,7 @@ public class DefaultSolutionService implements SolutionService {
List<CalculatedFieldDefinition> cfs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "calculated_fields.json", new TypeReference<>() {
});
cfs.addAll(loadListOfEntitiesFromDirectory(ctx.getTempDir(), "calculated_fields", CalculatedFieldDefinition.class));
List<CalculatedFieldDefinition> createOnly = new ArrayList<>();
TreeMap<Integer, List<CalculatedFieldDefinition>> 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<Integer, List<CalculatedFieldDefinition>> entry : ordered.entrySet()) {
List<CalculatedFieldDefinition> 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) {

2
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;
}

46
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());
}
}

2
pom.xml

@ -62,7 +62,7 @@
<pkg.implementationTitle>${project.name}</pkg.implementationTitle>
<pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<thingsboard.client.version>4.3.1.2</thingsboard.client.version>
<thingsboard.client.version>4.3.1.3</thingsboard.client.version>
<spring-boot.version>3.5.15</spring-boot.version>
<!-- Pins spring-boot-test to 3.5.13: the ImportsContextCustomizer change in 3.5.14 causes "Duplicate spy
definition" failures on legacy @SpyBean fields (see PR #15557). Spring declined to restore the prior

4
ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html

@ -69,7 +69,7 @@
<ng-template #userComment>
<div class="user-comment flex flex-row items-center justify-start gap-2"
*ngIf="!displayDataElement.edit; else commentEditing"
(mouseenter)="onCommentMouseEnter(displayDataElement.commentId, i)"
(mouseenter)="onCommentMouseEnter(i)"
(mouseleave)="onCommentMouseLeave(i)">
<div *ngIf="displayDataElement.userExists; else userDeleted"
class="user-avatar flex flex-row items-center justify-center self-start xs:!hidden"
@ -98,11 +98,13 @@
<div class="action-buttons flex flex-row xs:flex-col"
[class.show-buttons]="displayDataElement.showActions">
<button mat-icon-button
*ngIf="displayDataElement.canEdit"
type="button"
(click)="editComment(displayDataElement.commentId)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<button mat-icon-button
*ngIf="displayDataElement.canDelete"
type="button"
(click)="deleteComment(displayDataElement.commentId)">
<mat-icon class="material-icons">delete</mat-icon>

16
ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts

@ -22,6 +22,7 @@ import { AlarmCommentService } from '@core/http/alarm-comment.service';
import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms';
import { DialogService } from '@core/services/dialog.service';
import { AuthUser } from '@shared/models/user.model';
import { Authority } from '@shared/models/authority.enum';
import { getCurrentAuthUser, selectUserDetails } from '@core/auth/auth.selectors';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link';
@ -52,6 +53,8 @@ interface AlarmCommentsDisplayData {
editedTime?: string;
editedDateAgo?: string;
showActions?: boolean;
canEdit?: boolean;
canDelete?: boolean;
commentText?: string;
isSystemComment?: boolean;
avatarBgColor?: string;
@ -143,6 +146,11 @@ export class AlarmCommentComponent implements OnInit {
displayDataElement.editedTime = this.datePipe.transform(alarmComment.comment.editedOn, 'yyyy-MM-dd HH:mm:ss');
displayDataElement.editedDateAgo = this.dateAgoPipe.transform(alarmComment.comment.editedOn) + '\n';
displayDataElement.showActions = false;
const isCommentAuthor = this.authUser.userId === alarmComment.userId?.id;
// Mirrors backend AlarmCommentController#deleteAlarmComment / checkUserPermission:
// author may edit and delete own comments; tenant admin may delete any comment.
displayDataElement.canEdit = isCommentAuthor;
displayDataElement.canDelete = isCommentAuthor || this.authUser.authority === Authority.TENANT_ADMIN;
displayDataElement.isSystemComment = false;
displayDataElement.avatarBgColor = this.utilsService.stringToHslColor(displayDataElement.displayName,
40, 60);
@ -273,11 +281,11 @@ export class AlarmCommentComponent implements OnInit {
return this.alarmCommentSortOrder.direction === Direction.ASC;
}
onCommentMouseEnter(commentId: string, displayDataIndex: number): void {
onCommentMouseEnter(displayDataIndex: number): void {
if (!this.editMode) {
const alarmUserId = this.getAlarmCommentById(commentId).userId.id;
if (this.authUser.userId === alarmUserId) {
this.displayData[displayDataIndex].showActions = true;
const displayDataElement = this.displayData[displayDataIndex];
if (displayDataElement.canEdit || displayDataElement.canDelete) {
displayDataElement.showActions = true;
}
}
}

6
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<VolumeFlowMetricUnits> = {
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'],

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

@ -7311,6 +7311,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",

Loading…
Cancel
Save