Merge branch 'rc' into feature/add_show_total_legend_setting_to_latest-chart-widgets
This commit is contained in:
commit
360d47b56f
@ -300,6 +300,7 @@ public class ImageController extends BaseController {
|
|||||||
tbImageService.putETag(cacheKey, descriptor.getEtag());
|
tbImageService.putETag(cacheKey, descriptor.getEtag());
|
||||||
var result = ResponseEntity.ok()
|
var result = ResponseEntity.ok()
|
||||||
.header("Content-Type", descriptor.getMediaType())
|
.header("Content-Type", descriptor.getMediaType())
|
||||||
|
.header("Content-Security-Policy", "default-src 'none'")
|
||||||
.eTag(descriptor.getEtag());
|
.eTag(descriptor.getEtag());
|
||||||
if (!cacheKey.isPublic()) {
|
if (!cacheKey.isPublic()) {
|
||||||
result
|
result
|
||||||
|
|||||||
@ -442,13 +442,13 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
|
|||||||
boolean check(long threshold, long warnThreshold, long value);
|
boolean check(long threshold, long warnThreshold, long value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkStartOfNextCycle() {
|
public void checkStartOfNextCycle() {
|
||||||
updateLock.lock();
|
updateLock.lock();
|
||||||
try {
|
try {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
myUsageStates.values().forEach(state -> {
|
myUsageStates.values().forEach(state -> {
|
||||||
if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) {
|
if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) {
|
||||||
state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth());
|
state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextMonth());
|
||||||
if (log.isTraceEnabled()) {
|
if (log.isTraceEnabled()) {
|
||||||
log.trace("[{}][{}] Updating state cycles (currentCycleTs={},nextCycleTs={})", state.getTenantId(), state.getEntityId(), state.getCurrentCycleTs(), state.getNextCycleTs());
|
log.trace("[{}][{}] Updating state cycles (currentCycleTs={},nextCycleTs={})", state.getTenantId(), state.getEntityId(), state.getCurrentCycleTs(), state.getNextCycleTs());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,14 +93,13 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS
|
|||||||
@TbCoreComponent
|
@TbCoreComponent
|
||||||
public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService {
|
public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService {
|
||||||
|
|
||||||
private static final int DESTROY_SESSION_MAX_ATTEMPTS = 10;
|
|
||||||
|
|
||||||
private final ConcurrentMap<EdgeId, EdgeGrpcSession> sessions = new ConcurrentHashMap<>();
|
private final ConcurrentMap<EdgeId, EdgeGrpcSession> sessions = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentMap<EdgeId, Lock> sessionNewEventsLocks = new ConcurrentHashMap<>();
|
private final ConcurrentMap<EdgeId, Lock> sessionNewEventsLocks = new ConcurrentHashMap<>();
|
||||||
private final Map<EdgeId, Boolean> sessionNewEvents = new HashMap<>();
|
private final Map<EdgeId, Boolean> sessionNewEvents = new HashMap<>();
|
||||||
private final ConcurrentMap<EdgeId, ScheduledFuture<?>> sessionEdgeEventChecks = new ConcurrentHashMap<>();
|
private final ConcurrentMap<EdgeId, ScheduledFuture<?>> sessionEdgeEventChecks = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentMap<UUID, Consumer<FromEdgeSyncResponse>> localSyncEdgeRequests = new ConcurrentHashMap<>();
|
private final ConcurrentMap<UUID, Consumer<FromEdgeSyncResponse>> localSyncEdgeRequests = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentMap<EdgeId, Boolean> edgeEventsMigrationProcessed = new ConcurrentHashMap<>();
|
private final ConcurrentMap<EdgeId, Boolean> edgeEventsMigrationProcessed = new ConcurrentHashMap<>();
|
||||||
|
private final List<EdgeGrpcSession> zombieSessions = new ArrayList<>();
|
||||||
|
|
||||||
@Value("${edges.rpc.port}")
|
@Value("${edges.rpc.port}")
|
||||||
private int rpcPort;
|
private int rpcPort;
|
||||||
@ -193,7 +192,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
|
|||||||
this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler");
|
this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler");
|
||||||
this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler");
|
this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler");
|
||||||
this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service");
|
this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service");
|
||||||
this.executorService.scheduleAtFixedRate(this::destroyKafkaSessionIfDisconnectedAndConsumerActive, 60, 60, TimeUnit.SECONDS);
|
this.executorService.scheduleAtFixedRate(this::cleanupZombieSessions, 60, 60, TimeUnit.SECONDS);
|
||||||
log.info("Edge RPC service initialized!");
|
log.info("Edge RPC service initialized!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -518,14 +517,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
|
|||||||
|
|
||||||
private void destroySession(EdgeGrpcSession session) {
|
private void destroySession(EdgeGrpcSession session) {
|
||||||
try (session) {
|
try (session) {
|
||||||
for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) {
|
if (!session.destroy()) {
|
||||||
if (session.destroy()) {
|
log.warn("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.",
|
||||||
break;
|
session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId());
|
||||||
} else {
|
zombieSessions.add(session);
|
||||||
try {
|
|
||||||
Thread.sleep(100);
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -634,7 +629,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void destroyKafkaSessionIfDisconnectedAndConsumerActive() {
|
private void cleanupZombieSessions() {
|
||||||
try {
|
try {
|
||||||
List<EdgeId> toRemove = new ArrayList<>();
|
List<EdgeId> toRemove = new ArrayList<>();
|
||||||
for (EdgeGrpcSession session : sessions.values()) {
|
for (EdgeGrpcSession session : sessions.values()) {
|
||||||
@ -655,6 +650,17 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
zombieSessions.removeIf(zombie -> {
|
||||||
|
if (zombie.destroy()) {
|
||||||
|
log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].",
|
||||||
|
zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName());
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.warn("[{}][{}] Failed to remove zombie session [{}] for edge [{}].",
|
||||||
|
zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Failed to cleanup kafka sessions", e);
|
log.warn("Failed to cleanup kafka sessions", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -238,7 +238,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
|
|||||||
entitiesSortOrder = sortOrder;
|
entitiesSortOrder = sortOrder;
|
||||||
}
|
}
|
||||||
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder);
|
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder);
|
||||||
return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters());
|
return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -519,6 +519,67 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
|
|||||||
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
|
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindAlarmsWithEntityFilterAndLatestValues() throws Exception {
|
||||||
|
loginTenantAdmin();
|
||||||
|
List<Device> devices = new ArrayList<>();
|
||||||
|
List<String> temps = new ArrayList<>();
|
||||||
|
List<String> deviceNames = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
Device device = new Device();
|
||||||
|
device.setCustomerId(customerId);
|
||||||
|
device.setName("Device" + i);
|
||||||
|
device.setType("default");
|
||||||
|
device.setLabel("testLabel" + (int) (Math.random() * 1000));
|
||||||
|
device = doPost("/api/device", device, Device.class);
|
||||||
|
devices.add(device);
|
||||||
|
deviceNames.add(device.getName());
|
||||||
|
|
||||||
|
int temp = i * 10;
|
||||||
|
temps.add(String.valueOf(temp));
|
||||||
|
JsonNode content = JacksonUtil.toJsonNode("{\"temperature\": " + temp + "}");
|
||||||
|
doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + device.getUuidId() + "/timeseries/SERVER_SCOPE", content)
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
Thread.sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < devices.size(); i++) {
|
||||||
|
Alarm alarm = new Alarm();
|
||||||
|
alarm.setCustomerId(customerId);
|
||||||
|
alarm.setOriginator(devices.get(i).getId());
|
||||||
|
String type = "device alarm" + i;
|
||||||
|
alarm.setType(type);
|
||||||
|
alarm.setSeverity(AlarmSeverity.WARNING);
|
||||||
|
doPost("/api/alarm", alarm, Alarm.class);
|
||||||
|
Thread.sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlarmDataPageLink pageLink = new AlarmDataPageLink();
|
||||||
|
pageLink.setPage(0);
|
||||||
|
pageLink.setPageSize(100);
|
||||||
|
pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "created_time")));
|
||||||
|
|
||||||
|
List<EntityKey> alarmFields = new ArrayList<>();
|
||||||
|
alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type"));
|
||||||
|
|
||||||
|
List<EntityKey> entityFields = new ArrayList<>();
|
||||||
|
entityFields.add(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
|
||||||
|
|
||||||
|
List<EntityKey> latestValues = new ArrayList<>();
|
||||||
|
latestValues.add(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"));
|
||||||
|
|
||||||
|
EntityTypeFilter deviceTypeFilter = new EntityTypeFilter();
|
||||||
|
deviceTypeFilter.setEntityType(EntityType.DEVICE);
|
||||||
|
AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields);
|
||||||
|
|
||||||
|
PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10);
|
||||||
|
List<String> retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList();
|
||||||
|
assertThat(retrievedAlarmTemps).containsExactlyInAnyOrderElementsOf(temps);
|
||||||
|
|
||||||
|
List<String> retrievedDeviceNames = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).toList();
|
||||||
|
assertThat(retrievedDeviceNames).containsExactlyInAnyOrderElementsOf(deviceNames);
|
||||||
|
}
|
||||||
|
|
||||||
private void testCountAlarmsByQuery(List<Alarm> alarms) throws Exception {
|
private void testCountAlarmsByQuery(List<Alarm> alarms) throws Exception {
|
||||||
AlarmCountQuery countQuery = new AlarmCountQuery();
|
AlarmCountQuery countQuery = new AlarmCountQuery();
|
||||||
|
|
||||||
|
|||||||
@ -20,9 +20,12 @@ import org.junit.Before;
|
|||||||
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.server.common.data.ApiUsageRecordKey;
|
import org.thingsboard.server.common.data.ApiUsageRecordKey;
|
||||||
|
import org.thingsboard.server.common.data.ApiUsageState;
|
||||||
import org.thingsboard.server.common.data.ApiUsageStateValue;
|
import org.thingsboard.server.common.data.ApiUsageStateValue;
|
||||||
import org.thingsboard.server.common.data.Tenant;
|
import org.thingsboard.server.common.data.Tenant;
|
||||||
import org.thingsboard.server.common.data.TenantProfile;
|
import org.thingsboard.server.common.data.TenantProfile;
|
||||||
|
import org.thingsboard.server.common.data.id.ApiUsageStateId;
|
||||||
|
import org.thingsboard.server.common.data.id.EntityId;
|
||||||
import org.thingsboard.server.common.data.id.TenantId;
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
|
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
|
||||||
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
|
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
|
||||||
@ -33,8 +36,17 @@ import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
|
|||||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||||
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
|
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static java.time.ZoneOffset.UTC;
|
||||||
|
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
|
||||||
|
import static java.time.temporal.ChronoUnit.MONTHS;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
@DaoSqlTest
|
@DaoSqlTest
|
||||||
@ -48,6 +60,7 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
|
|||||||
|
|
||||||
private TenantId tenantId;
|
private TenantId tenantId;
|
||||||
private Tenant savedTenant;
|
private Tenant savedTenant;
|
||||||
|
private TenantProfile savedTenantProfile;
|
||||||
|
|
||||||
private static final int MAX_ENABLE_VALUE = 5000;
|
private static final int MAX_ENABLE_VALUE = 5000;
|
||||||
private static final long VALUE_WARNING = 4500L;
|
private static final long VALUE_WARNING = 4500L;
|
||||||
@ -59,7 +72,7 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
|
|||||||
loginSysAdmin();
|
loginSysAdmin();
|
||||||
|
|
||||||
TenantProfile tenantProfile = createTenantProfile();
|
TenantProfile tenantProfile = createTenantProfile();
|
||||||
TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
|
savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
|
||||||
Assert.assertNotNull(savedTenantProfile);
|
Assert.assertNotNull(savedTenantProfile);
|
||||||
|
|
||||||
Tenant tenant = new Tenant();
|
Tenant tenant = new Tenant();
|
||||||
@ -109,6 +122,41 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
|
|||||||
assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState());
|
assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checkStartOfNextCycle_setsNextCycleToNextMonth() throws Exception {
|
||||||
|
ApiUsageState apiUsageState = new ApiUsageState(new ApiUsageStateId(UUID.randomUUID()));
|
||||||
|
apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED);
|
||||||
|
apiUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED);
|
||||||
|
apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED);
|
||||||
|
apiUsageState.setTbelExecState(ApiUsageStateValue.ENABLED);
|
||||||
|
apiUsageState.setReExecState(ApiUsageStateValue.ENABLED);
|
||||||
|
apiUsageState.setTransportState(ApiUsageStateValue.ENABLED);
|
||||||
|
apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED);
|
||||||
|
apiUsageState.setJsExecState(ApiUsageStateValue.ENABLED);
|
||||||
|
apiUsageState.setTenantId(tenantId);
|
||||||
|
apiUsageState.setEntityId(tenantId);
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long currentCycleTs = now - TimeUnit.DAYS.toMillis(30);
|
||||||
|
long nextCycleTs = now - TimeUnit.MINUTES.toMillis(5); // < 1h ago
|
||||||
|
TenantApiUsageState tenantApiUsageState = new TenantApiUsageState(savedTenantProfile, apiUsageState);
|
||||||
|
tenantApiUsageState.setCycles(currentCycleTs, nextCycleTs);
|
||||||
|
Map<EntityId, BaseApiUsageState> map = new HashMap<>();
|
||||||
|
map.put(tenantId, tenantApiUsageState);
|
||||||
|
|
||||||
|
Field fieldToSet = DefaultTbApiUsageStateService.class.getDeclaredField("myUsageStates");
|
||||||
|
fieldToSet.setAccessible(true);
|
||||||
|
fieldToSet.set(service, map);
|
||||||
|
|
||||||
|
service.checkStartOfNextCycle();
|
||||||
|
|
||||||
|
long firstOfNextMonth = LocalDate.now()
|
||||||
|
.with((temporal) -> temporal.with(DAY_OF_MONTH, 1)
|
||||||
|
.plus(1, MONTHS))
|
||||||
|
.atStartOfDay(UTC).toInstant().toEpochMilli();
|
||||||
|
assertThat(tenantApiUsageState.getNextCycleTs()).isEqualTo(firstOfNextMonth);
|
||||||
|
}
|
||||||
|
|
||||||
private TenantProfile createTenantProfile() {
|
private TenantProfile createTenantProfile() {
|
||||||
TenantProfile tenantProfile = new TenantProfile();
|
TenantProfile tenantProfile = new TenantProfile();
|
||||||
tenantProfile.setName("Tenant Profile");
|
tenantProfile.setName("Tenant Profile");
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.MobileAppId;
|
|||||||
import org.thingsboard.server.common.data.id.TenantId;
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig;
|
import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig;
|
||||||
import org.thingsboard.server.common.data.validation.Length;
|
import org.thingsboard.server.common.data.validation.Length;
|
||||||
|
import org.thingsboard.server.common.data.validation.NoXss;
|
||||||
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@Data
|
@Data
|
||||||
@ -40,9 +41,11 @@ public class MobileAppBundle extends BaseData<MobileAppBundleId> implements HasT
|
|||||||
private TenantId tenantId;
|
private TenantId tenantId;
|
||||||
@Schema(description = "Application bundle title. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(description = "Application bundle title. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
@NotBlank
|
@NotBlank
|
||||||
|
@NoXss
|
||||||
@Length(fieldName = "title")
|
@Length(fieldName = "title")
|
||||||
private String title;
|
private String title;
|
||||||
@Schema(description = "Application bundle description.")
|
@Schema(description = "Application bundle description.")
|
||||||
|
@NoXss
|
||||||
@Length(fieldName = "description")
|
@Length(fieldName = "description")
|
||||||
private String description;
|
private String description;
|
||||||
@Schema(description = "Android application id")
|
@Schema(description = "Android application id")
|
||||||
|
|||||||
@ -62,6 +62,7 @@ public class NotificationRule extends BaseData<NotificationRuleId> implements Ha
|
|||||||
@Valid
|
@Valid
|
||||||
private NotificationRuleRecipientsConfig recipientsConfig;
|
private NotificationRuleRecipientsConfig recipientsConfig;
|
||||||
|
|
||||||
|
@Valid
|
||||||
private NotificationRuleConfig additionalConfig;
|
private NotificationRuleConfig additionalConfig;
|
||||||
|
|
||||||
private NotificationRuleId externalId;
|
private NotificationRuleId externalId;
|
||||||
|
|||||||
@ -16,12 +16,14 @@
|
|||||||
package org.thingsboard.server.common.data.notification.rule;
|
package org.thingsboard.server.common.data.notification.rule;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.thingsboard.server.common.data.validation.NoXss;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class NotificationRuleConfig implements Serializable {
|
public class NotificationRuleConfig implements Serializable {
|
||||||
|
|
||||||
|
@NoXss
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,16 +60,4 @@ public class SchedulerUtils {
|
|||||||
return LocalDate.now(UTC).with(TemporalAdjusters.firstDayOfNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli();
|
return LocalDate.now(UTC).with(TemporalAdjusters.firstDayOfNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long getStartOfNextNextMonth() {
|
|
||||||
return getStartOfNextNextMonth(UTC);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long getStartOfNextNextMonth(ZoneId zoneId) {
|
|
||||||
return LocalDate.now(UTC).with(firstDayOfNextNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TemporalAdjuster firstDayOfNextNextMonth() {
|
|
||||||
return (temporal) -> temporal.with(DAY_OF_MONTH, 1).plus(2, MONTHS);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import { DialogService } from '@core/services/dialog.service';
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { parseHttpErrorMessage } from '@core/utils';
|
import { parseHttpErrorMessage } from '@core/utils';
|
||||||
import { getInterceptorConfig } from './interceptor.util';
|
import { getInterceptorConfig } from './interceptor.util';
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
|
||||||
const tmpHeaders = {};
|
const tmpHeaders = {};
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
|
|||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
private sanitizer: DomSanitizer
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
@ -129,7 +131,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (unhandled && !ignoreErrors) {
|
if (unhandled && !ignoreErrors) {
|
||||||
const errorMessageWithTimeout = parseHttpErrorMessage(errorResponse, this.translate, req.responseType);
|
const errorMessageWithTimeout = parseHttpErrorMessage(errorResponse, this.translate, req.responseType, this.sanitizer);
|
||||||
this.showError(errorMessageWithTimeout.message, errorMessageWithTimeout.timeout);
|
this.showError(errorMessageWithTimeout.message, errorMessageWithTimeout.timeout);
|
||||||
}
|
}
|
||||||
return throwError(() => errorResponse);
|
return throwError(() => errorResponse);
|
||||||
|
|||||||
@ -31,6 +31,8 @@ import {
|
|||||||
isNotEmptyTbFunction,
|
isNotEmptyTbFunction,
|
||||||
TbFunction
|
TbFunction
|
||||||
} from '@shared/models/js-function.models';
|
} from '@shared/models/js-function.models';
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
import { SecurityContext } from '@angular/core';
|
||||||
|
|
||||||
const varsRegex = /\${([^}]*)}/g;
|
const varsRegex = /\${([^}]*)}/g;
|
||||||
|
|
||||||
@ -809,7 +811,7 @@ export function getEntityDetailsPageURL(id: string, entityType: EntityType): str
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseHttpErrorMessage(errorResponse: HttpErrorResponse,
|
export function parseHttpErrorMessage(errorResponse: HttpErrorResponse,
|
||||||
translate: TranslateService, responseType?: string): {message: string; timeout: number} {
|
translate: TranslateService, responseType?: string, sanitizer?:DomSanitizer): {message: string; timeout: number} {
|
||||||
let error = null;
|
let error = null;
|
||||||
let errorMessage: string;
|
let errorMessage: string;
|
||||||
let timeout = 0;
|
let timeout = 0;
|
||||||
@ -837,6 +839,9 @@ export function parseHttpErrorMessage(errorResponse: HttpErrorResponse,
|
|||||||
errorText += errorKey ? translate.instant(errorKey) : errorResponse.statusText;
|
errorText += errorKey ? translate.instant(errorKey) : errorResponse.statusText;
|
||||||
errorMessage = errorText;
|
errorMessage = errorText;
|
||||||
}
|
}
|
||||||
|
if(sanitizer) {
|
||||||
|
errorMessage = sanitizer.sanitize(SecurityContext.HTML,errorMessage);
|
||||||
|
}
|
||||||
return {message: errorMessage, timeout};
|
return {message: errorMessage, timeout};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import {
|
|||||||
NgZone,
|
NgZone,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
|
SecurityContext,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
@ -53,6 +54,7 @@ import {
|
|||||||
import { deepClone } from '@core/utils';
|
import { deepClone } from '@core/utils';
|
||||||
import { hidePageSizePixelValue } from '@shared/models/constants';
|
import { hidePageSizePixelValue } from '@shared/models/constants';
|
||||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-manage-widget-actions',
|
selector: 'tb-manage-widget-actions',
|
||||||
@ -106,7 +108,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
|
|||||||
private dialogs: DialogService,
|
private dialogs: DialogService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private elementRef: ElementRef,
|
private elementRef: ElementRef,
|
||||||
private zone: NgZone) {
|
private zone: NgZone,
|
||||||
|
private sanitizer: DomSanitizer) {
|
||||||
super();
|
super();
|
||||||
const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC };
|
const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC };
|
||||||
this.pageLink = new PageLink(10, 0, null, sortOrder);
|
this.pageLink = new PageLink(10, 0, null, sortOrder);
|
||||||
@ -289,7 +292,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
|
|||||||
}
|
}
|
||||||
const title = this.translate.instant('widget-config.delete-action-title');
|
const title = this.translate.instant('widget-config.delete-action-title');
|
||||||
const content = this.translate.instant('widget-config.delete-action-text', {actionName: action.name});
|
const content = this.translate.instant('widget-config.delete-action-text', {actionName: action.name});
|
||||||
this.dialogs.confirm(title, content,
|
const safeContent = this.sanitizer.sanitize(SecurityContext.HTML, content);
|
||||||
|
this.dialogs.confirm(title, safeContent,
|
||||||
this.translate.instant('action.no'),
|
this.translate.instant('action.no'),
|
||||||
this.translate.instant('action.yes'), true).subscribe(
|
this.translate.instant('action.yes'), true).subscribe(
|
||||||
(res) => {
|
(res) => {
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
QueryList,
|
QueryList,
|
||||||
Renderer2,
|
Renderer2,
|
||||||
|
SecurityContext,
|
||||||
SkipSelf,
|
SkipSelf,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
ViewChildren,
|
ViewChildren,
|
||||||
@ -97,6 +98,7 @@ import { HttpStatusCode } from '@angular/common/http';
|
|||||||
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
|
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
|
||||||
import { EntityDebugSettings } from '@shared/models/entity.models';
|
import { EntityDebugSettings } from '@shared/models/entity.models';
|
||||||
import Timeout = NodeJS.Timeout;
|
import Timeout = NodeJS.Timeout;
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-rulechain-page',
|
selector: 'tb-rulechain-page',
|
||||||
@ -273,6 +275,7 @@ export class RuleChainPageComponent extends PageComponent
|
|||||||
private renderer: Renderer2,
|
private renderer: Renderer2,
|
||||||
private viewContainerRef: ViewContainerRef,
|
private viewContainerRef: ViewContainerRef,
|
||||||
private changeDetector: ChangeDetectorRef,
|
private changeDetector: ChangeDetectorRef,
|
||||||
|
private sanitizer:DomSanitizer,
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
public dialogService: DialogService,
|
public dialogService: DialogService,
|
||||||
public fb: FormBuilder) {
|
public fb: FormBuilder) {
|
||||||
@ -1360,9 +1363,13 @@ export class RuleChainPageComponent extends PageComponent
|
|||||||
name = node.name;
|
name = node.name;
|
||||||
desc = this.translate.instant(ruleNodeTypeDescriptors.get(node.component.type).name) + ' - ' + node.component.name;
|
desc = this.translate.instant(ruleNodeTypeDescriptors.get(node.component.type).name) + ' - ' + node.component.name;
|
||||||
if (node.additionalInfo) {
|
if (node.additionalInfo) {
|
||||||
details = node.additionalInfo.description;
|
details = this.sanitizer.sanitize(SecurityContext.HTML, node.additionalInfo.description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
name = this.sanitizer.sanitize(SecurityContext.HTML, name);
|
||||||
|
desc = this.sanitizer.sanitize(SecurityContext.HTML, desc);
|
||||||
|
|
||||||
let tooltipContent = '<div class="tb-rule-node-tooltip">' +
|
let tooltipContent = '<div class="tb-rule-node-tooltip">' +
|
||||||
'<div id="tb-node-content">' +
|
'<div id="tb-node-content">' +
|
||||||
'<div class="tb-node-title">' + name + '</div>' +
|
'<div class="tb-node-title">' + name + '</div>' +
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user