Merge branch 'rc' into features/add_tooltip_option_to_show_stack_mode_total_value_on_timeseries_chart_widgets
This commit is contained in:
		
						commit
						54dafc9a97
					
				@ -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