Browse Source

Merge pull request #15225 from thingsboard/master-rc-merge

Merge rc into master
pull/15232/head
Viacheslav Klimov 7 months ago
committed by GitHub
parent
commit
a77567c852
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  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/** .run/**
.run .run
.claude/ .claude

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

@ -14,3 +14,8 @@
-- limitations under the License. -- 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 @DaoSqlTest
public class AuditLogControllerTest extends AbstractControllerTest { 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 Tenant savedTenant;
private User tenantAdmin; private User tenantAdmin;
@ -113,17 +116,14 @@ public class AuditLogControllerTest extends AbstractControllerTest {
doPost("/api/device", device, Device.class); doPost("/api/device", device, Device.class);
} }
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs?"); Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs?")).hasSize(11 + 1));
Assert.assertEquals(11 + 1, loadedAuditLogs.size());
loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?");
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() + "?"); Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?")).hasSize(11 + 1));
Assert.assertEquals(11 + 1, loadedAuditLogs.size());
} }
@Test @Test
@ -138,7 +138,7 @@ public class AuditLogControllerTest extends AbstractControllerTest {
} }
int expectedSize = loadedAuditLogsBefore.size() + 3; 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", Assert.assertEquals("Have X audit log before this test + New tenant profiles in the test",
expectedSize, getAuditLogs(100, "/api/audit/logs?").size())); expectedSize, getAuditLogs(100, "/api/audit/logs?").size()));
} }
@ -170,9 +170,9 @@ public class AuditLogControllerTest extends AbstractControllerTest {
savedDevice = doPost("/api/device", savedDevice, Device.class); savedDevice = doPost("/api/device", savedDevice, Device.class);
} }
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?"); Device finalSavedDevice = savedDevice;
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
Assert.assertEquals(11 + 1, loadedAuditLogs.size()); assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/DEVICE/" + finalSavedDevice.getId().getId() + "?")).hasSize(11 + 1));
} }
@Test @Test
@ -188,9 +188,10 @@ public class AuditLogControllerTest extends AbstractControllerTest {
tenantProfile.setName(tenantProfile.getName() + "(old)"); tenantProfile.setName(tenantProfile.getName() + "(old)");
tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/entity/" +tenantProfile.getId().getEntityType()+ "/" + tenantProfile.getId().getId() + "?"); TenantProfile finalTenantProfile = tenantProfile;
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
Assert.assertEquals("Audit logs count by Tenant Profile entity", 2, loadedAuditLogs.size()); 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 //cleanup
doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString()); 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.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -108,6 +109,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig()); assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig());
assertThat(savedCalculatedField.getAdditionalInfo()).isEqualTo(calculatedField.getAdditionalInfo());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF"); savedCalculatedField.setName("Test CF");
@ -115,6 +117,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
assertThat(updatedCalculatedField.getAdditionalInfo()).isEqualTo(savedCalculatedField.getAdditionalInfo());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString()) doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString())
@ -322,6 +325,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
calculatedField.setType(cfType); calculatedField.setType(cfType);
calculatedField.setName("Test Calculated Field for " + entityId); calculatedField.setName("Test Calculated Field for " + entityId);
calculatedField.setConfigurationVersion(1); calculatedField.setConfigurationVersion(1);
calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode());
if (customConfiguration != null) { if (customConfiguration != null) {
calculatedField.setConfiguration(customConfiguration); calculatedField.setConfiguration(customConfiguration);
} else switch (cfType) { } 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(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName());
Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId()); Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId());
Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration()); Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration());
Assert.assertEquals(calculatedField.getAdditionalInfo(), calculatedFieldFromMsg.getAdditionalInfo());
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
savedCalculatedField.setName(UPDATED_CF_NAME); savedCalculatedField.setName(UPDATED_CF_NAME);
@ -229,6 +230,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
config.setOutput(output); config.setOutput(output);
calculatedField.setConfiguration(config); calculatedField.setConfiguration(config);
calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode());
return calculatedField; return calculatedField;
} }
@ -260,6 +262,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class); CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class);
Assert.assertNotNull(calculatedField); Assert.assertNotNull(calculatedField);
Assert.assertEquals(resourceTitle, calculatedField.getName()); 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) { if (this.leshanClient != null) {
log.info("Stop/reboot LwM2M client {}", this.leshanClient.getEndpoint(identity)); 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)); log.info("Start after update fw LwM2M client {}", this.leshanClient.getEndpoint(identity));
this.leshanClient.start(); 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 lombok.extern.slf4j.Slf4j;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
@ -37,6 +38,12 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID
@Slf4j @Slf4j
public class RpcLwm2mIntegrationReadCollectedValueTest extends AbstractRpcLwM2MIntegrationTest { 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"} * Read {"id":"/3303/12/5700"}
* Trigger a Send operation from the client with multiple values for the same resource as a payload * 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.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.EntityType; 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.HasDebugSettings;
import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId; 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.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Map; import java.util.Map;
@ -48,10 +47,7 @@ import java.util.Set;
@Schema @Schema
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class CalculatedField extends BaseData<CalculatedFieldId> implements HasName, HasTenantId, HasVersion, HasDebugSettings { public class CalculatedField extends BaseData<CalculatedFieldId> implements HasName, HasTenantId, HasVersion, HasDebugSettings, HasAdditionalInfo {
@Serial
private static final long serialVersionUID = 4491966747773381420L;
public static final Map<EntityType, Set<CalculatedFieldType>> SUPPORTED_ENTITIES = Map.of( public static final Map<EntityType, Set<CalculatedFieldType>> SUPPORTED_ENTITIES = Map.of(
EntityType.DEVICE, CalculatedFieldType.all, EntityType.DEVICE, CalculatedFieldType.all,
@ -90,28 +86,17 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
@Valid @Valid
@NotNull @NotNull
private CalculatedFieldConfiguration configuration; private CalculatedFieldConfiguration configuration;
@Getter
@Setter
private Long version; private Long version;
@NoXss
@Schema(description = "Additional parameters of the calculated field")
private JsonNode additionalInfo;
public CalculatedField() { public CalculatedField() {}
super();
}
public CalculatedField(CalculatedFieldId id) { public CalculatedField(CalculatedFieldId id) {
super(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) { public CalculatedField(CalculatedField calculatedField) {
super(calculatedField); super(calculatedField);
this.tenantId = calculatedField.tenantId; this.tenantId = calculatedField.tenantId;
@ -123,6 +108,7 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
this.configurationVersion = calculatedField.configurationVersion; this.configurationVersion = calculatedField.configurationVersion;
this.configuration = calculatedField.configuration; this.configuration = calculatedField.configuration;
this.version = calculatedField.version; 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.") @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(", name='").append(name)
.append(", configurationVersion=").append(configurationVersion) .append(", configurationVersion=").append(configurationVersion)
.append(", configuration=").append(configuration) .append(", configuration=").append(configuration)
.append(", additionalInfo=").append(additionalInfo)
.append(", version=").append(version) .append(", version=").append(version)
.append(", createdTime=").append(createdTime) .append(", createdTime=").append(createdTime)
.append(", id=").append(id).append(']') .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}") @Value("${transport.mqtt.proxy_enabled:false}")
private boolean proxyEnabled; private boolean proxyEnabled;
private final AtomicInteger connectionsCounter = new AtomicInteger(); private final AtomicInteger connectionsActiveCounterMQTT = new AtomicInteger();
private final AtomicInteger connectionsActiveCounterMQTTS = new AtomicInteger();
@PostConstruct @PostConstruct
public void init() { public void init() {
super.init(); super.init();
transportService.createGaugeStats("openConnections", connectionsCounter); transportService.createGaugeStats("connections_active", connectionsActiveCounterMQTT, "protocol", "MQTT");
transportService.createGaugeStats("connections_active", connectionsActiveCounterMQTTS, "protocol", "MQTTS");
} }
public void channelRegistered() { public void channelRegistered(boolean isSSL) {
connectionsCounter.incrementAndGet(); if (isSSL) {
connectionsActiveCounterMQTTS.incrementAndGet();
} else {
connectionsActiveCounterMQTT.incrementAndGet();
}
} }
public void channelUnregistered() { public void channelUnregistered(boolean isSSL) {
connectionsCounter.decrementAndGet(); if (isSSL) {
connectionsActiveCounterMQTTS.decrementAndGet();
} else {
connectionsActiveCounterMQTT.decrementAndGet();
}
} }
public boolean checkAddress(InetSocketAddress address) { 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<>(); this.rpcAwaitingAck = new ConcurrentHashMap<>();
} }
boolean isSSL() {
return sslHandler != null;
}
@Override @Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception { public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
super.channelRegistered(ctx); super.channelRegistered(ctx);
context.channelRegistered(); context.channelRegistered(isSSL());
} }
@Override @Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
super.channelUnregistered(ctx); super.channelUnregistered(ctx);
context.channelUnregistered(); context.channelUnregistered(isSSL());
} }
@Override @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) { InetSocketAddress getAddress(ChannelHandlerContext ctx) {
var address = ctx.channel().attr(MqttTransportService.ADDRESS).get(); var address = ctx.channel().attr(MqttTransportService.ADDRESS).get();
if (address == null) { if (address == null) {
@ -1150,19 +1165,25 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (cause instanceof IOException) { if (cause instanceof IOException) {
if (log.isDebugEnabled()) { 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::getDeviceId).orElse(null),
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""), Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""),
clientAddr,
cause.getClass().getSimpleName(),
cause.getMessage(), cause.getMessage(),
cause); cause);
} else if (log.isInfoEnabled()) { } 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::getDeviceId).orElse(null),
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""), Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""),
clientAddr,
cause.getClass().getSimpleName(),
cause.getMessage()); cause.getMessage());
} }
} else { } else {
log.error("[{}] Unexpected Exception", sessionId, cause); log.error("[{}][{}] Unexpected Exception", sessionId, getClientAddr(ctx), cause);
} }
closeCtx(ctx, MqttReasonCodes.Disconnect.SERVER_SHUTTING_DOWN); 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); 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 @Override
public void createGaugeStats(String statsName, AtomicInteger number) { public void createGaugeStats(String statsName, AtomicInteger number, String... tags) {
statsFactory.createGauge(StatsType.TRANSPORT + "." + statsName, number); String key = "thingsboard" + "." + StatsType.TRANSPORT.getName() + "." + statsName;
statsMap.put(statsName, number); 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}") @Scheduled(fixedDelayString = "${transport.stats.print-interval-ms:60000}")

1
dao/pom.xml

@ -280,7 +280,6 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>${jar-plugin.version}</version>
<executions> <executions>
<execution> <execution>
<goals> <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_VERSION = "configuration_version";
public static final String CALCULATED_FIELD_CONFIGURATION = "configuration"; public static final String CALCULATED_FIELD_CONFIGURATION = "configuration";
public static final String CALCULATED_FIELD_VERSION = "version"; public static final String CALCULATED_FIELD_VERSION = "version";
public static final String CALCULATED_FIELD_ADDITIONAL_INFO = ADDITIONAL_INFO_PROPERTY;
/** /**
* Tasks constants. * 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 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;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION_VERSION; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION_VERSION;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_ID; 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) @Column(name = DEBUG_SETTINGS)
private String debugSettings; private String debugSettings;
public CalculatedFieldEntity() { @Convert(converter = JsonConverter.class)
super(); @Column(name = CALCULATED_FIELD_ADDITIONAL_INFO)
} private JsonNode additionalInfo;
public CalculatedFieldEntity() {}
public CalculatedFieldEntity(CalculatedField calculatedField) { public CalculatedFieldEntity(CalculatedField calculatedField) {
this.setUuid(calculatedField.getUuidId()); this.setUuid(calculatedField.getUuidId());
@ -97,6 +100,7 @@ public class CalculatedFieldEntity extends BaseVersionedEntity<CalculatedField>
this.configuration = JacksonUtil.valueToTree(calculatedField.getConfiguration()); this.configuration = JacksonUtil.valueToTree(calculatedField.getConfiguration());
this.version = calculatedField.getVersion(); this.version = calculatedField.getVersion();
this.debugSettings = JacksonUtil.toString(calculatedField.getDebugSettings()); this.debugSettings = JacksonUtil.toString(calculatedField.getDebugSettings());
this.additionalInfo = calculatedField.getAdditionalInfo();
} }
@Override @Override
@ -111,6 +115,7 @@ public class CalculatedFieldEntity extends BaseVersionedEntity<CalculatedField>
calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class)); calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class));
calculatedField.setVersion(version); calculatedField.setVersion(version);
calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class)); calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class));
calculatedField.setAdditionalInfo(additionalInfo);
return calculatedField; 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")); JsonNode configuration = JacksonUtil.toJsonNode((String) row.get("configuration"));
long version = row.get("version") != null ? (long) row.get("version") : 0; long version = row.get("version") != null ? (long) row.get("version") : 0;
String debugSettings = (String) row.get("debug_settings"); String debugSettings = (String) row.get("debug_settings");
JsonNode additionalInfo = JacksonUtil.toJsonNode((String) row.get("additional_info"));
CalculatedField calculatedField = new CalculatedField(); CalculatedField calculatedField = new CalculatedField();
calculatedField.setId(new CalculatedFieldId(id)); calculatedField.setId(new CalculatedFieldId(id));
@ -91,6 +92,7 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
} }
calculatedField.setVersion(version); calculatedField.setVersion(version);
calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class)); calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class));
calculatedField.setAdditionalInfo(additionalInfo);
return calculatedField; return calculatedField;
}).collect(Collectors.toList()); }).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), configuration varchar(1000000),
version BIGINT DEFAULT 1, version BIGINT DEFAULT 1,
debug_settings varchar(1024), debug_settings varchar(1024),
additional_info varchar,
CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name) 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.apache.commons.lang3.RandomUtils;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; 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.AttributeScope;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
@ -83,6 +84,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(calculatedField.getConfiguration()); assertThat(savedCalculatedField.getConfiguration()).isEqualTo(calculatedField.getConfiguration());
assertThat(savedCalculatedField.getAdditionalInfo()).isEqualTo(calculatedField.getAdditionalInfo());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF"); savedCalculatedField.setName("Test CF");
@ -90,6 +92,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
CalculatedField updatedCalculatedField = calculatedFieldService.save(savedCalculatedField); CalculatedField updatedCalculatedField = calculatedFieldService.save(savedCalculatedField);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
assertThat(updatedCalculatedField.getAdditionalInfo()).isEqualTo(savedCalculatedField.getAdditionalInfo());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
} }
@ -495,6 +498,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
calculatedField.setName("Test Calculated Field"); calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1); calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId)); calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId));
calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode());
return calculatedField; 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> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>3.1.1</version>
<configuration> <configuration>
<archive> <archive>
<manifest> <manifest>

4
pom.xml

@ -72,7 +72,7 @@
<paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version> <paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version>
<os-maven-plugin.version>1.7.1</os-maven-plugin.version> <os-maven-plugin.version>1.7.1</os-maven-plugin.version>
<surefire.version>3.5.4</surefire.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> <springdoc-swagger.version>2.8.8TB</springdoc-swagger.version>
<swagger-annotations.version>2.2.30</swagger-annotations.version> <swagger-annotations.version>2.2.30</swagger-annotations.version>
<spatial4j.version>0.8</spatial4j.version> <spatial4j.version>0.8</spatial4j.version>
@ -636,7 +636,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>3.1.1</version> <version>${jar-plugin.version}</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <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)"> (click)="deleteTelemetry($event)">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
<button mat-raised-button color="accent" <button [class.!hidden]="isSysAdmin"
mat-raised-button color="accent"
class="button-widget-action" class="button-widget-action"
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
matTooltip="{{ 'attribute.show-on-widget' | translate }}" 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 { AttributeDatasource } from '@home/models/datasource/attribute-datasource';
import { AttributeService } from '@app/core/http/attribute.service'; import { AttributeService } from '@app/core/http/attribute.service';
import { EntityType } from '@shared/models/entity-type.models'; 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 { coerceBooleanProperty } from '@angular/cdk/coercion';
import { import {
AddAttributeDialogComponent, AddAttributeDialogComponent,
@ -91,7 +93,6 @@ import { FormBuilder } from '@angular/forms';
import { AggregationType, defaultTimewindow } from '@shared/models/time/time.models'; import { AggregationType, defaultTimewindow } from '@shared/models/time/time.models';
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
@Component({ @Component({
selector: 'tb-attribute-table', selector: 'tb-attribute-table',
templateUrl: './attribute-table.component.html', templateUrl: './attribute-table.component.html',
@ -186,6 +187,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
textSearch = this.fb.control('', {nonNullable: true}); textSearch = this.fb.control('', {nonNullable: true});
isSysAdmin = false;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
selectAllModel: boolean = false; selectAllModel: boolean = false;
@ -207,6 +210,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
private fb: FormBuilder, private fb: FormBuilder,
private timeService: TimeService) { private timeService: TimeService) {
super(store); super(store);
this.isSysAdmin = getCurrentAuthUser(this.store).authority === Authority.SYS_ADMIN;
this.dirtyValue = !this.activeValue; this.dirtyValue = !this.activeValue;
const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC }; const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC };
this.pageLink = new PageLink(10, 0, null, sortOrder); 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, InboxNotificationDialogComponent,
InboxNotificationDialogData InboxNotificationDialogData
} from '@home/pages/notification/inbox/inbox-notification-dialog.component'; } 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 { ActivatedRouteSnapshot } from '@angular/router';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { DomSanitizer } from '@angular/platform-browser';
@Injectable() @Injectable()
export class InboxTableConfigResolver { export class InboxTableConfigResolver {
@ -50,7 +51,8 @@ export class InboxTableConfigResolver {
private translate: TranslateService, private translate: TranslateService,
private dialog: MatDialog, private dialog: MatDialog,
private datePipe: DatePipe, private datePipe: DatePipe,
private utilsService: UtilsService) { private utilsService: UtilsService,
private sanitizer: DomSanitizer) {
this.config.entityType = EntityType.NOTIFICATION; this.config.entityType = EntityType.NOTIFICATION;
this.config.detailsPanelEnabled = false; this.config.detailsPanelEnabled = false;
@ -95,14 +97,14 @@ export class InboxTableConfigResolver {
new EntityTableColumn<Notification>('type', 'notification.type', '10%', (notification) => new EntityTableColumn<Notification>('type', 'notification.type', '10%', (notification) =>
this.translate.instant(NotificationTemplateTypeTranslateMap.get(notification.type).name)), this.translate.instant(NotificationTemplateTypeTranslateMap.get(notification.type).name)),
new EntityTableColumn<Notification>('subject', 'notification.subject', '30%', 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%', 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; return this.config;
} }
@ -128,7 +130,7 @@ export class InboxTableConfigResolver {
}); });
} }
private markAsRead($event, entity){ private markAsRead($event: Event, entity){
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }

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

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

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

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output, SecurityContext } from '@angular/core';
import { import {
ActionButtonLinkType, ActionButtonLinkType,
Notification, Notification,
@ -33,6 +33,7 @@ import tinycolor from 'tinycolor2';
import { StateObject } from '@core/api/widget-api.models'; import { StateObject } from '@core/api/widget-api.models';
import { objToBase64URI } from '@core/utils'; import { objToBase64URI } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { DomSanitizer } from '@angular/platform-browser';
@Component({ @Component({
selector: 'tb-notification', selector: 'tb-notification',
@ -71,7 +72,8 @@ export class NotificationComponent implements OnInit {
constructor( constructor(
private utils: UtilsService, 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.showIcon = this.notification.additionalConfig?.icon?.enabled;
this.showButton = this.notification.additionalConfig?.actionButtonConfig?.enabled; this.showButton = this.notification.additionalConfig?.actionButtonConfig?.enabled;
this.hideMarkAsReadButton = this.notification.status === NotificationStatus.READ; this.hideMarkAsReadButton = this.notification.status === NotificationStatus.READ;
this.title = this.utils.customTranslation(this.notification.subject, this.notification.subject); this.title = this.sanitizer.sanitize(SecurityContext.HTML, this.utils.customTranslation(this.notification.subject));
this.message = this.utils.customTranslation(this.notification.text, this.notification.text); this.message = this.sanitizer.sanitize(SecurityContext.HTML, this.utils.customTranslation(this.notification.text));
if (this.showButton) { if (this.showButton) {
this.buttonLabel = this.utils.customTranslation(this.notification.additionalConfig.actionButtonConfig.text, this.buttonLabel = this.utils.customTranslation(this.notification.additionalConfig.actionButtonConfig.text);
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> { interface BaseCalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity<CalculatedFieldId> {
entityId: EntityId; entityId: EntityId;
additionalInfo?: any;
} }
export interface CalculatedFieldSimple extends BaseCalculatedField { export interface CalculatedFieldSimple extends BaseCalculatedField {

Loading…
Cancel
Save