Browse Source

Merge remote-tracking branch 'origin/ws-api-keys' into ws-api-keys

pull/15170/head
Andrii Landiak 7 months ago
parent
commit
2ab1faadce
  1. 56
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  2. 3
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  3. 3
      application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java
  4. 9
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  5. 8
      application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java
  6. 2
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java
  7. 1
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java
  8. 9
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/rpc/MqttV5CloseTransportSessionOnRpcDeliveryTimeoutIntegrationTest.java
  9. 2
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java
  10. 2
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java
  11. 1
      application/src/test/resources/application-test.properties
  12. 7
      common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
  13. 7
      common/data/src/main/java/org/thingsboard/server/common/data/Device.java
  14. 5
      common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java
  15. 6
      common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
  16. 11
      common/data/src/main/java/org/thingsboard/server/common/data/User.java
  17. 5
      common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
  18. 195
      common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
  19. 10
      common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java
  20. 4
      common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java
  21. 7
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
  22. 18
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java
  23. 14
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java
  24. 18
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MModelConfigStore.java
  25. 137
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStoreTest.java
  26. 2
      msa/js-executor/package.json
  27. 13
      msa/js-executor/pom.xml
  28. 8
      msa/js-executor/yarn.lock
  29. 2
      msa/web-ui/package.json
  30. 13
      msa/web-ui/pom.xml
  31. 8
      msa/web-ui/yarn.lock
  32. 2
      pom.xml
  33. 13
      ui-ngx/pom.xml
  34. 4
      ui-ngx/src/app/core/services/calculated-field-form.service.ts
  35. 8
      ui-ngx/src/app/core/services/resources.service.ts
  36. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  37. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html
  38. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html
  39. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  40. 2
      ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html
  41. 2
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
  42. 2
      ui-ngx/src/app/modules/home/pages/asset/asset.component.html
  43. 10
      ui-ngx/src/app/modules/home/pages/customer/customer.component.html
  44. 12
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html
  45. 2
      ui-ngx/src/app/modules/home/pages/device/device.component.html
  46. 2
      ui-ngx/src/app/modules/home/pages/edge/edge.component.html
  47. 2
      ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html
  48. 2
      ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html
  49. 2
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html
  50. 28
      ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html
  51. 2
      ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html
  52. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html
  53. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html
  54. 4
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html
  55. 8
      ui-ngx/src/app/modules/home/pages/user/user.component.html
  56. 10
      ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html
  57. 2
      ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts
  58. 2
      ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts

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

@ -61,15 +61,18 @@ import org.springframework.http.HttpStatus;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse; import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse;
import org.thingsboard.server.exception.ThingsboardErrorResponse; import org.thingsboard.server.exception.ThingsboardErrorResponse;
import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.auth.rest.LoginResponse; import org.thingsboard.server.service.security.auth.rest.LoginResponse;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -301,6 +304,24 @@ public class SwaggerConfiguration {
schema.setProperties(null); schema.setProperties(null);
} }
} }
} else if (schema != null && schema.getProperties() != null && !schema.getProperties().isEmpty()) {
try {
var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType);
var orderedNames = resolvePropertyOrder(cls, beanDesc);
if (!orderedNames.isEmpty()) {
@SuppressWarnings("unchecked")
Map<String, Schema> current = schema.getProperties();
var reordered = new LinkedHashMap<String, Schema>();
for (String name : orderedNames) {
Schema prop = current.get(name);
if (prop != null) reordered.put(name, prop);
}
current.forEach((k, v) -> reordered.putIfAbsent(k, v));
schema.setProperties(reordered);
}
} catch (Exception ignored) {
log.trace("Failed to resolve property order for {}", cls.getName(), ignored);
}
} }
} }
return schema; return schema;
@ -627,7 +648,7 @@ public class SwaggerConfiguration {
ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))); ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)));
unauthorizedExamples.put("credentials-expired", errorExample("Expired credentials", unauthorizedExamples.put("credentials-expired", errorExample("Expired credentials",
ThingsboardCredentialsExpiredResponse.of("User password expired!", StringUtils.randomAlphanumeric(30)))); ThingsboardCredentialsExpiredResponse.of("User password expired!", "udgDQOpS1Q4ZFEL8qHF9s8cSKQ7d1h")));
Schema<? extends ThingsboardErrorResponse> unauthorizedSchema = new Schema<>(); Schema<? extends ThingsboardErrorResponse> unauthorizedSchema = new Schema<>();
unauthorizedSchema.oneOf(List.of( unauthorizedSchema.oneOf(List.of(
@ -655,6 +676,37 @@ public class SwaggerConfiguration {
return new ApiResponse().description(description).content(content); return new ApiResponse().description(description).content(content);
} }
private static List<String> resolvePropertyOrder(Class<?> cls, com.fasterxml.jackson.databind.BeanDescription beanDesc) {
// Map backing field names to their JSON property names (respects @JsonProperty)
Map<String, String> fieldToJsonName = new LinkedHashMap<>();
LinkedHashSet<String> getterOnlyNames = new LinkedHashSet<>();
for (var prop : beanDesc.findProperties()) {
if (prop.getField() != null) {
fieldToJsonName.put(prop.getField().getName(), prop.getName());
} else {
getterOnlyNames.add(prop.getName());
}
}
// Walk class hierarchy (superclass first) to get field declaration order
List<Class<?>> hierarchy = new ArrayList<>();
for (Class<?> c = cls; c != null && c != Object.class; c = c.getSuperclass()) {
hierarchy.add(0, c);
}
List<String> ordered = new ArrayList<>();
for (Class<?> c : hierarchy) {
for (Field f : c.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) continue;
String jsonName = fieldToJsonName.get(f.getName());
if (jsonName != null) ordered.add(jsonName);
}
}
// Append getter-only properties (no backing field) at the end
ordered.addAll(getterOnlyNames);
return ordered;
}
private static Example errorExample(String summary, ThingsboardErrorResponse example) { private static Example errorExample(String summary, ThingsboardErrorResponse example) {
var node = (ObjectNode) JacksonUtil.valueToTree(example); var node = (ObjectNode) JacksonUtil.valueToTree(example);
node.put("timestamp", 1609459200000L); node.put("timestamp", 1609459200000L);

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

@ -608,12 +608,13 @@ public class TelemetryController extends BaseController {
@ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)", @ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)",
notes = "Delete entity attributes using provided Entity Id, scope and a list of keys. " + notes = "Delete entity attributes using provided Entity Id, scope and a list of keys. " +
"This operation is idempotent: keys that do not exist are silently ignored and the response is still 200 OK. " +
INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@Parameters({ @Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
}) })
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Entity attributes was removed for the selected keys in the request. " + @ApiResponse(responseCode = "200", description = "Entity attributes were removed for the selected keys in the request (keys that did not exist are silently ignored). " +
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."), "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."),

3
application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java

@ -31,7 +31,8 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
// map of versions from which the upgrade to the current version is possible // map of versions from which the upgrade to the current version is possible
// key - supported version prefix, value - display name // key - supported version prefix, value - display name
private static final Map<String, String> SUPPORTED_VERSIONS_FOR_UPGRADE = Map.of( private static final Map<String, String> SUPPORTED_VERSIONS_FOR_UPGRADE = Map.of(
"4.3.0", "4.3.0.x" "4.3.0", "4.3.0.x",
"4.3.1", "4.3.1.x"
); );
private final ProjectInfo projectInfo; private final ProjectInfo projectInfo;

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

@ -425,6 +425,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
public void teardownWebTest() throws Exception { public void teardownWebTest() throws Exception {
log.debug("Executing web test teardown"); log.debug("Executing web test teardown");
// Drain any pending housekeeper work left by the test body (e.g., bulk tenant deletes)
// before proceeding with teardown deletions, to avoid 90s per-tenant wait timing out.
awaitHousekeeperDrained();
loginSysAdmin(); loginSysAdmin();
deleteTenant(tenantId); deleteTenant(tenantId);
deleteDifferentTenant(); deleteDifferentTenant();
@ -457,6 +461,11 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
.until(() -> storage.getLag("tb_housekeeper") == 0); .until(() -> storage.getLag("tb_housekeeper") == 0);
} }
protected void awaitHousekeeperDrained() {
Awaitility.await("housekeeper drained").atMost(5, TimeUnit.MINUTES).during(300, TimeUnit.MILLISECONDS)
.until(() -> storage.getLag("tb_housekeeper") == 0);
}
private List<Tenant> getAllTenants() throws Exception { private List<Tenant> getAllTenants() throws Exception {
List<Tenant> loadedTenants = new ArrayList<>(); List<Tenant> loadedTenants = new ArrayList<>();
PageLink pageLink = new PageLink(10); PageLink pageLink = new PageLink(10);

8
application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java

@ -19,6 +19,7 @@ import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
@ -136,9 +137,10 @@ public class AuditLogControllerTest extends AbstractControllerTest {
doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
} }
List<AuditLog> loadedAuditLogs = getAuditLogs(100, "/api/audit/logs?"); int expectedSize = loadedAuditLogsBefore.size() + 3;
Awaitility.await().atMost(10, TimeUnit.SECONDS).untilAsserted(() ->
Assert.assertEquals("Have X audit log before this test + New tenant profiles in the test", loadedAuditLogsBefore.size() + 3, loadedAuditLogs.size()); Assert.assertEquals("Have X audit log before this test + New tenant profiles in the test",
expectedSize, getAuditLogs(100, "/api/audit/logs?").size()));
} }
private List<AuditLog> getAuditLogs(int pageSize, String urlTemplate) throws Exception { private List<AuditLog> getAuditLogs(int pageSize, String urlTemplate) throws Exception {

2
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java

@ -193,7 +193,7 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
} catch (Exception e) { } catch (Exception e) {
log.error("Error during firmware update", e); log.error("Error during firmware update", e);
} }
}, 0, TimeUnit.SECONDS); // start immediately, without further delay }, 1, TimeUnit.SECONDS); // delay 1 sec to allow CoAP Execute response to be delivered before client stops
} }
protected void setLeshanClient(LeshanClient leshanClient) { protected void setLeshanClient(LeshanClient leshanClient) {

1
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java

@ -420,6 +420,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
Awaitility.await() Awaitility.await()
.atMost(10, TimeUnit.SECONDS) .atMost(10, TimeUnit.SECONDS)
.ignoreExceptions()
.until(() -> { .until(() -> {
List<Map<String, Object>> attributes = doGetAsyncTyped(attributeValuesUrl, new TypeReference<>() { List<Map<String, Object>> attributes = doGetAsyncTyped(attributeValuesUrl, new TypeReference<>() {
}); });

9
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/rpc/MqttV5CloseTransportSessionOnRpcDeliveryTimeoutIntegrationTest.java

@ -42,6 +42,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC; import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC;
@ -97,7 +98,13 @@ public class MqttV5CloseTransportSessionOnRpcDeliveryTimeoutIntegrationTest exte
callback.getDisconnectLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); callback.getDisconnectLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertThat(callback.getReturnCode()).isEqualTo(MqttReturnCode.RETURN_CODE_ADMINISTRITIVE_ACTION); assertThat(callback.getReturnCode()).isEqualTo(MqttReturnCode.RETURN_CODE_ADMINISTRITIVE_ACTION);
Rpc persistedRpc = doGet("/api/rpc/persistent/" + response.get("rpcId").asText(), Rpc.class); // The server re-queues the RPC asynchronously after closing the session.
// Poll until the status transitions from SENT to QUEUED.
String rpcId = response.get("rpcId").asText();
Rpc persistedRpc = await("RPC re-queued after session close")
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> doGet("/api/rpc/persistent/" + rpcId, Rpc.class),
rpc -> RpcStatus.QUEUED.equals(rpc.getStatus()));
assertThat(persistedRpc).isNotNull(); assertThat(persistedRpc).isNotNull();
assertThat(persistedRpc.getStatus()).isEqualTo(RpcStatus.QUEUED); assertThat(persistedRpc.getStatus()).isEqualTo(RpcStatus.QUEUED);
assertThat(persistedRpc.getResponse()).isInstanceOf(NullNode.class); assertThat(persistedRpc.getResponse()).isInstanceOf(NullNode.class);

2
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java

@ -191,6 +191,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
AtomicReference<Device> device = new AtomicReference<>(); AtomicReference<Device> device = new AtomicReference<>();
await(alias + "find device [" + deviceName + "] after created") await(alias + "find device [" + deviceName + "] after created")
.atMost(200, TimeUnit.SECONDS) .atMost(200, TimeUnit.SECONDS)
.ignoreExceptions()
.until(() -> { .until(() -> {
device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class)); device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class));
return device.get() != null; return device.get() != null;
@ -236,6 +237,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
AtomicReference<Device> device = new AtomicReference<>(); AtomicReference<Device> device = new AtomicReference<>();
await(alias + "find device [" + deviceName + "] after created") await(alias + "find device [" + deviceName + "] after created")
.atMost(200, TimeUnit.SECONDS) .atMost(200, TimeUnit.SECONDS)
.ignoreExceptions()
.until(() -> { .until(() -> {
device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class)); device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class));
return device.get() != null; return device.get() != null;

2
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java

@ -468,6 +468,7 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
AtomicReference<List<String>> actualKeys = new AtomicReference<>(); AtomicReference<List<String>> actualKeys = new AtomicReference<>();
await(alias + SparkplugMessageType.NBIRTH.name()) await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS) .atMost(40, TimeUnit.SECONDS)
.ignoreExceptions()
.until(() -> { .until(() -> {
actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() { actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() {
})); }));
@ -483,6 +484,7 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
AtomicReference<List<String>> actualKeys = new AtomicReference<>(); AtomicReference<List<String>> actualKeys = new AtomicReference<>();
await(alias + SparkplugMessageType.DBIRTH.name()) await(alias + SparkplugMessageType.DBIRTH.name())
.atMost(40, TimeUnit.SECONDS) .atMost(40, TimeUnit.SECONDS)
.ignoreExceptions()
.until(() -> { .until(() -> {
actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() { actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() {
})); }));

1
application/src/test/resources/application-test.properties

@ -44,6 +44,7 @@ queue.transport_api.response_poll_interval=5
queue.transport.poll_interval=5 queue.transport.poll_interval=5
queue.core.poll-interval=5 queue.core.poll-interval=5
queue.core.partitions=2 queue.core.partitions=2
queue.core.housekeeper.task-reprocessing-delay-ms=0
queue.rule-engine.poll-interval=5 queue.rule-engine.poll-interval=5
queue.rule-engine.stats.enabled=true queue.rule-engine.stats.enabled=true

7
common/data/src/main/java/org/thingsboard/server/common/data/Customer.java

@ -140,7 +140,12 @@ public class Customer extends ContactBased<CustomerId> implements HasTenantId, E
return super.getEmail(); return super.getEmail();
} }
@Schema(description = "Additional parameters of the device",implementation = com.fasterxml.jackson.databind.JsonNode.class) @Schema(description = "Additional parameters of the customer. " +
"May include: 'description' (string), 'homeDashboardId' (string, UUID of the home dashboard), " +
"'homeDashboardHideToolbar' (boolean, whether to hide the dashboard toolbar), " +
"'isPublic' (boolean, whether this is a public customer).",
implementation = com.fasterxml.jackson.databind.JsonNode.class,
example = "{\"description\":\"Regional customer\",\"homeDashboardId\":\"784f394c-42b6-435a-983c-b7beff2784f9\",\"homeDashboardHideToolbar\":false,\"isPublic\":false}")
@Override @Override
public JsonNode getAdditionalInfo() { public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo(); return super.getAdditionalInfo();

7
common/data/src/main/java/org/thingsboard/server/common/data/Device.java

@ -231,7 +231,12 @@ public class Device extends BaseDataWithAdditionalInfo<DeviceId> implements HasL
this.softwareId = softwareId; this.softwareId = softwareId;
} }
@Schema(description = "Additional parameters of the device",implementation = com.fasterxml.jackson.databind.JsonNode.class) @Schema(description = "Additional parameters of the device. " +
"May include: 'gateway' (boolean, whether the device is a gateway), " +
"'description' (string), " +
"'lastConnectedGateway' (string, UUID of the last gateway that connected this device).",
implementation = com.fasterxml.jackson.databind.JsonNode.class,
example = "{\"gateway\":false,\"description\":\"Temperature sensor\",\"lastConnectedGateway\":\"784f394c-42b6-435a-983c-b7beff2784f9\"}")
@Override @Override
public JsonNode getAdditionalInfo() { public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo(); return super.getAdditionalInfo();

5
common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java

@ -116,7 +116,10 @@ public class EntityView extends BaseDataWithAdditionalInfo<EntityViewId>
return super.getCreatedTime(); return super.getCreatedTime();
} }
@Schema(description = "Additional parameters of the device", implementation = com.fasterxml.jackson.databind.JsonNode.class) @Schema(description = "Additional parameters of the entity view. " +
"May include: 'description' (string).",
implementation = com.fasterxml.jackson.databind.JsonNode.class,
example = "{\"description\":\"Temperature readings view\"}")
@Override @Override
public JsonNode getAdditionalInfo() { public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo(); return super.getAdditionalInfo();

6
common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java

@ -164,7 +164,11 @@ public class Tenant extends ContactBased<TenantId> implements HasTenantId, HasTi
return super.getEmail(); return super.getEmail();
} }
@Schema(description = "Additional parameters of the device", implementation = com.fasterxml.jackson.databind.JsonNode.class) @Schema(description = "Additional parameters of the tenant. " +
"May include: 'description' (string), 'homeDashboardId' (string, UUID of the home dashboard), " +
"'homeDashboardHideToolbar' (boolean, whether to hide the dashboard toolbar).",
implementation = com.fasterxml.jackson.databind.JsonNode.class,
example = "{\"description\":\"Main tenant\",\"homeDashboardId\":\"784f394c-42b6-435a-983c-b7beff2784f9\",\"homeDashboardHideToolbar\":true}")
@Override @Override
public JsonNode getAdditionalInfo() { public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo(); return super.getAdditionalInfo();

11
common/data/src/main/java/org/thingsboard/server/common/data/User.java

@ -161,7 +161,16 @@ public class User extends BaseDataWithAdditionalInfo<UserId> implements HasName,
this.phone = phone; this.phone = phone;
} }
@Schema(description = "Additional parameters of the user", implementation = com.fasterxml.jackson.databind.JsonNode.class) @Schema(description = "Additional parameters of the user. " +
"May include: 'defaultDashboardId' (string, UUID of the default dashboard), " +
"'defaultDashboardFullscreen' (boolean), " +
"'homeDashboardId' (string, UUID of the home dashboard), " +
"'homeDashboardHideToolbar' (boolean), " +
"'lang' (string, user locale, e.g. 'en_US'), " +
"'authProviderName' (string, name of the authentication provider).",
implementation = com.fasterxml.jackson.databind.JsonNode.class,
example = "{\"defaultDashboardId\":\"784f394c-42b6-435a-983c-b7beff2784f9\",\"defaultDashboardFullscreen\":false," +
"\"homeDashboardId\":\"784f394c-42b6-435a-983c-b7beff2784f9\",\"homeDashboardHideToolbar\":true,\"lang\":\"en_US\"}")
@Override @Override
public JsonNode getAdditionalInfo() { public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo(); return super.getAdditionalInfo();

5
common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java

@ -169,7 +169,10 @@ public class Asset extends BaseDataWithAdditionalInfo<AssetId> implements HasLab
this.assetProfileId = assetProfileId; this.assetProfileId = assetProfileId;
} }
@Schema(description = "Additional parameters of the asset",implementation = com.fasterxml.jackson.databind.JsonNode.class) @Schema(description = "Additional parameters of the asset. " +
"May include: 'description' (string).",
implementation = com.fasterxml.jackson.databind.JsonNode.class,
example = "{\"description\":\"Building A asset\"}")
@Override @Override
public JsonNode getAdditionalInfo() { public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo(); return super.getAdditionalInfo();

195
common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java

@ -22,42 +22,203 @@ import java.util.Optional;
public enum ActionType { public enum ActionType {
ADDED(TbMsgType.ENTITY_CREATED), // log entity /**
DELETED(TbMsgType.ENTITY_DELETED), // log string id * Entity created. Pushes {@link TbMsgType#ENTITY_CREATED} to rule engine.
UPDATED(TbMsgType.ENTITY_UPDATED), // log entity * Audit log payload: full entity JSON.
ATTRIBUTES_UPDATED(TbMsgType.ATTRIBUTES_UPDATED), // log attributes/values */
ATTRIBUTES_DELETED(TbMsgType.ATTRIBUTES_DELETED), // log attributes ADDED(TbMsgType.ENTITY_CREATED),
TIMESERIES_UPDATED(TbMsgType.TIMESERIES_UPDATED), // log timeseries update /**
TIMESERIES_DELETED(TbMsgType.TIMESERIES_DELETED), // log timeseries * Entity deleted. Pushes {@link TbMsgType#ENTITY_DELETED} to rule engine.
RPC_CALL, // log method and params * Audit log payload: entity string id.
CREDENTIALS_UPDATED, // log new credentials */
ASSIGNED_TO_CUSTOMER(TbMsgType.ENTITY_ASSIGNED), // log customer name DELETED(TbMsgType.ENTITY_DELETED),
UNASSIGNED_FROM_CUSTOMER(TbMsgType.ENTITY_UNASSIGNED), // log customer name /**
ACTIVATED, // log string id * Entity updated. Pushes {@link TbMsgType#ENTITY_UPDATED} to rule engine.
SUSPENDED, // log string id * Audit log payload: full entity JSON.
CREDENTIALS_READ(true), // log device id */
ATTRIBUTES_READ(true), // log attributes UPDATED(TbMsgType.ENTITY_UPDATED),
/**
* Server-side or shared attributes updated via API.
* Pushes {@link TbMsgType#ATTRIBUTES_UPDATED} to rule engine.
* Rule engine msg metadata includes {@code scope} ({@code SERVER_SCOPE} or {@code SHARED_SCOPE}).
* Rule engine msg data: key-value pairs of the updated attributes.
* Audit log payload: updated attributes and their values.
*/
ATTRIBUTES_UPDATED(TbMsgType.ATTRIBUTES_UPDATED),
/**
* Attributes deleted via API.
* Pushes {@link TbMsgType#ATTRIBUTES_DELETED} to rule engine.
* Rule engine msg metadata includes {@code scope} ({@code SERVER_SCOPE} or {@code SHARED_SCOPE}).
* Rule engine msg data: {@code {"attributes": ["key1", "key2"]}}.
* Audit log payload: list of deleted attribute keys.
*/
ATTRIBUTES_DELETED(TbMsgType.ATTRIBUTES_DELETED),
/**
* Timeseries data saved via API (not from device transport).
* Pushes {@link TbMsgType#TIMESERIES_UPDATED} to rule engine.
* Rule engine msg data: {@code {"timeseries": [{"ts": ..., "values": {...}}, ...]}}.
* Audit log payload: timeseries entries.
*/
TIMESERIES_UPDATED(TbMsgType.TIMESERIES_UPDATED),
/**
* Timeseries data deleted via API.
* Pushes {@link TbMsgType#TIMESERIES_DELETED} to rule engine.
* Rule engine msg data: {@code {"timeseries": ["key1", ...], "startTs": ..., "endTs": ...}}.
* Audit log payload: deleted timeseries keys.
*/
TIMESERIES_DELETED(TbMsgType.TIMESERIES_DELETED),
/**
* RPC call to device. Does not push to rule engine (RPC has its own lifecycle messages).
* Audit log payload: RPC method and params.
*/
RPC_CALL,
/**
* Device credentials updated. Does not push to rule engine.
* Audit log payload: new credentials value.
*/
CREDENTIALS_UPDATED,
/**
* Entity assigned to a customer. Pushes {@link TbMsgType#ENTITY_ASSIGNED} to rule engine.
* Rule engine msg metadata includes {@code assignedCustomerId} and {@code assignedCustomerName}.
* Audit log payload: customer name.
*/
ASSIGNED_TO_CUSTOMER(TbMsgType.ENTITY_ASSIGNED),
/**
* Entity unassigned from a customer. Pushes {@link TbMsgType#ENTITY_UNASSIGNED} to rule engine.
* Rule engine msg metadata includes {@code unassignedCustomerId} and {@code unassignedCustomerName}.
* Audit log payload: customer name.
*/
UNASSIGNED_FROM_CUSTOMER(TbMsgType.ENTITY_UNASSIGNED),
/**
* User account or integration activated. Does not push to rule engine.
* Audit log payload: entity string id.
*/
ACTIVATED,
/**
* User account or integration suspended. Does not push to rule engine.
* Audit log payload: entity string id.
*/
SUSPENDED,
/**
* Device credentials read. Read-only action. Does not push to rule engine.
* Audit log payload: device id.
*/
CREDENTIALS_READ(true),
/**
* Attributes read. Read-only action. Does not push to rule engine.
* Audit log payload: attribute keys read.
*/
ATTRIBUTES_READ(true),
/**
* Relation created or updated. Pushes {@link TbMsgType#RELATION_ADD_OR_UPDATE} to rule engine.
* Rule engine msg data: relation JSON ({@code from}, {@code to}, {@code type}, {@code typeGroup}).
*/
RELATION_ADD_OR_UPDATE(TbMsgType.RELATION_ADD_OR_UPDATE), RELATION_ADD_OR_UPDATE(TbMsgType.RELATION_ADD_OR_UPDATE),
/**
* Relation deleted. Pushes {@link TbMsgType#RELATION_DELETED} to rule engine.
* Rule engine msg data: relation JSON ({@code from}, {@code to}, {@code type}, {@code typeGroup}).
*/
RELATION_DELETED(TbMsgType.RELATION_DELETED), RELATION_DELETED(TbMsgType.RELATION_DELETED),
/**
* All relations for an entity deleted. Pushes {@link TbMsgType#RELATIONS_DELETED} to rule engine.
* Rule engine msg data: empty JSON object.
*/
RELATIONS_DELETED(TbMsgType.RELATIONS_DELETED), RELATIONS_DELETED(TbMsgType.RELATIONS_DELETED),
REST_API_RULE_ENGINE_CALL, // log call to rule engine from REST API /**
* REST API call to rule engine. Does not push to rule engine directly
* (the REST controller creates a {@link TbMsgType#REST_API_REQUEST} message itself).
* Audit log payload: call details.
*/
REST_API_RULE_ENGINE_CALL,
/**
* Alarm acknowledged by a user. Pushes {@link TbMsgType#ALARM_ACK} to rule engine.
* Rule engine msg data: full alarm JSON. Originator: alarm id.
*/
ALARM_ACK(TbMsgType.ALARM_ACK, true), ALARM_ACK(TbMsgType.ALARM_ACK, true),
/**
* Alarm cleared by a user. Pushes {@link TbMsgType#ALARM_CLEAR} to rule engine.
* Rule engine msg data: full alarm JSON. Originator: alarm id.
*/
ALARM_CLEAR(TbMsgType.ALARM_CLEAR, true), ALARM_CLEAR(TbMsgType.ALARM_CLEAR, true),
/**
* Alarm deleted by a user. Pushes {@link TbMsgType#ALARM_DELETE} to rule engine.
* Rule engine msg data: full alarm JSON. Originator: alarm id.
*/
ALARM_DELETE(TbMsgType.ALARM_DELETE, true), ALARM_DELETE(TbMsgType.ALARM_DELETE, true),
/**
* Alarm assigned to a user. Pushes {@link TbMsgType#ALARM_ASSIGNED} to rule engine.
* Rule engine msg data: full alarm JSON. Originator: alarm id.
*/
ALARM_ASSIGNED(TbMsgType.ALARM_ASSIGNED, true), ALARM_ASSIGNED(TbMsgType.ALARM_ASSIGNED, true),
/**
* Alarm unassigned from a user. Pushes {@link TbMsgType#ALARM_UNASSIGNED} to rule engine.
* Rule engine msg data: full alarm JSON. Originator: alarm id.
*/
ALARM_UNASSIGNED(TbMsgType.ALARM_UNASSIGNED, true), ALARM_UNASSIGNED(TbMsgType.ALARM_UNASSIGNED, true),
/**
* User logged in. Does not push to rule engine.
*/
LOGIN, LOGIN,
/**
* User logged out. Does not push to rule engine.
*/
LOGOUT, LOGOUT,
/**
* User account locked out due to too many failed login attempts. Does not push to rule engine.
*/
LOCKOUT, LOCKOUT,
/**
* Entity assigned from another tenant (incoming side of cross-tenant transfer).
* Pushes {@link TbMsgType#ENTITY_ASSIGNED_FROM_TENANT} to rule engine.
* Rule engine msg metadata includes {@code assignedFromTenantId} and {@code assignedFromTenantName}.
*/
ASSIGNED_FROM_TENANT(TbMsgType.ENTITY_ASSIGNED_FROM_TENANT), ASSIGNED_FROM_TENANT(TbMsgType.ENTITY_ASSIGNED_FROM_TENANT),
/**
* Entity assigned to another tenant (outgoing side of cross-tenant transfer).
* Pushes {@link TbMsgType#ENTITY_ASSIGNED_TO_TENANT} to rule engine.
* Rule engine msg metadata includes {@code assignedToTenantId} and {@code assignedToTenantName}.
*/
ASSIGNED_TO_TENANT(TbMsgType.ENTITY_ASSIGNED_TO_TENANT), ASSIGNED_TO_TENANT(TbMsgType.ENTITY_ASSIGNED_TO_TENANT),
/**
* Device provisioned successfully. Pushes {@link TbMsgType#PROVISION_SUCCESS} to rule engine.
* Rule engine msg data: full device JSON.
*/
PROVISION_SUCCESS(TbMsgType.PROVISION_SUCCESS), PROVISION_SUCCESS(TbMsgType.PROVISION_SUCCESS),
/**
* Device provisioning failed. Pushes {@link TbMsgType#PROVISION_FAILURE} to rule engine.
* Rule engine msg data: full device JSON.
*/
PROVISION_FAILURE(TbMsgType.PROVISION_FAILURE), PROVISION_FAILURE(TbMsgType.PROVISION_FAILURE),
ASSIGNED_TO_EDGE(TbMsgType.ENTITY_ASSIGNED_TO_EDGE), // log edge name /**
* Entity assigned to an Edge instance. Pushes {@link TbMsgType#ENTITY_ASSIGNED_TO_EDGE} to rule engine.
* Rule engine msg metadata includes {@code assignedEdgeId} and {@code assignedEdgeName}.
* Audit log payload: edge name.
*/
ASSIGNED_TO_EDGE(TbMsgType.ENTITY_ASSIGNED_TO_EDGE),
/**
* Entity unassigned from an Edge instance. Pushes {@link TbMsgType#ENTITY_UNASSIGNED_FROM_EDGE} to rule engine.
* Rule engine msg metadata includes {@code unassignedEdgeId} and {@code unassignedEdgeName}.
*/
UNASSIGNED_FROM_EDGE(TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE), UNASSIGNED_FROM_EDGE(TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE),
/**
* Comment added to an alarm. Pushes {@link TbMsgType#COMMENT_CREATED} to rule engine.
* Rule engine msg metadata includes {@code comment} (JSON string of the AlarmComment object).
* Rule engine msg data: full alarm JSON. Originator: alarm id.
*/
ADDED_COMMENT(TbMsgType.COMMENT_CREATED), ADDED_COMMENT(TbMsgType.COMMENT_CREATED),
/**
* Alarm comment updated. Pushes {@link TbMsgType#COMMENT_UPDATED} to rule engine.
* Rule engine msg metadata includes {@code comment} (JSON string of the AlarmComment object).
* Rule engine msg data: full alarm JSON. Originator: alarm id.
*/
UPDATED_COMMENT(TbMsgType.COMMENT_UPDATED), UPDATED_COMMENT(TbMsgType.COMMENT_UPDATED),
/**
* Alarm comment deleted. Does not push to rule engine.
*/
DELETED_COMMENT, DELETED_COMMENT,
/**
* SMS sent. Does not push to rule engine.
*/
SMS_SENT; SMS_SENT;
@Getter @Getter

10
common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.data.edge; package org.thingsboard.server.common.data.edge;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
@ -153,4 +154,13 @@ public class Edge extends BaseDataWithAdditionalInfo<EdgeId> implements HasLabel
return this.secret; return this.secret;
} }
@Schema(description = "Additional parameters of the edge. " +
"May include: 'description' (string).",
implementation = com.fasterxml.jackson.databind.JsonNode.class,
example = "{\"description\":\"Edge at location A\"}")
@Override
public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo();
}
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java

@ -103,7 +103,9 @@ public class EntityRelation implements HasVersion, Serializable, EdqsObject {
this.version = entityRelation.getVersion(); this.version = entityRelation.getVersion();
} }
@Schema(description = "Additional parameters of the relation", implementation = JsonNode.class) @Schema(description = "Additional parameters of the relation.",
implementation = JsonNode.class,
example = "{\"description\":\"Power supply connection\"}")
public JsonNode getAdditionalInfo() { public JsonNode getAdditionalInfo() {
return BaseDataWithAdditionalInfo.getJson(() -> additionalInfo, () -> additionalInfoBytes); return BaseDataWithAdditionalInfo.getJson(() -> additionalInfo, () -> additionalInfoBytes);
} }

7
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java

@ -105,7 +105,12 @@ public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements
return super.getCreatedTime(); return super.getCreatedTime();
} }
@Schema(description = "Additional parameters of the rule node. Contains 'layoutX' and 'layoutY' properties for visualization.", implementation = JsonNode.class) @Schema(description = "Additional parameters of the rule node. " +
"May include: 'layoutX' (number, X coordinate for visualization), " +
"'layoutY' (number, Y coordinate for visualization), " +
"'description' (string).",
implementation = JsonNode.class,
example = "{\"layoutX\":320,\"layoutY\":160,\"description\":\"Filter temperature data\"}")
@Override @Override
public JsonNode getAdditionalInfo() { public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo(); return super.getAdditionalInfo();

18
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java

@ -323,22 +323,24 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
@Override @Override
public Iterator<Registration> getAllRegistrations() { public Iterator<Registration> getAllRegistrations() {
try (var connection = connectionFactory.getConnection()) { try (var scanConnection = connectionFactory.getConnection();
var getConnection = connectionFactory.getConnection()) {
Collection<Registration> list = new LinkedList<>(); Collection<Registration> list = new LinkedList<>();
ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(REG_EP + "*").build(); ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(REG_EP + "*").build();
List<Cursor<byte[]>> scans = new ArrayList<>(); List<Cursor<byte[]>> scans = new ArrayList<>();
if (connection instanceof RedisClusterConnection) { if (scanConnection instanceof RedisClusterConnection clusterConnection) {
((RedisClusterConnection) connection).clusterGetNodes().forEach(node -> { clusterConnection.clusterGetNodes().forEach(node ->
scans.add(((RedisClusterConnection) connection).scan(node, scanOptions)); scans.add(clusterConnection.scan(node, scanOptions)));
});
} else { } else {
scans.add(connection.scan(scanOptions)); scans.add(scanConnection.scan(scanOptions));
} }
scans.forEach(scan -> { scans.forEach(scan -> {
scan.forEachRemaining(key -> { scan.forEachRemaining(key -> {
byte[] element = connection.get(key); byte[] element = getConnection.get(key);
list.add(deserializeReg(element)); if (element != null) {
list.add(deserializeReg(element));
}
}); });
}); });
return list.iterator(); return list.iterator();

14
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java

@ -61,21 +61,21 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore {
@Override @Override
public Set<LwM2mClient> getAll() { public Set<LwM2mClient> getAll() {
try (var connection = connectionFactory.getConnection()) { try (var scanConnection = connectionFactory.getConnection();
var getConnection = connectionFactory.getConnection()) {
Set<LwM2mClient> clients = new HashSet<>(); Set<LwM2mClient> clients = new HashSet<>();
ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(CLIENT_EP + "*").build(); ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(CLIENT_EP + "*").build();
List<Cursor<byte[]>> scans = new ArrayList<>(); List<Cursor<byte[]>> scans = new ArrayList<>();
if (connection instanceof RedisClusterConnection) { if (scanConnection instanceof RedisClusterConnection clusterConnection) {
((RedisClusterConnection) connection).clusterGetNodes().forEach(node -> { clusterConnection.clusterGetNodes().forEach(node ->
scans.add(((RedisClusterConnection) connection).scan(node, scanOptions)); scans.add(clusterConnection.scan(node, scanOptions)));
});
} else { } else {
scans.add(connection.scan(scanOptions)); scans.add(scanConnection.scan(scanOptions));
} }
scans.forEach(scan -> { scans.forEach(scan -> {
scan.forEachRemaining(key -> { scan.forEachRemaining(key -> {
byte[] element = connection.get(key); byte[] element = getConnection.get(key);
if (element != null) { if (element != null) {
try { try {
clients.add(deserialize(element)); clients.add(deserialize(element));

18
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MModelConfigStore.java

@ -35,22 +35,24 @@ public class TbRedisLwM2MModelConfigStore implements TbLwM2MModelConfigStore {
@Override @Override
public List<LwM2MModelConfig> getAll() { public List<LwM2MModelConfig> getAll() {
try (var connection = connectionFactory.getConnection()) { try (var scanConnection = connectionFactory.getConnection();
var getConnection = connectionFactory.getConnection()) {
List<LwM2MModelConfig> configs = new ArrayList<>(); List<LwM2MModelConfig> configs = new ArrayList<>();
ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(MODEL_EP + "*").build(); ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(MODEL_EP + "*").build();
List<Cursor<byte[]>> scans = new ArrayList<>(); List<Cursor<byte[]>> scans = new ArrayList<>();
if (connection instanceof RedisClusterConnection) { if (scanConnection instanceof RedisClusterConnection clusterConnection) {
((RedisClusterConnection) connection).clusterGetNodes().forEach(node -> { clusterConnection.clusterGetNodes().forEach(node ->
scans.add(((RedisClusterConnection) connection).scan(node, scanOptions)); scans.add(clusterConnection.scan(node, scanOptions)));
});
} else { } else {
scans.add(connection.scan(scanOptions)); scans.add(scanConnection.scan(scanOptions));
} }
scans.forEach(scan -> { scans.forEach(scan -> {
scan.forEachRemaining(key -> { scan.forEachRemaining(key -> {
byte[] element = connection.get(key); byte[] element = getConnection.get(key);
configs.add(JacksonUtil.fromBytes(element, LwM2MModelConfig.class)); if (element != null) {
configs.add(JacksonUtil.fromBytes(element, LwM2MModelConfig.class));
}
}); });
}); });
return configs; return configs;

137
common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStoreTest.java

@ -0,0 +1,137 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.server.store;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.thingsboard.server.transport.lwm2m.server.store.util.LwM2MClientSerDes.serialize;
/**
* Verifies that {@link TbRedisLwM2MClientStore#getAll()} uses separate connections for
* SCAN and GET operations to prevent Jedis 5.x response-ordering corruption that occurs
* when both commands share the same connection.
*/
@ExtendWith(MockitoExtension.class)
class TbRedisLwM2MClientStoreTest {
@Mock
RedisConnectionFactory connectionFactory;
@Mock
RedisConnection scanConnection;
@Mock
RedisConnection getConnection;
TbRedisLwM2MClientStore store;
@BeforeEach
void setUp() {
// First getConnection() call → scanConnection, second → getConnection
when(connectionFactory.getConnection())
.thenReturn(scanConnection)
.thenReturn(getConnection);
store = new TbRedisLwM2MClientStore(connectionFactory);
}
@Test
void getAll_returnsSingleClient() {
LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint");
client.setState(LwM2MClientState.REGISTERED);
byte[] key = "CLIENT#EP#testEndpoint".getBytes();
byte[] value = serialize(client);
// Cursor created before thenReturn to avoid Mockito unfinished-stubbing error
Cursor<byte[]> cursor = cursorOf(key);
when(scanConnection.scan(any(ScanOptions.class))).thenReturn(cursor);
when(getConnection.get(key)).thenReturn(value);
Set<LwM2mClient> result = store.getAll();
assertThat(result).hasSize(1);
assertThat(result.iterator().next().getEndpoint()).isEqualTo("testEndpoint");
}
@Test
void getAll_getIsNeverCalledOnScanConnection() {
Cursor<byte[]> cursor = cursorOf();
when(scanConnection.scan(any(ScanOptions.class))).thenReturn(cursor);
store.getAll();
verify(scanConnection, never()).get(any(byte[].class));
}
@Test
void getAll_scanIsNeverCalledOnGetConnection() {
Cursor<byte[]> cursor = cursorOf();
when(scanConnection.scan(any(ScanOptions.class))).thenReturn(cursor);
store.getAll();
verify(getConnection, never()).scan(any(ScanOptions.class));
}
@Test
void getAll_skipsKeyWhenValueIsNull() {
byte[] key = "CLIENT#EP#gone".getBytes();
Cursor<byte[]> cursor = cursorOf(key);
when(scanConnection.scan(any(ScanOptions.class))).thenReturn(cursor);
// getConnection.get(key) returns null by default — no stubbing needed
Set<LwM2mClient> result = store.getAll();
assertThat(result).isEmpty();
}
/**
* Creates a mock {@link Cursor} that iterates over the given keys via {@code forEachRemaining}.
* The cursor is created separately (not inside a {@code thenReturn()} argument) to avoid
* Mockito's "unfinished stubbing" error caused by nested {@code when()} calls.
*/
@SuppressWarnings("unchecked")
private static Cursor<byte[]> cursorOf(byte[]... keys) {
Cursor<byte[]> cursor = mock(Cursor.class);
List<byte[]> keyList = List.of(keys);
doAnswer(inv -> {
Consumer<byte[]> action = inv.getArgument(0);
keyList.forEach(action);
return null;
}).when(cursor).forEachRemaining(any(Consumer.class));
return cursor;
}
}

2
msa/js-executor/package.json

@ -39,7 +39,7 @@
"fs-extra": "^11.3.1", "fs-extra": "^11.3.1",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "5.9.2" "typescript": "~5.9.3"
}, },
"resolutions": { "resolutions": {
"@yao-pkg/pkg/tar": ">=7.5.8" "@yao-pkg/pkg/tar": ">=7.5.8"

13
msa/js-executor/pom.xml

@ -81,7 +81,7 @@
<goal>yarn</goal> <goal>yarn</goal>
</goals> </goals>
<configuration> <configuration>
<arguments>install --non-interactive --network-concurrency 4 --network-timeout 100000 --mutex network</arguments> <arguments>install --non-interactive --check-files --network-concurrency 4 --network-timeout 100000 --mutex network</arguments>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
@ -96,6 +96,17 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<configuration>
<filesets>
<fileset>
<directory>${basedir}/node_modules</directory>
</fileset>
</filesets>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId> <artifactId>maven-dependency-plugin</artifactId>

8
msa/js-executor/yarn.lock

@ -1634,10 +1634,10 @@ type-is@^2.0.0, type-is@^2.0.1:
media-typer "^1.1.0" media-typer "^1.1.0"
mime-types "^3.0.0" mime-types "^3.0.0"
typescript@5.9.2: typescript@~5.9.3:
version "5.9.2" version "5.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
undefsafe@^2.0.5: undefsafe@^2.0.5:
version "2.0.5" version "2.0.5"

2
msa/web-ui/package.json

@ -42,7 +42,7 @@
"fs-extra": "^11.3.1", "fs-extra": "^11.3.1",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "5.9.2" "typescript": "~5.9.3"
}, },
"resolutions": { "resolutions": {
"@yao-pkg/pkg/tar": ">=7.5.8" "@yao-pkg/pkg/tar": ">=7.5.8"

13
msa/web-ui/pom.xml

@ -90,7 +90,7 @@
<goal>yarn</goal> <goal>yarn</goal>
</goals> </goals>
<configuration> <configuration>
<arguments>install --non-interactive --network-concurrency 4 --network-timeout 100000 --mutex network</arguments> <arguments>install --non-interactive --check-files --network-concurrency 4 --network-timeout 100000 --mutex network</arguments>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
@ -105,6 +105,17 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<configuration>
<filesets>
<fileset>
<directory>${basedir}/node_modules</directory>
</fileset>
</filesets>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId> <artifactId>maven-dependency-plugin</artifactId>

8
msa/web-ui/yarn.lock

@ -1716,10 +1716,10 @@ type-is@^2.0.0, type-is@^2.0.1:
media-typer "^1.1.0" media-typer "^1.1.0"
mime-types "^3.0.0" mime-types "^3.0.0"
typescript@5.9.2: typescript@~5.9.3:
version "5.9.2" version "5.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
undefsafe@^2.0.5: undefsafe@^2.0.5:
version "2.0.5" version "2.0.5"

2
pom.xml

@ -656,7 +656,7 @@
<plugin> <plugin>
<groupId>com.github.eirslett</groupId> <groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId> <artifactId>frontend-maven-plugin</artifactId>
<version>1.12.0</version> <version>2.0.0</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

13
ui-ngx/pom.xml

@ -66,11 +66,22 @@
<goal>yarn</goal> <goal>yarn</goal>
</goals> </goals>
<configuration> <configuration>
<arguments>install --non-interactive --network-concurrency 4 --network-timeout 100000 --mutex network</arguments> <arguments>install --non-interactive --check-files --network-concurrency 4 --network-timeout 100000 --mutex network</arguments>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<configuration>
<filesets>
<fileset>
<directory>${basedir}/node_modules</directory>
</fileset>
</filesets>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>
<profiles> <profiles>

4
ui-ngx/src/app/core/services/calculated-field-form.service.ts

@ -38,7 +38,7 @@ export class CalculatedFieldFormService {
buildForm(): FormGroup { buildForm(): FormGroup {
return this.fb.group({ return this.fb.group({
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], name: ['', [Validators.required, Validators.maxLength(255)]],
entityId: [null, Validators.required], entityId: [null, Validators.required],
type: [CalculatedFieldType.SIMPLE], type: [CalculatedFieldType.SIMPLE],
debugSettings: [], debugSettings: [],
@ -48,7 +48,7 @@ export class CalculatedFieldFormService {
buildAlarmRuleForm(): FormGroup { buildAlarmRuleForm(): FormGroup {
return this.fb.group({ return this.fb.group({
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], name: ['', [Validators.required, Validators.maxLength(255)]],
entityId: [null, Validators.required], entityId: [null, Validators.required],
type: [CalculatedFieldType.ALARM], type: [CalculatedFieldType.ALARM],
debugSettings: [], debugSettings: [],

8
ui-ngx/src/app/core/services/resources.service.ts

@ -24,7 +24,9 @@ import {
ɵCssSelectorList, ɵCssSelectorList,
ɵNG_COMP_DEF, ɵNG_COMP_DEF,
ɵNG_MOD_DEF, ɵNG_MOD_DEF,
ɵNG_PIPE_DEF,
ɵNgModuleDef, ɵNgModuleDef,
ɵPipeDef,
DOCUMENT DOCUMENT
} from '@angular/core'; } from '@angular/core';
@ -302,6 +304,12 @@ export class ResourcesService {
(component as any).standalone = false; (component as any).standalone = false;
} }
} }
if (ɵNG_PIPE_DEF in decl) {
const pipe: ɵPipeDef<any> = decl[ɵNG_PIPE_DEF];
if (isUndefined(pipe.standalone) || pipe.standalone) {
(pipe as any).standalone = false;
}
}
} }
} }
modulesWithComponents.modules.push(moduleInfo); modulesWithComponents.modules.push(moduleInfo);

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

@ -40,8 +40,6 @@
<mat-error> <mat-error>
@if (fieldFormGroup.get('name').hasError('required')) { @if (fieldFormGroup.get('name').hasError('required')) {
{{ 'alarm-rule.alarm-type-required' | translate }} {{ 'alarm-rule.alarm-type-required' | translate }}
} @else if (fieldFormGroup.get('name').hasError('pattern')) {
{{ 'alarm-rule.alarm-type-pattern' | translate }}
} @else if (fieldFormGroup.get('name').hasError('maxlength')) { } @else if (fieldFormGroup.get('name').hasError('maxlength')) {
{{ 'alarm-rule.alarm-type-max-length' | translate }} {{ 'alarm-rule.alarm-type-max-length' | translate }}
} }

2
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html

@ -47,8 +47,6 @@
<mat-error> <mat-error>
@if (entityForm.get('name').hasError('required')) { @if (entityForm.get('name').hasError('required')) {
{{ 'common.hint.title-required' | translate }} {{ 'common.hint.title-required' | translate }}
} @else if (entityForm.get('name').hasError('pattern')) {
{{ 'common.hint.title-pattern' | translate }}
} @else if (entityForm.get('name').hasError('maxlength')) { } @else if (entityForm.get('name').hasError('maxlength')) {
{{ 'common.hint.title-max-length' | translate }} {{ 'common.hint.title-max-length' | translate }}
} }

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

@ -47,8 +47,6 @@
<mat-error> <mat-error>
@if (entityForm.get('name').hasError('required')) { @if (entityForm.get('name').hasError('required')) {
{{ 'common.hint.title-required' | translate }} {{ 'common.hint.title-required' | translate }}
} @else if (entityForm.get('name').hasError('pattern')) {
{{ 'common.hint.title-pattern' | translate }}
} @else if (entityForm.get('name').hasError('maxlength')) { } @else if (entityForm.get('name').hasError('maxlength')) {
{{ 'common.hint.title-max-length' | translate }} {{ 'common.hint.title-max-length' | translate }}
} }

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html

@ -39,8 +39,6 @@
<mat-error> <mat-error>
@if (fieldFormGroup.get('name').hasError('required')) { @if (fieldFormGroup.get('name').hasError('required')) {
{{ 'common.hint.title-required' | translate }} {{ 'common.hint.title-required' | translate }}
} @else if (fieldFormGroup.get('name').hasError('pattern')) {
{{ 'common.hint.title-pattern' | translate }}
} @else if (fieldFormGroup.get('name').hasError('maxlength')) { } @else if (fieldFormGroup.get('name').hasError('maxlength')) {
{{ 'common.hint.title-max-length' | translate }} {{ 'common.hint.title-max-length' | translate }}
} }

2
ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html

@ -89,7 +89,7 @@
</tb-gallery-image-input> </tb-gallery-image-input>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>asset-profile.description</mat-label> <mat-label translate>asset-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</fieldset> </fieldset>
</form> </form>

2
ui-ngx/src/app/modules/home/components/profile/device-profile.component.html

@ -114,7 +114,7 @@
</tb-gallery-image-input> </tb-gallery-image-input>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>device-profile.description</mat-label> <mat-label translate>device-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="standalone" class="mat-block"> <mat-form-field *ngIf="standalone" class="mat-block">
<mat-label translate>device-profile.transport-type</mat-label> <mat-label translate>device-profile.transport-type</mat-label>

2
ui-ngx/src/app/modules/home/pages/asset/asset.component.html

@ -110,7 +110,7 @@
<div formGroupName="additionalInfo"> <div formGroupName="additionalInfo">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>asset.description</mat-label> <mat-label translate>asset.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
</fieldset> </fieldset>

10
ui-ngx/src/app/modules/home/pages/customer/customer.component.html

@ -83,11 +83,8 @@
{{ 'customer.title-max-length' | translate }} {{ 'customer.title-max-length' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
<div formGroupName="additionalInfo" class="flex flex-col"> <div formGroupName="additionalInfo" class="flex flex-col">
<mat-form-field class="mat-block">
<mat-label translate>customer.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
<section class="tb-default-dashboard flex flex-1 flex-col" *ngIf="entity?.id"> <section class="tb-default-dashboard flex flex-1 flex-col" *ngIf="entity?.id">
<section class="flex flex-1 flex-col gt-sm:flex-row"> <section class="flex flex-1 flex-col gt-sm:flex-row">
<tb-dashboard-autocomplete <tb-dashboard-autocomplete
@ -103,8 +100,11 @@
</mat-checkbox> </mat-checkbox>
</section> </section>
</section> </section>
<mat-form-field class="mat-block">
<mat-label translate>customer.description</mat-label>
<textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field>
</div> </div>
<tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
</fieldset> </fieldset>
</form> </form>
</div> </div>

12
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html

@ -124,12 +124,6 @@
{{ 'dashboard.title-max-length' | translate }} {{ 'dashboard.title-max-length' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<div formGroupName="configuration" class="flex flex-col">
<mat-form-field class="mat-block">
<mat-label translate>dashboard.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</div>
<tb-entity-list <tb-entity-list
*ngIf="isAdd" *ngIf="isAdd"
formControlName="assignedCustomerIds" formControlName="assignedCustomerIds"
@ -150,6 +144,12 @@
formControlName="image"> formControlName="image">
</tb-gallery-image-input> </tb-gallery-image-input>
</div> </div>
<div formGroupName="configuration" class="flex flex-col mt-5">
<mat-form-field class="mat-block">
<mat-label translate>dashboard.description</mat-label>
<textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field>
</div>
</fieldset> </fieldset>
</form> </form>
</div> </div>

2
ui-ngx/src/app/modules/home/pages/device/device.component.html

@ -150,7 +150,7 @@
</div> </div>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>device.description</mat-label> <mat-label translate>device.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
</fieldset> </fieldset>

2
ui-ngx/src/app/modules/home/pages/edge/edge.component.html

@ -198,7 +198,7 @@
<div formGroupName="additionalInfo" class="flex flex-col"> <div formGroupName="additionalInfo" class="flex flex-col">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>edge.description</mat-label> <mat-label translate>edge.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
</fieldset> </fieldset>

2
ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html

@ -161,7 +161,7 @@
<div formGroupName="additionalInfo"> <div formGroupName="additionalInfo">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>entity-view.description</mat-label> <mat-label translate>entity-view.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
</fieldset> </fieldset>

2
ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html

@ -73,7 +73,7 @@
</tb-entity-autocomplete> </tb-entity-autocomplete>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label translate>mobile.description</mat-label> <mat-label translate>mobile.description</mat-label>
<textarea matInput formControlName="description" rows="1"></textarea> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</form> </form>
</mat-step> </mat-step>

2
ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html

@ -155,7 +155,7 @@
</section> </section>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</div> </div>

28
ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html

@ -150,7 +150,7 @@
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</form> </form>
@ -204,7 +204,7 @@
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</form> </form>
@ -233,7 +233,7 @@
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</form> </form>
@ -287,7 +287,7 @@
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</form> </form>
@ -343,7 +343,7 @@
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</form> </form>
@ -402,7 +402,7 @@
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</form> </form>
@ -439,7 +439,7 @@
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</form> </form>
@ -464,7 +464,7 @@
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</form> </form>
@ -501,7 +501,7 @@
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</form> </form>
@ -538,7 +538,7 @@
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</form> </form>
@ -551,7 +551,7 @@
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</form> </form>
@ -574,7 +574,7 @@
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</form> </form>
@ -587,7 +587,7 @@
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</form> </form>
@ -650,7 +650,7 @@
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</section> </section>
</form> </form>

2
ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html

@ -177,7 +177,7 @@
<div formGroupName="additionalInfo"> <div formGroupName="additionalInfo">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>ota-update.description</mat-label> <mat-label translate>ota-update.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
</fieldset> </fieldset>

2
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html

@ -75,7 +75,7 @@
<div formGroupName="additionalInfo"> <div formGroupName="additionalInfo">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>rulenode.rule-node-description</mat-label> <mat-label translate>rulenode.rule-node-description</mat-label>
<textarea matInput formControlName="description" rows="1"></textarea> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>

2
ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html

@ -94,7 +94,7 @@
<div formGroupName="additionalInfo"> <div formGroupName="additionalInfo">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>rulechain.description</mat-label> <mat-label translate>rulechain.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea> <textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
</fieldset> </fieldset>

4
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html

@ -37,8 +37,10 @@
<div class="fixed-title-width" translate>scada.description</div> <div class="fixed-title-width" translate>scada.description</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<textarea matInput <textarea matInput
cdkTextareaAutosize
cdkAutosizeMinRows="1"
formControlName="description" formControlName="description"
rows="2"></textarea> rows="1"></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row"> <div class="tb-form-row">

8
ui-ngx/src/app/modules/home/pages/user/user.component.html

@ -119,10 +119,6 @@
} }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>user.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
<section class="tb-default-dashboard flex flex-1 flex-col" *ngIf="entity?.id"> <section class="tb-default-dashboard flex flex-1 flex-col" *ngIf="entity?.id">
<section class="flex flex-1 flex-col items-baseline gt-sm:flex-row"> <section class="flex flex-1 flex-col items-baseline gt-sm:flex-row">
<tb-dashboard-autocomplete <tb-dashboard-autocomplete
@ -155,6 +151,10 @@
</mat-checkbox> </mat-checkbox>
</section> </section>
</section> </section>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>user.description</mat-label>
<textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1"></textarea>
</mat-form-field>
</div> </div>
</fieldset> </fieldset>
</form> </form>

10
ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html

@ -58,11 +58,6 @@
label="{{'widget.image-preview' | translate}}" label="{{'widget.image-preview' | translate}}"
formControlName="image"> formControlName="image">
</tb-gallery-image-input> </tb-gallery-image-input>
<mat-form-field class="mat-block">
<mat-label translate>widget.description</mat-label>
<textarea matInput formControlName="description" rows="2" maxlength="1024" #descriptionInput></textarea>
<mat-hint align="end">{{descriptionInput.value?.length || 0}}/1024</mat-hint>
</mat-form-field>
<tb-string-items-list <tb-string-items-list
label="{{ 'widget.tags' | translate }}" label="{{ 'widget.tags' | translate }}"
formControlName="tags"> formControlName="tags">
@ -75,6 +70,11 @@
{{ 'widget.deprecated' | translate }} {{ 'widget.deprecated' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<mat-form-field class="mat-block mt-5">
<mat-label translate>widget.description</mat-label>
<textarea matInput formControlName="description" cdkTextareaAutosize cdkAutosizeMinRows="1" rows="1" maxlength="1024" #descriptionInput></textarea>
<mat-hint align="end">{{descriptionInput.value?.length || 0}}/1024</mat-hint>
</mat-form-field>
</fieldset> </fieldset>
</form> </form>
</div> </div>

2
ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts

@ -41,7 +41,7 @@ export class HslaInputComponent {
@Input() @Input()
public suffixValue = '%'; public suffixValue = '%';
public get value() { public get value(): ReturnType<Color['getHsla']> {
return this.color.getHsla(); return this.color.getHsla();
} }

2
ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts

@ -41,7 +41,7 @@ export class RgbaInputComponent {
@Input() @Input()
public suffixValue = '%'; public suffixValue = '%';
public get value() { public get value(): ReturnType<Color['getRgba']> {
return this.color.getRgba(); return this.color.getRgba();
} }

Loading…
Cancel
Save