Browse Source

Merge remote-tracking branch 'upstream/master' into api-key

pull/14074/head
Andrii Landiak 7 months ago
parent
commit
e244f7a4fa
  1. 7
      application/src/main/data/upgrade/basic/schema_update.sql
  2. 23
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  3. 4
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  4. 6
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  5. 80
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  6. 10
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java
  8. 23
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  9. 60
      application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java
  10. 34
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  11. 14
      common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java
  12. 3
      common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
  13. 46
      common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java
  14. 17
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java
  15. 12
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java
  16. 12
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java
  17. 45
      common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java
  18. 1
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  19. 2
      common/proto/src/main/proto/queue.proto
  20. 62
      dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java
  21. 42
      dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java
  22. 3
      dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java
  23. 12
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  24. 79
      dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java
  25. 41
      dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java
  26. 36
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java
  27. 38
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java
  28. 94
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java
  29. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java
  30. 10
      dao/src/main/resources/sql/schema-entities.sql
  31. 23
      dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java
  32. 5
      dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java
  33. 10
      dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java
  34. 57
      dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java
  35. 10
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java
  36. 128
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java
  37. 271
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  38. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java
  39. 7
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java
  40. 81
      ui-ngx/src/app/core/http/attribute.service.ts
  41. 31
      ui-ngx/src/app/core/http/http-utils.ts
  42. 2
      ui-ngx/src/app/core/http/queue.service.ts
  43. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  44. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss
  45. 2
      ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts
  46. 13
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts
  47. 13
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts
  48. 13
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts
  49. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts
  50. 2
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html

7
application/src/main/data/upgrade/basic/schema_update.sql

@ -69,3 +69,10 @@ ALTER TABLE calculated_field DROP CONSTRAINT IF EXISTS calculated_field_unq_key;
ALTER TABLE calculated_field ADD CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name);
-- CALCULATED FIELD UNIQUE CONSTRAINT UPDATE END
-- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE START
DROP TABLE IF EXISTS calculated_field_link;
ANALYZE calculated_field;
-- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE END

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

@ -611,7 +611,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
var proto = msg.getProto();
List<CalculatedFieldEntityCtxId> result = new ArrayList<>();
for (var link : getCalculatedFieldLinksByEntityId(entityId)) {
CalculatedFieldCtx ctx = calculatedFields.get(link.getCalculatedFieldId());
CalculatedFieldCtx ctx = calculatedFields.get(link.calculatedFieldId());
if (ctx.linkMatches(entityId, proto)) {
result.add(ctx.toCalculatedFieldEntityCtxId());
}
@ -738,13 +738,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
private void addLinks(CalculatedField newCf) {
var newLinks = newCf.getConfiguration().buildCalculatedFieldLinks(tenantId, newCf.getEntityId(), newCf.getId());
newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link));
newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link));
}
private void deleteLinks(CalculatedFieldCtx cfCtx) {
var oldCf = cfCtx.getCalculatedField();
var oldLinks = oldCf.getConfiguration().buildCalculatedFieldLinks(tenantId, oldCf.getEntityId(), oldCf.getId());
oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).remove(link));
oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).remove(link));
}
public void onPartitionChange(CalculatedFieldPartitionChangeMsg msg) {
@ -757,15 +757,11 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
log.trace("Processing calculated field record: {}", cf);
try {
initCalculatedField(cf);
initCalculatedFieldLinks(cf);
} catch (CalculatedFieldException e) {
log.error("Failed to process calculated field record: {}", cf, e);
}
});
PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(pageLink -> cfDaoService.findAllCalculatedFieldLinksByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize());
cfls.forEach(link -> {
log.trace("Processing calculated field link record: {}", link);
initCalculatedFieldLink(link);
});
}
private void initCalculatedField(CalculatedField cf) throws CalculatedFieldException {
@ -782,10 +778,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
}
}
private void initCalculatedFieldLink(CalculatedFieldLink link) {
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link);
private void initCalculatedFieldLinks(CalculatedField cf) {
List<CalculatedFieldLink> links = cf.getConfiguration().buildCalculatedFieldLinks(cf.getTenantId(), cf.getEntityId(), cf.getId());
for (CalculatedFieldLink link : links) {
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link);
}
}
private void initEntitiesCache() {

4
application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java

@ -63,10 +63,10 @@ import org.thingsboard.server.service.security.permission.Operation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION;
@ -287,7 +287,7 @@ public class CalculatedFieldController extends BaseController {
}
private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException {
List<EntityId> referencedEntityIds = calculatedFieldConfig.getReferencedEntities();
Set<EntityId> referencedEntityIds = calculatedFieldConfig.getReferencedEntities();
for (EntityId referencedEntityId : referencedEntityIds) {
EntityType entityType = referencedEntityId.getEntityType();
switch (entityType) {

6
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -1632,11 +1632,13 @@ public class ControllerConstants {
protected static final String ENTITY_VIEW_INFO_DESCRIPTION = "Entity Views Info extends the Entity View with customer title and 'is public' flag. " + ENTITY_VIEW_DESCRIPTION;
protected static final String ATTRIBUTES_SCOPE_DESCRIPTION = "A string value representing the attributes scope. For example, 'SERVER_SCOPE'.";
protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'.";
protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'. " +
"If attribute keys contain comma, duplicate 'key' parameter for each key, for example '?key=my,key&key=my,second,key";
protected static final String ATTRIBUTES_JSON_REQUEST_DESCRIPTION = "A string value representing the json object. For example, '{\"key\":\"value\"}'. See API call description for more details.";
protected static final String TELEMETRY_KEYS_BASE_DESCRIPTION = "A string value representing the comma-separated list of telemetry keys.";
protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest time series. For example, 'temperature,humidity'.";
protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest time series. For example, 'temperature,humidity'. " +
"If telemetry keys contain comma, duplicate 'key' parameter for each key, for example '?key=my,key&key=my,second,key";
protected static final String TELEMETRY_SCOPE_DESCRIPTION = "Value is deprecated, reserved for backward compatibility and not used in the API call implementation. Specify any scope for compatibility";
protected static final String TELEMETRY_JSON_REQUEST_DESCRIPTION = "A JSON with the telemetry values. See API call description for more details.";

80
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -36,6 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@ -208,10 +209,12 @@ public class TelemetryController extends BaseController {
public DeferredResult<ResponseEntity> getAttributes(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
SecurityUser user = getCurrentUser();
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr,
(result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr));
(result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keys));
}
@ -231,10 +234,12 @@ public class TelemetryController extends BaseController {
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
SecurityUser user = getCurrentUser();
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr,
(result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr));
(result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keys));
}
@ApiOperation(value = "Get time series keys (getTimeseriesKeys)",
@ -270,10 +275,12 @@ public class TelemetryController extends BaseController {
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(description = STRICT_DATA_TYPES_DESCRIPTION)
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException {
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
SecurityUser user = getCurrentUser();
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr,
(result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr, useStrictDataTypes));
(result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keys, useStrictDataTypes));
}
@ApiOperation(value = "Get time series data (getTimeseries)",
@ -291,7 +298,7 @@ public class TelemetryController extends BaseController {
public DeferredResult<ResponseEntity> getTimeseries(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION, required = true) @RequestParam(name = "keys") String keys,
@Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(description = "A long value representing the start timestamp of the time range in milliseconds, UTC.")
@RequestParam(name = "startTs") Long startTs,
@Parameter(description = "A long value representing the end timestamp of the time range in milliseconds, UTC.")
@ -312,9 +319,11 @@ public class TelemetryController extends BaseController {
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy,
@Parameter(description = STRICT_DATA_TYPES_DESCRIPTION)
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException {
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
DeferredResult<ResponseEntity> response = new DeferredResult<>();
Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), toKeysList(keys), startTs, endTs,
Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), keys, startTs, endTs,
intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()),
getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor());
return response;
@ -466,7 +475,7 @@ public class TelemetryController extends BaseController {
public DeferredResult<ResponseEntity> deleteEntityTimeseries(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = TELEMETRY_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr,
@Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(description = "A boolean value to specify if should be deleted all data for selected keys or only data that are in the selected time range.")
@RequestParam(name = "deleteAllDataForKeys", defaultValue = "false") boolean deleteAllDataForKeys,
@Parameter(description = "A long value representing the start timestamp of removal time range in milliseconds.")
@ -476,16 +485,17 @@ public class TelemetryController extends BaseController {
@Parameter(description = "If the parameter is set to true, the latest telemetry can be removed, otherwise, in case that parameter is set to false the latest value will not removed.")
@RequestParam(name = "deleteLatest", required = false, defaultValue = "true") boolean deleteLatest,
@Parameter(description = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.")
@RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException {
@RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest);
return deleteTimeseries(entityId, keys, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest);
}
private DeferredResult<ResponseEntity> deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys,
private DeferredResult<ResponseEntity> deleteTimeseries(EntityId entityIdStr, List<String> keys, boolean deleteAllDataForKeys,
Long startTs, Long endTs, boolean rewriteLatestIfDeleted, boolean deleteLatest) throws ThingsboardException {
List<String> keys = toKeysList(keysStr);
if (keys.isEmpty()) {
return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST);
return getImmediateDeferredResult("Empty keys: " + keys, HttpStatus.BAD_REQUEST);
}
SecurityUser user = getCurrentUser();
@ -547,9 +557,11 @@ public class TelemetryController extends BaseController {
public DeferredResult<ResponseEntity> deleteDeviceAttributes(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
return deleteAttributes(entityId, scope, keysStr);
return deleteAttributes(entityId, scope, keys);
}
@ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)",
@ -570,15 +582,20 @@ public class TelemetryController extends BaseController {
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) @PathVariable("scope") AttributeScope scope,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return deleteAttributes(entityId, scope, keysStr);
return deleteAttributes(entityId, scope, keys);
}
private List<String> getKeys(String keysStr, MultiValueMap<String, String> params) {
return params.get("key") != null ? params.get("key") : toKeysList(keysStr);
}
private DeferredResult<ResponseEntity> deleteAttributes(EntityId entityIdSrc, AttributeScope scope, String keysStr) throws ThingsboardException {
List<String> keys = toKeysList(keysStr);
private DeferredResult<ResponseEntity> deleteAttributes(EntityId entityIdSrc, AttributeScope scope, List<String> keys) throws ThingsboardException {
if (keys.isEmpty()) {
return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST);
return getImmediateDeferredResult("Empty keys: " + keys, HttpStatus.BAD_REQUEST);
}
SecurityUser user = getCurrentUser();
@ -709,30 +726,29 @@ public class TelemetryController extends BaseController {
});
}
private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String keys, Boolean useStrictDataTypes) {
private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, List<String> keys, Boolean useStrictDataTypes) {
ListenableFuture<List<TsKvEntry>> future;
if (StringUtils.isEmpty(keys)) {
if (keys.isEmpty()) {
future = tsService.findAllLatest(user.getTenantId(), entityId);
} else {
future = tsService.findLatest(user.getTenantId(), entityId, toKeysList(keys));
future = tsService.findLatest(user.getTenantId(), entityId, keys);
}
Futures.addCallback(future, getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor());
}
private void getAttributeValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, AttributeScope scope, String keys) {
List<String> keyList = toKeysList(keys);
FutureCallback<List<AttributeKvEntry>> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keyList);
private void getAttributeValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, AttributeScope scope, List<String> keys) {
FutureCallback<List<AttributeKvEntry>> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keys);
if (scope != null) {
if (keyList != null && !keyList.isEmpty()) {
Futures.addCallback(attributesService.find(user.getTenantId(), entityId, scope, keyList), callback, MoreExecutors.directExecutor());
if (keys != null && !keys.isEmpty()) {
Futures.addCallback(attributesService.find(user.getTenantId(), entityId, scope, keys), callback, MoreExecutors.directExecutor());
} else {
Futures.addCallback(attributesService.findAll(user.getTenantId(), entityId, scope), callback, MoreExecutors.directExecutor());
}
} else {
List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
for (AttributeScope tmpScope : AttributeScope.values()) {
if (keyList != null && !keyList.isEmpty()) {
futures.add(attributesService.find(user.getTenantId(), entityId, tmpScope, keyList));
if (keys != null && !keys.isEmpty()) {
futures.add(attributesService.find(user.getTenantId(), entityId, tmpScope, keys));
} else {
futures.add(attributesService.findAll(user.getTenantId(), entityId, tmpScope));
}

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

@ -82,19 +82,17 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
cfs.forEach(cf -> {
if (cf != null) {
calculatedFields.putIfAbsent(cf.getId(), cf);
List<CalculatedFieldLink> links = cf.getConfiguration().buildCalculatedFieldLinks(cf.getTenantId(), cf.getEntityId(), cf.getId());
calculatedFieldLinks.put(cf.getId(), new CopyOnWriteArrayList<>(links));
}
});
calculatedFields.values().forEach(cf -> {
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cf);
});
PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(calculatedFieldService::findAllCalculatedFieldLinks, initFetchPackSize);
cfls.forEach(link -> {
calculatedFieldLinks.computeIfAbsent(link.getCalculatedFieldId(), id -> new CopyOnWriteArrayList<>()).add(link);
});
calculatedFieldLinks.values().stream()
.flatMap(List::stream)
.forEach(link ->
entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link)
entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link)
);
}
@ -226,7 +224,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
log.debug("[{}] evict calculated field links from cache: {}", calculatedFieldId, oldCalculatedField);
calculatedFieldsCtx.remove(calculatedFieldId);
log.debug("[{}] evict calculated field ctx from cache: {}", calculatedFieldId, oldCalculatedField);
entityIdCalculatedFieldLinks.forEach((entityId, calculatedFieldLinks) -> calculatedFieldLinks.removeIf(link -> link.getCalculatedFieldId().equals(calculatedFieldId)));
entityIdCalculatedFieldLinks.forEach((entityId, calculatedFieldLinks) -> calculatedFieldLinks.removeIf(link -> link.calculatedFieldId().equals(calculatedFieldId)));
log.debug("[{}] evict calculated field links from cached links by entity id: {}", calculatedFieldId, oldCalculatedField);
}

2
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java

@ -172,7 +172,7 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
List<CalculatedFieldLink> links = calculatedFieldCache.getCalculatedFieldLinksByEntityId(entityId);
for (CalculatedFieldLink link : links) {
CalculatedFieldCtx ctx = calculatedFieldCache.getCalculatedFieldCtx(link.getCalculatedFieldId());
CalculatedFieldCtx ctx = calculatedFieldCache.getCalculatedFieldCtx(link.calculatedFieldId());
if (ctx != null && linkedEntityFilter.test(ctx)) {
return true;
}

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

@ -212,7 +212,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected static final String TEST_DIFFERENT_TENANT_NAME = "TEST DIFFERENT TENANT";
protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org";
private static final String SYS_ADMIN_PASSWORD = "sysadmin";
protected static final String SYS_ADMIN_PASSWORD = "sysadmin";
protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org";
protected static final String TENANT_ADMIN_PASSWORD = "tenant";
@ -767,6 +767,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return doPost("/api/device-with-credentials", request, Device.class);
}
protected ResultActions doGetAsync(String urlTemplate, MultiValueMap<String, String> params) throws Exception {
MockHttpServletRequestBuilder getRequest = get(urlTemplate).params(params);
setJwtToken(getRequest);
return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn()));
}
protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception {
MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);
setJwtToken(getRequest);
@ -992,6 +998,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return mockMvc.perform(deleteRequest);
}
protected ResultActions doDeleteAsync(String urlTemplate, MultiValueMap<String, String> params) throws Exception {
MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate)
.params(params);
setJwtToken(deleteRequest);
MvcResult result = mockMvc.perform(deleteRequest).andReturn();
result.getAsyncResult(DEFAULT_TIMEOUT);
return mockMvc.perform(asyncDispatch(result));
}
protected ResultActions doDeleteWithApiKey(String urlTemplate, String... params) throws Exception {
MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate);
setApiKey(deleteRequest);
@ -1394,12 +1409,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) {
@ -1408,7 +1423,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected PageData<CalculatedField> getCalculatedFields(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<EventInfo> getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception {

60
application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java

@ -19,7 +19,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.test.context.TestPropertySource;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
@ -232,6 +235,63 @@ public class TelemetryControllerTest extends AbstractControllerTest {
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody2, String.class, status().isBadRequest());
}
@Test
public void testDeleteTelemetryByKeyWithComma() throws Exception {
loginTenantAdmin();
Device device = createDevice();
String tsKey = "key1,key2";
String testBody = JacksonUtil.newObjectNode()
.put(tsKey, "value")
.toString();
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk());
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("key", tsKey);
params.add("deleteAllDataForKeys", "true");
ObjectNode tsData = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class);
assertThat(tsData.get("key1,key2").get(0).get("value").asText()).isEqualTo("value");
doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete", params);
ObjectNode tsDataAfterDeletion = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class);
Assert.assertTrue(tsDataAfterDeletion.get("key1,key2").get(0).get("value").isNull());
}
@Test
public void testDeleteTelemetryByKeysWithComma() throws Exception {
loginTenantAdmin();
Device device = createDevice();
String keyWithComma = "key1,key2";
String testBody = JacksonUtil.newObjectNode()
.put(keyWithComma, "value")
.toString();
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk());
String key = "key3";
String testBody2 = JacksonUtil.newObjectNode()
.put(key, "value")
.toString();
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("key", keyWithComma);
params.add("key", key);
params.add("deleteAllDataForKeys", "true");
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody2, String.class, status().isOk());
ObjectNode tsData = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class);
assertThat(tsData.get("key1,key2").get(0).get("value").asText()).isEqualTo("value");
assertThat(tsData.get("key3").get(0).get("value").asText()).isEqualTo("value");
doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete", params);
ObjectNode tsDataAfterDeletion = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class);
Assert.assertTrue(tsDataAfterDeletion.get("key1,key2").get(0).get("value").isNull());
Assert.assertTrue(tsDataAfterDeletion.get("key3").get(0).get("value").isNull());
}
private Device createDevice() throws Exception {
String testToken = "TEST_TOKEN";

34
application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
@ -415,7 +416,8 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
logInWithMfaToken(username, password, Authority.MFA_CONFIGURATION_TOKEN);
TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, totpTwoFaProviderConfig.getProviderType());
TotpTwoFaAccountConfig totpTwoFaAccountConfig = doPost("/api/2fa/account/config/generate?providerType=" + totpTwoFaProviderConfig.getProviderType(), TotpTwoFaAccountConfig.class);
String secret = UriComponentsBuilder.fromUriString(totpTwoFaAccountConfig.getAuthUrl()).build()
.getQueryParams().getFirst("secret");
String verificationCode = new Totp(secret).now();
@ -433,6 +435,36 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
doGet("/api/user/" + savedDifferentTenantUser.getId()).andExpect(status().isOk());
}
@Test
public void testEnforceTwoFa_sysadmin() throws Exception {
TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb");
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList()));
twoFaSettings.setMinVerificationCodeSendPeriod(5);
twoFaSettings.setTotalAllowedTimeForVerification(100);
twoFaSettings.setEnforceTwoFa(true);
AllUsersFilter enforcedUsersFilter = new AllUsersFilter();
twoFaSettings.setEnforcedUsersFilter(enforcedUsersFilter);
twoFaSettings = twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
logInWithMfaToken(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD, Authority.MFA_CONFIGURATION_TOKEN);
TotpTwoFaAccountConfig totpTwoFaAccountConfig = doPost("/api/2fa/account/config/generate?providerType=" + totpTwoFaProviderConfig.getProviderType(), TotpTwoFaAccountConfig.class);
String secret = UriComponentsBuilder.fromUriString(totpTwoFaAccountConfig.getAuthUrl()).build()
.getQueryParams().getFirst("secret");
String verificationCode = new Totp(secret).now();
readResponse(doPost("/api/2fa/account/config?verificationCode=" + verificationCode, totpTwoFaAccountConfig).andExpect(status().isOk()), JsonNode.class);
JwtPair tokenPair = readResponse(doPost("/api/auth/2fa/login").andExpect(status().isOk()), JwtPair.class);
assertThat(tokenPair.getToken()).isNotEmpty();
assertThat(tokenPair.getRefreshToken()).isNotEmpty();
validateAndSetJwtToken(tokenPair, SYS_ADMIN_EMAIL);
doGet("/api/user/" + user.getId()).andExpect(status().isOk());
}
private void logInWithMfaToken(String username, String password, Authority expectedScope) throws Exception {
LoginRequest loginRequest = new LoginRequest(username, password);

14
common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java

@ -16,10 +16,8 @@
package org.thingsboard.server.dao.cf;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -52,18 +50,6 @@ public interface CalculatedFieldService extends EntityDaoService {
int deleteAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId);
CalculatedFieldLink saveCalculatedFieldLink(TenantId tenantId, CalculatedFieldLink calculatedFieldLink);
CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId);
List<CalculatedFieldLink> findAllCalculatedFieldLinksById(TenantId tenantId, CalculatedFieldId calculatedFieldId);
List<CalculatedFieldLink> findAllCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId);
PageData<CalculatedFieldLink> findAllCalculatedFieldLinksByTenantId(TenantId tenantId, PageLink pageLink);
PageData<CalculatedFieldLink> findAllCalculatedFieldLinks(PageLink pageLink);
boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId);
}

3
common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java

@ -24,6 +24,7 @@ import java.util.EnumSet;
import java.util.List;
public enum EntityType {
TENANT(1),
CUSTOMER(2),
USER(3, "tb_user"),
@ -62,7 +63,7 @@ public enum EntityType {
MOBILE_APP(37),
MOBILE_APP_BUNDLE(38),
CALCULATED_FIELD(39),
CALCULATED_FIELD_LINK(40),
// CALCULATED_FIELD_LINK(40), - was removed in 4.3
JOB(41),
ADMIN_SETTINGS(42),
AI_MODEL(43, "ai_model") {

46
common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java

@ -15,52 +15,8 @@
*/
package org.thingsboard.server.common.data.cf;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@Schema
@Data
@EqualsAndHashCode(callSuper = true)
public class CalculatedFieldLink extends BaseData<CalculatedFieldLinkId> {
private static final long serialVersionUID = 6492846246722091530L;
private TenantId tenantId;
private EntityId entityId;
@Schema(description = "JSON object with the Calculated Field Id. ", accessMode = Schema.AccessMode.READ_ONLY)
private CalculatedFieldId calculatedFieldId;
public CalculatedFieldLink() {
super();
}
public CalculatedFieldLink(CalculatedFieldLinkId id) {
super(id);
}
public CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId) {
this.tenantId = tenantId;
this.entityId = entityId;
this.calculatedFieldId = calculatedFieldId;
}
@Override
public String toString() {
return new StringBuilder()
.append("CalculatedFieldLink[")
.append("tenantId=").append(tenantId)
.append(", entityId=").append(entityId)
.append(", calculatedFieldId=").append(calculatedFieldId)
.append(", createdTime=").append(createdTime)
.append(", id=").append(id).append(']')
.toString();
}
}
public record CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId) {}

17
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java

@ -19,10 +19,12 @@ import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.List;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.Set;
import static java.util.stream.Collectors.toSet;
public interface ArgumentsBasedCalculatedFieldConfiguration extends CalculatedFieldConfiguration {
@ -30,14 +32,15 @@ public interface ArgumentsBasedCalculatedFieldConfiguration extends CalculatedFi
@NotEmpty
Map<String, Argument> getArguments();
default List<EntityId> getReferencedEntities() {
if (getArguments() == null) {
return List.of();
default Set<EntityId> getReferencedEntities() {
var args = getArguments();
if (args == null) {
return Collections.emptySet();
}
return getArguments().values().stream()
return args.values().stream()
.map(Argument::getRefEntityId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
.collect(toSet());
}
}

12
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java

@ -28,7 +28,9 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@JsonTypeInfo(
@ -55,16 +57,12 @@ public interface CalculatedFieldConfiguration {
default void validate() {}
@JsonIgnore
default List<EntityId> getReferencedEntities() {
return List.of();
default Set<EntityId> getReferencedEntities() {
return Collections.emptySet();
}
default CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId) {
CalculatedFieldLink link = new CalculatedFieldLink();
link.setTenantId(tenantId);
link.setEntityId(referencedEntityId);
link.setCalculatedFieldId(calculatedFieldId);
return link;
return new CalculatedFieldLink(tenantId, referencedEntityId, calculatedFieldId);
}
default List<CalculatedFieldLink> buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId) {

12
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java

@ -26,10 +26,13 @@ import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static java.util.stream.Collectors.toSet;
@Data
public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration {
@ -62,8 +65,11 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal
@Override
public List<EntityId> getReferencedEntities() {
return zoneGroups == null ? List.of() : zoneGroups.values().stream().map(ZoneGroupConfiguration::getRefEntityId).filter(Objects::nonNull).toList();
public Set<EntityId> getReferencedEntities() {
return zoneGroups == null ? Collections.emptySet() : zoneGroups.values().stream()
.map(ZoneGroupConfiguration::getRefEntityId)
.filter(Objects::nonNull)
.collect(toSet());
}
@Override

45
common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java

@ -1,45 +0,0 @@
/**
* 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.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@Schema
public class CalculatedFieldLinkId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@JsonCreator
public CalculatedFieldLinkId(@JsonProperty("id") UUID id) {
super(id);
}
public static CalculatedFieldLinkId fromString(String calculatedFieldLinkId) {
return new CalculatedFieldLinkId(UUID.fromString(calculatedFieldLinkId));
}
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CALCULATED_FIELD_LINK", allowableValues = "CALCULATED_FIELD_LINK")
@Override
public EntityType getEntityType() {
return EntityType.CALCULATED_FIELD_LINK;
}
}

1
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java

@ -80,7 +80,6 @@ public class EntityIdFactory {
case DOMAIN -> new DomainId(uuid);
case MOBILE_APP_BUNDLE -> new MobileAppBundleId(uuid);
case CALCULATED_FIELD -> new CalculatedFieldId(uuid);
case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid);
case JOB -> new JobId(uuid);
case ADMIN_SETTINGS -> new AdminSettingsId(uuid);
case AI_MODEL -> new AiModelId(uuid);

2
common/proto/src/main/proto/queue.proto

@ -62,7 +62,7 @@ enum EntityTypeProto {
MOBILE_APP = 37;
MOBILE_APP_BUNDLE = 38;
CALCULATED_FIELD = 39;
CALCULATED_FIELD_LINK = 40;
// CALCULATED_FIELD_LINK = 40; - was removed in 4.3
JOB = 41;
ADMIN_SETTINGS = 42;
AI_MODEL = 43;

62
dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java

@ -21,11 +21,9 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
@ -36,7 +34,6 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.validator.CalculatedFieldDataValidator;
import org.thingsboard.server.dao.service.validator.CalculatedFieldLinkDataValidator;
import java.util.EnumSet;
import java.util.List;
@ -57,9 +54,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
public static final String INCORRECT_ENTITY_ID = "Incorrect entityId ";
private final CalculatedFieldDao calculatedFieldDao;
private final CalculatedFieldLinkDao calculatedFieldLinkDao;
private final CalculatedFieldDataValidator calculatedFieldDataValidator;
private final CalculatedFieldLinkDataValidator calculatedFieldLinkDataValidator;
@Override
public CalculatedField save(CalculatedField calculatedField) {
@ -84,9 +79,13 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
log.trace("Executing save calculated field, [{}]", calculatedField);
updateDebugSettings(tenantId, calculatedField, System.currentTimeMillis());
CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField);
createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId())
.entity(savedCalculatedField).oldEntity(oldCalculatedField).created(calculatedField.getId() == null).build());
eventPublisher.publishEvent(SaveEntityEvent.builder()
.tenantId(savedCalculatedField.getTenantId())
.entityId(savedCalculatedField.getId())
.entity(savedCalculatedField)
.oldEntity(oldCalculatedField)
.created(calculatedField.getId() == null)
.build());
return savedCalculatedField;
} catch (Exception e) {
checkConstraintViolation(e,
@ -191,48 +190,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
return calculatedFields.size();
}
@Override
public CalculatedFieldLink saveCalculatedFieldLink(TenantId tenantId, CalculatedFieldLink calculatedFieldLink) {
calculatedFieldLinkDataValidator.validate(calculatedFieldLink, CalculatedFieldLink::getTenantId);
log.trace("Executing save calculated field link, [{}]", calculatedFieldLink);
return calculatedFieldLinkDao.save(tenantId, calculatedFieldLink);
}
@Override
public CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId) {
log.trace("Executing findCalculatedFieldLinkById, tenantId [{}], calculatedFieldLinkId [{}]", tenantId, calculatedFieldLinkId);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validateId(calculatedFieldLinkId, id -> "Incorrect calculatedFieldLinkId " + id);
return calculatedFieldLinkDao.findById(tenantId, calculatedFieldLinkId.getId());
}
@Override
public List<CalculatedFieldLink> findAllCalculatedFieldLinksById(TenantId tenantId, CalculatedFieldId calculatedFieldId) {
log.trace("Executing findAllCalculatedFieldLinksById, calculatedFieldId [{}]", calculatedFieldId);
return calculatedFieldLinkDao.findCalculatedFieldLinksByCalculatedFieldId(tenantId, calculatedFieldId);
}
@Override
public List<CalculatedFieldLink> findAllCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId) {
log.trace("Executing findAllCalculatedFieldLinksByEntityId, entityId [{}]", entityId);
return calculatedFieldLinkDao.findCalculatedFieldLinksByEntityId(tenantId, entityId);
}
@Override
public PageData<CalculatedFieldLink> findAllCalculatedFieldLinksByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findAllCalculatedFieldLinksByTenantId, tenantId[{}] pageLink [{}]", tenantId, pageLink);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validatePageLink(pageLink);
return calculatedFieldLinkDao.findAllByTenantId(tenantId, pageLink);
}
@Override
public PageData<CalculatedFieldLink> findAllCalculatedFieldLinks(PageLink pageLink) {
log.trace("Executing findAllCalculatedFieldLinks, pageLink [{}]", pageLink);
validatePageLink(pageLink);
return calculatedFieldLinkDao.findAll(pageLink);
}
@Override
public boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId) {
return calculatedFieldDao.findAllByTenantId(tenantId).stream()
@ -258,9 +215,4 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
return EntityType.CALCULATED_FIELD;
}
private void createOrUpdateCalculatedFieldLink(TenantId tenantId, CalculatedField calculatedField) {
List<CalculatedFieldLink> links = calculatedField.getConfiguration().buildCalculatedFieldLinks(tenantId, calculatedField.getEntityId(), calculatedField.getId());
links.forEach(link -> saveCalculatedFieldLink(tenantId, link));
}
}

42
dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java

@ -1,42 +0,0 @@
/**
* 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.dao.cf;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import java.util.List;
public interface CalculatedFieldLinkDao extends Dao<CalculatedFieldLink> {
List<CalculatedFieldLink> findCalculatedFieldLinksByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId);
List<CalculatedFieldLink> findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId);
List<CalculatedFieldLink> findCalculatedFieldLinksByTenantId(TenantId tenantId);
List<CalculatedFieldLink> findAll();
PageData<CalculatedFieldLink> findAll(PageLink pageLink);
PageData<CalculatedFieldLink> findAllByTenantId(TenantId tenantId, PageLink pageLink);
}

3
dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java

@ -43,9 +43,6 @@ public class DefaultEntityServiceRegistry implements EntityServiceRegistry {
if (EntityType.RULE_CHAIN.equals(entityType)) {
entityDaoServicesMap.put(EntityType.RULE_NODE, entityDaoService);
}
if (EntityType.CALCULATED_FIELD.equals(entityType)) {
entityDaoServicesMap.put(EntityType.CALCULATED_FIELD_LINK, entityDaoService);
}
});
log.debug("Initialized EntityServiceRegistry total [{}] entries", entityDaoServicesMap.size());
}

12
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -26,8 +26,7 @@ import java.util.UUID;
public class ModelConstants {
private ModelConstants() {
}
private ModelConstants() {}
public static final UUID NULL_UUID = Uuids.startOf(0);
public static final TenantId SYSTEM_TENANT = TenantId.fromUUID(ModelConstants.NULL_UUID);
@ -731,15 +730,6 @@ public class ModelConstants {
public static final String CALCULATED_FIELD_CONFIGURATION = "configuration";
public static final String CALCULATED_FIELD_VERSION = "version";
/**
* Calculated field links constants.
*/
public static final String CALCULATED_FIELD_LINK_TABLE_NAME = "calculated_field_link";
public static final String CALCULATED_FIELD_LINK_TENANT_ID_COLUMN = TENANT_ID_COLUMN;
public static final String CALCULATED_FIELD_LINK_ENTITY_TYPE = ENTITY_TYPE_COLUMN;
public static final String CALCULATED_FIELD_LINK_ENTITY_ID = ENTITY_ID_COLUMN;
public static final String CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID = "calculated_field_id";
/**
* Tasks constants.
*/

79
dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java

@ -1,79 +0,0 @@
/**
* 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.dao.model.sql;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_ID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_TYPE;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TENANT_ID_COLUMN;
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = CALCULATED_FIELD_LINK_TABLE_NAME)
public class CalculatedFieldLinkEntity extends BaseSqlEntity<CalculatedFieldLink> implements BaseEntity<CalculatedFieldLink> {
@Column(name = CALCULATED_FIELD_LINK_TENANT_ID_COLUMN)
private UUID tenantId;
@Column(name = CALCULATED_FIELD_LINK_ENTITY_TYPE)
private String entityType;
@Column(name = CALCULATED_FIELD_LINK_ENTITY_ID)
private UUID entityId;
@Column(name = CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID)
private UUID calculatedFieldId;
public CalculatedFieldLinkEntity() {
super();
}
public CalculatedFieldLinkEntity(CalculatedFieldLink calculatedFieldLink) {
super(calculatedFieldLink);
this.tenantId = calculatedFieldLink.getTenantId().getId();
this.entityType = calculatedFieldLink.getEntityId().getEntityType().name();
this.entityId = calculatedFieldLink.getEntityId().getId();
this.calculatedFieldId = calculatedFieldLink.getCalculatedFieldId().getId();
}
@Override
public CalculatedFieldLink toData() {
CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(new CalculatedFieldLinkId(id));
calculatedFieldLink.setCreatedTime(createdTime);
calculatedFieldLink.setTenantId(TenantId.fromUUID(tenantId));
calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId));
calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId));
return calculatedFieldLink;
}
}

41
dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java

@ -1,41 +0,0 @@
/**
* 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.dao.service.validator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
@Component
public class CalculatedFieldLinkDataValidator extends DataValidator<CalculatedFieldLink> {
@Autowired
private CalculatedFieldLinkDao calculatedFieldLinkDao;
@Override
protected CalculatedFieldLink validateUpdate(TenantId tenantId, CalculatedFieldLink calculatedFieldLink) {
CalculatedFieldLink old = calculatedFieldLinkDao.findById(calculatedFieldLink.getTenantId(), calculatedFieldLink.getId().getId());
if (old == null) {
throw new DataValidationException("Can't update non existing calculated field link!");
}
return old;
}
}

36
dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java

@ -1,36 +0,0 @@
/**
* 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.dao.sql.cf;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity;
import java.util.List;
import java.util.UUID;
public interface CalculatedFieldLinkRepository extends JpaRepository<CalculatedFieldLinkEntity, UUID> {
List<CalculatedFieldLinkEntity> findAllByTenantIdAndCalculatedFieldId(UUID tenantId, UUID calculatedFieldId);
List<CalculatedFieldLinkEntity> findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId);
List<CalculatedFieldLinkEntity> findAllByTenantId(UUID tenantId);
Page<CalculatedFieldLinkEntity> findAllByTenantId(UUID tenantId, Pageable pageable);
}

38
dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java

@ -25,12 +25,10 @@ import org.springframework.transaction.support.TransactionTemplate;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -49,9 +47,6 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
private final String CF_COUNT_QUERY = "SELECT count(id) FROM calculated_field;";
private final String CF_QUERY = "SELECT * FROM calculated_field ORDER BY created_time ASC LIMIT %s OFFSET %s";
private final String CFL_COUNT_QUERY = "SELECT count(id) FROM calculated_field_link;";
private final String CFL_QUERY = "SELECT * FROM calculated_field_link ORDER BY created_time ASC LIMIT %s OFFSET %s";
private final NamedParameterJdbcTemplate jdbcTemplate;
private final TransactionTemplate transactionTemplate;
@ -103,37 +98,4 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
});
}
@Override
public PageData<CalculatedFieldLink> findCalculatedFieldLinks(Pageable pageable) {
return transactionTemplate.execute(status -> {
long startTs = System.currentTimeMillis();
int totalElements = jdbcTemplate.queryForObject(CFL_COUNT_QUERY, Collections.emptyMap(), Integer.class);
log.debug("Count query took {} ms", System.currentTimeMillis() - startTs);
startTs = System.currentTimeMillis();
List<Map<String, Object>> rows = jdbcTemplate.queryForList(String.format(CFL_QUERY, pageable.getPageSize(), pageable.getOffset()), Collections.emptyMap());
log.debug("Main query took {} ms", System.currentTimeMillis() - startTs);
int totalPages = pageable.getPageSize() > 0 ? (int) Math.ceil((float) totalElements / pageable.getPageSize()) : 1;
boolean hasNext = pageable.getPageSize() > 0 && totalElements > pageable.getOffset() + rows.size();
var data = rows.stream().map(row -> {
UUID id = (UUID) row.get("id");
long createdTime = (long) row.get("created_time");
UUID tenantId = (UUID) row.get("tenant_id");
EntityType entityType = EntityType.valueOf((String) row.get("entity_type"));
UUID entityId = (UUID) row.get("entity_id");
UUID calculatedFieldId = (UUID) row.get("calculated_field_id");
CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink();
calculatedFieldLink.setId(new CalculatedFieldLinkId(id));
calculatedFieldLink.setCreatedTime(createdTime);
calculatedFieldLink.setTenantId(new TenantId(tenantId));
calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId));
calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId));
return calculatedFieldLink;
}).collect(Collectors.toList());
return new PageData<>(data, totalPages, totalElements, hasNext);
});
}
}

94
dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java

@ -1,94 +0,0 @@
/**
* 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.dao.sql.cf;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao;
import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
import java.util.UUID;
@Slf4j
@Component
@AllArgsConstructor
@SqlDao
public class JpaCalculatedFieldLinkDao extends JpaAbstractDao<CalculatedFieldLinkEntity, CalculatedFieldLink> implements CalculatedFieldLinkDao {
private final CalculatedFieldLinkRepository calculatedFieldLinkRepository;
private final NativeCalculatedFieldRepository nativeCalculatedFieldRepository;
@Override
public List<CalculatedFieldLink> findCalculatedFieldLinksByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId) {
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndCalculatedFieldId(tenantId.getId(), calculatedFieldId.getId()));
}
@Override
public List<CalculatedFieldLink> findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId) {
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId()));
}
@Override
public List<CalculatedFieldLink> findCalculatedFieldLinksByTenantId(TenantId tenantId) {
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId()));
}
@Override
public List<CalculatedFieldLink> findAll() {
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAll());
}
@Override
public PageData<CalculatedFieldLink> findAll(PageLink pageLink) {
log.debug("Try to find calculated field links by pageLink [{}]", pageLink);
return nativeCalculatedFieldRepository.findCalculatedFieldLinks(DaoUtil.toPageable(pageLink));
}
@Override
public PageData<CalculatedFieldLink> findAllByTenantId(TenantId tenantId, PageLink pageLink) {
log.debug("Try to find calculated field links by tenantId [{}], pageLink [{}]", tenantId, pageLink);
return DaoUtil.toPageData(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)));
}
@Override
protected Class<CalculatedFieldLinkEntity> getEntityClass() {
return CalculatedFieldLinkEntity.class;
}
@Override
protected JpaRepository<CalculatedFieldLinkEntity, UUID> getRepository() {
return calculatedFieldLinkRepository;
}
@Override
public EntityType getEntityType() {
return EntityType.CALCULATED_FIELD_LINK;
}
}

3
dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java

@ -17,13 +17,10 @@ package org.thingsboard.server.dao.sql.cf;
import org.springframework.data.domain.Pageable;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.page.PageData;
public interface NativeCalculatedFieldRepository {
PageData<CalculatedField> findCalculatedFields(Pageable pageable);
PageData<CalculatedFieldLink> findCalculatedFieldLinks(Pageable pageable);
}

10
dao/src/main/resources/sql/schema-entities.sql

@ -938,16 +938,6 @@ CREATE TABLE IF NOT EXISTS calculated_field (
CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name)
);
CREATE TABLE IF NOT EXISTS calculated_field_link (
id uuid NOT NULL CONSTRAINT calculated_field_link_pkey PRIMARY KEY,
created_time bigint NOT NULL,
tenant_id uuid NOT NULL,
entity_type VARCHAR(32),
entity_id uuid NOT NULL,
calculated_field_id uuid NOT NULL,
CONSTRAINT fk_calculated_field_id FOREIGN KEY (calculated_field_id) REFERENCES calculated_field(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS cf_debug_event (
id uuid NOT NULL,
tenant_id uuid NOT NULL ,

23
dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java

@ -15,13 +15,8 @@
*/
package org.thingsboard.server.dao.service;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@ -64,18 +59,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
@Autowired
private TbTenantProfileCache tbTenantProfileCache;
private ListeningExecutorService executor;
@Before
public void before() {
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass()));
}
@After
public void after() {
executor.shutdownNow();
}
@Test
public void testSaveCalculatedField() {
Device device = createTestDevice();
@ -144,7 +127,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
.isInstanceOf(DataValidationException.class)
.hasCauseInstanceOf(IllegalArgumentException.class)
.hasMessageStartingWith("Scheduled update interval is less than configured " +
"minimum allowed interval in tenant profile: ");
"minimum allowed interval in tenant profile: ");
}
@Test
@ -163,7 +146,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
.getMaxRelationLevelPerCfArgument();
// Zone-group argument (ATTRIBUTE)
ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration( "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
List<RelationPathLevel> levels = new ArrayList<>();
@ -203,7 +186,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
cfg.setEntityCoordinates(entityCoordinates);
// Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled
ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration( "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
dynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)));
zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration);

5
dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java

@ -20,7 +20,6 @@ import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.entity.EntityDaoService;
import org.thingsboard.server.dao.entity.EntityServiceRegistry;
import org.thingsboard.server.dao.rule.RuleChainService;
@ -45,8 +44,4 @@ public class EntityServiceRegistryTest extends AbstractServiceTest {
Assert.assertTrue(entityServiceRegistry.getServiceByEntityType(EntityType.RULE_NODE) instanceof RuleChainService);
}
@Test
public void givenCalculatedFieldLinkEntityType_whenGetServiceByEntityTypeCalled_thenReturnedCalculatedFieldService() {
Assert.assertTrue(entityServiceRegistry.getServiceByEntityType(EntityType.CALCULATED_FIELD_LINK) instanceof CalculatedFieldService);
}
}

10
dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java

@ -17,8 +17,8 @@ package org.thingsboard.server.dao.service.validator;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
@ -38,11 +38,11 @@ public class CalculatedFieldDataValidatorTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("7b5229e9-166e-41a9-a257-3b1dafad1b04"));
private final CalculatedFieldId CALCULATED_FIELD_ID = new CalculatedFieldId(UUID.fromString("060fbe45-fbb2-4549-abf3-f72a6be3cb9f"));
@MockBean
@MockitoBean
private CalculatedFieldDao calculatedFieldDao;
@MockBean
@MockitoBean
private DefaultApiLimitService apiLimitService;
@SpyBean
@MockitoSpyBean
private CalculatedFieldDataValidator validator;
@Test

57
dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java

@ -1,57 +0,0 @@
/**
* 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.dao.service.validator;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.given;
@SpringBootTest(classes = CalculatedFieldLinkDataValidator.class)
public class CalculatedFieldLinkDataValidatorTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("2ba09d99-6143-43dc-b645-381fc0c43ebe"));
private final CalculatedFieldLinkId CALCULATED_FIELD_LINK_ID = new CalculatedFieldLinkId(UUID.fromString("a5609ef4-cb42-43ce-9b23-e090a4878d1c"));
@MockBean
private CalculatedFieldLinkDao calculatedFieldLinkDao;
@SpyBean
private CalculatedFieldLinkDataValidator validator;
@Test
public void testUpdateNonExistingCalculatedField() {
CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(CALCULATED_FIELD_LINK_ID);
calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(UUID.fromString("136477af-fd07-4498-b9c9-54fe50e82992")));
given(calculatedFieldLinkDao.findById(TENANT_ID, CALCULATED_FIELD_LINK_ID.getId())).willReturn(null);
assertThatThrownBy(() -> validator.validateUpdate(TENANT_ID, calculatedFieldLink))
.isInstanceOf(DataValidationException.class)
.hasMessage("Can't update non existing calculated field link!");
}
}

10
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java

@ -17,7 +17,7 @@ package org.thingsboard.server.msa.cf;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
@ -600,7 +600,7 @@ public class CalculatedFieldTest extends AbstractContainerTest {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId);
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("C to F" + RandomStringUtils.randomAlphabetic(5));
calculatedField.setName("C to F" + RandomStringUtils.insecure().nextAlphabetic(5));
calculatedField.setDebugSettings(DebugSettings.all());
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
@ -624,15 +624,11 @@ public class CalculatedFieldTest extends AbstractContainerTest {
return testRestClient.postCalculatedField(calculatedField);
}
private CalculatedField createScriptCalculatedField() {
return createScriptCalculatedField(device.getId(), asset.getId());
}
private CalculatedField createScriptCalculatedField(EntityId entityId, EntityId refEntityId) {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId);
calculatedField.setType(CalculatedFieldType.SCRIPT);
calculatedField.setName("Air density" + RandomStringUtils.randomAlphabetic(5));
calculatedField.setName("Air density" + RandomStringUtils.insecure().nextAlphabetic(5));
calculatedField.setDebugSettings(DebugSettings.all());
ScriptCalculatedFieldConfiguration config = new ScriptCalculatedFieldConfiguration();

128
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java

@ -0,0 +1,128 @@
/**
* 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.msa.connectivity;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.core5.ssl.SSLContexts;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.thingsboard.rest.client.RestClient;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.msa.AbstractContainerTest;
import org.thingsboard.server.msa.TestProperties;
import javax.net.ssl.SSLContext;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype;
public class JavaRestClientTest extends AbstractContainerTest {
private RestClient restClient;
@BeforeClass
public void beforeClass() throws Exception {
SSLContext ssl = SSLContexts.custom()
.loadTrustMaterial((chain, authType) -> true)
.build();
var tls = new DefaultClientTlsStrategy(
ssl,
HostnameVerificationPolicy.CLIENT,
NoopHostnameVerifier.INSTANCE
);
HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(tls)
.build();
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
.build();
RestTemplate rt = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient));
restClient = new RestClient(rt, TestProperties.getBaseUrl());
}
@BeforeMethod
public void setUp() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant");
}
@AfterMethod
public void tearDown() {
}
@Test
public void testGetAlarmsV2() {
Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.randomAlphabetic(5)));
assertThat(device).isNotNull();
String type = "High temp" + RandomStringUtils.randomAlphabetic(5);
Alarm alarm = Alarm.builder()
.originator(device.getId())
.severity(AlarmSeverity.CRITICAL)
.type(type)
.build();
restClient.saveAlarm(alarm);
// get /api/v2/alarm
PageData<AlarmInfo> alarmsV2 = restClient.getAlarmsV2(device.getId(), null, null, List.of(type), null, new TimePageLink(10, 0));
assertThat(alarmsV2.getData()).hasSize(1);
PageData<AlarmInfo> activeAlarms = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.ACTIVE), null, List.of(type), null, new TimePageLink(10, 0));
assertThat(activeAlarms.getData()).hasSize(1);
PageData<AlarmInfo> cleared = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.CLEARED), null, List.of(type), null, new TimePageLink(10, 0));
assertThat(cleared.getData()).hasSize(0);
PageData<AlarmInfo> activeAndClearedAlarms = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACTIVE), null, null, null, new TimePageLink(10, 0));
assertThat(activeAndClearedAlarms.getData()).hasSize(1);
// get /api/v2/alarms
PageData<AlarmInfo> allAlarmsV2 = restClient.getAllAlarmsV2(List.of(AlarmSearchStatus.ACTIVE), null, List.of(type), null, new TimePageLink(10, 0));
assertThat(allAlarmsV2.getData()).hasSize(1);
PageData<AlarmInfo> allClearedAlarmsV2 = restClient.getAllAlarmsV2(List.of(AlarmSearchStatus.CLEARED), null, List.of(type), null, new TimePageLink(10, 0));
assertThat(allClearedAlarmsV2.getData()).hasSize(0);
// get /api/alarms
PageData<AlarmInfo> allAlarms = restClient.getAllAlarms(AlarmSearchStatus.ACTIVE, null, new TimePageLink(10, 0), null);
assertThat(allAlarms.getData()).hasSize(1);
PageData<AlarmInfo> allClearedAlarms = restClient.getAllAlarms(AlarmSearchStatus.CLEARED, null, new TimePageLink(10, 0), null);
assertThat(allClearedAlarms.getData()).hasSize(0);
}
}

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

@ -116,6 +116,8 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId;
import org.thingsboard.server.common.data.id.OtaPackageId;
@ -135,6 +137,13 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
import org.thingsboard.server.common.data.notification.NotificationRequestPreview;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo;
@ -521,6 +530,99 @@ public class RestClient implements Closeable {
params).getBody();
}
public PageData<AlarmInfo> getAllAlarms(AlarmSearchStatus searchStatus, AlarmStatus status, TimePageLink pageLink, Boolean fetchOriginator) {
String urlSecondPart = "/api/alarms?";
Map<String, String> params = new HashMap<>();
if (fetchOriginator != null) {
params.put("fetchOriginator", String.valueOf(fetchOriginator));
urlSecondPart += "&fetchOriginator={fetchOriginator}";
}
if (searchStatus != null) {
params.put("searchStatus", searchStatus.name());
urlSecondPart += "&searchStatus={searchStatus}";
}
if (status != null) {
params.put("status", status.name());
urlSecondPart += "&status={status}";
}
addTimePageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<AlarmInfo>>() {
},
params).getBody();
}
public PageData<AlarmInfo> getAlarmsV2(EntityId entityId, List<AlarmSearchStatus> statusList, List<AlarmSeverity> severityList,
List<String> typeList, String assignedId, TimePageLink pageLink) {
String urlSecondPart = "/api/v2/alarm/{entityType}/{entityId}?";
Map<String, String> params = new HashMap<>();
params.put("entityType", entityId.getEntityType().name());
params.put("entityId", entityId.getId().toString());
if (!CollectionUtils.isEmpty(statusList)) {
params.put("statusList", listEnumToString(statusList));
urlSecondPart += "&statusList={statusList}";
}
if (!CollectionUtils.isEmpty(severityList)) {
params.put("severityList", listEnumToString(severityList));
urlSecondPart += "&severityList={severityList}";
}
if (!CollectionUtils.isEmpty(typeList)) {
params.put("typeList", String.join(",", typeList));
urlSecondPart += "&typeList={typeList}";
}
if (assignedId != null) {
params.put("assignedId", assignedId);
urlSecondPart += "&assignedId={assignedId}";
}
addTimePageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<AlarmInfo>>() {
},
params).getBody();
}
public PageData<AlarmInfo> getAllAlarmsV2(List<AlarmSearchStatus> statusList, List<AlarmSeverity> severityList,
List<String> typeList, String assignedId, TimePageLink pageLink) {
String urlSecondPart = "/api/v2/alarms?";
Map<String, String> params = new HashMap<>();
if (!CollectionUtils.isEmpty(statusList)) {
params.put("statusList", listEnumToString(statusList));
urlSecondPart += "&statusList={statusList}";
}
if (!CollectionUtils.isEmpty(severityList)) {
params.put("severityList", listEnumToString(severityList));
urlSecondPart += "&severityList={severityList}";
}
if (!CollectionUtils.isEmpty(typeList)) {
params.put("typeList", String.join(",", typeList));
urlSecondPart += "&typeList={typeList}";
}
if (assignedId != null) {
params.put("assignedId", assignedId);
urlSecondPart += "&assignedId={assignedId}";
}
addTimePageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<AlarmInfo>>() {
},
params).getBody();
}
public Optional<AlarmSeverity> getHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus searchStatus, AlarmStatus status) {
Map<String, String> params = new HashMap<>();
params.put("entityType", entityId.getEntityType().name());
@ -1722,6 +1824,14 @@ 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 PageData<AlarmData> findAlarmDataByQuery(AlarmDataQuery query) {
return restTemplate.exchange(
baseURL + "/api/alarmsQuery/find",
@ -2170,9 +2280,11 @@ public class RestClient implements Closeable {
restTemplate.delete(baseURL + "/api/oauth2/client/{id}", oAuth2ClientId.getId());
}
public PageData<DomainInfo> getTenantDomainInfos() {
public PageData<DomainInfo> getTenantDomainInfos(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/domain/infos",
baseURL + "/api/domain/infos?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<DomainInfo>>() {
@ -2204,9 +2316,11 @@ public class RestClient implements Closeable {
restTemplate.postForLocation(baseURL + "/api/domain/{id}/oauth2Clients", oauth2ClientIds, domainId.getId());
}
public PageData<MobileApp> getTenantMobileApps() {
public PageData<MobileApp> getTenantMobileApps(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/mobile/app",
baseURL + "/api/mobile/app?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<MobileApp>>() {
@ -2234,9 +2348,11 @@ public class RestClient implements Closeable {
restTemplate.delete(baseURL + "/api/mobile/app/{id}", mobileAppId.getId());
}
public PageData<MobileAppBundleInfo> getTenantMobileBundleInfos() {
public PageData<MobileAppBundleInfo> getTenantMobileBundleInfos(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/mobile/bundle/infos",
baseURL + "/api/mobile/bundle/infos?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<MobileAppBundleInfo>>() {
@ -2858,6 +2974,17 @@ public class RestClient implements Closeable {
}, params).getBody();
}
public PageData<UserEmailInfo> getUsersByQuery(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/users/info?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<UserEmailInfo>>() {
}, params).getBody();
}
public PageData<User> getTenantAdmins(TenantId tenantId, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
params.put("tenantId", tenantId.getId().toString());
@ -4156,6 +4283,138 @@ public class RestClient implements Closeable {
}
}
public PageData<Notification> getNotifications(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/notifications?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<Notification>>() {
}, params).getBody();
}
public Integer getUnreadNotificationsCount(NotificationDeliveryMethod deliveryMethod) {
String uri = "/api/notifications/unread/count?";
Map<String, String> params = new HashMap<>();
if (deliveryMethod != null) {
params.put("deliveryMethod", deliveryMethod.name());
uri += "&deliveryMethod={deliveryMethod}";
}
return restTemplate.exchange(
baseURL + uri,
HttpMethod.GET,
HttpEntity.EMPTY,
Integer.class, params).getBody();
}
public void markNotificationAsRead(NotificationId notificationId) {
restTemplate.exchange(
baseURL + "/api/notification/{id}/read",
HttpMethod.PUT,
HttpEntity.EMPTY,
Void.class,
notificationId.getId());
}
public void markAllNotificationsAsRead(NotificationDeliveryMethod deliveryMethod) {
String uri = "/api/notifications/read?";
Map<String, String> params = new HashMap<>();
if (deliveryMethod != null) {
params.put("deliveryMethod", deliveryMethod.name());
uri += "&deliveryMethod={deliveryMethod}";
}
restTemplate.exchange(
baseURL + uri,
HttpMethod.PUT,
HttpEntity.EMPTY,
Void.class);
}
public void deleteNotification(NotificationId notificationId) {
restTemplate.delete(baseURL + "/api/notification/{id}", notificationId.getId());
}
public NotificationRequest createNotificationRequest(NotificationRequest notificationRequest) {
return restTemplate.postForEntity(baseURL + "/api/notification/request", notificationRequest, NotificationRequest.class).getBody();
}
public NotificationRequestPreview getNotificationRequestPreview(NotificationRequest notificationRequest, int recipientsPreviewSize) {
return restTemplate.postForEntity(baseURL + "/api/notification/request/preview?recipientsPreviewSize={recipientsPreviewSize}", notificationRequest, NotificationRequestPreview.class, recipientsPreviewSize).getBody();
}
public Optional<NotificationRequestInfo> getNotificationRequestById(NotificationRequestId notificationRequestId) {
try {
ResponseEntity<NotificationRequestInfo> notificationRequest = restTemplate.getForEntity(baseURL + "/api/notification/request/{id}", NotificationRequestInfo.class, notificationRequestId.getId());
return Optional.ofNullable(notificationRequest.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public PageData<NotificationRequestInfo> getNotificationRequests(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/notification/requests?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<NotificationRequestInfo>>() {
}, params).getBody();
}
public void deleteNotificationRequest(NotificationRequestId notificationRequestId) {
restTemplate.delete(baseURL + "/api/notification/request/{id}", notificationRequestId.getId());
}
public NotificationSettings saveNotificationSettings(NotificationSettings notificationSettings) {
return restTemplate.postForEntity(baseURL + "/api/notification/settings", notificationSettings, NotificationSettings.class).getBody();
}
public Optional<NotificationSettings> getNotificationSettings() {
try {
ResponseEntity<NotificationSettings> notificationSettings = restTemplate.getForEntity(baseURL + "/api/notification/settings", NotificationSettings.class);
return Optional.ofNullable(notificationSettings.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public List<NotificationDeliveryMethod> getAvailableDeliveryMethods() {
return restTemplate.exchange(URI.create(
baseURL + "/api/notification/deliveryMethods"),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<NotificationDeliveryMethod>>() {
}).getBody();
}
public UserNotificationSettings saveUserNotificationSettings(UserNotificationSettings userNotificationSettings) {
return restTemplate.postForEntity(baseURL + "/api/notification/settings/user", userNotificationSettings, UserNotificationSettings.class).getBody();
}
public Optional<UserNotificationSettings> getUserNotificationSettings() {
try {
ResponseEntity<UserNotificationSettings> userNotificationSettings = restTemplate.getForEntity(baseURL + "/api/notification/settings/user", UserNotificationSettings.class);
return Optional.ofNullable(userNotificationSettings.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public AiModel saveAiModel(AiModel aiModel) {
return restTemplate.postForEntity(baseURL + "/api/ai/model", aiModel, AiModel.class).getBody();
}

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java

@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.util;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.ApiKeyId;
@ -26,7 +25,6 @@ import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
@ -171,14 +169,6 @@ public class TenantIdLoader {
case CALCULATED_FIELD:
tenantEntity = ctx.getCalculatedFieldService().findById(ctxTenantId, new CalculatedFieldId(id));
break;
case CALCULATED_FIELD_LINK:
CalculatedFieldLink calculatedFieldLink = ctx.getCalculatedFieldService().findCalculatedFieldLinkById(ctxTenantId, new CalculatedFieldLinkId(id));
if (calculatedFieldLink != null) {
tenantEntity = ctx.getCalculatedFieldService().findById(ctxTenantId, calculatedFieldLink.getCalculatedFieldId());
} else {
tenantEntity = null;
}
break;
case JOB:
tenantEntity = ctx.getJobService().findJobById(ctxTenantId, new JobId(id));
break;

7
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java

@ -45,7 +45,6 @@ import org.thingsboard.server.common.data.alarm.Alarm;
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.CalculatedFieldLink;
import org.thingsboard.server.common.data.domain.Domain;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetProfileId;
@ -384,12 +383,6 @@ public class TenantIdLoaderTest {
when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService);
doReturn(calculatedField).when(calculatedFieldService).findById(eq(tenantId), any());
break;
case CALCULATED_FIELD_LINK:
CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink();
calculatedFieldLink.setTenantId(tenantId);
when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService);
doReturn(calculatedFieldLink).when(calculatedFieldService).findCalculatedFieldLinkById(eq(tenantId), any());
break;
case JOB:
Job job = new Job();
job.setTenantId(tenantId);

81
ui-ngx/src/app/core/http/attribute.service.ts

@ -15,7 +15,7 @@
///
import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { defaultHttpOptionsFromConfig, defaultHttpOptionsFromParams, RequestConfig } from './http-utils';
import { forkJoin, Observable, of } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { EntityId } from '@shared/models/id/entity-id';
@ -35,47 +35,37 @@ export class AttributeService {
public getEntityAttributes(entityId: EntityId, attributeScope?: AttributeScope,
keys?: Array<string>, config?: RequestConfig): Observable<Array<AttributeData>> {
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/attributes`;
let queryParams: object = null;
if (attributeScope) {
url += `/${attributeScope}`;
}
if (keys && keys.length) {
url += `?keys=${keys.join(',')}`;
queryParams = {key: keys};
}
return this.http.get<Array<AttributeData>>(url, defaultHttpOptionsFromConfig(config));
return this.http.get<Array<AttributeData>>(url, defaultHttpOptionsFromParams(queryParams, config));
}
public deleteEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>,
config?: RequestConfig): Observable<any> {
const keys = attributes.map(attribute => encodeURIComponent(attribute.key)).join(',');
return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}` +
`?keys=${keys}`,
defaultHttpOptionsFromConfig(config));
return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}`,
defaultHttpOptionsFromParams({key: attributes.map(attribute => attribute.key)}, config));
}
public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false,
startTs?: number, endTs?: number, rewriteLatestIfDeleted = false, deleteLatest = true,
config?: RequestConfig): Observable<any> {
const keys = timeseries.map(attribute => encodeURIComponent(attribute.key)).join(',');
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete?keys=${keys}`;
if (isDefinedAndNotNull(deleteAllDataForKeys)) {
url += `&deleteAllDataForKeys=${deleteAllDataForKeys}`;
}
if (isDefinedAndNotNull(rewriteLatestIfDeleted)) {
url += `&rewriteLatestIfDeleted=${rewriteLatestIfDeleted}`;
}
if (isDefinedAndNotNull(deleteLatest)) {
url += `&deleteLatest=${deleteLatest}`;
}
if (isDefinedAndNotNull(startTs)) {
url += `&startTs=${startTs}`;
}
if (isDefinedAndNotNull(endTs)) {
url += `&endTs=${endTs}`;
}
return this.http.delete(url, defaultHttpOptionsFromConfig(config));
const queryParams = {
key: timeseries.map(key => key.key),
deleteAllDataForKeys: deleteAllDataForKeys,
rewriteLatestIfDeleted: rewriteLatestIfDeleted,
deleteLatest: deleteLatest,
startTs: startTs,
endTs: endTs
};
return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete`,
defaultHttpOptionsFromParams(queryParams, config));
}
public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>,
@ -138,32 +128,29 @@ export class AttributeService {
limit: number = 100, agg: AggregationType = AggregationType.NONE, interval?: number,
orderBy: DataSortOrder = DataSortOrder.DESC, useStrictDataTypes: boolean = false,
config?: RequestConfig): Observable<TimeseriesData> {
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?keys=${keys.join(',')}&startTs=${startTs}&endTs=${endTs}`;
if (isDefinedAndNotNull(limit)) {
url += `&limit=${limit}`;
}
if (isDefinedAndNotNull(agg)) {
url += `&agg=${agg}`;
}
if (isDefinedAndNotNull(interval)) {
url += `&interval=${interval}`;
}
if (isDefinedAndNotNull(orderBy)) {
url += `&orderBy=${orderBy}`;
}
if (isDefinedAndNotNull(useStrictDataTypes)) {
url += `&useStrictDataTypes=${useStrictDataTypes}`;
}
return this.http.get<TimeseriesData>(url, defaultHttpOptionsFromConfig(config));
const queryParams = {
key: keys,
startTs: startTs,
endTs: endTs,
limit: limit,
agg: agg,
interval: interval,
orderBy: orderBy,
useStrictDataTypes: useStrictDataTypes
}
return this.http.get<TimeseriesData>(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries`,
defaultHttpOptionsFromParams(queryParams, config));
}
public getEntityTimeseriesLatest(entityId: EntityId, keys?: Array<string>,
useStrictDataTypes = false, config?: RequestConfig): Observable<TimeseriesData> {
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?useStrictDataTypes=${useStrictDataTypes}`;
const queryParams: Record<string, any> = {
useStrictDataTypes: useStrictDataTypes
}
if (isDefinedAndNotNull(keys) && keys.length) {
url += `&keys=${keys.join(',')}`;
queryParams.key = keys;
}
return this.http.get<TimeseriesData>(url, defaultHttpOptionsFromConfig(config));
return this.http.get<TimeseriesData>(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries`,
defaultHttpOptionsFromParams(queryParams, config));
}
}

31
ui-ngx/src/app/core/http/http-utils.ts

@ -38,7 +38,10 @@ export function createDefaultHttpOptions(queryParamsOrConfig?: QueryParams | Req
if (hasRequestConfig(queryParamsOrConfig)) {
return defaultHttpOptionsFromConfig(queryParamsOrConfig as RequestConfig);
}
const queryParams = queryParamsOrConfig as QueryParams;
return defaultHttpOptionsFromParams(queryParamsOrConfig as QueryParams, config);
}
export function defaultHttpOptionsFromParams(queryParams?: QueryParams, config?: RequestConfig) {
const finalConfig = {
...config,
...(queryParams && { queryParams }),
@ -57,9 +60,11 @@ export function defaultHttpOptions(ignoreLoading: boolean = false,
ignoreErrors: boolean = false,
resendRequest: boolean = false,
queryParams?: QueryParams) {
const cleanedParams = cleanQueryParams(queryParams);
return {
headers: new HttpHeaders({'Content-Type': 'application/json'}),
params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), queryParams)
params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), cleanedParams)
};
}
@ -67,7 +72,27 @@ export function defaultHttpUploadOptions(ignoreLoading: boolean = false,
ignoreErrors: boolean = false,
resendRequest: boolean = false,
queryParams?: QueryParams) {
const cleanedParams = cleanQueryParams(queryParams);
return {
params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), queryParams)
params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), cleanedParams)
};
}
function cleanQueryParams(params?: QueryParams): QueryParams | undefined {
if (!params) {
return undefined;
}
const entries = Object.entries(params);
const cleanedEntries = entries.filter(
([_, value]) => value !== null && value !== undefined
);
if (!cleanedEntries.length) {
return undefined;
}
return Object.fromEntries(cleanedEntries);
}

2
ui-ngx/src/app/core/http/queue.service.ts

@ -75,7 +75,7 @@ export class QueueService {
}
public getQueueStatisticsByIds(queueStatIds: Array<string>, config?: RequestConfig): Observable<Array<QueueStatisticsInfo>> {
return this.http.get<Array<QueueStatisticsInfo>>(`/api/queueStats?QueueStatsIds=${queueStatIds.join(',')}`,
return this.http.get<Array<QueueStatisticsInfo>>(`/api/queueStats?queueStatsIds=${queueStatIds.join(',')}`,
defaultHttpOptionsFromConfig(config)).pipe(
map(queueStats => queueStats.map(queueStat => this.parseQueueStatName(queueStat))
)

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

@ -19,7 +19,6 @@
<mat-toolbar color="primary">
<h2>{{ 'alarm-rule.alarm-rule' | translate}}</h2>
<span class="flex-1"></span>
<div tb-help="alarmRule"></div>
<button mat-icon-button
(click)="cancel()"
type="button">

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss

@ -44,6 +44,12 @@
}
}
.max-args-warning {
.mat-icon {
color: #FAA405;
}
}
.limit-field-row {
@media screen and (max-width: 520px) {
display: flex;

2
ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts

@ -94,7 +94,7 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
const progress = data[0][key.maxLimit.key] !== 0 ? Math.min(100, ((data[0][key.current.key] / data[0][key.maxLimit.key]) * 100)) : 0;
key.progress = isFinite(progress) ? progress : 0;
key.status.value = data[0][key.status.key] ? data[0][key.status.key].toLowerCase() : 'enabled';
key.maxLimit.value = isFinite(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞';
key.maxLimit.value = isFinite(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 && data[0][key.maxLimit.key] !== '' ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞';
key.current.value = isFinite(data[0][key.current.key]) ? this.toShortNumber(data[0][key.current.key]) : 0;
});
this.cd.detectChanges();

13
ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts

@ -332,11 +332,16 @@ export class LedIndicatorComponent extends PageComponent implements OnInit, OnDe
if (keyData && keyData.data && keyData.data[0]) {
const attrValue = keyData.data[0][1];
if (isDefined(attrValue)) {
let parsed = null;
let valueToParse = attrValue;
try {
parsed = this.parseValueFunction(JSON.parse(attrValue));
} catch (e){/**/}
value = !!parsed;
valueToParse = JSON.parse(attrValue);
} catch (e) {/**/}
try {
value = !!this.parseValueFunction(valueToParse);
} catch (e) {
value = false;
}
}
}
}

13
ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts

@ -330,11 +330,16 @@ export class RoundSwitchComponent extends PageComponent implements OnInit, OnDes
if (keyData && keyData.data && keyData.data[0]) {
const attrValue = keyData.data[0][1];
if (isDefined(attrValue)) {
let parsed = null;
let valueToParse = attrValue;
try {
parsed = this.parseValueFunction(JSON.parse(attrValue));
} catch (e){/**/}
value = !!parsed;
valueToParse = JSON.parse(attrValue);
} catch (e) {/**/}
try {
value = !!this.parseValueFunction(valueToParse);
} catch (e) {
value = false;
}
}
}
}

13
ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts

@ -373,11 +373,16 @@ export class SwitchComponent extends PageComponent implements AfterViewInit, OnD
if (keyData && keyData.data && keyData.data[0]) {
const attrValue = keyData.data[0][1];
if (isDefined(attrValue)) {
let parsed = null;
let valueToParse = attrValue;
try {
parsed = this.parseValueFunction(JSON.parse(attrValue));
} catch (e){/**/}
value = !!parsed;
valueToParse = JSON.parse(attrValue);
} catch (e) {/**/}
try {
value = !!this.parseValueFunction(valueToParse);
} catch (e) {
value = false;
}
}
}
}

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts

@ -29,7 +29,7 @@ import {
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors,
ValidatorFn
ValidatorFn, Validators
} from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -140,7 +140,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
protected onSettingsSet(settings: WidgetSettings) {
this.apiUsageWidgetSettingsForm = this.fb.group({
dsEntityAliasId: [settings?.dsEntityAliasId],
dsEntityAliasId: [settings?.dsEntityAliasId, Validators.required],
apiUsageDataKeys: this.prepareDataKeysFormArray(settings?.apiUsageDataKeys),
targetDashboardState: [settings?.targetDashboardState],
background: [settings?.background, []],

2
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html

@ -96,7 +96,7 @@
<mat-expansion-panel-header>
<mat-panel-title>
<mat-slide-toggle class="mat-slide flex items-center justify-start"
(mousedown)="toggleExtensionPanel($event, i, provider.get('enable').value)"
(mousedown)="toggleExtensionPanel($event, i+1, provider.get('enable').value)"
formControlName="enable">
{{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }}
</mat-slide-toggle>

Loading…
Cancel
Save