Browse Source

Merge pull request #12652 from dashevchenko/alarmCountQueryFilters

Entity filters for AlarmCountQuery
pull/12813/head
Andrew Shvayka 2 years ago
committed by GitHub
parent
commit
2363dc8edf
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 34
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  2. 119
      application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java
  3. 82
      application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java
  4. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  5. 9
      common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java
  6. 2
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
  7. 7
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  8. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  9. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java
  10. 39
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java
  11. 51
      dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
  12. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.html
  13. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.ts
  14. 8
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html
  15. 9
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  16. 4
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts

34
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java

@ -33,7 +33,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
@ -41,7 +41,6 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.ComparisonTsValue; import org.thingsboard.server.common.data.query.ComparisonTsValue;
import org.thingsboard.server.common.data.query.OriginatorAlarmFilter;
import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKey;
@ -55,17 +54,16 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.ws.WebSocketService; import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggHistoryCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggHistoryCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggKey; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggKey;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggTimeSeriesCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggTimeSeriesCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
@ -74,7 +72,6 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.GetTsCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd;
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -83,7 +80,6 @@ import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -430,13 +426,23 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
ctx.fetchData(); ctx.fetchData();
long end = System.currentTimeMillis(); long end = System.currentTimeMillis();
stats.getAlarmQueryInvocationCnt().incrementAndGet(); stats.getRegularQueryInvocationCnt().incrementAndGet();
stats.getAlarmQueryTimeSpent().addAndGet(end - start); stats.getRegularQueryTimeSpent().addAndGet(end - start);
TbAlarmCountSubCtx finalCtx = ctx; Set<EntityId> entitiesIds = ctx.getEntitiesIds();
ScheduledFuture<?> task = scheduler.scheduleWithFixedDelay( ctx.cancelTasks();
() -> refreshDynamicQuery(finalCtx), ctx.clearAlarmSubscriptions();
dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS); if (entitiesIds != null && entitiesIds.isEmpty()) {
finalCtx.setRefreshTask(task); AlarmCountUpdate update = new AlarmCountUpdate(cmd.getCmdId(), 0);
ctx.sendWsMsg(update);
} else {
ctx.doFetchAlarmCount();
ctx.createAlarmSubscriptions();
TbAlarmCountSubCtx finalCtx = ctx;
ScheduledFuture<?> task = scheduler.scheduleWithFixedDelay(
() -> refreshDynamicQuery(finalCtx),
dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS);
finalCtx.setRefreshTask(task);
}
} else { } else {
log.debug("[{}][{}] Received duplicate command: {}", session.getSessionId(), cmd.getCmdId(), cmd); log.debug("[{}][{}] Received duplicate command: {}", session.getSessionId(), cmd.getCmdId(), cmd);
} }
@ -555,7 +561,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
private TbAlarmCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) { private TbAlarmCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) {
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new ConcurrentHashMap<>()); Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new ConcurrentHashMap<>());
TbAlarmCountSubCtx ctx = new TbAlarmCountSubCtx(serviceId, wsService, entityService, localSubscriptionService, TbAlarmCountSubCtx ctx = new TbAlarmCountSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, alarmService, sessionRef, cmd.getCmdId()); attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription, maxAlarmQueriesPerRefreshInterval);
if (cmd.getQuery() != null) { if (cmd.getQuery() != null) {
ctx.setAndResolveQuery(cmd.getQuery()); ctx.setAndResolveQuery(cmd.getQuery());
} }

119
application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java

@ -19,49 +19,152 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityDataSortOrder;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.service.ws.WebSocketService; import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j @Slf4j
@ToString(callSuper = true) @ToString(callSuper = true)
public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx<AlarmCountQuery> { public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx<AlarmCountQuery> {
private final AlarmService alarmService; private final AlarmService alarmService;
protected final Map<Integer, EntityId> subToEntityIdMap;
@Getter
private LinkedHashSet<EntityId> entitiesIds;
private final int maxEntitiesPerAlarmSubscription;
private final int maxAlarmQueriesPerRefreshInterval;
@Getter @Getter
@Setter @Setter
private volatile int result; private volatile int result;
@Getter
@Setter
private boolean tooManyEntities;
private int alarmCountInvocationAttempts;
public TbAlarmCountSubCtx(String serviceId, WebSocketService wsService, public TbAlarmCountSubCtx(String serviceId, WebSocketService wsService,
EntityService entityService, TbLocalSubscriptionService localSubscriptionService, EntityService entityService, TbLocalSubscriptionService localSubscriptionService,
AttributesService attributesService, SubscriptionServiceStatistics stats, AlarmService alarmService, AttributesService attributesService, SubscriptionServiceStatistics stats, AlarmService alarmService,
WebSocketSessionRef sessionRef, int cmdId) { WebSocketSessionRef sessionRef, int cmdId, int maxEntitiesPerAlarmSubscription, int maxAlarmQueriesPerRefreshInterval) {
super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId);
this.alarmService = alarmService; this.alarmService = alarmService;
this.subToEntityIdMap = new ConcurrentHashMap<>();
this.maxEntitiesPerAlarmSubscription = maxEntitiesPerAlarmSubscription;
this.maxAlarmQueriesPerRefreshInterval = maxAlarmQueriesPerRefreshInterval;
this.entitiesIds = null;
}
@Override
public void clearSubscriptions() {
clearAlarmSubscriptions();
} }
@Override @Override
public void fetchData() { public void fetchData() {
result = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query); resetInvocationCounter();
sendWsMsg(new AlarmCountUpdate(cmdId, result)); if (query.getEntityFilter() != null) {
entitiesIds = new LinkedHashSet<>();
log.trace("[{}] Fetching data: {}", cmdId, alarmCountInvocationAttempts);
PageData<EntityData> data = entityService.findEntityDataByQuery(getTenantId(), getCustomerId(), buildEntityDataQuery());
entitiesIds.clear();
tooManyEntities = data.hasNext();
for (EntityData entityData : data.getData()) {
entitiesIds.add(entityData.getEntityId());
}
}
} }
@Override @Override
protected void update() { protected void update() {
int newCount = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query); resetInvocationCounter();
if (newCount != result) { fetchAlarmCount();
result = newCount;
sendWsMsg(new AlarmCountUpdate(cmdId, result));
}
} }
@Override @Override
public boolean isDynamic() { public boolean isDynamic() {
return true; return true;
} }
public void fetchAlarmCount() {
alarmCountInvocationAttempts++;
log.trace("[{}] Fetching alarms: {}", cmdId, alarmCountInvocationAttempts);
if (alarmCountInvocationAttempts <= maxAlarmQueriesPerRefreshInterval) {
int newCount = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query, entitiesIds);
if (newCount != result) {
result = newCount;
sendWsMsg(new AlarmCountUpdate(cmdId, result));
}
} else {
log.trace("[{}] Ignore alarm count fetch due to rate limit: [{}] of maximum [{}]", cmdId, alarmCountInvocationAttempts, maxAlarmQueriesPerRefreshInterval);
}
}
public void doFetchAlarmCount() {
result = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query, entitiesIds);
sendWsMsg(new AlarmCountUpdate(cmdId, result));
}
private EntityDataQuery buildEntityDataQuery() {
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null,
new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY)));
return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters());
}
private void resetInvocationCounter() {
alarmCountInvocationAttempts = 0;
}
public void createAlarmSubscriptions() {
for (EntityId entityId : entitiesIds) {
createAlarmSubscriptionForEntity(entityId);
}
}
private void createAlarmSubscriptionForEntity(EntityId entityId) {
int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet();
subToEntityIdMap.put(subIdx, entityId);
log.trace("[{}][{}][{}] Creating alarms subscription for [{}] ", serviceId, cmdId, subIdx, entityId);
TbAlarmsSubscription subscription = TbAlarmsSubscription.builder()
.serviceId(serviceId)
.sessionId(sessionRef.getSessionId())
.subscriptionId(subIdx)
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId)
.updateProcessor((sub, update) -> fetchAlarmCount())
.build();
localSubscriptionService.addSubscription(subscription, sessionRef);
}
public void clearAlarmSubscriptions() {
if (subToEntityIdMap != null) {
for (Integer subId : subToEntityIdMap.keySet()) {
localSubscriptionService.cancelSubscription(getTenantId(), getSessionId(), subId);
}
subToEntityIdMap.clear();
}
}
} }

82
application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java

@ -324,6 +324,83 @@ public class WebsocketApiTest extends AbstractControllerTest {
Assert.assertEquals(1, update.getCount()); Assert.assertEquals(1, update.getCount());
} }
@Test
public void testAlarmCountWsCmdWithSingleEntityFilter() throws Exception {
loginTenantAdmin();
SingleEntityFilter singleEntityFilter = new SingleEntityFilter();
singleEntityFilter.setSingleEntity(tenantId);
AlarmCountQuery alarmCountQuery = new AlarmCountQuery(singleEntityFilter);
AlarmCountCmd cmd1 = new AlarmCountCmd(1, alarmCountQuery);
getWsClient().send(cmd1);
AlarmCountUpdate update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(0, update.getCount());
//create alarm, check count = 1
getWsClient().registerWaitForUpdate();
Alarm alarm = new Alarm();
alarm.setOriginator(tenantId);
alarm.setType("TEST ALARM");
alarm.setSeverity(AlarmSeverity.WARNING);
alarm = doPost("/api/alarm", alarm, Alarm.class);
update = getWsClient().parseAlarmCountReply(getWsClient().waitForUpdate());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(1, update.getCount());
// set wrong entity id in filter, check count = 0
singleEntityFilter.setSingleEntity(tenantAdminUserId);
AlarmCountCmd cmd3 = new AlarmCountCmd(2, alarmCountQuery);
getWsClient().send(cmd3);
update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply());
Assert.assertEquals(2, update.getCmdId());
Assert.assertEquals(0, update.getCount());
}
@Test
public void testAlarmCountWsCmdWithDeviceType() throws Exception {
loginTenantAdmin();
DeviceTypeFilter deviceTypeFilter = new DeviceTypeFilter();
deviceTypeFilter.setDeviceTypes(List.of("default"));
AlarmCountQuery alarmCountQuery = new AlarmCountQuery(deviceTypeFilter);
AlarmCountCmd cmd1 = new AlarmCountCmd(1, alarmCountQuery);
getWsClient().send(cmd1);
AlarmCountUpdate update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(0, update.getCount());
getWsClient().registerWaitForUpdate();
Alarm alarm = new Alarm();
alarm.setOriginator(device.getId());
alarm.setType("TEST ALARM");
alarm.setSeverity(AlarmSeverity.WARNING);
alarm = doPost("/api/alarm", alarm, Alarm.class);
update = getWsClient().parseAlarmCountReply(getWsClient().waitForUpdate());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(1, update.getCount());
deviceTypeFilter.setDeviceTypes(List.of("non-existing"));
AlarmCountCmd cmd3 = new AlarmCountCmd(3, alarmCountQuery);
getWsClient().send(cmd3);
update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply());
Assert.assertEquals(3, update.getCmdId());
Assert.assertEquals(0, update.getCount());
}
@Test @Test
public void testAlarmStatusWsCmd() throws Exception { public void testAlarmStatusWsCmd() throws Exception {
loginTenantAdmin(); loginTenantAdmin();
@ -372,17 +449,18 @@ public class WebsocketApiTest extends AbstractControllerTest {
doPost("/api/alarm", alarm2, Alarm.class); doPost("/api/alarm", alarm2, Alarm.class);
AlarmStatusUpdate alarmStatusUpdate3 = JacksonUtil.fromString(getWsClient().waitForReply(), AlarmStatusUpdate.class); AlarmStatusUpdate alarmStatusUpdate3 = JacksonUtil.fromString(getWsClient().waitForUpdate(), AlarmStatusUpdate.class);
Assert.assertEquals(1, alarmStatusUpdate3.getCmdId()); Assert.assertEquals(1, alarmStatusUpdate3.getCmdId());
Assert.assertTrue(alarmStatusUpdate3.isActive()); Assert.assertTrue(alarmStatusUpdate3.isActive());
//change severity //change severity
getWsClient().registerWaitForUpdate();
alarm2.setSeverity(AlarmSeverity.MAJOR); alarm2.setSeverity(AlarmSeverity.MAJOR);
Alarm updatedAlarm = doPost("/api/alarm", alarm2, Alarm.class); Alarm updatedAlarm = doPost("/api/alarm", alarm2, Alarm.class);
Assert.assertNotNull(updatedAlarm); Assert.assertNotNull(updatedAlarm);
Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity()); Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity());
AlarmStatusUpdate alarmStatusUpdate4 = JacksonUtil.fromString(getWsClient().waitForReply(), AlarmStatusUpdate.class); AlarmStatusUpdate alarmStatusUpdate4 = JacksonUtil.fromString(getWsClient().waitForUpdate(), AlarmStatusUpdate.class);
Assert.assertEquals(1, alarmStatusUpdate4.getCmdId()); Assert.assertEquals(1, alarmStatusUpdate4.getCmdId());
Assert.assertFalse(alarmStatusUpdate4.isActive()); Assert.assertFalse(alarmStatusUpdate4.isActive());

2
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -118,6 +118,8 @@ public interface AlarmService extends EntityDaoService {
long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query);
long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection<EntityId> orderedEntityIds);
PageData<EntitySubtype> findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink); PageData<EntitySubtype> findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink);
List<UUID> findActiveOriginatorAlarms(TenantId tenantId, OriginatorAlarmFilter originatorAlarmFilter, int limit); List<UUID> findActiveOriginatorAlarms(TenantId tenantId, OriginatorAlarmFilter originatorAlarmFilter, int limit);

9
common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java

@ -17,7 +17,7 @@ package org.thingsboard.server.common.data.query;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.ToString; import lombok.ToString;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
@ -29,7 +29,7 @@ import java.util.List;
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Getter @Data
@ToString @ToString
public class AlarmCountQuery extends EntityCountQuery { public class AlarmCountQuery extends EntityCountQuery {
private long startTs; private long startTs;
@ -40,4 +40,9 @@ public class AlarmCountQuery extends EntityCountQuery {
private List<AlarmSeverity> severityList; private List<AlarmSeverity> severityList;
private boolean searchPropagatedAlarms; private boolean searchPropagatedAlarms;
private UserId assigneeId; private UserId assigneeId;
public AlarmCountQuery(EntityFilter entityFilter) {
super(entityFilter);
}
} }

2
dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java

@ -106,7 +106,7 @@ public interface AlarmDao extends Dao<Alarm> {
AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long unassignTime); AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long unassignTime);
long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection<EntityId> orderedEntityIds);
PageData<EntitySubtype> findTenantAlarmTypes(UUID tenantId, PageLink pageLink); PageData<EntitySubtype> findTenantAlarmTypes(UUID tenantId, PageLink pageLink);

7
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -351,8 +351,13 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
@Override @Override
public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) { public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) {
return countAlarmsByQuery(tenantId, customerId, query, null);
}
@Override
public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection<EntityId> orderedEntityIds) {
validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return alarmDao.countAlarmsByQuery(tenantId, customerId, query); return alarmDao.countAlarmsByQuery(tenantId, customerId, query, orderedEntityIds);
} }
@Override @Override

4
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java

@ -415,8 +415,8 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
} }
@Override @Override
public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) { public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection<EntityId> orderedEntityIds) {
return alarmQueryRepository.countAlarmsByQuery(tenantId, customerId, query); return alarmQueryRepository.countAlarmsByQuery(tenantId, customerId, query, orderedEntityIds);
} }
@Override @Override

2
dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java

@ -30,6 +30,6 @@ public interface AlarmQueryRepository {
PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId,
AlarmDataQuery query, Collection<EntityId> orderedEntityIds); AlarmDataQuery query, Collection<EntityId> orderedEntityIds);
long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection<EntityId> orderedEntityIds);
} }

39
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java

@ -44,6 +44,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Repository @Repository
@ -314,25 +315,41 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository {
} }
@Override @Override
public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) { public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection<EntityId> orderedEntityIds) {
QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, null, EntityType.ALARM)); QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, null, EntityType.ALARM));
if (query.isSearchPropagatedAlarms()) { if (query.isSearchPropagatedAlarms()) {
ctx.append("select count(distinct(a.id)) from alarm_info a "); ctx.append("select count(distinct(a.id)) from alarm_info a ");
ctx.append(JOIN_ENTITY_ALARMS); ctx.append(JOIN_ENTITY_ALARMS);
ctx.append("where a.tenant_id = :tenantId and ea.tenant_id = :tenantId"); if (orderedEntityIds != null) {
ctx.addUuidParameter("tenantId", tenantId.getId()); if (orderedEntityIds.isEmpty()) {
if (customerId != null && !customerId.isNullUid()) { return 0;
ctx.append(" and a.customer_id = :customerId and ea.customer_id = :customerId"); }
ctx.addUuidParameter("customerId", customerId.getId()); ctx.addUuidListParameter("entity_filter_entity_ids", orderedEntityIds.stream().map(EntityId::getId).collect(Collectors.toList()));
ctx.append("where ea.entity_id in (:entity_filter_entity_ids)");
} else {
ctx.append("where a.tenant_id = :tenantId and ea.tenant_id = :tenantId");
ctx.addUuidParameter("tenantId", tenantId.getId());
if (customerId != null && !customerId.isNullUid()) {
ctx.append(" and a.customer_id = :customerId and ea.customer_id = :customerId");
ctx.addUuidParameter("customerId", customerId.getId());
}
} }
} else { } else {
ctx.append("select count(id) from alarm_info a "); ctx.append("select count(id) from alarm_info a ");
ctx.append("where a.tenant_id = :tenantId"); if (orderedEntityIds != null) {
ctx.addUuidParameter("tenantId", tenantId.getId()); if (orderedEntityIds.isEmpty()) {
if (customerId != null && !customerId.isNullUid()) { return 0;
ctx.append(" and a.customer_id = :customerId"); }
ctx.addUuidParameter("customerId", customerId.getId()); ctx.addUuidListParameter("entity_filter_entity_ids", orderedEntityIds.stream().map(EntityId::getId).collect(Collectors.toList()));
ctx.append("where a.originator_id in (:entity_filter_entity_ids)");
} else {
ctx.append("where a.tenant_id = :tenantId");
ctx.addUuidParameter("tenantId", tenantId.getId());
if (customerId != null && !customerId.isNullUid()) {
ctx.append(" and a.customer_id = :customerId");
ctx.addUuidParameter("customerId", customerId.getId());
}
} }
} }

51
dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java

@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
@ -48,6 +49,7 @@ import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.query.EntityDataSortOrder; import org.thingsboard.server.common.data.query.EntityDataSortOrder;
import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.EntityListFilter;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
@ -936,4 +938,53 @@ public class AlarmServiceTest extends AbstractServiceTest {
Assert.assertEquals(0, alarms.getData().size()); Assert.assertEquals(0, alarms.getData().size());
} }
@Test
public void testCountAlarmsForEntities() throws ExecutionException, InterruptedException {
AssetId parentId = new AssetId(Uuids.timeBased());
AssetId childId = new AssetId(Uuids.timeBased());
EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation).get());
long ts = System.currentTimeMillis();
AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder()
.tenantId(tenantId)
.originator(childId)
.type(TEST_ALARM)
.severity(AlarmSeverity.CRITICAL)
.startTs(ts).build());
AlarmInfo created = result.getAlarm();
created.setPropagate(true);
result = alarmService.updateAlarm(AlarmUpdateRequest.fromAlarm(created));
created = result.getAlarm();
EntityListFilter entityListFilter = new EntityListFilter();
entityListFilter.setEntityList(List.of(childId.getId().toString(), parentId.getId().toString()));
entityListFilter.setEntityType(EntityType.ASSET);
AlarmCountQuery countQuery = new AlarmCountQuery(entityListFilter);
countQuery.setStartTs(0L);
countQuery.setEndTs(System.currentTimeMillis());
long alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId));
Assert.assertEquals(1, alarmsCount);
countQuery.setSearchPropagatedAlarms(true);
alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(parentId));
Assert.assertEquals(1, alarmsCount);
created = alarmService.acknowledgeAlarm(tenantId, created.getId(), System.currentTimeMillis()).getAlarm();
countQuery.setStatusList(List.of(AlarmSearchStatus.UNACK));
alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId));
Assert.assertEquals(0, alarmsCount);
alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null);
countQuery.setStatusList(List.of(AlarmSearchStatus.CLEARED));
alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId));
Assert.assertEquals(1, alarmsCount);
}
} }

9
ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.html

@ -16,6 +16,15 @@
--> -->
<ng-container [formGroup]="alarmCountWidgetConfigForm"> <ng-container [formGroup]="alarmCountWidgetConfigForm">
<tb-datasources
[configMode]="basicMode"
hideDatasourcesMode
hideDatasourceLabel
hideDataKeys
hideAlarmFilter
displayDatasourceFilterForBasicMode
formControlName="datasources">
</tb-datasources>
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title" translate>alarm.filter</div> <div class="tb-form-panel-title" translate>alarm.filter</div>

2
ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.ts

@ -69,6 +69,7 @@ export class AlarmCountBasicConfigComponent extends BasicWidgetConfigComponent {
const settings: CountWidgetSettings = {...countDefaultSettings(true), ...(configData.config.settings || {})}; const settings: CountWidgetSettings = {...countDefaultSettings(true), ...(configData.config.settings || {})};
this.alarmCountWidgetConfigForm = this.fb.group({ this.alarmCountWidgetConfigForm = this.fb.group({
alarmFilterConfig: [getAlarmFilterConfig(configData.config.datasources), []], alarmFilterConfig: [getAlarmFilterConfig(configData.config.datasources), []],
datasources: [configData.config.datasources, []],
settings: [settings, []], settings: [settings, []],
@ -81,6 +82,7 @@ export class AlarmCountBasicConfigComponent extends BasicWidgetConfigComponent {
} }
protected prepareOutputConfig(config: any): WidgetConfigComponentData { protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.datasources = config.datasources;
setAlarmFilterConfig(config.alarmFilterConfig, this.widgetConfig.config.datasources); setAlarmFilterConfig(config.alarmFilterConfig, this.widgetConfig.config.datasources);
this.widgetConfig.config.settings = {...(this.widgetConfig.config.settings || {}), ...config.settings}; this.widgetConfig.config.settings = {...(this.widgetConfig.config.settings || {}), ...config.settings};

8
ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html

@ -36,7 +36,7 @@
datasourceFormGroup.get('type').value === datasourceType.entity || datasourceFormGroup.get('type').value === datasourceType.entity ||
datasourceFormGroup.get('type').value === datasourceType.entityCount || datasourceFormGroup.get('type').value === datasourceType.entityCount ||
datasourceFormGroup.get('type').value === datasourceType.alarmCount ? datasourceFormGroup.get('type').value : ''"> datasourceFormGroup.get('type').value === datasourceType.alarmCount ? datasourceFormGroup.get('type').value : ''">
<tb-alarm-filter-config *ngIf="datasourceFormGroup.get('type').value === datasourceType.alarmCount" <tb-alarm-filter-config *ngIf="datasourceFormGroup.get('type').value === datasourceType.alarmCount && !hideAlarmFilter"
propagatedFilter="false" propagatedFilter="false"
[initialAlarmFilterConfig]="{ statusList: [alarmSearchStatus.ACTIVE] }" [initialAlarmFilterConfig]="{ statusList: [alarmSearchStatus.ACTIVE] }"
style="height: 56px; margin-bottom: 22px;" style="height: 56px; margin-bottom: 22px;"
@ -47,9 +47,9 @@
formControlName="deviceId"> formControlName="deviceId">
</tb-entity-autocomplete> </tb-entity-autocomplete>
<tb-entity-alias-select <tb-entity-alias-select
*ngIf="datasourceFormGroup.get('type').value !== datasourceType.device && datasourceFormGroup.get('type').value !== datasourceType.alarmCount" *ngIf="datasourceFormGroup.get('type').value !== datasourceType.device"
[showLabel]="true" [showLabel]="true"
[tbRequired]="!datasourcesOptional" [tbRequired]="!entityAliasOptional"
[aliasController]="aliasController" [aliasController]="aliasController"
formControlName="entityAliasId" formControlName="entityAliasId"
[callbacks]="entityAliasSelectCallbacks"> [callbacks]="entityAliasSelectCallbacks">
@ -98,7 +98,7 @@
</tb-data-keys> </tb-data-keys>
</section> </section>
<tb-filter-select <tb-filter-select
*ngIf="(!basicMode || displayDatasourceFilterForBasicMode) && ![datasourceType.function, datasourceType.alarmCount].includes(datasourceFormGroup.get('type').value)" *ngIf="(!basicMode || displayDatasourceFilterForBasicMode) && ![datasourceType.function].includes(datasourceFormGroup.get('type').value)"
[showLabel]="true" [showLabel]="true"
[aliasController]="aliasController" [aliasController]="aliasController"
formControlName="filterId" formControlName="filterId"

9
ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts

@ -106,6 +106,11 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
return this.widgetConfigComponent.modelValue?.typeParameters?.datasourcesOptional; return this.widgetConfigComponent.modelValue?.typeParameters?.datasourcesOptional;
} }
public get entityAliasOptional(): boolean {
const type: DatasourceType = this.datasourceFormGroup.get('type').value;
return this.datasourcesOptional || type === DatasourceType.alarmCount
}
public get maxDataKeys(): number { public get maxDataKeys(): number {
return this.widgetConfigComponent.modelValue?.typeParameters?.maxDataKeys; return this.widgetConfigComponent.modelValue?.typeParameters?.maxDataKeys;
} }
@ -170,6 +175,10 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
return this.datasourcesComponent?.hideLatestDataKeys; return this.datasourcesComponent?.hideLatestDataKeys;
} }
public get hideAlarmFilter(): boolean {
return this.datasourcesComponent?.hideAlarmFilter;
}
@Input() @Input()
disabled: boolean; disabled: boolean;

4
ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts

@ -131,6 +131,10 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
@coerceBoolean() @coerceBoolean()
hideLatestDataKeys = false; hideLatestDataKeys = false;
@Input()
@coerceBoolean()
hideAlarmFilter = false;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
forceSingleDatasource = false; forceSingleDatasource = false;

Loading…
Cancel
Save