Browse Source

Merge remote-tracking branch 'origin/rc' into master-rc-merge

# Conflicts:
#	application/src/main/data/upgrade/basic/schema_update.sql
#	common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
#	common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java
#	common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
pull/15225/head
Viacheslav Klimov 7 months ago
parent
commit
0f4cf61186
Failed to extract signature
  1. 2
      .gitignore
  2. 5
      application/src/main/data/upgrade/basic/schema_update.sql
  3. 33
      application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java
  4. 4
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  5. 3
      application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java
  6. 9
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java
  7. 7
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java
  8. 31
      common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java
  9. 22
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java
  10. 31
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  11. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
  12. 18
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  13. 1
      dao/pom.xml
  14. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  15. 11
      dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java
  16. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java
  17. 1
      dao/src/main/resources/sql/schema-entities.sql
  18. 4
      dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java
  19. 458
      docker/monitoring/grafana/provisioning/dashboards/transport_connections.json
  20. 1
      netty-mqtt/pom.xml
  21. 4
      pom.xml
  22. 3
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  23. 6
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  24. 14
      ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-config.resolver.ts
  25. 4
      ui-ngx/src/app/shared/components/notification/notification.component.html
  26. 13
      ui-ngx/src/app/shared/components/notification/notification.component.ts
  27. 1
      ui-ngx/src/app/shared/models/calculated-field.models.ts

2
.gitignore

@ -37,4 +37,4 @@ rebuild-docker.sh
*/.run/**
.run/**
.run
.claude/
.claude

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

@ -14,3 +14,8 @@
-- limitations under the License.
--
-- CALCULATED FIELD ADDITIONAL INFO ADDITION START
ALTER TABLE calculated_field ADD COLUMN IF NOT EXISTS additional_info varchar;
-- CALCULATED FIELD ADDITIONAL INFO ADDITION END

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

@ -62,6 +62,9 @@ import static org.mockito.Mockito.verify;
@DaoSqlTest
public class AuditLogControllerTest extends AbstractControllerTest {
// Small enough to force multiple pages, verifying pagination loop correctness
private static final int SMALL_PAGE_SIZE = 5;
private Tenant savedTenant;
private User tenantAdmin;
@ -113,17 +116,14 @@ public class AuditLogControllerTest extends AbstractControllerTest {
doPost("/api/device", device, Device.class);
}
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs?");
Assert.assertEquals(11 + 1, loadedAuditLogs.size());
loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?");
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs?")).hasSize(11 + 1));
Assert.assertEquals(11 + 1, loadedAuditLogs.size());
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?")).hasSize(11 + 1));
loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?");
Assert.assertEquals(11 + 1, loadedAuditLogs.size());
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?")).hasSize(11 + 1));
}
@Test
@ -138,7 +138,7 @@ public class AuditLogControllerTest extends AbstractControllerTest {
}
int expectedSize = loadedAuditLogsBefore.size() + 3;
Awaitility.await().atMost(10, TimeUnit.SECONDS).untilAsserted(() ->
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
Assert.assertEquals("Have X audit log before this test + New tenant profiles in the test",
expectedSize, getAuditLogs(100, "/api/audit/logs?").size()));
}
@ -170,9 +170,9 @@ public class AuditLogControllerTest extends AbstractControllerTest {
savedDevice = doPost("/api/device", savedDevice, Device.class);
}
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?");
Assert.assertEquals(11 + 1, loadedAuditLogs.size());
Device finalSavedDevice = savedDevice;
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/DEVICE/" + finalSavedDevice.getId().getId() + "?")).hasSize(11 + 1));
}
@Test
@ -188,9 +188,10 @@ public class AuditLogControllerTest extends AbstractControllerTest {
tenantProfile.setName(tenantProfile.getName() + "(old)");
tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/entity/" +tenantProfile.getId().getEntityType()+ "/" + tenantProfile.getId().getId() + "?");
Assert.assertEquals("Audit logs count by Tenant Profile entity", 2, loadedAuditLogs.size());
TenantProfile finalTenantProfile = tenantProfile;
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/" + finalTenantProfile.getId().getEntityType() + "/" + finalTenantProfile.getId().getId() + "?"))
.as("Audit logs count by Tenant Profile entity").hasSize(2));
//cleanup
doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString());

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

@ -18,6 +18,7 @@ package org.thingsboard.server.controller;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
@ -108,6 +109,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig());
assertThat(savedCalculatedField.getAdditionalInfo()).isEqualTo(calculatedField.getAdditionalInfo());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF");
@ -115,6 +117,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
assertThat(updatedCalculatedField.getAdditionalInfo()).isEqualTo(savedCalculatedField.getAdditionalInfo());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString())
@ -322,6 +325,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
calculatedField.setType(cfType);
calculatedField.setName("Test Calculated Field for " + entityId);
calculatedField.setConfigurationVersion(1);
calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode());
if (customConfiguration != null) {
calculatedField.setConfiguration(customConfiguration);
} else switch (cfType) {

3
application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java

@ -73,6 +73,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName());
Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId());
Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration());
Assert.assertEquals(calculatedField.getAdditionalInfo(), calculatedFieldFromMsg.getAdditionalInfo());
edgeImitator.expectMessageAmount(1);
savedCalculatedField.setName(UPDATED_CF_NAME);
@ -229,6 +230,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
config.setOutput(output);
calculatedField.setConfiguration(config);
calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode());
return calculatedField;
}
@ -260,6 +262,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class);
Assert.assertNotNull(calculatedField);
Assert.assertEquals(resourceTitle, calculatedField.getName());
Assert.assertEquals(JacksonUtil.newObjectNode(), calculatedField.getAdditionalInfo());
}
}

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

@ -171,7 +171,14 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
if (this.leshanClient != null) {
log.info("Stop/reboot LwM2M client {}", this.leshanClient.getEndpoint(identity));
this.leshanClient.stop(false);
try {
this.leshanClient.stop(false);
} catch (Exception stopEx) {
// Leshan may throw NPE during CoAP observe-relation cleanup when the server
// reference is null (race condition in NotificationDataStore.toKey()).
// The client is still considered stopped at this point — proceed with restart.
log.warn("Exception during LwM2M client stop, proceeding with restart: {}", stopEx.getMessage());
}
log.info("Start after update fw LwM2M client {}", this.leshanClient.getEndpoint(identity));
this.leshanClient.start();

7
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java

@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.concurrent.TimeUnit.SECONDS;
@ -37,6 +38,12 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID
@Slf4j
public class RpcLwm2mIntegrationReadCollectedValueTest extends AbstractRpcLwM2MIntegrationTest {
@Before
public void resetCollectedValueTimestamps() {
Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0 = 0;
Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_1 = 0;
}
/**
* Read {"id":"/3303/12/5700"}
* Trigger a Send operation from the client with multiple values for the same resource as a payload

31
common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java

@ -17,15 +17,15 @@ package org.thingsboard.server.common.data.cf;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasAdditionalInfo;
import org.thingsboard.server.common.data.HasDebugSettings;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
@ -39,7 +39,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
@ -48,10 +47,7 @@ import java.util.Set;
@Schema
@Data
@EqualsAndHashCode(callSuper = true)
public class CalculatedField extends BaseData<CalculatedFieldId> implements HasName, HasTenantId, HasVersion, HasDebugSettings {
@Serial
private static final long serialVersionUID = 4491966747773381420L;
public class CalculatedField extends BaseData<CalculatedFieldId> implements HasName, HasTenantId, HasVersion, HasDebugSettings, HasAdditionalInfo {
public static final Map<EntityType, Set<CalculatedFieldType>> SUPPORTED_ENTITIES = Map.of(
EntityType.DEVICE, CalculatedFieldType.all,
@ -90,28 +86,17 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
@Valid
@NotNull
private CalculatedFieldConfiguration configuration;
@Getter
@Setter
private Long version;
@NoXss
@Schema(description = "Additional parameters of the calculated field")
private JsonNode additionalInfo;
public CalculatedField() {
super();
}
public CalculatedField() {}
public CalculatedField(CalculatedFieldId id) {
super(id);
}
public CalculatedField(TenantId tenantId, EntityId entityId, CalculatedFieldType type, String name, int configurationVersion, CalculatedFieldConfiguration configuration, Long version) {
this.tenantId = tenantId;
this.entityId = entityId;
this.type = type;
this.name = name;
this.configurationVersion = configurationVersion;
this.configuration = configuration;
this.version = version;
}
public CalculatedField(CalculatedField calculatedField) {
super(calculatedField);
this.tenantId = calculatedField.tenantId;
@ -123,6 +108,7 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
this.configurationVersion = calculatedField.configurationVersion;
this.configuration = calculatedField.configuration;
this.version = calculatedField.version;
this.additionalInfo = calculatedField.additionalInfo;
}
@Schema(description = "JSON object with the Calculated Field Id. Referencing non-existing Calculated Field Id will cause error.")
@ -159,6 +145,7 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
.append(", name='").append(name)
.append(", configurationVersion=").append(configurationVersion)
.append(", configuration=").append(configuration)
.append(", additionalInfo=").append(additionalInfo)
.append(", version=").append(version)
.append(", createdTime=").append(createdTime)
.append(", id=").append(id).append(']')

22
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java

@ -88,20 +88,30 @@ public class MqttTransportContext extends TransportContext {
@Value("${transport.mqtt.proxy_enabled:false}")
private boolean proxyEnabled;
private final AtomicInteger connectionsCounter = new AtomicInteger();
private final AtomicInteger connectionsActiveCounterMQTT = new AtomicInteger();
private final AtomicInteger connectionsActiveCounterMQTTS = new AtomicInteger();
@PostConstruct
public void init() {
super.init();
transportService.createGaugeStats("openConnections", connectionsCounter);
transportService.createGaugeStats("connections_active", connectionsActiveCounterMQTT, "protocol", "MQTT");
transportService.createGaugeStats("connections_active", connectionsActiveCounterMQTTS, "protocol", "MQTTS");
}
public void channelRegistered() {
connectionsCounter.incrementAndGet();
public void channelRegistered(boolean isSSL) {
if (isSSL) {
connectionsActiveCounterMQTTS.incrementAndGet();
} else {
connectionsActiveCounterMQTT.incrementAndGet();
}
}
public void channelUnregistered() {
connectionsCounter.decrementAndGet();
public void channelUnregistered(boolean isSSL) {
if (isSSL) {
connectionsActiveCounterMQTTS.decrementAndGet();
} else {
connectionsActiveCounterMQTT.decrementAndGet();
}
}
public boolean checkAddress(InetSocketAddress address) {

31
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -180,16 +180,20 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
this.rpcAwaitingAck = new ConcurrentHashMap<>();
}
boolean isSSL() {
return sslHandler != null;
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
super.channelRegistered(ctx);
context.channelRegistered();
context.channelRegistered(isSSL());
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
super.channelUnregistered(ctx);
context.channelUnregistered();
context.channelUnregistered(isSSL());
}
@Override
@ -254,6 +258,17 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
}
String getClientAddr(ChannelHandlerContext ctx) {
try {
InetSocketAddress remote = getAddress(ctx);
if (remote == null) return "unknown";
String host = remote.getAddress() != null ? remote.getAddress().getHostAddress() : remote.getHostString();
return host + ":" + remote.getPort();
} catch (Exception ignored) {
return "unknown";
}
}
InetSocketAddress getAddress(ChannelHandlerContext ctx) {
var address = ctx.channel().attr(MqttTransportService.ADDRESS).get();
if (address == null) {
@ -1150,19 +1165,25 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (cause instanceof IOException) {
if (log.isDebugEnabled()) {
log.debug("[{}][{}][{}] IOException: {}", sessionId,
String clientAddr = getClientAddr(ctx);
log.debug("[{}][{}][{}][{}] {}: {}", sessionId,
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null),
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""),
clientAddr,
cause.getClass().getSimpleName(),
cause.getMessage(),
cause);
} else if (log.isInfoEnabled()) {
log.info("[{}][{}][{}] IOException: {}", sessionId,
String clientAddr = getClientAddr(ctx);
log.info("[{}][{}][{}][{}] {}: {}", sessionId,
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null),
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""),
clientAddr,
cause.getClass().getSimpleName(),
cause.getMessage());
}
} else {
log.error("[{}] Unexpected Exception", sessionId, cause);
log.error("[{}][{}] Unexpected Exception", sessionId, getClientAddr(ctx), cause);
}
closeCtx(ctx, MqttReasonCodes.Disconnect.SERVER_SHUTTING_DOWN);

2
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java

@ -161,5 +161,5 @@ public interface TransportService {
boolean hasSession(SessionInfoProto sessionInfo);
void createGaugeStats(String openConnections, AtomicInteger connectionsCounter);
void createGaugeStats(String statsName, AtomicInteger number, String... tags);
}

18
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -1257,9 +1257,21 @@ public class DefaultTransportService extends TransportActivityManager implements
}
@Override
public void createGaugeStats(String statsName, AtomicInteger number) {
statsFactory.createGauge(StatsType.TRANSPORT + "." + statsName, number);
statsMap.put(statsName, number);
public void createGaugeStats(String statsName, AtomicInteger number, String... tags) {
String key = "thingsboard" + "." + StatsType.TRANSPORT.getName() + "." + statsName;
statsFactory.createGauge(key, number, tags);
statsMap.put(statsName + tagsKey(tags), number);
}
private String tagsKey(String... tags) {
if (tags == null || tags.length < 2) return "";
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < tags.length; i += 2) {
if (i > 0) sb.append(',');
sb.append(tags[i]).append('=').append(i + 1 < tags.length ? tags[i + 1] : "");
}
sb.append(']');
return sb.toString();
}
@Scheduled(fixedDelayString = "${transport.stats.print-interval-ms:60000}")

1
dao/pom.xml

@ -280,7 +280,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>${jar-plugin.version}</version>
<executions>
<execution>
<goals>

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

@ -730,6 +730,7 @@ public class ModelConstants {
public static final String CALCULATED_FIELD_CONFIGURATION_VERSION = "configuration_version";
public static final String CALCULATED_FIELD_CONFIGURATION = "configuration";
public static final String CALCULATED_FIELD_VERSION = "version";
public static final String CALCULATED_FIELD_ADDITIONAL_INFO = ADDITIONAL_INFO_PROPERTY;
/**
* Tasks constants.

11
dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java

@ -36,6 +36,7 @@ import org.thingsboard.server.dao.util.mapping.JsonConverter;
import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ADDITIONAL_INFO;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION_VERSION;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_ID;
@ -81,9 +82,11 @@ public class CalculatedFieldEntity extends BaseVersionedEntity<CalculatedField>
@Column(name = DEBUG_SETTINGS)
private String debugSettings;
public CalculatedFieldEntity() {
super();
}
@Convert(converter = JsonConverter.class)
@Column(name = CALCULATED_FIELD_ADDITIONAL_INFO)
private JsonNode additionalInfo;
public CalculatedFieldEntity() {}
public CalculatedFieldEntity(CalculatedField calculatedField) {
this.setUuid(calculatedField.getUuidId());
@ -97,6 +100,7 @@ public class CalculatedFieldEntity extends BaseVersionedEntity<CalculatedField>
this.configuration = JacksonUtil.valueToTree(calculatedField.getConfiguration());
this.version = calculatedField.getVersion();
this.debugSettings = JacksonUtil.toString(calculatedField.getDebugSettings());
this.additionalInfo = calculatedField.getAdditionalInfo();
}
@Override
@ -111,6 +115,7 @@ public class CalculatedFieldEntity extends BaseVersionedEntity<CalculatedField>
calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class));
calculatedField.setVersion(version);
calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class));
calculatedField.setAdditionalInfo(additionalInfo);
return calculatedField;
}

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

@ -74,6 +74,7 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
JsonNode configuration = JacksonUtil.toJsonNode((String) row.get("configuration"));
long version = row.get("version") != null ? (long) row.get("version") : 0;
String debugSettings = (String) row.get("debug_settings");
JsonNode additionalInfo = JacksonUtil.toJsonNode((String) row.get("additional_info"));
CalculatedField calculatedField = new CalculatedField();
calculatedField.setId(new CalculatedFieldId(id));
@ -91,6 +92,7 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
}
calculatedField.setVersion(version);
calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class));
calculatedField.setAdditionalInfo(additionalInfo);
return calculatedField;
}).collect(Collectors.toList());

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

@ -935,6 +935,7 @@ CREATE TABLE IF NOT EXISTS calculated_field (
configuration varchar(1000000),
version BIGINT DEFAULT 1,
debug_settings varchar(1024),
additional_info varchar,
CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name)
);

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

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.service;
import org.apache.commons.lang3.RandomUtils;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.TenantProfile;
@ -83,6 +84,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(calculatedField.getConfiguration());
assertThat(savedCalculatedField.getAdditionalInfo()).isEqualTo(calculatedField.getAdditionalInfo());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF");
@ -90,6 +92,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
CalculatedField updatedCalculatedField = calculatedFieldService.save(savedCalculatedField);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
assertThat(updatedCalculatedField.getAdditionalInfo()).isEqualTo(savedCalculatedField.getAdditionalInfo());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
}
@ -495,6 +498,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId));
calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode());
return calculatedField;
}

458
docker/monitoring/grafana/provisioning/dashboards/transport_connections.json

@ -0,0 +1,458 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "dark-orange",
"value": null
}
]
},
"unit": "short"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Total connections"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "light-orange",
"mode": "fixed"
}
}
]
}
]
},
"gridPos": {
"h": 15,
"w": 19,
"x": 0,
"y": 0
},
"id": 19,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by (protocol) (thingsboard_transport_connections_active{})",
"format": "time_series",
"instant": false,
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{protocol}}",
"refId": "A"
}
],
"title": "Active Connections by Protocol",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "rgba(209, 138, 78, 1)",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 15,
"w": 5,
"x": 19,
"y": 0
},
"id": 21,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"text": {},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum (thingsboard_transport_connections_active)",
"instant": true,
"interval": "",
"legendFormat": "",
"refId": "A"
}
],
"title": "Total Connections",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 15,
"w": 19,
"x": 0,
"y": 15
},
"id": 22,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"exemplar": true,
"expr": "thingsboard_transport_connections_active",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{protocol}} {{pod_name}}",
"range": true,
"refId": "A"
}
],
"title": "Active Connections by Pod",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"filterable": false,
"inspect": false
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "rgba(209, 138, 78, 1)",
"value": null
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Connections"
},
"properties": [
{
"id": "custom.cellOptions",
"value": {
"mode": "gradient",
"type": "gauge"
}
}
]
}
]
},
"gridPos": {
"h": 15,
"w": 5,
"x": 19,
"y": 15
},
"id": 23,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": []
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum by (pod_name) (thingsboard_transport_connections_active)",
"format": "table",
"instant": true,
"interval": "",
"legendFormat": "__auto",
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "Connection Count by Pod",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true,
"__name__": true,
"app": true,
"controller_revision_hash": true,
"instance": true,
"job": true,
"kubernetes_name": true,
"kubernetes_namespace": true,
"node_name": true,
"pod_name": false,
"statefulset_kubernetes_io_pod_name": true
},
"indexByName": {},
"renameByName": {
"Time": "",
"Value": "Connections",
"pod_name": "Pod"
}
}
},
{
"id": "sortBy",
"options": {
"fields": {},
"sort": [
{
"field": "Pod"
}
]
}
}
],
"type": "table"
}
],
"refresh": "30s",
"schemaVersion": 39,
"tags": [
"thingsboard"
],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "Transport Connections",
"uid": "lewbrlwjerwkj3",
"version": 1,
"weekStart": ""
}

1
netty-mqtt/pom.xml

@ -125,7 +125,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<archive>
<manifest>

4
pom.xml

@ -72,7 +72,7 @@
<paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version>
<os-maven-plugin.version>1.7.1</os-maven-plugin.version>
<surefire.version>3.5.4</surefire.version>
<jar-plugin.version>3.4.0</jar-plugin.version>
<jar-plugin.version>3.5.0</jar-plugin.version>
<springdoc-swagger.version>2.8.8TB</springdoc-swagger.version>
<swagger-annotations.version>2.2.30</swagger-annotations.version>
<spatial4j.version>0.8</spatial4j.version>
@ -636,7 +636,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.1</version>
<version>${jar-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>

3
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -93,7 +93,8 @@
(click)="deleteTelemetry($event)">
<mat-icon>delete</mat-icon>
</button>
<button mat-raised-button color="accent"
<button [class.!hidden]="isSysAdmin"
mat-raised-button color="accent"
class="button-widget-action"
[disabled]="isLoading$ | async"
matTooltip="{{ 'attribute.show-on-widget' | translate }}"

6
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts

@ -56,6 +56,8 @@ import {
import { AttributeDatasource } from '@home/models/datasource/attribute-datasource';
import { AttributeService } from '@app/core/http/attribute.service';
import { EntityType } from '@shared/models/entity-type.models';
import { Authority } from '@shared/models/authority.enum';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
AddAttributeDialogComponent,
@ -91,7 +93,6 @@ import { FormBuilder } from '@angular/forms';
import { AggregationType, defaultTimewindow } from '@shared/models/time/time.models';
import { TimeService } from '@core/services/time.service';
@Component({
selector: 'tb-attribute-table',
templateUrl: './attribute-table.component.html',
@ -186,6 +187,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
textSearch = this.fb.control('', {nonNullable: true});
isSysAdmin = false;
private destroy$ = new Subject<void>();
selectAllModel: boolean = false;
@ -207,6 +210,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
private fb: FormBuilder,
private timeService: TimeService) {
super(store);
this.isSysAdmin = getCurrentAuthUser(this.store).authority === Authority.SYS_ADMIN;
this.dirtyValue = !this.activeValue;
const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC };
this.pageLink = new PageLink(10, 0, null, sortOrder);

14
ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-config.resolver.ts

@ -37,9 +37,10 @@ import {
InboxNotificationDialogComponent,
InboxNotificationDialogData
} from '@home/pages/notification/inbox/inbox-notification-dialog.component';
import { Injectable } from '@angular/core';
import { Injectable, SecurityContext } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { UtilsService } from '@core/services/utils.service';
import { DomSanitizer } from '@angular/platform-browser';
@Injectable()
export class InboxTableConfigResolver {
@ -50,7 +51,8 @@ export class InboxTableConfigResolver {
private translate: TranslateService,
private dialog: MatDialog,
private datePipe: DatePipe,
private utilsService: UtilsService) {
private utilsService: UtilsService,
private sanitizer: DomSanitizer) {
this.config.entityType = EntityType.NOTIFICATION;
this.config.detailsPanelEnabled = false;
@ -95,14 +97,14 @@ export class InboxTableConfigResolver {
new EntityTableColumn<Notification>('type', 'notification.type', '10%', (notification) =>
this.translate.instant(NotificationTemplateTypeTranslateMap.get(notification.type).name)),
new EntityTableColumn<Notification>('subject', 'notification.subject', '30%',
(entity) => this.utilsService.customTranslation(entity.subject, entity.subject)),
(entity) => this.sanitizer.sanitize(SecurityContext.HTML, this.utilsService.customTranslation(entity.subject, entity.subject))),
new EntityTableColumn<Notification>('text', 'notification.message', '60%',
(entity) => this.utilsService.customTranslation(entity.text, entity.text))
(entity) => this.sanitizer.sanitize(SecurityContext.HTML, this.utilsService.customTranslation(entity.text, entity.text)))
);
}
resolve(route: ActivatedRouteSnapshot): EntityTableConfig<Notification> {
resolve(_route: ActivatedRouteSnapshot): EntityTableConfig<Notification> {
return this.config;
}
@ -128,7 +130,7 @@ export class InboxTableConfigResolver {
});
}
private markAsRead($event, entity){
private markAsRead($event: Event, entity){
if ($event) {
$event.stopPropagation();
}

4
ui-ngx/src/app/shared/components/notification/notification.component.html

@ -28,8 +28,8 @@
</tb-icon>
</ng-template>
<div class="content flex-1">
<div class="title" [innerHTML]="(title | safe: 'html' )"></div>
<div class="message" [innerHTML]="(message | safe: 'html' )"></div>
<div class="title" [innerHTML]="title"></div>
<div class="message" [innerHTML]="message"></div>
<button *ngIf="showButton" mat-stroked-button class="button" color="primary"
(click)="navigate($event)">
{{ buttonLabel }}

13
ui-ngx/src/app/shared/components/notification/notification.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output, SecurityContext } from '@angular/core';
import {
ActionButtonLinkType,
Notification,
@ -33,6 +33,7 @@ import tinycolor from 'tinycolor2';
import { StateObject } from '@core/api/widget-api.models';
import { objToBase64URI } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'tb-notification',
@ -71,7 +72,8 @@ export class NotificationComponent implements OnInit {
constructor(
private utils: UtilsService,
private router: Router
private router: Router,
private sanitizer: DomSanitizer
) {
}
@ -79,11 +81,10 @@ export class NotificationComponent implements OnInit {
this.showIcon = this.notification.additionalConfig?.icon?.enabled;
this.showButton = this.notification.additionalConfig?.actionButtonConfig?.enabled;
this.hideMarkAsReadButton = this.notification.status === NotificationStatus.READ;
this.title = this.utils.customTranslation(this.notification.subject, this.notification.subject);
this.message = this.utils.customTranslation(this.notification.text, this.notification.text);
this.title = this.sanitizer.sanitize(SecurityContext.HTML, this.utils.customTranslation(this.notification.subject));
this.message = this.sanitizer.sanitize(SecurityContext.HTML, this.utils.customTranslation(this.notification.text));
if (this.showButton) {
this.buttonLabel = this.utils.customTranslation(this.notification.additionalConfig.actionButtonConfig.text,
this.notification.additionalConfig.actionButtonConfig.text);
this.buttonLabel = this.utils.customTranslation(this.notification.additionalConfig.actionButtonConfig.text);
}
}

1
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -38,6 +38,7 @@ export const FORBIDDEN_NAMES = ['ctx', 'e', 'pi'];
interface BaseCalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity<CalculatedFieldId> {
entityId: EntityId;
additionalInfo?: any;
}
export interface CalculatedFieldSimple extends BaseCalculatedField {

Loading…
Cancel
Save