Browse Source

Merge branch 'master' into fix-dashboard-customer-unassignment

pull/14460/head
Mazurenko Nikita 9 months ago
committed by GitHub
parent
commit
9600ce1a98
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 2
      application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
  3. 2
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  4. 116
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  5. 86
      application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
  6. 70
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  7. 8
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  8. 7
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  9. 6
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java
  10. 55
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java
  11. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java
  12. 27
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java
  13. 129
      application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
  14. 9
      application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java
  15. 2
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  16. 2
      application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java
  17. 9
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
  18. 9
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
  19. 4
      application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java
  20. 2
      application/src/main/resources/thingsboard.yml
  21. 56
      application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java
  22. 31
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  23. 20
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  24. 7
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java
  25. 7
      application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java
  26. 5
      application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java
  27. 23
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  28. 69
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  29. 4
      application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java
  30. 187
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  31. 6
      application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java
  32. 5
      common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java
  33. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
  34. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
  35. 2
      common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java
  36. 6
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java
  37. 17
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java
  38. 5
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java
  39. 19
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java
  40. 6
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java
  41. 4
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java
  42. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java
  43. 4
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java
  44. 43
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java
  45. 4
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java
  46. 7
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java
  47. 67
      common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeys.java
  48. 2
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java
  49. 7
      dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
  50. 6
      dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
  51. 5
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  52. 9
      dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java
  53. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
  54. 4
      dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java
  55. 12
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  56. 9
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  57. 8
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java
  58. 6
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java
  59. 14
      dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java
  60. 2
      dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java
  61. 20
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  62. 50
      ui-ngx/src/app/core/http/resource.service.ts
  63. 2
      ui-ngx/src/app/core/services/menu.models.ts
  64. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html
  65. 48
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  66. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html
  67. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts
  68. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  69. 53
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  70. 157
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
  71. 11
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
  72. 74
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
  73. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html
  74. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss
  75. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts
  76. 33
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html
  77. 18
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts
  78. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
  79. 53
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts
  80. 32
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
  81. 13
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss
  82. 30
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts
  83. 16
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
  84. 3
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss
  85. 26
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts
  86. 77
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html
  87. 166
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts
  88. 9
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html
  89. 33
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts
  90. 19
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html
  91. 122
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts
  92. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html
  93. 3
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss
  94. 36
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts
  95. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts
  96. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts
  97. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts
  98. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html
  99. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  100. 5
      ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts

2
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -670,7 +670,7 @@ public class ActorSystemContext {
@Getter
private long cfCheckInterval;
@Value("${actors.alarms.reevaluation_interval:120}")
@Value("${actors.alarms.reevaluation_interval:60}")
@Getter
private long alarmRulesReevaluationInterval;

2
application/src/main/java/org/thingsboard/server/actors/app/AppActor.java

@ -178,7 +178,7 @@ public class AppActor extends ContextAwareActor {
});
}
}
if (!msg.getEntityId().getEntityType().isOneOf(EntityType.TENANT_PROFILE, EntityType.TB_RESOURCE)) {
if (!msg.getEntityId().getEntityType().isOneOf(EntityType.TENANT_PROFILE, EntityType.TB_RESOURCE, EntityType.USER)) {
log.warn("Message has system tenant id: {}", msg);
}
} else {

2
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java

@ -180,7 +180,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
cfsReevaluationTask = systemContext.getScheduler().scheduleWithFixedDelay(() -> {
try {
calculatedFields.values().forEach(cf -> {
if (cf.isRequiresScheduledReevaluation()) {
if (cf.requiresScheduledReevaluation()) {
applyToTargetCfEntityActors(cf, TbCallback.EMPTY, (entityId, callback) -> {
log.debug("[{}][{}] Pushing scheduled CF reevaluate msg", entityId, cf.getCfId());
getOrCreateActor(entityId).tell(new CalculatedFieldReevaluateMsg(tenantId, cf));

116
application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java

@ -86,6 +86,9 @@ public class SwaggerConfiguration {
public static final String LOGIN_ENDPOINT = "/api/auth/login";
public static final String REFRESH_TOKEN_ENDPOINT = "/api/auth/token";
private static final String LOGIN_PASSWORD_SCHEME = "HTTP login form";
private static final String API_KEY_SCHEME = "API key form";
private static final ApiResponses loginResponses = loginResponses();
private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false);
private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true);
@ -142,14 +145,28 @@ public class SwaggerConfiguration {
.license(license)
.version(apiVersion);
SecurityScheme securityScheme = new SecurityScheme()
SecurityScheme loginPasswordScheme = new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.description("Enter Username / Password")
.scheme("loginPassword")
.bearerFormat("/api/auth/login|X-Authorization");
SecurityScheme apiKeyScheme = new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.name("X-Authorization")
.in(SecurityScheme.In.HEADER)
.description("""
Enter the API key value with 'ApiKey' prefix in format: **ApiKey <your_api_key_value>**
Example: **ApiKey tb_5te51SkLRYpjGrujUGwqkjFvooWBlQpVe2An2Dr3w13wjfxDW**
<br>**NOTE**: Use only ONE authentication method at a time. If both are authorized, JWT auth takes the priority.<br>
""");
var openApi = new OpenAPI()
.components(new Components().addSecuritySchemes("HTTP login form", securityScheme))
.components(new Components()
.addSecuritySchemes(LOGIN_PASSWORD_SCHEME, loginPasswordScheme)
.addSecuritySchemes(API_KEY_SCHEME, apiKeyScheme))
.info(info);
addDefaultSchemas(openApi);
addLoginOperation(openApi);
@ -198,13 +215,14 @@ public class SwaggerConfiguration {
operation.summary("Login method to get user JWT token data");
operation.description("""
Login method used to authenticate user and get JWT token data.
Value of the response **token** field can be used as **X-Authorization** header value:
`X-Authorization: Bearer $JWT_TOKEN_VALUE`.""");
var requestBody = new RequestBody().description("Login request")
.content(new Content().addMediaType(APPLICATION_JSON_VALUE,
new MediaType().schema(new Schema<LoginRequest>().$ref("#/components/schemas/LoginRequest"))));
new MediaType().schema(new Schema<LoginRequest>().$ref("#/components/schemas/LoginRequest"))));
operation.requestBody(requestBody);
operation.responses(loginResponses);
@ -218,11 +236,11 @@ public class SwaggerConfiguration {
var operation = new Operation();
operation.summary("Refresh user JWT token data");
operation.description("""
Method to refresh JWT token. Provide a valid refresh token to get a new JWT token.
The response contains a new token that can be used for authorization.
`X-Authorization: Bearer $JWT_TOKEN_VALUE`""");
Method to refresh JWT token. Provide a valid refresh token to get a new JWT token.
The response contains a new token that can be used for authorization.
`X-Authorization: Bearer $JWT_TOKEN_VALUE`""");
var requestBody = new RequestBody().description("Refresh token request")
.content(new Content().addMediaType(APPLICATION_JSON_VALUE,
@ -291,8 +309,9 @@ public class SwaggerConfiguration {
return (routerOperation, handlerMethod) -> {
String[] pNames = localSpringDocParameterNameDiscoverer.getParameterNames(handlerMethod.getMethod());
String[] reflectionParametersNames = Arrays.stream(handlerMethod.getMethod().getParameters()).map(java.lang.reflect.Parameter::getName).toArray(String[]::new);
if (pNames == null || Arrays.stream(pNames).anyMatch(Objects::isNull))
if (pNames == null || Arrays.stream(pNames).anyMatch(Objects::isNull)) {
pNames = reflectionParametersNames;
}
MethodParameter[] parameters = handlerMethod.getMethodParameters();
List<String> requestParams = new ArrayList<>();
for (var i = 0; i < parameters.length; i++) {
@ -324,26 +343,25 @@ public class SwaggerConfiguration {
}
private OpenApiCustomizer customOpenApiCustomizer() {
var loginForm = new SecurityRequirement().addList("HTTP login form", Arrays.asList(
Authority.SYS_ADMIN.name(),
Authority.TENANT_ADMIN.name(),
Authority.CUSTOMER_USER.name()
));
var loginRequirement = createSecurityRequirement(LOGIN_PASSWORD_SCHEME);
var apiKeyRequirement = createSecurityRequirement(API_KEY_SCHEME);
return openAPI -> {
var paths = openAPI.getPaths();
paths.entrySet().stream().peek(entry -> {
securityCustomization(loginForm, entry);
if (!entry.getKey().equals(LOGIN_ENDPOINT)) {
defaultErrorResponsesCustomization(entry.getValue());
}
}).map(this::tagsCustomization).filter(Objects::nonNull).distinct().sorted(Comparator.comparing(Tag::getName)).forEach(openAPI::addTagsItem);
paths.entrySet().stream()
.peek(entry -> {
securityCustomization(entry, loginRequirement, apiKeyRequirement);
if (!entry.getKey().equals(LOGIN_ENDPOINT)) {
defaultErrorResponsesCustomization(entry.getValue());
}
})
.map(this::extractTagFromPath).filter(Objects::nonNull).distinct().sorted(Comparator.comparing(Tag::getName)).forEach(openAPI::addTagsItem);
var pathItemsByTags = new TreeMap<String, Map<String, PathItem>>();
paths.forEach((k, v) -> {
var tagItem = tagItemFromPathItem(v);
if (tagItem != null) {
var pathItemMap = pathItemsByTags.computeIfAbsent(tagItem, k1 -> new TreeMap<>());
pathItemMap.put(k, v);
pathItemsByTags.computeIfAbsent(tagItem, k1 -> new TreeMap<>()).put(k, v);
}
});
var sortedPaths = new Paths();
@ -357,13 +375,17 @@ public class SwaggerConfiguration {
};
}
private SecurityRequirement createSecurityRequirement(String schemeName) {
return new SecurityRequirement().addList(schemeName, Arrays.asList(
Authority.SYS_ADMIN.name(),
Authority.TENANT_ADMIN.name(),
Authority.CUSTOMER_USER.name()
));
}
private Tag tagsCustomization(Map.Entry<String, PathItem> entry) {
var tagItem = tagItemFromPathItem(entry.getValue());
if (tagItem != null) {
return tagFromTagItem(tagItem);
}
return null;
private Tag extractTagFromPath(Map.Entry<String, PathItem> entry) {
var tagName = tagItemFromPathItem(entry.getValue());
return tagName != null ? tagFromTagItem(tagName) : null;
}
private String tagItemFromPathItem(PathItem item) {
@ -383,17 +405,20 @@ public class SwaggerConfiguration {
StringBuilder sb = new StringBuilder();
for (String word : words) {
sb.append(word.substring(0, 1).toUpperCase());
sb.append(word.substring(1).toLowerCase());
sb.append(" ");
if (!word.isEmpty()) {
sb.append(word.substring(0, 1).toUpperCase());
sb.append(word.substring(1).toLowerCase());
sb.append(" ");
}
}
return new Tag().name(tagItem).description(sb.toString().trim());
}
private void defaultErrorResponsesCustomization(PathItem pathItem) {
pathItem.readOperationsMap().forEach(((httpMethod, operation) -> {
pathItem.readOperationsMap().forEach((httpMethod, operation) -> {
var errorResponses = httpMethod.equals(PathItem.HttpMethod.POST) ? defaultPostErrorResponses : defaultErrorResponses;
var responses = operation.getResponses();
if (responses == null) {
responses = errorResponses;
@ -406,16 +431,19 @@ public class SwaggerConfiguration {
});
}
operation.setResponses(responses);
}));
});
}
private void securityCustomization(SecurityRequirement loginForm, Map.Entry<String, PathItem> entry) {
private void securityCustomization(Map.Entry<String, PathItem> entry, SecurityRequirement jwtBearerRequirement, SecurityRequirement apiKeyRequirement) {
var path = entry.getKey();
if (path.matches(securityPathRegex) && !path.matches(nonSecurityPathRegex) && !path.equals(LOGIN_ENDPOINT)) {
if (path.matches(securityPathRegex) && !path.matches(nonSecurityPathRegex) && !path.equals(LOGIN_ENDPOINT) && !path.equals(REFRESH_TOKEN_ENDPOINT)) {
entry.getValue()
.readOperationsMap()
.values()
.forEach(operation -> operation.addSecurityItem(loginForm));
.forEach(operation -> {
operation.addSecurityItem(jwtBearerRequirement);
operation.addSecurityItem(apiKeyRequirement);
});
}
}
@ -430,6 +458,7 @@ public class SwaggerConfiguration {
private static ApiResponses defaultErrorResponses(boolean isPost) {
ApiResponses apiResponses = new ApiResponses();
apiResponses.addApiResponse("400", errorResponse("400", "Bad Request",
ThingsboardErrorResponse.of(isPost ? "Invalid request body" : "Invalid UUID string: 123", ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST)));
@ -465,8 +494,7 @@ public class SwaggerConfiguration {
ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))
)
));
var credentialsExpiredSchema = new Schema<ThingsboardCredentialsExpiredResponse>();
credentialsExpiredSchema.$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse");
var credentialsExpiredSchema = new Schema<ThingsboardCredentialsExpiredResponse>().$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse");
apiResponses.addApiResponse("401 ", errorResponse("Unauthorized (**Expired credentials**)",
Map.of(
"credentials-expired", errorExample("Expired credentials",
@ -482,15 +510,13 @@ public class SwaggerConfiguration {
}
private static ApiResponse errorResponse(String description, Map<String, Example> examples) {
var schema = new Schema<ThingsboardErrorResponse>();
schema.$ref("#/components/schemas/ThingsboardErrorResponse");
var schema = new Schema<ThingsboardErrorResponse>().$ref("#/components/schemas/ThingsboardErrorResponse");
return errorResponse(description, examples, schema);
}
private static ApiResponse errorResponse(String description, Map<String, Example> examples, Schema<? extends ThingsboardErrorResponse> errorResponseSchema) {
MediaType mediaType = new MediaType().schema(errorResponseSchema);
mediaType.setExamples(examples);
Content content = new Content().addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType);
MediaType mediaType = new MediaType().schema(errorResponseSchema).examples(examples);
Content content = new Content().addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType);
return new ApiResponse().description(description).content(content);
}

86
application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java

@ -17,33 +17,30 @@ package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.edqs.EdqsState;
import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.AvailableEntityKeys;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityFilter;
import org.thingsboard.server.common.msg.edqs.EdqsApiService;
import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -51,52 +48,46 @@ import org.thingsboard.server.service.query.EntityQueryService;
import org.thingsboard.server.service.security.permission.Operation;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_SCOPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_DATA_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
public class EntityQueryController extends BaseController {
@Autowired
private EntityQueryService entityQueryService;
@Autowired
private EdqsService edqsService;
@Autowired
private EdqsApiService edqsApiService;
private final EntityQueryService entityQueryService;
private final EdqsService edqsService;
private static final int MAX_PAGE_SIZE = 100;
@ApiOperation(value = "Count Entities by Query", notes = ENTITY_COUNT_QUERY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entitiesQuery/count", method = RequestMethod.POST)
@ResponseBody
@PostMapping("/entitiesQuery/count")
public long countEntitiesByQuery(
@Parameter(description = "A JSON value representing the entity count query. See API call notes above for more details.")
@RequestBody EntityCountQuery query) throws ThingsboardException {
checkNotNull(query);
resolveQuery(query);
return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query);
return entityQueryService.countEntitiesByQuery(getCurrentUser(), query);
}
@ApiOperation(value = "Find Entity Data by Query", notes = ENTITY_DATA_QUERY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entitiesQuery/find", method = RequestMethod.POST)
@ResponseBody
@PostMapping("/entitiesQuery/find")
public PageData<EntityData> findEntityDataByQuery(
@Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.")
@RequestBody EntityDataQuery query) throws ThingsboardException {
checkNotNull(query);
resolveQuery(query);
return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query);
return entityQueryService.findEntityDataByQuery(getCurrentUser(), query);
}
@ApiOperation(value = "Find Alarms by Query", notes = ALARM_DATA_QUERY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarmsQuery/find", method = RequestMethod.POST)
@ResponseBody
@PostMapping("/alarmsQuery/find")
public PageData<AlarmData> findAlarmDataByQuery(
@Parameter(description = "A JSON value representing the alarm data query. See API call notes above for more details.")
@RequestBody AlarmDataQuery query) throws ThingsboardException {
@ -107,13 +98,12 @@ public class EntityQueryController extends BaseController {
checkUserId(assigneeId, Operation.READ);
}
resolveQuery(query);
return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query);
return entityQueryService.findAlarmDataByQuery(getCurrentUser(), query);
}
@ApiOperation(value = "Count Alarms by Query (countAlarmsByQuery)", notes = "Returns the number of alarms that match the query definition.")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarmsQuery/count", method = RequestMethod.POST)
@ResponseBody
@PostMapping("/alarmsQuery/count")
public long countAlarmsByQuery(@Parameter(description = "A JSON value representing the alarm count query.")
@RequestBody AlarmCountQuery query) throws ThingsboardException {
checkNotNull(query);
@ -122,31 +112,47 @@ public class EntityQueryController extends BaseController {
checkUserId(assigneeId, Operation.READ);
}
resolveQuery(query);
return this.entityQueryService.countAlarmsByQuery(getCurrentUser(), query);
return entityQueryService.countAlarmsByQuery(getCurrentUser(), query);
}
@ApiOperation(value = "Find Entity Keys by Query",
notes = "Uses entity data query (see 'Find Entity Data by Query') to find first 100 entities. Then fetch and return all unique time-series and/or attribute keys. Used mostly for UI hints.")
@ApiOperation(
value = "Find Available Entity Keys by Query",
notes = """
Returns unique time series and/or attribute key names from entities matching the query.\n
Executes the Entity Data Query to find up to 100 entities, then fetches and aggregates all distinct key names.\n
Primarily used for UI features like autocomplete suggestions.""" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entitiesQuery/find/keys", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> findEntityTimeseriesAndAttributesKeysByQuery(
@Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.")
@PostMapping("/entitiesQuery/find/keys")
public DeferredResult<AvailableEntityKeys> findAvailableEntityKeysByQuery(
@Parameter(description = "Entity data query to find entities. Page size is capped at 100.")
@RequestBody EntityDataQuery query,
@Parameter(description = "Include all unique time-series keys to the result.")
@RequestParam("timeseries") boolean isTimeseries,
@Parameter(description = "Include all unique attribute keys to the result.")
@RequestParam("attributes") boolean isAttributes,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}))
@RequestParam(value = "scope", required = false) String scope) throws ThingsboardException {
TenantId tenantId = getTenantId();
checkNotNull(query);
// fixme: combination of timeseries = false and attributes = false is allowed, but always results in empty response, therefore does not make any sense
// such combinations should NOT be allowed, but changing this will break clients
@Parameter(description = """
When true, includes unique time series key names in the response.
When false, the 'timeseries' list will be empty.""")
@RequestParam("timeseries") boolean includeTimeseries,
@Parameter(description = """
When true, includes unique attribute key names in the response.
When false, the 'attribute' list will be empty. Use 'scope' parameter to filter by attribute scope.""")
@RequestParam("attributes") boolean includeAttributes,
@Parameter(description = """
Filters attribute keys by scope. Only applies when 'attributes' is true.
If not specified, returns attribute keys from all scopes.""",
schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}))
@RequestParam(value = "scope", required = false) AttributeScope scope
) throws ThingsboardException {
resolveQuery(query);
EntityDataPageLink pageLink = query.getPageLink();
if (pageLink.getPageSize() > MAX_PAGE_SIZE) {
pageLink.setPageSize(MAX_PAGE_SIZE);
}
return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes, scope);
return wrapFuture(entityQueryService.getKeysByQuery(getCurrentUser(), getTenantId(), query, includeTimeseries, includeAttributes, scope));
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")

70
application/src/main/java/org/thingsboard/server/controller/TbResourceController.java

@ -32,12 +32,16 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
@ -215,6 +219,7 @@ public class TbResourceController extends BaseController {
"\n\nResource combination of the title with the key is unique in the scope of tenant. " +
"Remove 'id', 'tenantId' from the request body example (below) to create new Resource entity." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@Deprecated // resource should be save or update with an upload request
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping(value = "/resource")
public TbResourceInfo saveResource(@Parameter(description = "A JSON value representing the Resource.")
@ -224,6 +229,71 @@ public class TbResourceController extends BaseController {
return tbResourceService.save(resource, getCurrentUser());
}
@ApiOperation(value = "Upload Resource via Multipart File (uploadResource)",
notes = "Create the Resource using multipart file upload. " +
"\n\nResource combination of the title with the key is unique in the scope of tenant. " +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping(value = "/resource/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public TbResourceInfo uploadResource(@Parameter(description = "Resource title.", example = "Title")
@RequestPart(name = "title", required = false) String title,
@Parameter(description = "Resource type.")
@RequestPart(name = "resourceType") String resourceTypeStr,
@Parameter(description = "Resource descriptor (JSON).")
@RequestPart(name = "descriptor", required = false) String descriptor,
@Parameter(description = "Resource sub type.")
@RequestPart(name = "resourceSubType", required = false) String resourceSubTypeStr,
@Parameter(description = "Resource file.")
@RequestPart MultipartFile file) throws Exception {
TbResource resource = new TbResource();
resource.setTenantId(getTenantId());
resource.setTitle(StringUtils.isNotEmpty(title) ? title : file.getOriginalFilename());
ResourceType resourceType = ResourceType.valueOf(resourceTypeStr);
resource.setResourceType(resourceType);
if (StringUtils.isNotEmpty(descriptor)) {
resource.setDescriptor(JacksonUtil.toJsonNode(descriptor));
} else {
String mediaType = resourceType.getMediaType() != null ? resourceType.getMediaType() : file.getContentType();
resource.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mediaType));
}
if (StringUtils.isNotEmpty(resourceSubTypeStr)) {
resource.setResourceSubType(ResourceSubType.valueOf(resourceSubTypeStr));
}
resource.setFileName(file.getOriginalFilename());
resource.setData(file.getBytes());
checkEntity(resource.getId(), resource, Resource.TB_RESOURCE);
return tbResourceService.save(resource, getCurrentUser());
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PutMapping(value = "/resource/{id}/data", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public TbResourceInfo updateResourceData(@Parameter(description = "Unique identifier of the Resource to update", required = true)
@PathVariable UUID id,
@Parameter(description = "Resource file.")
@RequestPart MultipartFile file) throws Exception {
TbResourceId tbResourceId = new TbResourceId(id);
TbResource resource = checkResourceId(tbResourceId, Operation.WRITE);
resource.setFileName(file.getOriginalFilename());
resource.setData(file.getBytes());
return tbResourceService.save(resource, getCurrentUser());
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PutMapping("/resource/{id}/info")
public TbResourceInfo updateResourceInfo(@Parameter(description = "Unique identifier of the Resource to update", required = true)
@PathVariable UUID id,
@Parameter(description = "A JSON value representing the Resource Info.")
@RequestBody TbResourceInfo resourceInfo) throws Exception {
TbResourceId tbResourceId = new TbResourceId(id);
checkResourceInfoId(tbResourceId, Operation.WRITE);
resourceInfo.setId(tbResourceId);
TbResource resource = new TbResource(resourceInfo);
return tbResourceService.save(resource, getCurrentUser());
}
@ApiOperation(value = "Get Resource Infos (getResources)",
notes = "Returns a page of Resource Info objects owned by tenant or sysadmin. " +
PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)

8
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageDataIterable;
@ -243,11 +244,12 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
@Override
public EntityId getProfileId(TenantId tenantId, EntityId entityId) {
return switch (entityId.getEntityType()) {
case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId();
case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId();
HasId<? extends EntityId> profile = switch (entityId.getEntityType()) {
case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId);
case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId);
default -> null;
};
return profile != null ? profile.getId() : null;
}
@Override

7
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -212,7 +212,7 @@ public class CalculatedFieldCtx implements Closeable {
this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024;
}
public boolean isRequiresScheduledReevaluation() {
public boolean requiresScheduledReevaluation() {
long now = System.currentTimeMillis();
if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration entityAggregationConfig) {
Watermark watermark = entityAggregationConfig.getWatermark();
@ -232,8 +232,8 @@ public class CalculatedFieldCtx implements Closeable {
}
boolean requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation();
if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) {
long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval());
if (requiresScheduledReevaluation) {
long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval());
if (now - lastReevaluationTs >= reevaluationIntervalMillis) {
lastReevaluationTs = now;
return true;
@ -642,6 +642,9 @@ public class CalculatedFieldCtx implements Closeable {
// if the rules have any changes not tracked by hasStateChanges
return true;
}
if (!thisConfig.propagationSettingsEqual(otherConfig)) {
return true;
}
}
if (scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis) {
return true;

6
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java

@ -43,7 +43,6 @@ import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory;
import org.thingsboard.server.service.cf.AbstractCalculatedFieldStateService;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToString;
@ -105,11 +104,6 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta
this.stateProducer = (TbKafkaProducerTemplate<TbProtoQueueMsg<CalculatedFieldStateProto>>) queueFactory.createCalculatedFieldStateProducer();
}
@Override
public void restore(QueueKey queueKey, Set<TopicPartitionInfo> partitions) {
stateService.update(queueKey, partitions, null);
}
@Override
protected void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME, stateId.tenantId(), stateId.entityId());

55
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java

@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAl
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.BooleanFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.ComplexFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NoDataFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate;
import org.thingsboard.server.common.data.audit.ActionType;
@ -70,6 +71,8 @@ import java.util.function.Function;
import static org.thingsboard.server.common.data.StringUtils.equalsAny;
import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Cause.NEW_EVENT;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Cause.SCHEDULED_REEVALUATION;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.FALSE;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.NOT_YET_TRUE;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE;
@ -212,9 +215,9 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
state.setDurationCheckFuture(future);
}
}
return evalResult;
return evalResult.withCause(NEW_EVENT);
} else {
return state.reeval(System.currentTimeMillis(), ctx);
return state.reeval(System.currentTimeMillis(), ctx).withCause(SCHEDULED_REEVALUATION);
}
}, ctx);
return Futures.immediateFuture(AlarmCalculatedFieldResult.builder()
@ -247,12 +250,12 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
private TbAlarmResult createOrClearAlarms(Function<AlarmRuleState, AlarmEvalResult> evalFunction,
CalculatedFieldCtx ctx) {
TbAlarmResult result = null;
AlarmEvalResult evalResult = null;
AlarmRuleState resultState = null;
AlarmRuleState.StateInfo resultStateInfo = null;
for (AlarmRuleState state : createRuleStates.values()) {
AlarmEvalResult evalResult = evalFunction.apply(state);
evalResult = evalFunction.apply(state);
log.debug("Evaluated create rule {} with args {}. Result: {}", state, arguments, evalResult);
if (evalResult.getStatus() == TRUE) {
resultState = state;
@ -262,13 +265,14 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
}
}
TbAlarmResult result = null;
if (resultState != null) {
result = calculateAlarmResult(resultState, ctx);
result = calculateAlarmResult(resultState, evalResult, ctx);
resultStateInfo = resultState.getStateInfo();
log.debug("Alarm result for state {}: {}", resultState, result);
clearState(clearRuleState);
} else if (currentAlarm != null && clearRuleState != null) {
AlarmEvalResult evalResult = evalFunction.apply(clearRuleState);
evalResult = evalFunction.apply(clearRuleState);
log.debug("Evaluated clear rule {} with args {}. Result: {}", clearRuleState, arguments, evalResult);
if (evalResult.getStatus() == TRUE) {
resultStateInfo = clearRuleState.getStateInfo();
@ -315,21 +319,25 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
}
}
private TbAlarmResult calculateAlarmResult(AlarmRuleState ruleState, CalculatedFieldCtx ctx) {
private TbAlarmResult calculateAlarmResult(AlarmRuleState ruleState, AlarmEvalResult evalResult, CalculatedFieldCtx ctx) {
AlarmSeverity severity = ruleState.getSeverity();
if (currentAlarm != null) {
currentAlarm.setEndTs(System.currentTimeMillis());
AlarmSeverity oldSeverity = currentAlarm.getSeverity();
// Skip update if severity is decreased.
if (severity.ordinal() <= oldSeverity.ordinal()) {
currentAlarm.setDetails(createDetails(ruleState));
currentAlarm.setSeverity(severity);
AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm));
currentAlarm = result.getAlarm();
return TbAlarmResult.fromAlarmResult(result);
} else {
if (severity.ordinal() > oldSeverity.ordinal()) {
log.trace("Skipping alarm update for result state {} for eval result {} because severity is decreased", ruleState, evalResult);
return null;
}
if (severity.ordinal() == oldSeverity.ordinal() && evalResult.getCause() == SCHEDULED_REEVALUATION) {
log.trace("Skipping alarm update for result state {} for eval result {}", ruleState, evalResult);
return null;
}
currentAlarm.setEndTs(System.currentTimeMillis());
currentAlarm.setDetails(createDetails(ruleState));
currentAlarm.setSeverity(severity);
AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm));
currentAlarm = result.getAlarm();
return TbAlarmResult.fromAlarmResult(result);
} else {
var newAlarm = new Alarm();
newAlarm.setType(alarmType);
@ -338,7 +346,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
newAlarm.setSeverity(severity);
long startTs = latestTimestamp;
long currentTime = System.currentTimeMillis();
if (startTs == 0L || startTs > currentTime) {
if (startTs <= 0L || startTs > currentTime) {
startTs = currentTime;
}
newAlarm.setStartTs(startTs);
@ -428,6 +436,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
case STRING -> evalStrPredicate(argument, (StringFilterPredicate) predicate);
case NUMERIC -> evalNumPredicate(argument, (NumericFilterPredicate) predicate);
case BOOLEAN -> evalBooleanPredicate(argument, (BooleanFilterPredicate) predicate);
case NO_DATA -> evalNoDataPredicate(argument, (NoDataFilterPredicate) predicate);
case COMPLEX -> evalComplexPredicate(argument, (ComplexFilterPredicate) predicate);
};
}
@ -512,6 +521,18 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
};
}
private boolean evalNoDataPredicate(SingleValueArgumentEntry argument, NoDataFilterPredicate predicate) {
long passedMs = System.currentTimeMillis() - argument.getTs();
long duration = resolveValue(predicate.getDuration(), KvUtil::getLongValue);
if (duration > 0) {
long requiredDuration = predicate.getUnit().toMillis(duration);
log.trace("[{}] No data for argument {} during {} ms, required duration: {} ms", ctx, argument, passedMs, requiredDuration);
return passedMs >= requiredDuration;
} else {
return false;
}
}
protected <T> T resolveValue(AlarmConditionValue<T> conditionValue, Function<KvEntry, T> mapper) {
T value = conditionValue.getStaticValue();
if (value == null) {

10
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java

@ -30,6 +30,7 @@ public class AlarmEvalResult {
private final Status status;
private final long leftDuration;
private final long leftEvents;
private Cause cause;
public AlarmEvalResult(Status status) {
this(status, 0, 0);
@ -39,8 +40,17 @@ public class AlarmEvalResult {
return new AlarmEvalResult(Status.NOT_YET_TRUE, leftDuration, leftEvents);
}
public AlarmEvalResult withCause(Cause cause) {
this.cause = cause;
return this;
}
public enum Status {
FALSE, NOT_YET_TRUE, TRUE;
}
public enum Cause {
NEW_EVENT, SCHEDULED_REEVALUATION;
}
}

27
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java

@ -43,6 +43,8 @@ import java.time.ZonedDateTime;
import java.util.Optional;
import java.util.concurrent.ScheduledFuture;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE;
@Data
@Slf4j
public class AlarmRuleState {
@ -81,16 +83,20 @@ public class AlarmRuleState {
boolean active = isActive(ts);
switch (condition.getType()) {
case SIMPLE, REPEATING -> {
if (this.active == null || active != this.active) {
this.active = active;
if (active) {
return doEval(false, ctx);
}
boolean activeChanged = this.active == null || active != this.active;
this.active = active;
if (!active) {
return AlarmEvalResult.EMPTY;
}
if (active) {
return AlarmEvalResult.NOT_YET_TRUE;
} else {
return AlarmEvalResult.FALSE;
if ((condition.hasSchedule() && activeChanged) ||
condition.getExpression().requiresScheduledReevaluation()) {
AlarmEvalResult result = doEval(false, ctx);
if (result.getStatus() == TRUE) {
return result;
} else {
return AlarmEvalResult.EMPTY;
}
}
}
case DURATION -> {
@ -116,7 +122,7 @@ public class AlarmRuleState {
}
}
}
return AlarmEvalResult.FALSE;
return AlarmEvalResult.EMPTY;
}
public AlarmEvalResult doEval(boolean newEvent, CalculatedFieldCtx ctx) {
@ -338,6 +344,7 @@ public class AlarmRuleState {
}
public record StateInfo(Long eventCount, Long duration) {
static final StateInfo EMPTY = new StateInfo(null, null);
}

129
application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java

@ -15,24 +15,18 @@
*/
package org.thingsboard.server.service.query;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.KvUtil;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@ -40,6 +34,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.AvailableEntityKeys;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.EntityCountQuery;
@ -56,16 +51,13 @@ import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.sql.query.EntityKeyMapping;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
@ -73,9 +65,10 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.Futures.immediateFuture;
@Service
@Slf4j
@TbCoreComponent
@ -138,20 +131,12 @@ public class DefaultEntityQueryService implements EntityQueryService {
}
private <T> void resolveDynamicValue(DynamicValue<T> dynamicValue, SecurityUser user, FilterPredicateType predicateType) {
EntityId entityId;
switch (dynamicValue.getSourceType()) {
case CURRENT_TENANT:
entityId = user.getTenantId();
break;
case CURRENT_CUSTOMER:
entityId = user.getCustomerId();
break;
case CURRENT_USER:
entityId = user.getId();
break;
default:
throw new RuntimeException("Not supported operation for source type: {" + dynamicValue.getSourceType() + "}");
}
EntityId entityId = switch (dynamicValue.getSourceType()) {
case CURRENT_TENANT -> user.getTenantId();
case CURRENT_CUSTOMER -> user.getCustomerId();
case CURRENT_USER -> user.getId();
default -> throw new RuntimeException("Not supported operation for source type: {" + dynamicValue.getSourceType() + "}");
};
try {
Optional<AttributeKvEntry> valueOpt = attributesService.find(user.getTenantId(), entityId,
@ -242,101 +227,51 @@ public class DefaultEntityQueryService implements EntityQueryService {
}
@Override
public DeferredResult<ResponseEntity> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
boolean isTimeseries, boolean isAttributes, String attributesScope) {
final DeferredResult<ResponseEntity> response = new DeferredResult<>();
public ListenableFuture<AvailableEntityKeys> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
boolean isTimeseries, boolean isAttributes, AttributeScope scope) {
if (!isAttributes && !isTimeseries) {
replyWithEmptyResponse(response);
return response;
return immediateFuture(AvailableEntityKeys.none());
}
List<EntityId> ids = this.findEntityDataByQuery(securityUser, query).getData().stream()
List<EntityId> ids = findEntityDataByQuery(securityUser, query).getData().stream()
.map(EntityData::getEntityId)
.collect(Collectors.toList());
.toList();
if (ids.isEmpty()) {
replyWithEmptyResponse(response);
return response;
return immediateFuture(AvailableEntityKeys.none());
}
Set<EntityType> types = ids.stream().map(EntityId::getEntityType).collect(Collectors.toSet());
final ListenableFuture<List<String>> timeseriesKeysFuture;
final ListenableFuture<List<String>> attributesKeysFuture;
ListenableFuture<List<String>> timeseriesKeysFuture;
ListenableFuture<List<String>> attributesKeysFuture;
if (isTimeseries) {
timeseriesKeysFuture = dbCallbackExecutor.submit(() -> timeseriesService.findAllKeysByEntityIds(tenantId, ids));
timeseriesKeysFuture = timeseriesService.findAllKeysByEntityIdsAsync(tenantId, ids);
} else {
timeseriesKeysFuture = null;
timeseriesKeysFuture = immediateFuture(Collections.emptyList());
}
if (isAttributes) {
Map<EntityType, List<EntityId>> typesMap = ids.stream().collect(Collectors.groupingBy(EntityId::getEntityType));
List<ListenableFuture<List<String>>> futures = new ArrayList<>(typesMap.size());
typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIds(tenantId, entityIds, attributesScope))));
typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIds(tenantId, entityIds, scope))));
attributesKeysFuture = Futures.transform(Futures.allAsList(futures), lists -> {
if (CollectionUtils.isEmpty(lists)) {
return Collections.emptyList();
}
return lists.stream().flatMap(List::stream).distinct().sorted().collect(Collectors.toList());
}, dbCallbackExecutor);
} else {
attributesKeysFuture = null;
}
if (isTimeseries && isAttributes) {
Futures.whenAllComplete(timeseriesKeysFuture, attributesKeysFuture).run(() -> {
try {
replyWithResponse(response, types, timeseriesKeysFuture.get(), attributesKeysFuture.get());
} catch (Exception e) {
log.error("Failed to fetch timeseries and attributes keys!", e);
AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
}
return lists.stream().flatMap(List::stream).distinct().sorted().toList();
}, dbCallbackExecutor);
} else if (isTimeseries) {
addCallback(timeseriesKeysFuture, keys -> replyWithResponse(response, types, keys, null),
error -> {
log.error("Failed to fetch timeseries keys!", error);
AccessValidator.handleError(error, response, HttpStatus.INTERNAL_SERVER_ERROR);
});
} else {
addCallback(attributesKeysFuture, keys -> replyWithResponse(response, types, null, keys),
error -> {
log.error("Failed to fetch attributes keys!", error);
AccessValidator.handleError(error, response, HttpStatus.INTERNAL_SERVER_ERROR);
});
attributesKeysFuture = immediateFuture(Collections.emptyList());
}
return response;
}
private void replyWithResponse(DeferredResult<ResponseEntity> response, Set<EntityType> types, List<String> timeseriesKeys, List<String> attributesKeys) {
ObjectNode json = JacksonUtil.newObjectNode();
addItemsToArrayNode(json.putArray("entityTypes"), types);
addItemsToArrayNode(json.putArray("timeseries"), timeseriesKeys);
addItemsToArrayNode(json.putArray("attribute"), attributesKeys);
response.setResult(new ResponseEntity<>(json, HttpStatus.OK));
}
private void replyWithEmptyResponse(DeferredResult<ResponseEntity> response) {
replyWithResponse(response, Collections.emptySet(), Collections.emptyList(), Collections.emptyList());
}
private void addItemsToArrayNode(ArrayNode arrayNode, Collection<?> collection) {
if (!CollectionUtils.isEmpty(collection)) {
collection.forEach(item -> arrayNode.add(item.toString()));
}
}
private void addCallback(ListenableFuture<List<String>> future, Consumer<List<String>> success, Consumer<Throwable> error) {
Futures.addCallback(future, new FutureCallback<List<String>>() {
@Override
public void onSuccess(@Nullable List<String> keys) {
success.accept(keys);
}
@Override
public void onFailure(Throwable t) {
error.accept(t);
}
}, dbCallbackExecutor);
return Futures.whenAllComplete(timeseriesKeysFuture, attributesKeysFuture)
.call(() -> {
try {
return new AvailableEntityKeys(types, Futures.getDone(timeseriesKeysFuture), Futures.getDone(attributesKeysFuture));
} catch (ExecutionException e) {
throw new ThingsboardException(e.getCause(), ThingsboardErrorCode.DATABASE);
}
}, dbCallbackExecutor);
}
}

9
application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java

@ -15,13 +15,14 @@
*/
package org.thingsboard.server.service.query;
import org.springframework.http.ResponseEntity;
import org.springframework.web.context.request.async.DeferredResult;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.AvailableEntityKeys;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
@ -37,7 +38,7 @@ public interface EntityQueryService {
long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query);
DeferredResult<ResponseEntity> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
boolean isTimeseries, boolean isAttributes, String attributesScope);
ListenableFuture<AvailableEntityKeys> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
boolean isTimeseries, boolean isAttributes, AttributeScope scope);
}

2
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -75,7 +75,7 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
ActionType actionType = resource.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = resource.getTenantId();
try {
if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType())) {
if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType()) && resource.getId() == null) {
toLwm2mResource(resource);
} else if (resource.getResourceKey() == null) {
resource.setResourceKey(resource.getFileName());

2
application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java

@ -19,8 +19,8 @@ import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDeleteResult;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;

9
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.sync.ie.exporting.impl;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
@ -24,6 +25,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -170,6 +172,13 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
});
}
}
if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration alarmCfConfig) {
alarmCfConfig.getAllRules().map(Pair::getValue).forEach(rule -> {
if (rule.getDashboardId() != null) {
rule.setDashboardId(getExternalIdOrElseInternal(ctx, rule.getDashboardId()));
}
});
}
});
return calculatedFields;
}

9
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java

@ -22,6 +22,7 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
@ -35,6 +36,7 @@ import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -338,6 +340,13 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
});
}
}
if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration alarmCfConfig) {
alarmCfConfig.getAllRules().map(Pair::getValue).forEach(rule -> {
if (rule.getDashboardId() != null) {
rule.setDashboardId(idProvider.getInternalId(rule.getDashboardId(), ctx.isFinalImportAttempt()));
}
});
}
}).toList();
for (CalculatedField existingField : existing) {

4
application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java

@ -73,7 +73,7 @@ public class LwM2mObjectModelUtils {
try {
List<ObjectModel> objectModels =
ddfFileParser.parse(new ByteArrayInputStream(resource.getData()), resource.getSearchText());
if (objectModels.size() == 0) {
if (objectModels.isEmpty()) {
return null;
} else {
ObjectModel obj = objectModels.get(0);
@ -95,7 +95,7 @@ public class LwM2mObjectModelUtils {
resources.add(lwM2MResourceObserve);
}
});
if (isSave || resources.size() > 0) {
if (isSave || !resources.isEmpty()) {
instance.setResources(resources.toArray(LwM2mResourceObserve[]::new));
lwM2mObject.setInstances(new LwM2mInstance[]{instance});
return lwM2mObject;

2
application/src/main/resources/thingsboard.yml

@ -542,7 +542,7 @@ actors:
check_interval: "${ACTORS_CALCULATED_FIELDS_CHECK_INTERVAL_SEC:60}"
alarms:
# Interval in seconds to re-evaluate Alarm rules that have a time schedule. 2 minutes by default.
reevaluation_interval: "${ACTORS_ALARMS_REEVALUATION_INTERVAL_SEC:120}"
reevaluation_interval: "${ACTORS_ALARMS_REEVALUATION_INTERVAL_SEC:60}"
debug:
settings:

56
application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmC
import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NoDataFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate.NumericOperation;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate;
@ -475,6 +476,61 @@ public class AlarmRulesTest extends AbstractControllerTest {
});
}
@Test
public void testCreateAlarm_noDataPredicate() throws Exception {
Argument temperatureArgument = new Argument();
temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
temperatureArgument.setDefaultValue("0");
Map<String, Argument> arguments = Map.of(
"temperature", temperatureArgument
);
long majorNoDataDuration = 3L;
long criticalNoDataDuration = 10L;
SimpleAlarmConditionExpression majorExpression = new SimpleAlarmConditionExpression();
AlarmConditionFilter majorFilter = new AlarmConditionFilter();
majorFilter.setArgument("temperature");
majorFilter.setValueType(EntityKeyValueType.NUMERIC);
majorFilter.setPredicates(List.of(
new NumericFilterPredicate(NumericOperation.GREATER, new AlarmConditionValue<>(25.0, null)),
new NoDataFilterPredicate(TimeUnit.SECONDS, new AlarmConditionValue<Long>(majorNoDataDuration, null))
));
majorExpression.setFilters(List.of(majorFilter));
SimpleAlarmConditionExpression criticalExpression = new SimpleAlarmConditionExpression();
AlarmConditionFilter criticalFilter = new AlarmConditionFilter();
criticalFilter.setArgument("temperature");
criticalFilter.setValueType(EntityKeyValueType.NUMERIC);
criticalFilter.setPredicates(List.of(
new NumericFilterPredicate(NumericOperation.GREATER, new AlarmConditionValue<>(25.0, null)),
new NoDataFilterPredicate(TimeUnit.SECONDS, new AlarmConditionValue<Long>(criticalNoDataDuration, null))
));
criticalExpression.setFilters(List.of(criticalFilter));
Map<AlarmSeverity, Condition> createRules = Map.of(
AlarmSeverity.MAJOR, new Condition(majorExpression, null, null),
AlarmSeverity.CRITICAL, new Condition(criticalExpression, null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "No Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
checkAlarmResult(calculatedField, alarmResult -> {
assertThat(alarmResult.isSeverityUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
}
@Test
public void testChangeAlarmType() throws Exception {
Argument temperatureArgument = new Argument();

31
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -84,6 +84,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.StringUtils;
@ -93,6 +94,7 @@ import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldInfo;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
@ -1207,8 +1209,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
Map<CalculatedFieldId, CalculatedFieldState> statesMap = (Map<CalculatedFieldId, CalculatedFieldState>) ReflectionTestUtils.getField(processor, "states");
Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> {
CalculatedFieldState calculatedFieldState = statesMap.get(cfId);
boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs()
< System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() <
System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady);
return isReady;
});
@ -1399,7 +1401,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected List<Job> findJobs(List<JobType> types, List<UUID> entities) throws Exception {
return doGetTypedWithPageLink("/api/jobs?types=" + types.stream().map(Enum::name).collect(Collectors.joining(",")) +
"&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&",
"&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&",
new TypeReference<PageData<Job>>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData();
}
@ -1413,12 +1415,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected void postTelemetry(EntityId entityId, String payload) throws Exception {
doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() +
"/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk());
"/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk());
}
protected void postAttributes(EntityId entityId, AttributeScope scope, String payload) throws Exception {
doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() +
"/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk());
"/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk());
}
protected CalculatedField saveCalculatedField(CalculatedField calculatedField) {
@ -1427,7 +1429,24 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected PageData<CalculatedField> getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception {
return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" +
(type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink);
(type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink);
}
protected PageData<String> getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception {
return doGetTypedWithPageLink("/api/calculatedFields/names?type=" + type + "&",
new TypeReference<PageData<String>>() {}, pageLink);
}
protected List<CalculatedFieldInfo> getCalculatedFields(CalculatedFieldType type,
EntityType entityType,
List<UUID> entities,
List<String> names) throws Exception {
return doGetTypedWithPageLink("/api/calculatedFields?type=" + type + "&" +
(entityType != null ? "entityType=" + entityType + "&" : "") +
(entities != null ? "entities=" + String.join(",",
entities.stream().map(UUID::toString).toList()) + "&" : "") +
(names != null ? names.stream().map(name -> "name=" + name + "&").collect(Collectors.joining("")) : ""),
new TypeReference<PageData<CalculatedFieldInfo>>() {}, new PageLink(10)).getData();
}
protected PageData<EventInfo> getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception {

20
application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -56,9 +55,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
@ -295,23 +292,6 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
assertThat(names.getData()).containsOnly(deviceCalculatedField.getName());
}
private PageData<String> getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception {
return doGetTypedWithPageLink("/api/calculatedFields/names?type=" + type + "&",
new TypeReference<PageData<String>>() {}, pageLink);
}
private List<CalculatedFieldInfo> getCalculatedFields(CalculatedFieldType type,
EntityType entityType,
List<UUID> entities,
List<String> names) throws Exception {
return doGetTypedWithPageLink("/api/calculatedFields?type=" + type + "&" +
(entityType != null ? "entityType=" + entityType + "&" : "") +
(entities != null ? "entities=" + String.join(",",
entities.stream().map(UUID::toString).toList()) + "&" : "") +
(names != null ? names.stream().map(name -> "name=" + name + "&").collect(Collectors.joining("")) : ""),
new TypeReference<PageData<CalculatedFieldInfo>>() {}, new PageLink(10)).getData();
}
@Test
public void testDeleteCalculatedField() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");

7
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java

@ -321,9 +321,10 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
"\n" +
" # Environment variables\n" +
" environment:\n" +
" - host=host.docker.internal\n" +
" - port=1883\n" +
" - accessToken=" + credentials.getCredentialsId() + "\n" +
" - TB_GW_HOST=host.docker.internal\n" +
" - TB_GW_PORT=1883\n" +
" - TB_GW_SECURITY_TYPE=accessToken\n" +
" - TB_GW_ACCESS_TOKEN=" + credentials.getCredentialsId() + "\n" +
"\n" +
" # Volumes bind\n" +
" volumes:\n" +

7
application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java

@ -22,6 +22,7 @@ import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device;
@ -41,6 +42,7 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.StringFilterPredicate;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edqs.util.EdqsRocksDb;
import java.util.ArrayList;
import java.util.Collections;
@ -62,8 +64,11 @@ public class EdqsControllerTest extends AbstractControllerTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@MockitoBean
private EdqsRocksDb edqsRocksDb;
@Before
public void beforeEdqsControllerTest() throws Exception {
public void before() throws Exception {
loginTenantAdmin();
}

5
application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java

@ -19,7 +19,6 @@ import org.assertj.core.api.ThrowingConsumer;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.edqs.EdqsState;
import org.thingsboard.server.common.data.edqs.EdqsState.EdqsApiMode;
@ -33,7 +32,6 @@ import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edqs.util.EdqsRocksDb;
import org.thingsboard.server.queue.discovery.DiscoveryService;
import java.util.concurrent.TimeUnit;
@ -59,9 +57,6 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest {
@Autowired
private DiscoveryService discoveryService;
@MockBean // so that we don't do backup for tests
private EdqsRocksDb edqsRocksDb;
@Before
public void before() {
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.getState().isApiEnabled());

23
application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java

@ -23,6 +23,7 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.ResultActions;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil;
@ -71,6 +72,7 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edqs.util.EdqsRocksDb;
import java.util.ArrayList;
import java.util.Arrays;
@ -102,6 +104,9 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
@Autowired
private QueueStatsService queueStatsService;
@MockitoBean
private EdqsRocksDb edqsRocksDb;
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
@ -436,7 +441,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityTypeFilter assetTypeFilter = new EntityTypeFilter();
assetTypeFilter.setEntityType(EntityType.ASSET);
AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields);
AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields);
PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10);
List<String> retrievedAlarmTypes = alarmPageData.getData().stream().map(AlarmData::getType).toList();
@ -446,8 +451,8 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1");
List<KeyFilter> keyFilters = Collections.singletonList(nameFilter);
AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields);
PageData<AlarmData> filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() {
AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields);
PageData<AlarmData> filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() {
});
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
}
@ -509,16 +514,16 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityTypeFilter assetTypeFilter = new EntityTypeFilter();
assetTypeFilter.setEntityType(EntityType.ASSET);
AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, Collections.emptyList());
AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, Collections.emptyList());
PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10);
PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10);
List<String> retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList();
assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes);
KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1");
List<KeyFilter> keyFilters = Collections.singletonList(nameFilter);
AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList());
PageData<AlarmData> filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() {
AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList());
PageData<AlarmData> filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() {
});
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
}
@ -574,7 +579,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityTypeFilter deviceTypeFilter = new EntityTypeFilter();
deviceTypeFilter.setEntityType(EntityType.DEVICE);
AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields);
AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields);
PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10);
List<String> retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList();
@ -1291,7 +1296,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
findByQueryAndCheck(query, 0);
}
private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer<Integer,EntityData> checkFunction) throws Exception {
private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer<Integer, EntityData> checkFunction) throws Exception {
await()
.alias("data by query")
.atMost(30, TimeUnit.SECONDS)

69
application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java

@ -25,11 +25,12 @@ import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockPart;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
@ -47,15 +48,14 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
@ -64,7 +64,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@DaoSqlTest
public class TbResourceControllerTest extends AbstractControllerTest {
private IdComparator<TbResourceInfo> idComparator = new IdComparator<>();
private final IdComparator<TbResourceInfo> idComparator = new IdComparator<>();
private static final String DEFAULT_FILE_NAME = "test.jks";
private static final String DEFAULT_FILE_NAME_2 = "test2.jks";
@ -126,13 +126,10 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertEquals(DEFAULT_FILE_NAME, savedResource.getResourceKey());
Assert.assertArrayEquals(resource.getData(), download(savedResource.getId()));
TbResource foundResource = doGet("/api/resource/" + savedResource.getId().getId().toString(), TbResource.class);
foundResource.setTitle("My new resource");
foundResource.setData(null);
savedResource = save(foundResource);
Assert.assertEquals(foundResource.getTitle(), savedResource.getTitle());
String resourceTitle = "My new resource";
savedResource.setTitle(resourceTitle);
savedResource = doPut("/api/resource/" + savedResource.getUuidId() + "/info", savedResource, TbResourceInfo.class);
assertThat(savedResource.getTitle()).isEqualTo(resourceTitle);
testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(savedResource, savedResource.getId(), savedResource.getId(),
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
@ -501,8 +498,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.ADDED, cntEntity, cntEntity, cntEntity);
Collections.sort(resources, idComparator);
Collections.sort(loadedResources, idComparator);
resources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(resources, loadedResources);
}
@ -549,8 +546,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED,
jksCntEntity + lwm2mCntEntity, jksCntEntity + lwm2mCntEntity, jksCntEntity + lwm2mCntEntity);
Collections.sort(resources, idComparator);
Collections.sort(loadedResources, idComparator);
resources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(resources, loadedResources);
}
@ -581,8 +578,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}
} while (pageData.hasNext());
Collections.sort(resources, idComparator);
Collections.sort(loadedResources, idComparator);
resources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(resources, loadedResources);
@ -654,8 +651,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}
} while (pageData.hasNext());
Collections.sort(jksResources, idComparator);
Collections.sort(loadedResources, idComparator);
jksResources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(jksResources, loadedResources);
@ -736,8 +733,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}
} while (pageData.hasNext());
Collections.sort(expectedResources, idComparator);
Collections.sort(loadedResources, idComparator);
expectedResources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(expectedResources, loadedResources);
@ -770,7 +767,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
MockHttpServletResponse response = resultActions.andReturn().getResponse();
String eTag = response.getHeader("ETag");
Assert.assertNotNull(eTag);
Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA);
Assert.assertEquals(TEST_DATA, Base64.getEncoder().encodeToString(response.getContentAsByteArray()));
//download with if-none-match header
HttpHeaders headers = new HttpHeaders();
@ -814,7 +811,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
MockHttpServletResponse response = resultActions.andReturn().getResponse();
String eTag = response.getHeader("ETag");
Assert.assertNotNull(eTag);
Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA);
Assert.assertEquals(TEST_DATA, Base64.getEncoder().encodeToString(response.getContentAsByteArray()));
//download with if-none-match header
HttpHeaders headers = new HttpHeaders();
@ -859,10 +856,10 @@ public class TbResourceControllerTest extends AbstractControllerTest {
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("can't be updated")));
foundResource.setData(null);
foundResource.setTitle("Updated resource");
savedResource = doPost("/api/resource", foundResource, TbResource.class);
assertThat(savedResource.getTitle()).isEqualTo("Updated resource");
String resourceTitle = "Updated resource";
savedResource.setTitle(resourceTitle);
savedResource = doPut("/api/resource/" + savedResource.getUuidId() + "/info", savedResource, TbResourceInfo.class);
assertThat(savedResource.getTitle()).isEqualTo(resourceTitle);
assertThat(savedResource.getFileName()).isEqualTo(resource.getFileName());
assertThat(savedResource.getEtag()).isEqualTo(resource.getEtag());
assertThat(download(savedResource.getId())).asBase64Encoded().isEqualTo(TEST_DATA);
@ -923,8 +920,20 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}
private TbResourceInfo save(TbResource tbResource) throws Exception {
return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>() {
});
byte[] data = tbResource.getData() != null ? tbResource.getData() : tbResource.getEncodedData() != null ? Base64.getDecoder().decode(tbResource.getEncodedData()) : null;
List<MockPart> parts = new ArrayList<>();
parts.add(new MockPart("resourceType", tbResource.getResourceType().name().getBytes()));
if (tbResource.getTitle() != null) {
parts.add(new MockPart("title", tbResource.getTitle().getBytes()));
}
if (tbResource.getDescriptor() != null) {
parts.add(new MockPart("descriptor", tbResource.getDescriptor().toString().getBytes()));
}
if (tbResource.getResourceSubType() != null) {
parts.add(new MockPart("resourceSubType", tbResource.getResourceSubType().name().getBytes()));
}
return uploadResource(HttpMethod.POST, "/api/resource/upload", tbResource.getFileName(), tbResource.getResourceType().getMediaType(), data, parts);
}
private TbResourceInfo findResourceInfo(TbResourceId id) throws Exception {
@ -949,7 +958,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
for (String model : models) {
String fileName = model + ".xml";
byte[] bytes = IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream("lwm2m/" + fileName));
byte[] bytes = IOUtils.toByteArray(Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream("lwm2m/" + fileName)));
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.LWM2M_MODEL);

4
application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java

@ -19,8 +19,8 @@ import com.google.common.collect.Lists;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.CustomerId;
@ -61,7 +61,7 @@ public class EdqsEntityServiceTest extends EntityServiceTest {
@Autowired
private EdqsService edqsService;
@MockBean
@MockitoBean
private EdqsRocksDb edqsRocksDb;
@Before

187
application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java

@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.collect.Streams;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
@ -45,10 +46,15 @@ import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.rule.AlarmRule;
import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
@ -306,6 +312,54 @@ public class VersionControlTest extends AbstractControllerTest {
checkImportedOtaPackageData(software, importedSoftwareOta);
}
@Test
public void testDeviceVc_withAlarmRules_betweenTenants() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile of tenant 1");
Dashboard dashboard = createDashboard(null, "Mobile dashboard");
createAlarmRule(deviceProfile.getId(), "Profile alarm rule", dashboard.getId());
Device device = createDevice(deviceProfile.getId(), "Device of tenant 1", "test1");
createAlarmRule(device.getId(), "Device alarm rule", dashboard.getId());
String version = createVersion("devices, profiles and dashboards", EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.DASHBOARD);
loginTenant2();
Map<EntityType, EntityTypeLoadResult> result = loadVersion(version, config -> {
config.setLoadCredentials(false);
}, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.DASHBOARD);
assertThat(result.get(EntityType.DEVICE).getCreated()).isEqualTo(1);
assertThat(result.get(EntityType.DEVICE_PROFILE).getCreated()).isEqualTo(1);
assertThat(result.get(EntityType.DASHBOARD).getCreated()).isEqualTo(1);
Device importedDevice = findDevice(device.getName());
checkImportedEntity(tenantId1, device, tenantId2, importedDevice);
checkImportedDeviceData(device, importedDevice);
DeviceProfile importedDeviceProfile = findDeviceProfile(deviceProfile.getName());
checkImportedEntity(tenantId1, deviceProfile, tenantId2, importedDeviceProfile);
checkImportedDeviceProfileData(deviceProfile, importedDeviceProfile);
assertThat(importedDevice.getDeviceProfileId()).isEqualTo(importedDeviceProfile.getId());
Dashboard importedDashboard = findDashboard(dashboard.getName());
checkImportedEntity(tenantId1, dashboard, tenantId2, importedDashboard);
checkImportedDashboardData(dashboard, importedDashboard);
getCalculatedFields(CalculatedFieldType.ALARM, EntityType.DEVICE_PROFILE,
List.of(importedDeviceProfile.getUuidId()), null).forEach(alarmRuleCf -> {
assertThat(alarmRuleCf.getName()).isEqualTo("Profile alarm rule");
AlarmCalculatedFieldConfiguration config = (AlarmCalculatedFieldConfiguration) alarmRuleCf.getConfiguration();
config.getAllRules().map(Pair::getValue).forEach(alarmRule -> {
assertThat(alarmRule.getDashboardId()).isEqualTo(importedDashboard.getId());
});
});
getCalculatedFields(CalculatedFieldType.ALARM, EntityType.DEVICE_PROFILE,
List.of(importedDevice.getUuidId()), null).forEach(alarmRuleCf -> {
assertThat(alarmRuleCf.getName()).isEqualTo("Device alarm rule");
AlarmCalculatedFieldConfiguration config = (AlarmCalculatedFieldConfiguration) alarmRuleCf.getConfiguration();
config.getAllRules().map(Pair::getValue).forEach(alarmRule -> {
assertThat(alarmRule.getDashboardId()).isEqualTo(importedDashboard.getId());
});
});
}
@Test
public void testDashboardVc_betweenTenants() throws Exception {
Dashboard dashboard = createDashboard(null, "Dashboard of tenant 1");
@ -368,42 +422,42 @@ public class VersionControlTest extends AbstractControllerTest {
String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c";
String unknownUuid = "ea0dc8b0-3d85-11ed-9200-77fc04fa14fa";
String entityAliases = "{\n" +
"\"" + aliasId + "\": {\n" +
"\"alias\": \"assets\",\n" +
"\"filter\": {\n" +
" \"entityList\": [\n" +
" \"" + asset1.getId() + "\",\n" +
" \"" + asset2.getId() + "\",\n" +
" \"" + tenantId1.getId() + "\",\n" +
" \"" + existingDeviceProfile.getId() + "\",\n" +
" \"" + unknownUuid + "\"\n" +
" ],\n" +
" \"id\":\"" + asset1.getId() + "\",\n" +
" \"resolveMultiple\": true\n" +
"},\n" +
"\"id\": \"" + aliasId + "\"\n" +
"}\n" +
"}";
"\"" + aliasId + "\": {\n" +
"\"alias\": \"assets\",\n" +
"\"filter\": {\n" +
" \"entityList\": [\n" +
" \"" + asset1.getId() + "\",\n" +
" \"" + asset2.getId() + "\",\n" +
" \"" + tenantId1.getId() + "\",\n" +
" \"" + existingDeviceProfile.getId() + "\",\n" +
" \"" + unknownUuid + "\"\n" +
" ],\n" +
" \"id\":\"" + asset1.getId() + "\",\n" +
" \"resolveMultiple\": true\n" +
"},\n" +
"\"id\": \"" + aliasId + "\"\n" +
"}\n" +
"}";
String widgetId = "ea8f34a0-264a-f11f-cde3-05201bb4ff4b";
String actionId = "4a8e6efa-3e68-fa59-7feb-d83366130cae";
String widgets = "{\n" +
" \"" + widgetId + "\": {\n" +
" \"config\": {\n" +
" \"actions\": {\n" +
" \"rowClick\": [\n" +
" {\n" +
" \"name\": \"go to dashboard\",\n" +
" \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" +
" \"id\": \"" + actionId + "\"\n" +
" }\n" +
" ]\n" +
" }\n" +
" },\n" +
" \"row\": 0,\n" +
" \"col\": 0,\n" +
" \"id\": \"" + widgetId + "\"\n" +
" }\n" +
"}";
" \"" + widgetId + "\": {\n" +
" \"config\": {\n" +
" \"actions\": {\n" +
" \"rowClick\": [\n" +
" {\n" +
" \"name\": \"go to dashboard\",\n" +
" \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" +
" \"id\": \"" + actionId + "\"\n" +
" }\n" +
" ]\n" +
" }\n" +
" },\n" +
" \"row\": 0,\n" +
" \"col\": 0,\n" +
" \"id\": \"" + widgetId + "\"\n" +
" }\n" +
"}";
ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode();
dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases));
@ -499,11 +553,12 @@ public class VersionControlTest extends AbstractControllerTest {
generatorNodeConfig.setMsgCount(1);
generatorNodeConfig.setScriptLang(ScriptLanguage.JS);
UUID someUuid = UUID.randomUUID();
generatorNodeConfig.setJsScript("var msg = { temp: 42, humidity: 77 };\n" +
"var metadata = { data: 40 };\n" +
"var msgType = \"POST_TELEMETRY_REQUEST\";\n" +
"var someUuid = \"" + someUuid + "\";\n" +
"return { msg: msg, metadata: metadata, msgType: msgType };");
generatorNodeConfig.setJsScript("""
var msg = { temp: 42, humidity: 77 };
var metadata = { data: 40 };
var msgType = "POST_TELEMETRY_REQUEST";
var someUuid = "%s";
return { msg: msg, metadata: metadata, msgType: msgType };""".formatted(someUuid));
generatorNode.setConfiguration(JacksonUtil.valueToTree(generatorNodeConfig));
nodes.add(generatorNode);
metaData.setNodes(nodes);
@ -1018,20 +1073,21 @@ public class VersionControlTest extends AbstractControllerTest {
protected Dashboard createDashboard(CustomerId customerId, String name, AssetId assetForEntityAlias) {
Dashboard dashboard = createDashboard(customerId, name);
String entityAliases = "{\n" +
"\t\"23c4185d-1497-9457-30b2-6d91e69a5b2c\": {\n" +
"\t\t\"alias\": \"assets\",\n" +
"\t\t\"filter\": {\n" +
"\t\t\t\"entityList\": [\n" +
"\t\t\t\t\"" + assetForEntityAlias.getId().toString() + "\"\n" +
"\t\t\t],\n" +
"\t\t\t\"entityType\": \"ASSET\",\n" +
"\t\t\t\"resolveMultiple\": true,\n" +
"\t\t\t\"type\": \"entityList\"\n" +
"\t\t},\n" +
"\t\t\"id\": \"23c4185d-1497-9457-30b2-6d91e69a5b2c\"\n" +
"\t}\n" +
"}";
String entityAliases = """
{
"23c4185d-1497-9457-30b2-6d91e69a5b2c": {
"alias": "assets",
"filter": {
"entityList": [
"%s"
],
"entityType": "ASSET",
"resolveMultiple": true,
"type": "entityList"
},
"id": "23c4185d-1497-9457-30b2-6d91e69a5b2c"
}
}""".formatted(assetForEntityAlias.getId().toString());
ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode();
dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases));
dashboardConfiguration.set("description", new TextNode("hallo"));
@ -1134,6 +1190,33 @@ public class VersionControlTest extends AbstractControllerTest {
return doPost("/api/calculatedField", calculatedField, CalculatedField.class);
}
private CalculatedField createAlarmRule(EntityId entityId, String alarmType, DashboardId mobileDashboardId) {
Argument temperatureArgument = new Argument();
temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
temperatureArgument.setDefaultValue("0");
Map<String, Argument> arguments = Map.of(
"temperature", temperatureArgument
);
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId);
calculatedField.setName(alarmType);
calculatedField.setType(CalculatedFieldType.ALARM);
AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration();
configuration.setArguments(arguments);
SimpleAlarmCondition createCondition = new SimpleAlarmCondition();
createCondition.setExpression(new TbelAlarmConditionExpression("return temperature >= 50;"));
configuration.setCreateRules(Map.of(
AlarmSeverity.CRITICAL, new AlarmRule(createCondition, "", mobileDashboardId)
));
SimpleAlarmCondition clearCondition = new SimpleAlarmCondition();
clearCondition.setExpression(new TbelAlarmConditionExpression("return temperature < 50;"));
configuration.setClearRule(new AlarmRule(clearCondition, "", mobileDashboardId));
calculatedField.setConfiguration(configuration);
calculatedField.setDebugSettings(DebugSettings.all());
return saveCalculatedField(calculatedField);
}
private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) {
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();

6
application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java

@ -162,7 +162,7 @@ class DefaultTelemetrySubscriptionServiceTest {
apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED);
lenient().when(apiUsageStateService.getApiUsageState(tenantId)).thenReturn(apiUsageState);
lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId)).thenReturn(tpi);
lenient().when(partitionService.resolve(eq(ServiceType.TB_CORE), eq(tenantId), any())).thenReturn(tpi);
lenient().when(tsService.save(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size()))));
lenient().when(tsService.saveWithoutLatest(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), null)));
@ -310,8 +310,6 @@ class DefaultTelemetrySubscriptionServiceTest {
given(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).willReturn(immediateFuture(List.of(entityView)));
// mock that save latest call for entity view is successful
given(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).willReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size()))));
// mock TPI for entity view
given(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).willReturn(tpi);
var request = TimeseriesSaveRequest.builder()
.tenantId(tenantId)
@ -373,8 +371,6 @@ class DefaultTelemetrySubscriptionServiceTest {
lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).thenReturn(immediateFuture(List.of(entityView)));
// mock that save latest call for entity view is successful
lenient().when(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size()))));
// mock TPI for entity view
lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).thenReturn(tpi);
var request = TimeseriesSaveRequest.builder()
.tenantId(tenantId)

5
common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java

@ -70,11 +70,10 @@ public class TbCaffeineCacheConfiguration {
}
private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) {
final Caffeine<Object, Object> caffeineBuilder
= Caffeine.newBuilder()
Caffeine<Object, Object> caffeineBuilder = Caffeine.newBuilder()
.weigher(collectionSafeWeigher())
.maximumWeight(cacheSpec.getMaxSize())
.recordStats()
.ticker(ticker());
if (!cacheSpec.getTimeToLiveInMinutes().equals(0)) {
caffeineBuilder.expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java

@ -48,7 +48,7 @@ public interface AttributesService {
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds);
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds, String scope);
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds, AttributeScope scope);
int removeAllByEntityId(TenantId tenantId, EntityId entityId);

3
common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java

@ -63,5 +63,8 @@ public interface TimeseriesService {
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds);
ListenableFuture<List<String>> findAllKeysByEntityIdsAsync(TenantId tenantId, List<EntityId> entityIds);
void cleanup(long systemTtl);
}

2
common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
import java.util.function.UnaryOperator;
@Schema
@ -36,6 +37,7 @@ import java.util.function.UnaryOperator;
@EqualsAndHashCode(callSuper = true)
public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, HasTenantId, ExportableEntity<TbResourceId> {
@Serial
private static final long serialVersionUID = 7282664529021651736L;
@Schema(description = "JSON object with Tenant Id. Tenant Id of the resource can't be changed.", accessMode = Schema.AccessMode.READ_ONLY)

6
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java

@ -18,11 +18,15 @@ package org.thingsboard.server.common.data.alarm.rule;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition;
import org.thingsboard.server.common.data.id.DashboardId;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AlarmRule {
@Valid
@ -33,7 +37,7 @@ public class AlarmRule {
@JsonIgnore
public boolean requiresScheduledReevaluation() {
return condition.hasSchedule();
return condition.requiresScheduledReevaluation();
}
}

17
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java

@ -21,9 +21,10 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.validation.Valid;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression;
import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule;
import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AnyTimeSchedule;
@ -50,6 +51,20 @@ public abstract class AlarmCondition {
return schedule != null && !(schedule.getStaticValue() instanceof AnyTimeSchedule);
}
@JsonIgnore
public boolean requiresScheduledReevaluation() {
return hasSchedule() || expression.requiresScheduledReevaluation();
}
@JsonIgnore
@AssertTrue(message = "Expressions requiring scheduled reevaluation can only be used with simple alarm conditions")
public boolean isValid() {
if (getType() != AlarmConditionType.SIMPLE && expression.requiresScheduledReevaluation()) {
return false;
}
return true;
}
@JsonIgnore
public abstract AlarmConditionType getType();

5
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java

@ -32,4 +32,9 @@ public interface AlarmConditionExpression {
@JsonIgnore
AlarmConditionExpressionType getType();
@JsonIgnore
default boolean requiresScheduledReevaluation() {
return false;
}
}

19
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java

@ -17,8 +17,11 @@ package org.thingsboard.server.common.data.alarm.rule.condition.expression;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.ComplexFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.FilterPredicateType;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate;
import org.thingsboard.server.common.data.query.EntityKeyValueType;
@ -34,7 +37,21 @@ public class AlarmConditionFilter implements Serializable {
private EntityKeyValueType valueType;
private ComplexOperation operation;
@Valid
@NotNull
@NotEmpty
private List<KeyFilterPredicate> predicates;
public boolean hasPredicate(FilterPredicateType type) {
return containsPredicate(predicates, type);
}
private boolean containsPredicate(List<KeyFilterPredicate> predicates, FilterPredicateType type) {
return predicates.stream().anyMatch(predicate -> {
if (predicate instanceof ComplexFilterPredicate complexPredicate) {
return containsPredicate(complexPredicate.getPredicates(), type);
} else {
return predicate.getType() == type;
}
});
}
}

6
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java

@ -20,6 +20,7 @@ import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.FilterPredicateType;
import java.util.List;
@ -38,4 +39,9 @@ public class SimpleAlarmConditionExpression implements AlarmConditionExpression
return AlarmConditionExpressionType.SIMPLE;
}
@Override
public boolean requiresScheduledReevaluation() {
return filters.stream().anyMatch(filter -> filter.hasPredicate(FilterPredicateType.NO_DATA));
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java

@ -16,9 +16,13 @@
package org.thingsboard.server.common.data.alarm.rule.condition.expression;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TbelAlarmConditionExpression implements AlarmConditionExpression {
@NotBlank

1
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java

@ -19,5 +19,6 @@ public enum FilterPredicateType {
STRING,
NUMERIC,
BOOLEAN,
NO_DATA,
COMPLEX
}

4
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java

@ -27,7 +27,9 @@ import java.io.Serializable;
@Type(value = StringFilterPredicate.class, name = "STRING"),
@Type(value = NumericFilterPredicate.class, name = "NUMERIC"),
@Type(value = BooleanFilterPredicate.class, name = "BOOLEAN"),
@Type(value = ComplexFilterPredicate.class, name = "COMPLEX")})
@Type(value = NoDataFilterPredicate.class, name = "NO_DATA"),
@Type(value = ComplexFilterPredicate.class, name = "COMPLEX")
})
public interface KeyFilterPredicate extends Serializable {
@JsonIgnore

43
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2025 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.alarm.rule.condition.expression.predicate;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue;
import java.util.concurrent.TimeUnit;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class NoDataFilterPredicate implements KeyFilterPredicate {
@NotNull
private TimeUnit unit;
@Valid
@NotNull
private AlarmConditionValue<Long> duration;
@Override
public FilterPredicateType getType() {
return FilterPredicateType.NO_DATA;
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java

@ -17,10 +17,14 @@ package org.thingsboard.server.common.data.alarm.rule.condition.expression.predi
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class NumericFilterPredicate implements SimpleKeyFilterPredicate<Double> {
@NotNull

7
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java

@ -87,4 +87,11 @@ public class AlarmCalculatedFieldConfiguration implements ArgumentsBasedCalculat
});
}
public boolean propagationSettingsEqual(AlarmCalculatedFieldConfiguration other) {
return this.propagate == other.propagate &&
this.propagateToOwner == other.propagateToOwner &&
this.propagateToTenant == other.propagateToTenant &&
Objects.equals(this.propagateRelationTypes, other.propagateRelationTypes);
}
}

67
common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeys.java

@ -0,0 +1,67 @@
/**
* Copyright © 2016-2025 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.query;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.List;
import java.util.Set;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static java.util.Objects.requireNonNullElse;
@Schema(
description = "Contains unique time series and attribute key names discovered from entities matching a query. Used primarily for UI hints such as autocomplete suggestions."
)
public record AvailableEntityKeys(
@Schema(
description = "Set of entity types found among the matched entities.",
example = "[\"DEVICE\", \"ASSET\"]",
requiredMode = Schema.RequiredMode.REQUIRED
)
Set<EntityType> entityTypes,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
@ArraySchema(
arraySchema = @Schema(description = "List of unique time series key names available on the matched entities."),
schema = @Schema(implementation = String.class, example = "temperature"),
uniqueItems = true
)
List<String> timeseries,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
@ArraySchema(
arraySchema = @Schema(description = "List of unique attribute key names available on the matched entities."),
schema = @Schema(implementation = String.class, example = "serialNumber"),
uniqueItems = true
)
List<String> attribute
) {
public AvailableEntityKeys {
entityTypes = requireNonNullElse(entityTypes, emptySet());
timeseries = requireNonNullElse(timeseries, emptyList());
attribute = requireNonNullElse(attribute, emptyList());
}
public static AvailableEntityKeys none() {
return new AvailableEntityKeys(emptySet(), emptyList(), emptyList());
}
}

2
dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java

@ -53,7 +53,7 @@ public interface AttributesDao {
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds);
List<String> findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, List<EntityId> entityIds, String attributeType);
List<String> findAllKeysByEntityIdsAndScope(TenantId tenantId, List<EntityId> entityIds, AttributeScope scope);
List<Pair<AttributeScope, String>> removeAllByEntityId(TenantId tenantId, EntityId entityId);

7
dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java

@ -28,7 +28,6 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edqs.AttributeKv;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
@ -93,11 +92,11 @@ public class BaseAttributesService implements AttributesService {
}
@Override
public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds, String scope) {
if (StringUtils.isEmpty(scope)) {
public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds, AttributeScope scope) {
if (scope == null) {
return attributesDao.findAllKeysByEntityIds(tenantId, entityIds);
} else {
return attributesDao.findAllKeysByEntityIdsAndAttributeType(tenantId, entityIds, scope);
return attributesDao.findAllKeysByEntityIdsAndScope(tenantId, entityIds, scope);
}
}

6
dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java

@ -212,11 +212,11 @@ public class CachedAttributesService implements AttributesService {
}
@Override
public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds, String scope) {
if (StringUtils.isEmpty(scope)) {
public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds, AttributeScope scope) {
if (scope == null) {
return attributesDao.findAllKeysByEntityIds(tenantId, entityIds);
} else {
return attributesDao.findAllKeysByEntityIdsAndAttributeType(tenantId, entityIds, scope);
return attributesDao.findAllKeysByEntityIdsAndScope(tenantId, entityIds, scope);
}
}

5
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -91,7 +91,9 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@Primary
public class BaseResourceService extends AbstractCachedEntityService<ResourceInfoCacheKey, TbResourceInfo, ResourceInfoEvictEvent> implements ResourceService {
public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
protected static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
protected static final int MAX_ENTITIES_TO_FIND = 10;
protected final TbResourceDao resourceDao;
protected final TbResourceInfoDao resourceInfoDao;
protected final ResourceDataValidator resourceValidator;
@ -100,7 +102,6 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
protected final RuleChainDao ruleChainDao;
private final Map<EntityType, ResourceContainerDao<?>> resourceLinkContainerDaoMap = new HashMap<>();
private final Map<EntityType, ResourceContainerDao<?>> generalResourceContainerDaoMap = new HashMap<>();
protected static final int MAX_ENTITIES_TO_FIND = 10;
@PostConstruct
public void init() {

9
dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java

@ -17,9 +17,11 @@ package org.thingsboard.server.dao.service.validator;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
@ -54,8 +56,8 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
@Override
protected TbResource validateUpdate(TenantId tenantId, TbResource resource) {
if (resource.getData() != null && !resource.getResourceType().isUpdatable() &&
tenantId != null && !tenantId.isSysTenantId()) {
if ((resource.getData() != null && !resource.getResourceType().isUpdatable() && tenantId != null && !tenantId.isSysTenantId())
|| resource.getResourceType().equals(ResourceType.LWM2M_MODEL)) {
throw new DataValidationException("This type of resource can't be updated");
}
return resource;
@ -81,7 +83,7 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
if (StringUtils.isEmpty(resource.getFileName())) {
throw new DataValidationException("Resource file name should be specified!");
}
if (StringUtils.containsAny(resource.getFileName(), "/", "\\")) {
if (Strings.CS.containsAny(resource.getFileName(), "/", "\\")) {
throw new DataValidationException("File name contains forbidden symbols");
}
if (StringUtils.isEmpty(resource.getResourceKey())) {
@ -104,4 +106,5 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, dataSize, TB_RESOURCE);
}
}
}

8
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java

@ -177,10 +177,12 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
}
@Override
public List<String> findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, List<EntityId> entityIds, String attributeType) {
public List<String> findAllKeysByEntityIdsAndScope(TenantId tenantId, List<EntityId> entityIds, AttributeScope scope) {
return attributeKvRepository
.findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()), AttributeScope.valueOf(attributeType).getId())
.stream().map(id -> keyDictionaryDao.getKey(id)).collect(Collectors.toList());
.findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).toList(), scope.getId())
.stream()
.map(keyDictionaryDao::getKey)
.toList();
}
@Override

4
dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java

@ -167,5 +167,9 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries
return sqlDao.findAllKeysByEntityIds(tenantId, entityIds);
}
@Override
public ListenableFuture<List<String>> findAllKeysByEntityIdsAsync(TenantId tenantId, List<EntityId> entityIds) {
return sqlDao.findAllKeysByEntityIdsAsync(tenantId, entityIds);
}
}

12
dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java

@ -24,7 +24,6 @@ import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
@ -38,8 +37,6 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.dictionary.KeyDictionaryDao;
@ -64,7 +61,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
@Component
@ -185,9 +181,13 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
@Override
public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds) {
return tsKvLatestRepository.findAllKeysByEntityIds(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()));
return tsKvLatestRepository.findAllKeysByEntityIds(entityIds.stream().map(EntityId::getId).toList());
}
@Override
public ListenableFuture<List<String>> findAllKeysByEntityIdsAsync(TenantId tenantId, List<EntityId> entityIds) {
return service.submit(() -> findAllKeysByEntityIds(tenantId, entityIds));
}
private ListenableFuture<TsKvLatestRemovingResult> getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query, Long version) {
ListenableFuture<List<TsKvEntry>> future = findNewLatestEntryFuture(tenantId, entityId, query);
@ -211,7 +211,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
ReadTsKvQueryResult::getData, MoreExecutors.directExecutor());
}
protected TsKvEntry doFindLatestSync(EntityId entityId, String key) {
protected TsKvEntry doFindLatestSync(EntityId entityId, String key) {
TsKvLatestCompositeKey compositeKey =
new TsKvLatestCompositeKey(
entityId.getId(),

9
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -156,6 +156,11 @@ public class BaseTimeseriesService implements TimeseriesService {
return timeseriesLatestDao.findAllKeysByEntityIds(tenantId, entityIds);
}
@Override
public ListenableFuture<List<String>> findAllKeysByEntityIdsAsync(TenantId tenantId, List<EntityId> entityIds) {
return timeseriesLatestDao.findAllKeysByEntityIdsAsync(tenantId, entityIds);
}
@Override
public void cleanup(long systemTtl) {
timeseriesDao.cleanup(systemTtl);
@ -300,13 +305,13 @@ public class BaseTimeseriesService implements TimeseriesService {
long interval = query.getInterval();
if (interval < 1) {
throw new IncorrectParameterException("Invalid TsKvQuery: 'interval' must be greater than 0, but got " + interval +
". Please check your query parameters and ensure 'endTs' is greater than 'startTs' or increase 'interval'.");
". Please check your query parameters and ensure 'endTs' is greater than 'startTs' or increase 'interval'.");
}
long step = Math.max(interval, 1000);
long intervalCounts = (query.getEndTs() - query.getStartTs()) / step;
if (intervalCounts > maxTsIntervals || intervalCounts < 0) {
throw new IncorrectParameterException("Incorrect TsKvQuery. Number of intervals is to high - " + intervalCounts + ". " +
"Please increase 'interval' parameter for your query or reduce the time range of the query.");
"Please increase 'interval' parameter for your query or reduce the time range of the query.");
}
}
}

8
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java

@ -36,17 +36,13 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity;
import org.thingsboard.server.dao.nosql.TbResultSet;
import org.thingsboard.server.dao.sqlts.AggregationTimeseriesDao;
import org.thingsboard.server.dao.util.NoSqlTsLatestDao;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal;
@ -103,6 +99,10 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes
return Collections.emptyList();
}
@Override
public ListenableFuture<List<String>> findAllKeysByEntityIdsAsync(TenantId tenantId, List<EntityId> entityIds) {
return Futures.immediateFuture(Collections.emptyList());
}
@Override
public ListenableFuture<Long> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {

6
dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java

@ -22,12 +22,8 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public interface TimeseriesLatestDao {
@ -54,4 +50,6 @@ public interface TimeseriesLatestDao {
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds);
ListenableFuture<List<String>> findAllKeysByEntityIdsAsync(TenantId tenantId, List<EntityId> entityIds);
}

14
dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java

@ -117,24 +117,26 @@ public class DeviceConnectivityUtil {
dockerComposeBuilder.append("\n");
dockerComposeBuilder.append(" # Environment variables\n");
dockerComposeBuilder.append(" environment:\n");
dockerComposeBuilder.append(" - host=").append(isLocalhost(host) ? HOST_DOCKER_INTERNAL : host).append("\n");
dockerComposeBuilder.append(" - port=1883\n");
dockerComposeBuilder.append(" - TB_GW_HOST=").append(isLocalhost(host) ? HOST_DOCKER_INTERNAL : host).append("\n");
dockerComposeBuilder.append(" - TB_GW_PORT=1883\n");
switch (deviceCredentials.getCredentialsType()) {
case ACCESS_TOKEN:
dockerComposeBuilder.append(" - accessToken=").append(deviceCredentials.getCredentialsId()).append("\n");
dockerComposeBuilder.append(" - TB_GW_SECURITY_TYPE=accessToken\n");
dockerComposeBuilder.append(" - TB_GW_ACCESS_TOKEN=").append(deviceCredentials.getCredentialsId()).append("\n");
break;
case MQTT_BASIC:
dockerComposeBuilder.append(" - TB_GW_SECURITY_TYPE=usernamePassword\n");
BasicMqttCredentials credentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(),
BasicMqttCredentials.class);
if (credentials != null) {
if (StringUtils.isNotEmpty(credentials.getClientId())) {
dockerComposeBuilder.append(" - clientId=").append(credentials.getClientId()).append("\n");
dockerComposeBuilder.append(" - TB_GW_CLIENT_ID=").append(credentials.getClientId()).append("\n");
}
if (StringUtils.isNotEmpty(credentials.getUserName())) {
dockerComposeBuilder.append(" - username=").append(credentials.getUserName()).append("\n");
dockerComposeBuilder.append(" - TB_GW_USERNAME=").append(credentials.getUserName()).append("\n");
}
if (StringUtils.isNotEmpty(credentials.getPassword())) {
dockerComposeBuilder.append(" - password=").append(credentials.getPassword()).append("\n");
dockerComposeBuilder.append(" - TB_GW_PASSWORD=").append(credentials.getPassword()).append("\n");
}
}
break;

2
dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java

@ -223,7 +223,7 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest {
saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123");
Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
List<String> keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId), AttributeScope.SERVER_SCOPE.name());
List<String> keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId), AttributeScope.SERVER_SCOPE);
assertThat(keys).containsOnly("key1", "key2");
});
}

20
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -39,10 +39,12 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.rest.client.utils.RestJsonConverter;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.ClaimRequest;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
@ -160,6 +162,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.AvailableEntityKeys;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
@ -592,7 +595,7 @@ public class RestClient implements Closeable {
}
public PageData<AlarmInfo> getAllAlarmsV2(List<AlarmSearchStatus> statusList, List<AlarmSeverity> severityList,
List<String> typeList, String assignedId, TimePageLink pageLink) {
List<String> typeList, String assignedId, TimePageLink pageLink) {
String urlSecondPart = "/api/v2/alarms?";
Map<String, String> params = new HashMap<>();
if (!CollectionUtils.isEmpty(statusList)) {
@ -1824,12 +1827,15 @@ public class RestClient implements Closeable {
}).getBody();
}
public JsonNode findEntityTimeseriesAndAttributesKeysByQuery(EntityDataQuery query) {
return restTemplate.exchange(
baseURL + "/api/entitiesQuery/find/keys",
HttpMethod.POST, new HttpEntity<>(query),
new ParameterizedTypeReference<JsonNode>() {
}).getBody();
public AvailableEntityKeys findAvailableEntityKeysByQuery(EntityDataQuery query, boolean includeTimeseries, boolean includeAttributes, AttributeScope scope) {
var uri = UriComponentsBuilder.fromUriString(baseURL)
.path("/api/entitiesQuery/find/keys")
.queryParam("timeseries", includeTimeseries)
.queryParam("attributes", includeAttributes)
.queryParamIfPresent("scope", Optional.ofNullable(scope))
.build()
.toUri();
return restTemplate.exchange(uri, HttpMethod.POST, new HttpEntity<>(query), new ParameterizedTypeReference<AvailableEntityKeys>() {}).getBody();
}
public PageData<AlarmData> findAlarmDataByQuery(AlarmDataQuery query) {

50
ui-ngx/src/app/core/http/resource.service.ts

@ -17,7 +17,7 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig } from '@core/http/http-utils';
import { forkJoin, Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { Resource, ResourceInfo, ResourceSubType, ResourceType, TBResourceScope } from '@shared/models/resource.models';
@ -90,6 +90,54 @@ export class ResourceService {
return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config));
}
public uploadResources(resources: Resource[], config?: RequestConfig): Observable<Resource[]> {
let partSize = 100;
partSize = resources.length > partSize ? partSize : resources.length;
const resourceObservables: Observable<Resource>[] = [];
for (let i = 0; i < partSize; i++) {
resourceObservables.push(this.uploadResource(resources[i], config).pipe(catchError(() => of({} as Resource))));
}
return forkJoin(resourceObservables).pipe(
mergeMap((resource) => {
resources.splice(0, partSize);
if (resources.length) {
return this.uploadResources(resources, config);
} else {
return of(resource);
}
})
);
}
public uploadResource(resource: Resource, config?: RequestConfig): Observable<Resource> {
if (!config) {
config = {};
}
const formData = new FormData();
formData.append('file', resource.data);
formData.append('title', resource.title);
formData.append('resourceType', resource.resourceType);
if (resource.resourceSubType) {
formData.append('resourceSubType', resource.resourceSubType);
}
return this.http.post<Resource>('/api/resource/upload', formData,
defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest));
}
public updatedResourceInfo(resourceId: string, updatedResources: Partial<Omit<Resource, 'data'>>, config?: RequestConfig): Observable<Resource> {
return this.http.put<Resource>(`/api/resource/${resourceId}/info`, updatedResources, defaultHttpOptionsFromConfig(config));
}
public updatedResourceData(resourceId: string, data: File, config?: RequestConfig): Observable<Resource> {
if (!config) {
config = {};
}
const formData = new FormData();
formData.append('file', data);
return this.http.put<Resource>(`/api/resource/${resourceId}/data`, formData,
defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest));
}
public deleteResource(resourceId: string, force = false, config?: RequestConfig) {
return this.http.delete(`/api/resource/${resourceId}?force=${force}`, defaultHttpOptionsFromConfig(config));
}

2
ui-ngx/src/app/core/services/menu.models.ts

@ -510,7 +510,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
MenuId.alarms,
{
id: MenuId.alarms,
name: 'alarm.alarms',
name: 'alarm.alarm-list',
type: 'link',
path: '/alarms/alarms',
icon: 'mdi:alert-outline'

4
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html

@ -27,8 +27,10 @@
<mat-form-field class="mat-block" appearance="outline">
<textarea matInput [formControl]="alarmDetailsControl" rows="5"
placeholder="{{ 'alarm-rule.alarm-rule-additional-info-placeholder' | translate }}"></textarea>
<mat-hint [innerHTML]="'alarm-rule.alarm-rule-additional-info-hint' | translate | safe: 'html'"></mat-hint>
</mat-form-field>
<div class="tb-form-hint tb-primary-fill flex items-center gap-2">
<span [innerHTML]="'alarm-rule.alarm-rule-additional-info-hint' | translate | safe: 'html'"></span>
</div>
</div>
</div>
<div mat-dialog-actions class="justify-end">

48
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html

@ -130,30 +130,32 @@
{{ 'alarm-rule.advanced-settings' | translate }}
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide margin" formControlName="propagate">
{{ 'alarm-rule.propagate-alarm' | translate }}
</mat-slide-toggle>
<div class="tb-form-panel stroked no-padding no-gap">
<div class="tb-form-row no-border">
<mat-slide-toggle class="mat-slide margin" formControlName="propagate">
{{ 'alarm-rule.propagate-alarm' | translate }}
</mat-slide-toggle>
</div>
@if (configFormGroup.get('propagate').value) {
<mat-form-field floatLabel="always" class="mat-block p-4" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.alarm-rule-relation-types-list</mat-label>
<mat-chip-grid #relationTypesChipList>
<mat-chip-row
*ngFor="let key of configFormGroup.get('propagateRelationTypes').value;"
(removed)="removeRelationType(key)">
{{key}}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip-row>
<input matInput type="text" placeholder="{{'alarm-rule.alarm-rule-relation-types-list' | translate}}"
[matChipInputFor]="relationTypesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addRelationType($event)">
</mat-chip-grid>
<mat-hint innerHTML="{{ 'alarm-rule.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
</mat-form-field>
}
</div>
@if (configFormGroup.get('propagate').value) {
<mat-form-field floatLabel="always" class="mat-block" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.alarm-rule-relation-types-list</mat-label>
<mat-chip-grid #relationTypesChipList>
<mat-chip-row
*ngFor="let key of configFormGroup.get('propagateRelationTypes').value;"
(removed)="removeRelationType(key)">
{{key}}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip-row>
<input matInput type="text" placeholder="{{'alarm-rule.alarm-rule-relation-types-list' | translate}}"
[matChipInputFor]="relationTypesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addRelationType($event)">
</mat-chip-grid>
<mat-hint innerHTML="{{ 'alarm-rule.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
</mat-form-field>
}
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide margin" formControlName="propagateToOwner">
{{ 'alarm-rule.propagate-alarm-to-owner' | translate }}

4
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html

@ -66,7 +66,7 @@
</tb-entity-subtype-list>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>alarm-rule.entity-type</div>
<div class="fixed-title-width" translate>alarm-rule.target-entity-type</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="entityType" placeholder="{{ 'alarm-rule.any-type' | translate }}">
<mat-option>{{ 'alarm-rule.any-type' | translate }}</mat-option>
@ -78,7 +78,7 @@
</div>
@if (alarmRuleFilterConfigForm.get('entityType').value) {
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>alarm-rule.alarm-rule-entity-list</div>
<div class="fixed-title-width" translate>alarm-rule.target-entities</div>
<tb-entity-list appearance="outline"
subscriptSizing="dynamic"
class="flex flex-1"

6
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts

@ -50,6 +50,9 @@ import { AlarmRuleFilterDialogComponent } from "@home/components/alarm-rules/fil
import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/alarm-rule-details-dialog.component";
import { AlarmRuleFilterConfigComponent } from "@home/components/alarm-rules/alarm-rule-filter-config.component";
import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component";
import {
AlarmRuleFilterPredicateNoDataValueComponent
} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component";
@NgModule({
declarations: [
@ -69,7 +72,8 @@ import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alar
AlarmRuleComplexFilterPredicateDialogComponent,
AlarmRuleDetailsDialogComponent,
AlarmRuleFilterConfigComponent,
AlarmRuleTableHeaderComponent
AlarmRuleTableHeaderComponent,
AlarmRuleFilterPredicateNoDataValueComponent
],
imports: [
CommonModule,

6
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts

@ -138,16 +138,16 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('name', 'alarm-rule.alarm-type', this.pageMode ? '30%' :'33%',
entity => this.utilsService.customTranslation(entity.name, entity.name)));
if (this.pageMode) {
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('entityType', 'alarm-rule.entity-type', '15%',
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('entityType', 'alarm-rule.target-entity-type', '15%',
entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type)));
this.columns.push(new EntityLinkTableColumn<CalculatedFieldAlarmRule>('entityName', 'alarm-rule.entity-name', '30%',
this.columns.push(new EntityLinkTableColumn<CalculatedFieldAlarmRule>('entityName', 'alarm-rule.target-entity', '30%',
entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']),
entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false));
}
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('createRule', 'alarm-rule.severities', this.pageMode ? '15%' :'67%',
entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '),
() => ({}), false));
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('clearRule', 'alarm-rule.cleared', '70px',
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('clearRule', 'alarm-rule.cleared', '90px',
entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: 0, textAlign: 'center'}}, false));
this.cellActionDescriptors.push(

53
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html

@ -90,15 +90,47 @@
</div>
}
</div>
@if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) {
<div class="tb-form-panel no-gap">
<mat-expansion-panel class="tb-settings" [expanded]="true">
<mat-expansion-panel-header>
{{ 'alarm-rule.filter-preview' | translate }}
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div>
@if (specText) {
<span class="spec-text">{{ specText }}</span>
}
@if (conditionFormGroup.get('expression.filters').value?.length) {
<tb-alarm-rule-filter-text [alarmRuleExpression]="conditionFormGroup.get('expression').value"
[arguments]="arguments"
class="flex-1"
[nowrap]="false"
required>
</tb-alarm-rule-filter-text>
} @else {
<span class="tb-prompt justify-center">{{ 'alarm-rule.no-filter-preview' | translate }}</span>
}
</div>
</ng-template>
</mat-expansion-panel>
</div>
}
<section class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.condition-settings' | translate }}</div>
<mat-form-field class="mat-block" hideRequiredMarker appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.condition-type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType"
[disabled]="isNoData ? alarmConditionType === AlarmConditionType.REPEATING || alarmConditionType === AlarmConditionType.DURATION : false">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</mat-option>
</mat-select>
@if (isNoData) {
<mat-hint translate>alarm-rule.condition-type-hint</mat-hint>
}
</mat-form-field>
@if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) {
<div class="tb-form-panel stroked no-padding-bottom">
@ -146,7 +178,7 @@
<ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.REPEATING, groupName: 'count'}"></ng-container>
</div>
<div class="flex-1" [class.!hidden]="!repeatingDynamicModeControl.value">
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'count'}"></ng-container>
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{type: AlarmConditionType.REPEATING, groupName: 'count'}"></ng-container>
</div>
</div>
</div>
@ -162,9 +194,8 @@
</button>
@if (!readonly) {
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="conditionFormGroup.invalid || !conditionFormGroup.dirty">
[disabled]="conditionFormGroup.invalid || !conditionFormGroup.dirty || !filtersValid">
{{ 'action.save' | translate }}
</button>
}
@ -186,6 +217,9 @@
} @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('pattern')) {
<mat-error>{{ defaultValuePatternError | translate }}</mat-error>
}
@if (type === AlarmConditionType.REPEATING) {
<mat-hint translate>alarm-rule.condition-repeating-value-hint</mat-hint>
}
</mat-form-field>
</div>
</ng-template>
@ -198,9 +232,14 @@
<mat-option [value]="argument">{{ argument }}</mat-option>
}
</mat-select>
<mat-error *ngIf="conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')">
{{ 'calculated-fields.hint.argument-name-required' | translate }}
</mat-error>
@if (conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')) {
<mat-error>
{{ 'calculated-fields.hint.argument-name-required' | translate }}
</mat-error>
}
@if (type === AlarmConditionType.REPEATING) {
<mat-hint translate>alarm-rule.condition-repeating-value-hint</mat-hint>
}
</mat-form-field>
</ng-container>
</ng-template>

157
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts

@ -24,13 +24,6 @@ import { DialogComponent } from '@app/shared/components/dialog.component';
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from "@shared/models/rule-node.models";
import {
AlarmRuleCondition,
AlarmRuleConditionType,
AlarmRuleConditionTypeTranslationMap,
alarmRuleDefaultScript,
AlarmRuleExpressionType
} from "@shared/models/alarm-rule.models";
import {
CalculatedFieldArgument,
getCalculatedFieldArgumentsEditorCompleter,
@ -38,8 +31,18 @@ import {
} from "@shared/models/calculated-field.models";
import { TbEditorCompleter } from "@shared/models/ace/completion.models";
import { AceHighlightRules } from "@shared/models/ace/ace.models";
import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models";
import { ComplexOperation } from "@shared/models/query/query.models";
import { Observable } from "rxjs";
import { TranslateService } from "@ngx-translate/core";
import {
AlarmRuleCondition,
AlarmRuleConditionType,
AlarmRuleConditionTypeTranslationMap,
alarmRuleDefaultScript,
AlarmRuleExpressionType,
AlarmRuleFilter,
filterOperationTranslationMap
} from "@shared/models/alarm-rule.models";
export interface CfAlarmRuleConditionDialogData {
readonly: boolean;
@ -96,7 +99,11 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
repeatingDynamicModeControl = this.fb.control<boolean>(false);
ComplexOperation = ComplexOperation;
complexOperationTranslationMap = complexOperationTranslationMap;
complexOperationTranslationMap = filterOperationTranslationMap;
specText = '';
filtersValid: boolean = false;
functionArgs: Array<string>;
argumentsEditorCompleter: TbEditorCompleter;
@ -105,11 +112,14 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
arguments = this.data.arguments;
argumentsList: Array<string>;
isNoData: boolean = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData,
public dialogRef: MatDialogRef<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition>,
private fb: FormBuilder) {
private fb: FormBuilder,
private translate: TranslateService) {
super(store, router, dialogRef);
this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)];
@ -128,21 +138,38 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
unit: this.condition?.unit ?? TimeUnit.SECONDS,
value: {
staticValue: this.condition?.value?.staticValue,
dynamicValueArgument: this.condition?.value?.dynamicValueArgument,
dynamicValueArgument: Object.keys(this.data.arguments).includes(this.condition?.value?.dynamicValueArgument) ? this.condition?.value?.dynamicValueArgument : null,
},
count: {
staticValue: this.condition?.count?.staticValue ?? null,
dynamicValueArgument: this.condition?.count?.dynamicValueArgument
dynamicValueArgument: Object.keys(this.data.arguments).includes(this.condition?.count?.dynamicValueArgument) ? this.condition?.count?.dynamicValueArgument : null
}
}, {emitEvent: false});
this.durationDynamicModeControl.patchValue(!!this.condition?.value?.dynamicValueArgument, {emitEvent: false});
this.repeatingDynamicModeControl.patchValue(!!this.condition?.count?.dynamicValueArgument, {emitEvent: false});
this.filtersValid = this.areFilterAndPredicateArgumentsValid(this.condition?.expression?.filters, this.argumentsList);
this.checkIsNoData(this.condition?.expression?.filters);
this.conditionFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed()
).subscribe((type) => {
this.updateValidators(type, true);
this.updateValidators(type);
});
this.conditionFormGroup.valueChanges.pipe(
takeUntilDestroyed()
).subscribe((value) => {
this.updateSpecText(value.type);
})
this.conditionFormGroup.get('expression.filters').valueChanges.pipe(
takeUntilDestroyed()
).subscribe((filters) => {
this.filtersValid = this.areFilterAndPredicateArgumentsValid(filters, this.argumentsList);
this.checkIsNoData(filters);
});
this.conditionFormGroup.get('expression.type').valueChanges.pipe(
@ -164,6 +191,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
});
this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
this.updateSpecText(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE');
}
@ -178,6 +206,39 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
}
}
private areFilterAndPredicateArgumentsValid(obj: any, validArguments: string[]): boolean {
const validSet = new Set(validArguments);
const filters = obj || [];
for (const filter of filters) {
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
}
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (p.value?.dynamicValueArgument) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
for (const filter of filters) {
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates)) {
return false;
}
}
}
return true;
}
updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') {
if (type === 'SIMPLE') {
this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false});
@ -188,7 +249,26 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
}
}
private updateValidators(type: AlarmRuleConditionType, emitEvent = false) {
private checkIsNoData(filters: Array<AlarmRuleFilter>) {
this.isNoData = this.hasNoData(filters);
if (this.isNoData && this.conditionFormGroup.get('type').value !== AlarmRuleConditionType.SIMPLE) {
this.conditionFormGroup.get('type').patchValue(AlarmRuleConditionType.SIMPLE);
}
}
private hasNoData(data: Array<AlarmRuleFilter>) {
const search = (filter) => {
if (!filter) return false;
if (Array.isArray(filter)) return filter.some(search);
if (typeof filter !== 'object') return false;
if (filter.type === 'NO_DATA') return true;
if (filter.predicates?.length) return filter.predicates.some(search);
return false;
};
return search(data);
};
private updateValidators(type: AlarmRuleConditionType) {
switch (type) {
case AlarmRuleConditionType.DURATION:
this.conditionFormGroup.get('unit').enable({emitEvent: false});
@ -218,6 +298,55 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
}
}
private updateSpecText(type: AlarmRuleConditionType) {
this.specText = '';
const value = this.conditionFormGroup.get('value').value;
const count = this.conditionFormGroup.get('count').value;
switch (type) {
case AlarmRuleConditionType.SIMPLE:
break;
case AlarmRuleConditionType.DURATION:
let duringText = '';
switch (this.conditionFormGroup.get('unit').value) {
case TimeUnit.SECONDS:
duringText = this.translate.instant('timewindow.seconds', {seconds: value.staticValue});
break;
case TimeUnit.MINUTES:
duringText = this.translate.instant('timewindow.minutes', {minutes: value.staticValue});
break;
case TimeUnit.HOURS:
duringText = this.translate.instant('timewindow.hours', {hours: value.staticValue});
break;
case TimeUnit.DAYS:
duringText = this.translate.instant('timewindow.days', {days: value.staticValue});
break;
}
if (value.dynamicValueArgument) {
this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', {
attribute: `${value.dynamicValueArgument}`
});
} else {
this.specText = this.translate.instant('alarm-rule.condition-during', {
during: duringText
});
}
break;
case AlarmRuleConditionType.REPEATING:
if (count.dynamicValueArgument) {
this.specText = this.translate.instant('alarm-rule.condition-repeat-times-dynamic', {
attribute: `${count.dynamicValueArgument}`
});
} else {
this.specText = this.translate.instant('alarm-rule.condition-repeat-times',
{count: count.staticValue});
}
break;
}
if (this.specText.length > 0) {
this.specText = this.specText + ':';
}
}
cancel(): void {
this.dialogRef.close(null);
}

11
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html

@ -21,7 +21,7 @@
<button [disabled]="disabled"
type="button"
class="tb-alarm-rule-condition-button"
mat-stroked-button [color]="conditionSet() ? 'primary' : 'warn'"
mat-stroked-button [color]="conditionSet() && this.filtersArgumentsValid ? 'primary' : 'warn'"
(click)="openFilterDialog($event)">
<div class="flex items-center gap-2 justify-between">
<tb-alarm-rule-filter-text [alarmRuleExpression]="alarmRuleConditionFormGroup.get('expression').value"
@ -32,20 +32,23 @@
required
addFilterPrompt="{{ (isClearCondition ? 'alarm-rule.enter-alarm-rule-clear-condition-prompt' :'alarm-rule.enter-alarm-rule-condition-prompt') | translate }}">
</tb-alarm-rule-filter-text>
<mat-icon [color]="conditionSet() ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">{{ conditionSet() ? 'edit' : 'add' }}</mat-icon>
<mat-icon [color]="conditionSet() && this.filtersArgumentsValid ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">
{{ conditionSet() ? 'edit' : 'add' }}
</mat-icon>
</div>
</button>
</div>
<div class="tb-form-row column-xs">
<div class="min-w-40 xs:min-w-fit">{{ 'alarm-rule.schedule-title' | translate }}</div>
<button [disabled]="disabled"
[color]="schedulerArgumentsValid ? 'primary' : 'warn'"
type="button"
class="tb-alarm-rule-condition-button"
mat-stroked-button color="primary"
mat-stroked-button
(click)="openScheduleDialog($event)">
<div class="flex items-center gap-2">
<span class="tb-alarm-rule-condition-label" tbTruncateWithTooltip [innerHTML]="scheduleText"></span>
<mat-icon class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">edit</mat-icon>
<mat-icon [color]="schedulerArgumentsValid ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">edit</mat-icon>
</div>
</button>
</div>

74
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts

@ -14,8 +14,9 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { ChangeDetectorRef, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
@ -68,7 +69,7 @@ import { Observable } from "rxjs";
}
]
})
export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator {
export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator, OnChanges {
@Input()
@coerceBoolean()
@ -96,11 +97,15 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
specText = '';
filtersArgumentsValid: boolean = true;
schedulerArgumentsValid: boolean = true;
scheduleText = '';
private modelValue: AlarmRuleCondition;
private propagateChange = (v: any) => { };
private onValidatorChange = () => { };
constructor(private dialog: MatDialog,
private fb: FormBuilder,
@ -115,6 +120,10 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
registerOnTouched(fn: any): void {
}
registerOnValidatorChange(fn: () => void): void {
this.onValidatorChange = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
@ -127,14 +136,68 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
writeValue(value: AlarmRuleCondition): void {
this.modelValue = value;
this.updateConditionInfo();
if (value) {
this.onValidatorChange();
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes.arguments) {
if (changes.arguments && !changes.arguments.firstChange && this.modelValue) {
this.onValidatorChange();
}
}
}
private isScheduleArgumentValid(obj: any, validArguments: string[]): boolean {
const arg = obj?.schedule?.dynamicValueArgument;
return !arg || validArguments.includes(arg);
}
private areFilterAndPredicateArgumentsValid(obj: any, validArguments: string[]): boolean {
const validSet = new Set(validArguments);
const filters = obj?.expression?.filters || obj?.filters || [];
for (const filter of filters) {
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
}
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (p.value?.dynamicValueArgument) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
for (const filter of filters) {
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates)) {
return false;
}
}
}
return true;
}
public conditionSet() {
return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters);
}
public validate(): ValidationErrors | null {
return this.conditionSet() ? null : {
public validate(control: AbstractControl): ValidationErrors | null {
this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, Object.keys(this.arguments));
this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments));
this.onValidatorChange = () => {
control.updateValueAndValidity({ emitEvent: true });
};
return this.conditionSet() && this.filtersArgumentsValid && this.schedulerArgumentsValid ? null : {
alarmRuleCondition: {
valid: false,
}
@ -226,6 +289,9 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
private updateModel() {
this.updateConditionInfo();
this.propagateChange(this.modelValue);
if (this.modelValue) {
this.onValidatorChange();
}
}
public openScheduleDialog($event: Event) {

6
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html

@ -20,11 +20,11 @@
</tb-cf-alarm-rule-condition>
@if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
<div class="tb-form-row space-between column-xs">
<div class="min-w-40 xs:min-w-fit" translate>
<div class="min-w-40 xs:min-w-fit" tb-hint-tooltip-icon="{{'alarm-rule.alarm-rule-additional-info-icon-hint' | translate }}" translate>
alarm-rule.alarm-rule-additional-info
</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="alarmDetails" placeholder="{{ 'alarm-rule.alarm-rule-additional-info' | translate }}">
<input matInput formControlName="alarmDetails" placeholder="{{ 'action.set' | translate }}">
<button type="button"
matSuffix mat-icon-button aria-label="Open in new"
(click)="openEditDetailsDialog($event)">
@ -42,7 +42,7 @@
subscriptSizing="dynamic"
inlineField
class="flex-1"
placeholder="{{ 'alarm-rule.alarm-rule-mobile-dashboard' | translate }}"
placeholder="{{ 'action.set' | translate }}"
formControlName="dashboardId">
</tb-dashboard-autocomplete>
</div>

5
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss

@ -20,6 +20,11 @@
display: grid;
grid-template-rows: min-content minmax(auto, 1fr) min-content;
}
.spec-text {
font-size: 14px;
color: rgba(0, 0, 0, 0.54);
}
}
.tbel-script-lang-chip {
line-height: 20px;

2
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts

@ -171,7 +171,7 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
if (value) {
this.modelValue = value;
if (this.modelValue.dynamicValueArgument) {
this.alarmScheduleForm.get('dynamicValueArgument').patchValue(this.modelValue.dynamicValueArgument, {emitEvent: false});
this.alarmScheduleForm.get('dynamicValueArgument').patchValue(Object.keys(this.arguments).includes(this.modelValue.dynamicValueArgument) ? this.modelValue.dynamicValueArgument : null, {emitEvent: false});
} else {
switch (this.modelValue.staticValue.type) {
case AlarmRuleScheduleType.SPECIFIC_TIME:

33
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html

@ -25,20 +25,25 @@
</button>
</mat-toolbar>
<div mat-dialog-content>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>filter.operation.operation</mat-label>
<mat-select required formControlName="operation">
<mat-option *ngFor="let operation of complexOperations" [value]="operation">
{{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<tb-alarm-rule-filter-predicate-list [valueType]="data.valueType"
[arguments]="arguments"
[argumentInUse]="data.argumentInUse"
[operation]="complexFilterFormGroup.get('operation').value"
formControlName="predicates">
</tb-alarm-rule-filter-predicate-list>
<section class="tb-form-panel">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title"
tb-hint-tooltip-icon="{{ data.valueType === EntityKeyValueType.DATE_TIME ? ('alarm-rule.date-time-hint' | translate) : '' }}">
{{ 'alarm-rule.filters' | translate }}
</div>
<tb-toggle-select formControlName="operation"
selectMediaBreakpoint="xs">
<tb-toggle-option [value]="complexOperationEnum.AND">{{ complexOperationTranslations.get(complexOperationEnum.AND) | translate }}</tb-toggle-option>
<tb-toggle-option [value]="complexOperationEnum.OR">{{ complexOperationTranslations.get(complexOperationEnum.OR) | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-alarm-rule-filter-predicate-list [valueType]="data.valueType"
[arguments]="arguments"
[argumentInUse]="data.argumentInUse"
[operation]="complexFilterFormGroup.get('operation').value"
formControlName="predicates">
</tb-alarm-rule-filter-predicate-list>
</section>
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary"

18
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts

@ -21,13 +21,13 @@ import { AppState } from '@core/core.state';
import { FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { ComplexOperation, EntityKeyValueType } from '@shared/models/query/query.models';
import {
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType,
FilterPredicateType
} from '@shared/models/query/query.models';
import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
AlarmRuleFilterPredicate,
AlarmRuleFilterPredicateType,
ComplexAlarmRuleFilterPredicate,
filterOperationTranslationMap
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
export interface AlarmRuleComplexFilterPredicateDialogData {
@ -55,9 +55,9 @@ export class AlarmRuleComplexFilterPredicateDialogComponent extends
}
);
complexOperations = Object.keys(ComplexOperation);
EntityKeyValueType = EntityKeyValueType;
complexOperationEnum = ComplexOperation;
complexOperationTranslations = complexOperationTranslationMap;
complexOperationTranslations = filterOperationTranslationMap;
isAdd: boolean;
@ -81,7 +81,7 @@ export class AlarmRuleComplexFilterPredicateDialogComponent extends
save(): void {
const predicate = this.complexFilterFormGroup.value as ComplexAlarmRuleFilterPredicate;
predicate.type = FilterPredicateType.COMPLEX;
predicate.type = AlarmRuleFilterPredicateType.COMPLEX;
this.dialogRef.close(predicate);
}
}

4
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html

@ -69,7 +69,7 @@
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title"
tb-hint-tooltip-icon="{{ filterFormGroup.get('valueType').value === entityKeyValueTypeEnum.DATE_TIME ? ('alarm-rule.date-time-hint' | translate) : '' }}">
{{ 'alarm-rule.filter' | translate }}
{{ 'alarm-rule.filters' | translate }}
</div>
<tb-toggle-select formControlName="operation"
selectMediaBreakpoint="xs">
@ -95,7 +95,7 @@
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="filterFormGroup.invalid || !filterFormGroup.dirty">
[disabled]="filterFormGroup.invalid || !filterFormGroup.dirty || !predicatesValid">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button>
</div>

53
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts

@ -21,18 +21,18 @@
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType,
entityKeyValueTypesMap
} from '@shared/models/query/query.models';
import { ComplexOperation, EntityKeyValueType, entityKeyValueTypesMap } from '@shared/models/query/query.models';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { AlarmRuleFilter, AlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
import {
AlarmRuleFilter,
AlarmRuleFilterPredicate,
filterOperationTranslationMap
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { FormControlsFrom } from "@shared/models/tenant.model";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { isDefinedAndNotNull } from "@core/utils";
export interface AlarmRuleFilterDialogData {
filter: AlarmRuleFilter;
@ -57,7 +57,9 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
entityKeyValueTypes = entityKeyValueTypesMap;
complexOperationTranslationMap = complexOperationTranslationMap;
complexOperationTranslationMap = filterOperationTranslationMap;
predicatesValid: boolean = false;
ComplexOperation = ComplexOperation;
@ -78,12 +80,20 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
this.filterFormGroup = this.fb.group(
{
argument: [this.data.filter.argument, [Validators.required]],
argument: [this.argumentsList.includes(this.data.filter.argument) ? this.data.filter.argument : '' , [Validators.required]],
valueType: [this.data.filter.valueType ?? EntityKeyValueType.STRING, [Validators.required]],
predicates: [this.data.filter.predicates, [Validators.required]],
operation: [this.data.filter.operation ?? ComplexOperation.AND]
}
);
this.predicatesValid = this.isPredicateArgumentsValid(this.data.filter.predicates);
this.filterFormGroup.get('predicates').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(predicates => {
this.predicatesValid = this.isPredicateArgumentsValid(predicates);
});
this.filterFormGroup.get('valueType').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((valueType: EntityKeyValueType) => {
@ -106,6 +116,31 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
});
}
private isPredicateArgumentsValid(predicates: any): boolean {
const validSet = new Set(Object.keys(this.data.arguments));
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (isDefinedAndNotNull(p.value?.dynamicValueArgument)) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
if (Array.isArray(predicates)) {
if (!checkPredicates(predicates)) {
return false;
}
}
return true;
}
argumentInUse(argument: string): boolean {
return this.data.usedArguments.includes(argument);
}

32
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html

@ -39,19 +39,26 @@
</div>
<div class="flex max-w-92% flex-full flex-col">
<div class="flex flex-row items-center justify-start">
<div class="flex-1">{{ filterControl.value?.argument }}</div>
<div class="flex-1">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.predicates[0]?.type) | translate }}</div>
<button mat-icon-button color="primary"
<div class="flex-1 filters-text">{{ filterControl.value?.argument }}</div>
<div class="flex-1 filters-text">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}</div>
<button mat-icon-button color="primary" class="tb-budge-button"
type="button"
(click)="editFilter(index)"
matTooltip="{{ 'filter.edit-key-filter' | translate }}"
matTooltip="{{ 'alarm-rule.edit-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>{{'edit'}}</mat-icon>
<mat-icon
[matBadgeHidden]="areFilterAndPredicateArgumentsValid(filterControl.value)"
matBadgeColor="warn"
matBadgeSize="small"
matBadge="*"
>
edit
</mat-icon>
</button>
<button mat-icon-button color="primary"
type="button"
(click)="removeFilter(index)"
matTooltip="{{ 'filter.remove-key-filter' | translate }}"
matTooltip="{{ 'alarm-rule.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
@ -62,14 +69,15 @@
}
</div>
}
<span [class.!hidden]="!!filtersFormArray.length"
class="no-data-found flex items-center justify-center"
translate>
filter.no-key-filters
</span>
@if (!filtersFormArray.length) {
<span class="no-data-found tb-prompt flex items-center justify-center" translate>
alarm-rule.no-filter
</span>
<tb-error noMargin [error]="'alarm-rule.filter-required' | translate" class="flex h-9 items-center pl-3"/>
}
</div>
</section>
<button mat-button mat-raised-button color="primary"
<button mat-button mat-stroked-button color="primary"
(click)="addFilter()"
type="button">
{{ 'alarm-rule.add-filter' | translate }}

13
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss

@ -30,6 +30,10 @@
&-divider {
border-top: 1px solid rgba(0, 0, 0, 0.12);
}
.filters-text {
font-size: 14px;
}
}
.filters-operation {
display: flex;
@ -39,7 +43,8 @@
background-color: white;
}
&-label {
font-weight: 500;
font-size: 15px;
font-weight: 400;
color: #00695C;
padding: 0 8px;
border-radius: 4px;
@ -47,4 +52,10 @@
background-color: rgba(#00695C, 0.04);
}
}
.tb-budge-button {
--mat-badge-legacy-small-size-container-size: 8px;
--mat-badge-small-size-container-overlap-offset: -5px;
--mat-badge-small-size-text-size: 0;
}
}

30
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts

@ -102,6 +102,36 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
};
}
public areFilterAndPredicateArgumentsValid(obj: any): boolean {
const validSet = new Set(Object.keys(this.arguments));
const filter = obj || [];
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (p.value?.dynamicValueArgument) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates)) {
return false;
}
}
return true;
}
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.filterListFormGroup.disable({emitEvent: false});

16
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html

@ -58,21 +58,23 @@
</div>
</div>
}
<span [class.!hidden]="!!predicatesFormArray.length"
[class.disabled]="disabled"
class="no-data-found flex items-center justify-center" translate>
filter.no-filters
</span>
@if (!predicatesFormArray.length) {
<span class="no-data-found tb-prompt flex items-center justify-center" translate>
alarm-rule.no-filter
</span>
<tb-error noMargin [error]="'alarm-rule.filter-required' | translate" class="flex h-9 items-center pl-3"/>
}
</div>
</div>
<div class="flex flex-row gap-2">
<button mat-button mat-raised-button color="primary"
<button mat-button mat-stroked-button color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(false)"
type="button">
{{ 'action.add' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
<button mat-button
color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(true)"
type="button">

3
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss

@ -39,7 +39,8 @@
background-color: white;
}
&-label {
font-weight: 500;
font-size: 15px;
font-weight: 400;
color: #00695C;
padding: 0 8px;
border-radius: 4px;

26
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts

@ -28,14 +28,10 @@ import {
} from '@angular/forms';
import { Observable, of } from 'rxjs';
import {
BooleanOperation,
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType,
entityKeyValueTypeToFilterPredicateType,
FilterPredicateType,
NumericOperation,
StringOperation
entityKeyValueTypeToFilterPredicateType
} from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog';
import { map } from 'rxjs/operators';
@ -44,8 +40,12 @@ import {
AlarmRuleComplexFilterPredicateDialogData
} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component";
import {
AlarmRuleBooleanOperation,
AlarmRuleFilterPredicate,
AlarmRuleFilterPredicateType,
AlarmRuleNumericOperation,
AlarmRulePredicateInfo,
AlarmRuleStringOperation,
ComplexAlarmRuleFilterPredicate
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
@ -157,29 +157,29 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess
private createDefaultFilterPredicate(valueType: EntityKeyValueType, complex: boolean): AlarmRuleFilterPredicate {
const predicate = {
type: complex ? FilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType)
type: complex ? AlarmRuleFilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType)
} as AlarmRuleFilterPredicate;
switch (predicate.type) {
case FilterPredicateType.STRING:
predicate.operation = StringOperation.STARTS_WITH;
case AlarmRuleFilterPredicateType.STRING:
predicate.operation = AlarmRuleStringOperation.STARTS_WITH;
predicate.value = {
staticValue: ''
};
predicate.ignoreCase = false;
break;
case FilterPredicateType.NUMERIC:
predicate.operation = NumericOperation.EQUAL;
case AlarmRuleFilterPredicateType.NUMERIC:
predicate.operation = AlarmRuleNumericOperation.EQUAL;
predicate.value = {
staticValue: valueType === EntityKeyValueType.DATE_TIME ? Date.now() : 0
};
break;
case FilterPredicateType.BOOLEAN:
predicate.operation = BooleanOperation.EQUAL;
case AlarmRuleFilterPredicateType.BOOLEAN:
predicate.operation = AlarmRuleBooleanOperation.EQUAL;
predicate.value = {
staticValue: false
};
break;
case FilterPredicateType.COMPLEX:
case AlarmRuleFilterPredicateType.COMPLEX:
predicate.operation = ComplexOperation.AND;
predicate.predicates = [];
break;

77
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html

@ -0,0 +1,77 @@
<!--
Copyright © 2016-2025 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.
-->
<div class="tb-form-panel no-border no-gap no-padding" [formGroup]="filterPredicateValueNoDataFormGroup">
<div class="tb-form-row no-padding no-border column-xs">
<mat-form-field hideRequiredMarker class="flex-1 w-full max-w-30% xs:max-w-full" appearance="outline" subscriptSizing="dynamic">
<mat-select [formControl]="dynamicModeControl" placeholder="{{'filter.dynamic-source-type' | translate}}">
<mat-option [value]="false">{{'alarm-rule.static' | translate}}</mat-option>
<mat-option [value]="true">{{'alarm-rule.dynamic' | translate}}</mat-option>
</mat-select>
</mat-form-field>
<ng-container formGroupName="duration">
@if (!dynamicModeControl.value) {
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input required type="number" matInput formControlName="staticValue" min="1"
placeholder="{{'filter.value' | translate}}">
@if (filterPredicateValueNoDataFormGroup.get('duration.staticValue').touched && filterPredicateValueNoDataFormGroup.get('duration.staticValue').errors) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(filterPredicateValueNoDataFormGroup.get('duration.staticValue').hasError('required') ? 'alarm-rule.value-required' : 'alarm-rule.min-value') | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-form-field>
} @else {
<mat-form-field class="flex-1 w-full" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="dynamicValueArgument" placeholder="{{ 'action.set' | translate }}">
@for (argument of argumentsList; track argument) {
<mat-option [value]="argument" [disabled]="argument === argumentInUse">{{ argument }}</mat-option>
}
</mat-select>
@if (filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').touched && filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').errors) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').hasError('required') ? 'calculated-fields.hint.argument-name-required' : 'alarm-rule.argument-in-use') | translate"
class="tb-error !block">
warning
</mat-icon>
}
@if (filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').hasError('argumentInUse')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'alarm-rule.argument-in-use' | translate"
class="tb-error !block">
warning
</mat-icon>
}
</mat-form-field>
}
</ng-container>
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="unit" required placeholder="{{'alarm-rule.time-unit' | translate}}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitsTranslationMap.get(timeUnit) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>

166
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts

@ -0,0 +1,166 @@
///
/// Copyright © 2016-2025 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.
///
import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { TimeUnit, timeUnitTranslations } from "@home/components/rule-node/rule-node-config.models";
import { AlarmRuleFilterPredicateType, NoDataAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
import { isDefinedAndNotNull } from "@core/utils";
@Component({
selector: 'tb-alarm-rule-filter-predicate-no-data-value',
templateUrl: './alarm-rule-filter-predicate-no-data-value.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AlarmRuleFilterPredicateNoDataValueComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AlarmRuleFilterPredicateNoDataValueComponent),
multi: true
}
]
})
export class AlarmRuleFilterPredicateNoDataValueComponent implements ControlValueAccessor, Validator, OnInit, OnChanges {
@Input()
arguments: Record<string, CalculatedFieldArgument>;
@Input()
valueType: AlarmRuleFilterPredicateType;
@Input()
argumentInUse: string;
valueTypeEnum = AlarmRuleFilterPredicateType;
filterPredicateValueNoDataFormGroup = this.fb.group({
type: ['NO_DATA'],
unit: [TimeUnit.MINUTES, Validators.required],
duration: this.fb.group({
staticValue: [null as null | number, [Validators.required, Validators.min(1)]],
dynamicValueArgument: ['', Validators.required]
})
});
timeUnits = [TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS];
timeUnitsTranslationMap = timeUnitTranslations;
dynamicModeControl = this.fb.control(false);
argumentsList: Array<string>;
private propagateChange= (v: any) => { };
constructor(private fb: FormBuilder,
private destroyRef: DestroyRef) {
}
ngOnInit(): void {
this.argumentsList = this.arguments ? Object.keys(this.arguments): [];
this.filterPredicateValueNoDataFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateModel();
});
this.dynamicModeControl.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(value => this.updateValueModeValidators(value));
}
ngOnChanges(changes: SimpleChanges) {
if (changes.argumentInUse) {
const argumentInUseChanges = changes.argumentInUse;
if (!argumentInUseChanges.firstChange && argumentInUseChanges.currentValue !== argumentInUseChanges.previousValue) {
if (this.dynamicModeControl.value) {
if (this.argumentInUse === this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').value) {
this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').setErrors({argumentInUse: true});
this.filterPredicateValueNoDataFormGroup.updateValueAndValidity();
}
}
}
}
}
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.filterPredicateValueNoDataFormGroup.disable({emitEvent: false});
this.dynamicModeControl.disable({emitEvent: false});
} else {
this.filterPredicateValueNoDataFormGroup.enable({emitEvent: false});
this.dynamicModeControl.enable({emitEvent: false});
this.updateValueModeValidators(this.dynamicModeControl.value);
}
}
private updateValueModeValidators(isDynamicMode: boolean): void {
if (isDynamicMode) {
this.filterPredicateValueNoDataFormGroup.get('duration.staticValue').disable({emitEvent: false});
this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').enable();
setTimeout(()=> {
if (this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').value && this.argumentInUse === this.filterPredicateValueNoDataFormGroup.get('dynamicValueArgument').value) {
this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').setErrors({argumentInUse: true});
this.filterPredicateValueNoDataFormGroup.updateValueAndValidity();
}
}, 0);
} else {
this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').disable({emitEvent: false});
this.filterPredicateValueNoDataFormGroup.get('duration.staticValue').enable();
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
validate(): ValidationErrors | null {
return this.filterPredicateValueNoDataFormGroup.valid ? null : {
filterPredicateValue: {valid: false}
};
}
writeValue(predicateValue: NoDataAlarmRuleFilterPredicate): void {
if (isDefinedAndNotNull(predicateValue.duration.dynamicValueArgument)) {
const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse);
if (!availableArgument.includes(predicateValue.duration.dynamicValueArgument)) {
predicateValue.duration.dynamicValueArgument = '';
}
this.dynamicModeControl.patchValue(true, {emitEvent: false});
}
this.filterPredicateValueNoDataFormGroup.patchValue(predicateValue, {emitEvent: false});
}
private updateModel() {
this.propagateChange(this.filterPredicateValueNoDataFormGroup.value);
}
}

9
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html

@ -67,6 +67,15 @@
warning
</mat-icon>
}
@if (filterPredicateValueFormGroup.get('dynamicValueArgument').hasError('argumentInUse')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'alarm-rule.argument-in-use' | translate"
class="tb-error !block">
warning
</mat-icon>
}
</mat-form-field>
}
</div>

33
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
@ -31,6 +31,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { AlarmRuleValue } from "@shared/models/alarm-rule.models";
import { FormControlsFrom } from "@shared/models/tenant.model";
import { isDefinedAndNotNull } from "@core/utils";
@Component({
selector: 'tb-alarm-rule-filter-predicate-value',
@ -49,7 +50,7 @@ import { FormControlsFrom } from "@shared/models/tenant.model";
}
]
})
export class AlarmRuleFilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit {
export class AlarmRuleFilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit, OnChanges {
@Input()
arguments: Record<string, CalculatedFieldArgument>;
@ -110,6 +111,20 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
).subscribe(value => this.updateValueModeValidators(value));
}
ngOnChanges(changes: SimpleChanges) {
if (changes.argumentInUse) {
const argumentInUseChanges = changes.argumentInUse;
if (!argumentInUseChanges.firstChange && argumentInUseChanges.currentValue !== argumentInUseChanges.previousValue) {
if (this.dynamicModeControl.value) {
if (this.argumentInUse === this.filterPredicateValueFormGroup.get('dynamicValueArgument').value) {
this.filterPredicateValueFormGroup.get('dynamicValueArgument').setErrors({argumentInUse: true});
this.filterPredicateValueFormGroup.updateValueAndValidity();
}
}
}
}
}
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.filterPredicateValueFormGroup.disable({emitEvent: false});
@ -125,6 +140,12 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
if (isDynamicMode) {
this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable();
setTimeout(()=> {
if (this.filterPredicateValueFormGroup.get('dynamicValueArgument').value && this.argumentInUse === this.filterPredicateValueFormGroup.get('dynamicValueArgument').value) {
this.filterPredicateValueFormGroup.get('dynamicValueArgument').setErrors({argumentInUse: true});
this.filterPredicateValueFormGroup.updateValueAndValidity();
}
}, 0);
} else {
this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false});
this.filterPredicateValueFormGroup.get('staticValue').enable();
@ -145,8 +166,14 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
}
writeValue(predicateValue: AlarmRuleValue<string | number | boolean>): void {
if (isDefinedAndNotNull(predicateValue.dynamicValueArgument)) {
const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse);
if (!availableArgument.includes(predicateValue.dynamicValueArgument)) {
predicateValue.dynamicValueArgument = '';
}
this.dynamicModeControl.patchValue(true, {emitEvent: false});
}
this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false});
this.dynamicModeControl.patchValue(!!predicateValue.dynamicValueArgument?.length, {emitEvent: false});
}
private updateModel() {

19
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html

@ -27,9 +27,11 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-chip-listbox formControlName="ignoreCase" [hideSingleSelectionIndicator]="true">
<mat-chip-option color="primary" [value]="true">{{ 'alarm-rule.ignore-case' | translate }}</mat-chip-option>
</mat-chip-listbox>
@if (filterPredicateFormGroup.get('operation').value !== stringOperation.NO_DATA) {
<mat-chip-listbox formControlName="ignoreCase" [hideSingleSelectionIndicator]="true">
<mat-chip-option color="primary" [value]="true">{{ 'alarm-rule.ignore-case' | translate }}</mat-chip-option>
</mat-chip-listbox>
}
</div>
}
@case (filterPredicateType.NUMERIC) {
@ -58,7 +60,7 @@
<div class="tb-form-row no-border no-padding flex-1">
<button type="button" style="--mat-outlined-button-horizontal-padding: 3px 0px 12px;"
class="block w-full"
mat-stroked-button color="primary"
mat-stroked-button [color]="predicateValid ? 'primary' : 'warn'"
(click)="openComplexFilterDialog()">
<div class="flex items-center gap-2">
<span class="w-full text-start" translate>filter.complex-filter</span>
@ -68,7 +70,14 @@
</div>
}
}
@if (type !== filterPredicateType.COMPLEX) {
@if (filterPredicateFormGroup.get('operation').value === stringOperation.NO_DATA) {
<tb-alarm-rule-filter-predicate-no-data-value class="flex-full"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
formControlName="duration">
</tb-alarm-rule-filter-predicate-no-data-value>
} @else if (type !== filterPredicateType.COMPLEX) {
<tb-alarm-rule-filter-predicate-value class="flex-full"
[arguments]="arguments"
[argumentInUse]="argumentInUse"

122
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts

@ -23,18 +23,19 @@ import {
ValidationErrors,
Validator
} from '@angular/forms';
import {
BooleanOperation,
booleanOperationTranslationMap,
EntityKeyValueType,
FilterPredicateType,
NumericOperation,
numericOperationTranslationMap,
StringOperation,
stringOperationTranslationMap
} from '@shared/models/query/query.models';
import { EntityKeyValueType } from '@shared/models/query/query.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
import {
AlarmRuleBooleanOperation,
alarmRuleBooleanOperationTranslationMap,
AlarmRuleFilterPredicate,
AlarmRuleFilterPredicateType,
AlarmRuleNumericOperation,
alarmRuleNumericOperationTranslationMap,
AlarmRuleStringOperation,
alarmRuleStringOperationTranslationMap,
ComplexAlarmRuleFilterPredicate
} from "@shared/models/alarm-rule.models";
import { MatDialog } from "@angular/material/dialog";
import {
AlarmRuleComplexFilterPredicateDialogComponent,
@ -77,24 +78,27 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
operation: [],
ignoreCase: false,
predicates: [],
value: []
value: [],
duration: []
});
type: FilterPredicateType;
type: AlarmRuleFilterPredicateType;
filterPredicateType = AlarmRuleFilterPredicateType;
filterPredicateType = FilterPredicateType;
stringOperations = Object.keys(AlarmRuleStringOperation);
stringOperation = AlarmRuleStringOperation;
stringOperationTranslationMap = alarmRuleStringOperationTranslationMap;
stringOperations = Object.keys(StringOperation);
stringOperation = StringOperation;
stringOperationTranslationMap = stringOperationTranslationMap;
numericOperations = Object.keys(AlarmRuleNumericOperation);
numericOperationEnum = AlarmRuleNumericOperation;
numericOperationTranslations = alarmRuleNumericOperationTranslationMap;
numericOperations = Object.keys(NumericOperation);
numericOperationEnum = NumericOperation;
numericOperationTranslations = numericOperationTranslationMap;
booleanOperations = Object.keys(AlarmRuleBooleanOperation);
booleanOperationEnum = AlarmRuleBooleanOperation;
booleanOperationTranslations = alarmRuleBooleanOperationTranslationMap;
booleanOperations = Object.keys(BooleanOperation);
booleanOperationEnum = BooleanOperation;
booleanOperationTranslations = booleanOperationTranslationMap;
predicateValid: boolean = false;
private propagateChange= (v: any) => { };
@ -106,6 +110,12 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
).subscribe(() => {
this.updateModel();
});
this.filterPredicateFormGroup.get('predicates').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(predicates => {
this.predicateValid = this.isPredicateArgumentsValid(predicates);
})
}
registerOnChange(fn: any): void {
@ -130,13 +140,75 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
}
}
private isPredicateArgumentsValid(predicates: any): boolean {
const validSet = new Set(Object.keys(this.arguments));
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (p.value?.dynamicValueArgument) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
if (Array.isArray(predicates)) {
if (!checkPredicates(predicates)) {
return false;
}
}
return true;
}
writeValue(predicate: AlarmRuleFilterPredicate): void {
this.type = predicate.type;
this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false});
if ((predicate as ComplexAlarmRuleFilterPredicate)?.predicates) {
this.predicateValid = this.isPredicateArgumentsValid((predicate as ComplexAlarmRuleFilterPredicate)?.predicates);
}
if (predicate.type === AlarmRuleFilterPredicateType.NO_DATA) {
this.type = AlarmRuleFilterPredicateType[this.valueType];
this.filterPredicateFormGroup.patchValue({operation: 'NO_DATA', duration: predicate}, {emitEvent: false});
} else {
this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false});
}
}
private updateModel() {
this.propagateChange({type: this.type, ...this.filterPredicateFormGroup.value});
const predicate = this.filterPredicateFormGroup.value;
if (predicate.operation === 'NO_DATA') {
this.propagateChange(predicate.duration);
} else {
if (!predicate.value) {
switch (this.valueType) {
case EntityKeyValueType.STRING:
predicate.value = {
staticValue: ''
};
break;
case EntityKeyValueType.NUMERIC:
predicate.value = {
staticValue: 0
};
break;
case EntityKeyValueType.DATE_TIME:
predicate.value = {
staticValue: Date.now()
};
break;
case EntityKeyValueType.BOOLEAN:
predicate.value = {
staticValue: false
};
break;
}
}
this.propagateChange({type: this.type, ...predicate});
}
}
public openComplexFilterDialog() {

1
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html

@ -19,5 +19,6 @@
tbTruncateWithTooltip
[class.required]="isRequired"
[class.nowrap]="nowrap"
[class.wrap]="!nowrap"
[innerHTML]="filterText">
</div>

3
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss

@ -28,6 +28,9 @@
text-overflow: ellipsis;
overflow: hidden;
}
&.wrap {
white-space: pre-wrap !important;
}
}
}

36
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts

@ -16,25 +16,27 @@
import { Component, Input } from '@angular/core';
import {
booleanOperationTranslationMap,
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType,
FilterPredicateType,
numericOperationTranslationMap,
stringOperationTranslationMap
EntityKeyValueType
} from '@shared/models/query/query.models';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import {
alarmRuleBooleanOperationTranslationMap,
AlarmRuleExpression,
AlarmRuleExpressionType,
AlarmRuleFilter,
AlarmRuleFilterPredicate,
AlarmRuleFilterPredicateType,
alarmRuleNumericOperationTranslationMap,
AlarmRuleStringOperation,
alarmRuleStringOperationTranslationMap,
ComplexAlarmRuleFilterPredicate
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { coerceBoolean } from "@shared/decorators/coercion";
import { timeUnitTranslationMap } from "@shared/models/time/time.models";
@Component({
selector: 'tb-alarm-rule-filter-text',
@ -96,7 +98,7 @@ export class AlarmRuleFilterTextComponent {
private updateFilterText(value: AlarmRuleExpression) {
this.isRequired = false;
if (value && (value.expression || value.filters)) {
if (value && (value.expression || value.filters?.length)) {
if (value.type === AlarmRuleExpressionType.SIMPLE) {
this.filterText = this.keyFiltersToText(this.translate, this.datePipe, value.filters, value.operation);
} else {
@ -137,21 +139,21 @@ export class AlarmRuleFilterTextComponent {
const filterOperation: ComplexOperation = complexOperation ? complexOperation : (keyFilter.operation ?? ComplexOperation.AND);
const predicates = keyFilterPredicates.map((keyFilterPredicate: AlarmRuleFilterPredicate) => {
if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) {
if (keyFilterPredicate.type === AlarmRuleFilterPredicateType.COMPLEX) {
const complexPredicate = keyFilterPredicate as ComplexAlarmRuleFilterPredicate;
const complexOperation = complexPredicate.operation ?? ComplexOperation.AND;
return this.filterPredicateToText(translate, datePipe, keyFilter, complexPredicate.predicates, complexOperation);
} else {
let operation: string;
let value: string;
const val = keyFilterPredicate.value;
const val = keyFilterPredicate.type === AlarmRuleFilterPredicateType.NO_DATA ? keyFilterPredicate.duration : keyFilterPredicate.value;
const dynamicValue = val?.dynamicValueArgument?.length;
if (dynamicValue) {
value = '<span class="tb-filter-dynamic-value"><span class="tb-filter-value">' + val?.dynamicValueArgument + '</span></span>';
}
switch (keyFilterPredicate.type) {
case FilterPredicateType.STRING:
operation = translate.instant(stringOperationTranslationMap.get(keyFilterPredicate.operation));
case AlarmRuleFilterPredicateType.STRING:
operation = translate.instant(alarmRuleStringOperationTranslationMap.get(keyFilterPredicate.operation));
if (keyFilterPredicate.ignoreCase) {
operation += ' ' + translate.instant('filter.ignore-case');
}
@ -159,8 +161,8 @@ export class AlarmRuleFilterTextComponent {
value = `'${keyFilterPredicate.value.staticValue}'`;
}
break;
case FilterPredicateType.NUMERIC:
operation = translate.instant(numericOperationTranslationMap.get(keyFilterPredicate.operation));
case AlarmRuleFilterPredicateType.NUMERIC:
operation = translate.instant(alarmRuleNumericOperationTranslationMap.get(keyFilterPredicate.operation));
if (!dynamicValue) {
if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) {
value = datePipe.transform(keyFilterPredicate.value.staticValue, 'yyyy-MM-dd HH:mm');
@ -169,12 +171,18 @@ export class AlarmRuleFilterTextComponent {
}
}
break;
case FilterPredicateType.BOOLEAN:
operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation));
case AlarmRuleFilterPredicateType.BOOLEAN:
operation = translate.instant(alarmRuleBooleanOperationTranslationMap.get(keyFilterPredicate.operation));
if (!dynamicValue) {
value = translate.instant(keyFilterPredicate.value.staticValue ? 'value.true' : 'value.false');
}
break;
case AlarmRuleFilterPredicateType.NO_DATA:
operation = translate.instant(alarmRuleStringOperationTranslationMap.get(AlarmRuleStringOperation.NO_DATA));
if (!dynamicValue) {
value = keyFilterPredicate.duration.staticValue + ' ' + translate.instant(timeUnitTranslationMap.get(keyFilterPredicate.unit)).toLowerCase();
}
break;
}
if (!dynamicValue) {
value = `<span class="tb-filter-value">${value}</span>`;

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts

@ -64,6 +64,7 @@ export class EntityAggregationArgumentsTableComponent extends CalculatedFieldArg
hint: 'calculated-fields.entity-aggregation.argument-setting-hint',
hiddenDefaultValue: true,
hiddenEntityKeyTypes: true,
watchKeyChange: true,
};
this.isScript = false;

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts

@ -123,7 +123,7 @@ export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTa
this.errorText = 'calculated-fields.hint.arguments-entity-not-found';
} else if (!this.argumentsFormArray.controls.length) {
this.errorText = 'calculated-fields.hint.arguments-empty';
} if (this.isScript && !this.argumentsFormArray.controls.some(control => isUndefinedOrNull(control.value?.refEntityId) && isUndefinedOrNull(control.value.refDynamicSourceConfiguration))) {
} else if (this.isScript && !this.argumentsFormArray.controls.some(control => isUndefinedOrNull(control.value?.refEntityId) && isUndefinedOrNull(control.value.refDynamicSourceConfiguration))) {
this.errorText = 'calculated-fields.hint.arguments-propagate-argument-must-current-entity';
} else {
this.errorText = '';

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts

@ -87,7 +87,8 @@ export class RelatedAggregationArgumentsTableComponent extends CalculatedFieldAr
hiddenEntityTypes: true,
defaultValueRequired: true,
argumentEntityTypes: [ArgumentEntityType.Current],
hint: 'calculated-fields.hint.setting-arguments-aggregation'
hint: 'calculated-fields.hint.setting-arguments-aggregation',
watchKeyChange: true,
};
this.isScript = false;

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html

@ -59,7 +59,7 @@
</div>
<div class="tb-form-panel no-gap" [class.!hidden]="!this.propagateConfiguration.get('applyExpressionToResolvedArguments').value">
<div class="tb-form-panel-title tb-required">
{{ 'calculated-fields.expression' | translate }}
{{ 'calculated-fields.script' | translate }}
</div>
<div>
<tb-js-func required

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts

@ -145,7 +145,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
const formValue: any = deepClone(value);
if (this.isScript) {
formValue.expressionSCRIPT = formValue.expression ?? calculatedFieldDefaultScript;
} else {
} else if (value.type === CalculatedFieldType.SIMPLE) {
formValue.expressionSIMPLE = formValue.expression;
}
this.simpleConfiguration.patchValue(formValue, {emitEvent: false});

5
ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts

@ -98,18 +98,17 @@ export class ResourcesDialogComponent extends DialogComponent<ResourcesDialogCom
resources.push({
resourceType: resource.resourceType,
data,
fileName: resource.fileName[index],
title: resource.title
});
});
this.resourceService.saveResources(resources, {resendRequest: true}).pipe(
this.resourceService.uploadResources(resources, {resendRequest: true}).pipe(
map((response) => response[0])
).subscribe(result => this.dialogRef.close(result));
} else {
if (resource.resourceType !== ResourceType.GENERAL) {
delete resource.descriptor;
}
this.resourceService.saveResource(resource).subscribe(result => this.dialogRef.close(result));
this.resourceService.uploadResource(resource).subscribe(result => this.dialogRef.close(result));
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save