From 3176c208a8dce86970a402ee83abeef3a202ad52 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Mon, 1 Mar 2021 14:47:50 +0200 Subject: [PATCH 1/5] Data structures --- .../cmd/TelemetryPluginCmdsWrapper.java | 6 ++ .../telemetry/cmd/v2/AlarmDataUpdate.java | 8 +-- .../service/telemetry/cmd/v2/CmdUpdate.java | 31 ++++++++++ ...DataUpdateType.java => CmdUpdateType.java} | 5 +- .../service/telemetry/cmd/v2/DataUpdate.java | 19 ++++--- .../telemetry/cmd/v2/EntityCountCmd.java | 35 ++++++++++++ .../cmd/v2/EntityCountUnsubscribeCmd.java | 25 ++++++++ .../telemetry/cmd/v2/EntityCountUpdate.java | 57 +++++++++++++++++++ .../telemetry/cmd/v2/EntityDataUpdate.java | 8 +-- .../telemetry/cmd/v2/UnsubscribeCmd.java | 2 - .../common/data/query/AbstractDataQuery.java | 9 +-- .../common/data/query/AlarmDataQuery.java | 4 +- .../common/data/query/EntityCountQuery.java | 14 ++++- .../common/data/query/EntityDataQuery.java | 4 +- .../dao/service/BaseEntityServiceTest.java | 46 ++++++++++----- 15 files changed, 230 insertions(+), 43 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java rename application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/{DataUpdateType.java => CmdUpdateType.java} (92%) create mode 100644 application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java create mode 100644 application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java create mode 100644 application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java index 896babdb6a..1d2de2584d 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java @@ -21,6 +21,8 @@ import org.thingsboard.server.service.telemetry.cmd.v1.GetHistoryCmd; import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd; import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUnsubscribeCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUnsubscribeCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; @@ -46,4 +48,8 @@ public class TelemetryPluginCmdsWrapper { private List alarmDataUnsubscribeCmds; + private List entityCountCmds; + + private List entityCountUnsubscribeCmds; + } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java index 5a2ea4ff00..f23484f71f 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java @@ -18,14 +18,14 @@ package org.thingsboard.server.service.telemetry.cmd.v2; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.ToString; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; -import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; import java.util.List; +@ToString public class AlarmDataUpdate extends DataUpdate { @Getter @@ -44,8 +44,8 @@ public class AlarmDataUpdate extends DataUpdate { } @Override - public DataUpdateType getDataUpdateType() { - return DataUpdateType.ALARM_DATA; + public CmdUpdateType getCmdUpdateType() { + return CmdUpdateType.ALARM_DATA; } @JsonCreator diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java new file mode 100644 index 0000000000..1a78a4d29a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public abstract class CmdUpdate { + + private final int cmdId; + private final int errorCode; + private final String errorMsg; + + public abstract CmdUpdateType getCmdUpdateType(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdateType.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdateType.java similarity index 92% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdateType.java rename to application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdateType.java index 2a9930e2b1..9df281cbb9 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdateType.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdateType.java @@ -15,7 +15,8 @@ */ package org.thingsboard.server.service.telemetry.cmd.v2; -public enum DataUpdateType { +public enum CmdUpdateType { ENTITY_DATA, - ALARM_DATA + ALARM_DATA, + COUNT_DATA } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java index 5540acd782..5da682502c 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java @@ -18,21 +18,27 @@ package org.thingsboard.server.service.telemetry.cmd.v2; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; import java.util.List; -@Data -@AllArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) -public abstract class DataUpdate { +public abstract class DataUpdate extends CmdUpdate { - private final int cmdId; + @Getter private final PageData data; + @Getter private final List update; - private final int errorCode; - private final String errorMsg; + + public DataUpdate(int cmdId, PageData data, List update, int errorCode, String errorMsg) { + super(cmdId, errorCode, errorMsg); + this.data = data; + this.update = update; + } public DataUpdate(int cmdId, PageData data, List update) { this(cmdId, data, update, SubscriptionErrorCode.NO_ERROR.getCode(), null); @@ -42,5 +48,4 @@ public abstract class DataUpdate { this(cmdId, null, null, errorCode, errorMsg); } - public abstract DataUpdateType getDataUpdateType(); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java new file mode 100644 index 0000000000..d0b7fc8454 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityDataQuery; + +public class EntityCountCmd extends DataCmd { + + @Getter + private final EntityCountQuery query; + + @JsonCreator + public EntityCountCmd(@JsonProperty("cmdId") int cmdId, + @JsonProperty("query") EntityCountQuery query) { + super(cmdId); + this.query = query; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java new file mode 100644 index 0000000000..c597fa6634 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import lombok.Data; + +@Data +public class EntityCountUnsubscribeCmd implements UnsubscribeCmd { + + private final int cmdId; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java new file mode 100644 index 0000000000..6617f8e63c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; + +import java.util.List; + +@ToString +public class EntityCountUpdate extends CmdUpdate { + + @Getter + private int count; + + public EntityCountUpdate(int cmdId, int count) { + super(cmdId, SubscriptionErrorCode.NO_ERROR.getCode(), null); + this.count = count; + } + + public EntityCountUpdate(int cmdId, int errorCode, String errorMsg) { + super(cmdId, errorCode, errorMsg); + } + + @Override + public CmdUpdateType getCmdUpdateType() { + return CmdUpdateType.COUNT_DATA; + } + + @JsonCreator + public EntityCountUpdate(@JsonProperty("cmdId") int cmdId, + @JsonProperty("count") int count, + @JsonProperty("errorCode") int errorCode, + @JsonProperty("errorMsg") String errorMsg) { + super(cmdId, errorCode, errorMsg); + this.count = count; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java index f08dee8ba3..0af6f85697 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java @@ -16,16 +16,16 @@ package org.thingsboard.server.service.telemetry.cmd.v2; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; +import lombok.ToString; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; import java.util.List; - +@ToString public class EntityDataUpdate extends DataUpdate { @Getter @@ -41,8 +41,8 @@ public class EntityDataUpdate extends DataUpdate { } @Override - public DataUpdateType getDataUpdateType() { - return DataUpdateType.ENTITY_DATA; + public CmdUpdateType getCmdUpdateType() { + return CmdUpdateType.ENTITY_DATA; } @JsonCreator diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java index d34f5c6fdf..5e046ee581 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java @@ -15,8 +15,6 @@ */ package org.thingsboard.server.service.telemetry.cmd.v2; -import lombok.Data; - public interface UnsubscribeCmd { int getCmdId(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java index 8a85158853..0e807f8c37 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java @@ -29,15 +29,13 @@ public abstract class AbstractDataQuery extends En protected List entityFields; @Getter protected List latestValues; - @Getter - protected List keyFilters; public AbstractDataQuery() { super(); } - public AbstractDataQuery(EntityFilter entityFilter) { - super(entityFilter); + public AbstractDataQuery(EntityFilter entityFilter, List keyFilters) { + super(entityFilter, keyFilters); } public AbstractDataQuery(EntityFilter entityFilter, @@ -45,11 +43,10 @@ public abstract class AbstractDataQuery extends En List entityFields, List latestValues, List keyFilters) { - super(entityFilter); + super(entityFilter, keyFilters); this.pageLink = pageLink; this.entityFields = entityFields; this.latestValues = latestValues; - this.keyFilters = keyFilters; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java index 590073410c..655ef7ecfd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java @@ -30,8 +30,8 @@ public class AlarmDataQuery extends AbstractDataQuery { public AlarmDataQuery() { } - public AlarmDataQuery(EntityFilter entityFilter) { - super(entityFilter); + public AlarmDataQuery(EntityFilter entityFilter, List keyFilters) { + super(entityFilter, keyFilters); } public AlarmDataQuery(EntityFilter entityFilter, AlarmDataPageLink pageLink, List entityFields, List latestValues, List keyFilters, List alarmFields) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java index b056b31dac..07878a3347 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java @@ -17,14 +17,26 @@ package org.thingsboard.server.common.data.query; import lombok.Getter; +import java.util.Collections; +import java.util.List; + public class EntityCountQuery { @Getter private EntityFilter entityFilter; - public EntityCountQuery() {} + @Getter + protected List keyFilters; + + public EntityCountQuery() { + } public EntityCountQuery(EntityFilter entityFilter) { + this(entityFilter, Collections.emptyList()); + } + + public EntityCountQuery(EntityFilter entityFilter, List keyFilters) { this.entityFilter = entityFilter; + this.keyFilters = keyFilters; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java index a7342d13f3..aca8437fe8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java @@ -27,8 +27,8 @@ public class EntityDataQuery extends AbstractDataQuery { public EntityDataQuery() { } - public EntityDataQuery(EntityFilter entityFilter) { - super(entityFilter); + public EntityDataQuery(EntityFilter entityFilter, List keyFilters) { + super(entityFilter, keyFilters); } public EntityDataQuery(EntityFilter entityFilter, EntityDataPageLink pageLink, List entityFields, List latestValues, List keyFilters) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java index d3c2deefba..98934cfaa0 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java @@ -15,9 +15,6 @@ */ package org.thingsboard.server.dao.service; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.junit.After; @@ -31,25 +28,48 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.id.*; -import org.thingsboard.server.common.data.kv.*; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.query.*; +import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; +import org.thingsboard.server.common.data.query.DeviceSearchQueryFilter; +import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EntityCountQuery; +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.common.data.query.EntityListFilter; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.NumericFilterPredicate; +import org.thingsboard.server.common.data.query.RelationsQueryFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.common.data.rule.RuleChain; -import org.thingsboard.server.common.data.rule.RuleChainMetaData; -import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; -import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.dao.util.DaoTestUtil; -import org.thingsboard.server.dao.util.SqlDbType; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Random; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; From 1e9016cb478c6494344903e6de45f33bb646d32a Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Mon, 1 Mar 2021 18:23:39 +0200 Subject: [PATCH 2/5] Implementation --- ...efaultTbEntityDataSubscriptionService.java | 54 ++- .../subscription/TbAbstractDataSubCtx.java | 267 +-------------- .../subscription/TbAbstractSubCtx.java | 315 ++++++++++++++++++ .../subscription/TbAlarmDataSubCtx.java | 3 +- .../subscription/TbEntityCountSubCtx.java | 55 +++ .../TbEntityDataSubscriptionService.java | 3 + .../DefaultTelemetryWebSocketService.java | 42 ++- .../telemetry/TelemetryWebSocketService.java | 3 +- .../service/telemetry/cmd/v2/CmdUpdate.java | 2 + .../service/telemetry/cmd/v2/DataUpdate.java | 6 - .../controller/BaseWebsocketApiTest.java | 99 ++++++ .../controller/ControllerSqlTestSuite.java | 4 +- .../query/DefaultEntityQueryRepository.java | 74 +++- .../dao/sql/query/EntityKeyMapping.java | 25 ++ 14 files changed, 652 insertions(+), 300 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java create mode 100644 application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index 689f80f757..330b096b78 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -54,6 +54,7 @@ import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; @@ -92,7 +93,7 @@ import java.util.stream.Collectors; public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubscriptionService { private static final int DEFAULT_LIMIT = 100; - private final Map> subscriptionsBySessionId = new ConcurrentHashMap<>(); + private final Map> subscriptionsBySessionId = new ConcurrentHashMap<>(); @Autowired private TelemetryWebSocketService wsService; @@ -202,7 +203,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc //TODO: validate number of dynamic page links against rate limits. Ignore dynamic flag if limit is reached. TbEntityDataSubCtx finalCtx = ctx; ScheduledFuture task = scheduler.scheduleWithFixedDelay( - () -> refreshDynamicQuery(tenantId, customerId, finalCtx), + () -> refreshDynamicQuery(finalCtx), dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS); finalCtx.setRefreshTask(task); } @@ -235,6 +236,26 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc }, wsCallBackExecutor); } + @Override + public void handleCmd(TelemetryWebSocketSessionRef session, EntityCountCmd cmd) { + TbEntityCountSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId()); + if (ctx == null) { + ctx = createSubCtx(session, cmd); + long start = System.currentTimeMillis(); + ctx.fetchData(); + long end = System.currentTimeMillis(); + stats.getRegularQueryInvocationCnt().incrementAndGet(); + stats.getRegularQueryTimeSpent().addAndGet(end - start); + TbEntityCountSubCtx finalCtx = ctx; + ScheduledFuture task = scheduler.scheduleWithFixedDelay( + () -> refreshDynamicQuery(finalCtx), + dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS); + finalCtx.setRefreshTask(task); + } else { + log.debug("[{}][{}] Received duplicate command: {}", session.getSessionId(), cmd.getCmdId(), cmd); + } + } + @Override public void handleCmd(TelemetryWebSocketSessionRef session, AlarmDataCmd cmd) { TbAlarmDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId()); @@ -267,7 +288,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } } - private void refreshDynamicQuery(TenantId tenantId, CustomerId customerId, TbEntityDataSubCtx finalCtx) { + private void refreshDynamicQuery(TbAbstractSubCtx finalCtx) { try { long start = System.currentTimeMillis(); finalCtx.update(); @@ -299,7 +320,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } private TbEntityDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) { - Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); + Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription); if (cmd.getQuery() != null) { @@ -309,8 +330,20 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc return ctx; } + private TbEntityCountSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) { + Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); + TbEntityCountSubCtx ctx = new TbEntityCountSubCtx(serviceId, wsService, entityService, localSubscriptionService, + attributesService, stats, sessionRef, cmd.getCmdId()); + if (cmd.getQuery() != null) { + ctx.setAndResolveQuery(cmd.getQuery()); + } + sessionSubs.put(cmd.getCmdId(), ctx); + return ctx; + } + + private TbAlarmDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { - Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); + Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); TbAlarmDataSubCtx ctx = new TbAlarmDataSubCtx(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription); ctx.setAndResolveQuery(cmd.getQuery()); @@ -319,8 +352,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } @SuppressWarnings("unchecked") - private T getSubCtx(String sessionId, int cmdId) { - Map sessionSubs = subscriptionsBySessionId.get(sessionId); + private T getSubCtx(String sessionId, int cmdId) { + Map sessionSubs = subscriptionsBySessionId.get(sessionId); if (sessionSubs != null) { return (T) sessionSubs.get(cmdId); } else { @@ -464,17 +497,16 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc cleanupAndCancel(getSubCtx(sessionId, cmd.getCmdId())); } - private void cleanupAndCancel(TbAbstractDataSubCtx ctx) { + private void cleanupAndCancel(TbAbstractSubCtx ctx) { if (ctx != null) { ctx.cancelTasks(); - ctx.clearEntitySubscriptions(); - ctx.clearDynamicValueSubscriptions(); + ctx.clearSubscriptions(); } } @Override public void cancelAllSessionSubscriptions(String sessionId) { - Map sessionSubs = subscriptionsBySessionId.remove(sessionId); + Map sessionSubs = subscriptionsBySessionId.remove(sessionId); if (sessionSubs != null) { sessionSubs.values().forEach(this::cleanupAndCancel); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java index a200f31a3f..08f7bfa1a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java @@ -15,32 +15,16 @@ */ package org.thingsboard.server.service.subscription; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.Data; import lombok.Getter; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AbstractDataQuery; -import org.thingsboard.server.common.data.query.ComplexFilterPredicate; -import org.thingsboard.server.common.data.query.DynamicValue; -import org.thingsboard.server.common.data.query.DynamicValueSourceType; 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.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; -import org.thingsboard.server.common.data.query.FilterPredicateType; -import org.thingsboard.server.common.data.query.KeyFilter; -import org.thingsboard.server.common.data.query.KeyFilterPredicate; -import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate; import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; @@ -52,140 +36,25 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ScheduledFuture; import java.util.function.Function; import java.util.stream.Collectors; @Slf4j -@Data -public abstract class TbAbstractDataSubCtx> { +public abstract class TbAbstractDataSubCtx> extends TbAbstractSubCtx { - protected final String serviceId; - protected final SubscriptionServiceStatistics stats; - protected final TelemetryWebSocketService wsService; - protected final EntityService entityService; - protected final TbLocalSubscriptionService localSubscriptionService; - protected final AttributesService attributesService; - protected final TelemetryWebSocketSessionRef sessionRef; - protected final int cmdId; protected final Map subToEntityIdMap; - protected final Set subToDynamicValueKeySet; - @Getter - protected final Map> dynamicValues; @Getter protected PageData data; - @Getter - @Setter - protected T query; - @Setter - protected volatile ScheduledFuture refreshTask; public TbAbstractDataSubCtx(String serviceId, TelemetryWebSocketService wsService, EntityService entityService, TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService, SubscriptionServiceStatistics stats, TelemetryWebSocketSessionRef sessionRef, int cmdId) { - this.serviceId = serviceId; - this.wsService = wsService; - this.entityService = entityService; - this.localSubscriptionService = localSubscriptionService; - this.attributesService = attributesService; - this.stats = stats; - this.sessionRef = sessionRef; - this.cmdId = cmdId; + super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); this.subToEntityIdMap = new ConcurrentHashMap<>(); - this.subToDynamicValueKeySet = ConcurrentHashMap.newKeySet(); - this.dynamicValues = new ConcurrentHashMap<>(); - } - - public void setAndResolveQuery(T query) { - dynamicValues.clear(); - this.query = query; - if (query != null && query.getKeyFilters() != null) { - for (KeyFilter filter : query.getKeyFilters()) { - registerDynamicValues(filter.getPredicate()); - } - } - resolve(getTenantId(), getCustomerId(), getUserId()); - } - - public void resolve(TenantId tenantId, CustomerId customerId, UserId userId) { - List> futures = new ArrayList<>(); - for (DynamicValueKey key : dynamicValues.keySet()) { - switch (key.getSourceType()) { - case CURRENT_TENANT: - futures.add(resolveEntityValue(tenantId, tenantId, key)); - break; - case CURRENT_CUSTOMER: - if (customerId != null && !customerId.isNullUid()) { - futures.add(resolveEntityValue(tenantId, customerId, key)); - } - break; - case CURRENT_USER: - if (userId != null && !userId.isNullUid()) { - futures.add(resolveEntityValue(tenantId, userId, key)); - } - break; - } - } - try { - Map> tmpSubMap = new HashMap<>(); - for (DynamicValueKeySub sub : Futures.successfulAsList(futures).get()) { - tmpSubMap.computeIfAbsent(sub.getEntityId(), tmp -> new HashMap<>()).put(sub.getKey().getSourceAttribute(), sub); - } - for (EntityId entityId : tmpSubMap.keySet()) { - Map keyStates = new HashMap<>(); - Map dynamicValueKeySubMap = tmpSubMap.get(entityId); - dynamicValueKeySubMap.forEach((k, v) -> keyStates.put(k, v.getLastUpdateTs())); - int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet(); - TbAttributeSubscription sub = TbAttributeSubscription.builder() - .serviceId(serviceId) - .sessionId(sessionRef.getSessionId()) - .subscriptionId(subIdx) - .tenantId(sessionRef.getSecurityCtx().getTenantId()) - .entityId(entityId) - .updateConsumer((s, subscriptionUpdate) -> dynamicValueSubUpdate(s, subscriptionUpdate, dynamicValueKeySubMap)) - .allKeys(false) - .keyStates(keyStates) - .scope(TbAttributeSubscriptionScope.SERVER_SCOPE) - .build(); - subToDynamicValueKeySet.add(subIdx); - localSubscriptionService.addSubscription(sub); - } - } catch (InterruptedException | ExecutionException e) { - log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet()); - } - - } - - private void dynamicValueSubUpdate(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, - Map dynamicValueKeySubMap) { - Map latestUpdate = new HashMap<>(); - subscriptionUpdate.getData().forEach((k, v) -> { - Object[] data = (Object[]) v.get(0); - latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1])); - }); - - boolean invalidateFilter = false; - for (Map.Entry entry : latestUpdate.entrySet()) { - String k = entry.getKey(); - TsValue tsValue = entry.getValue(); - DynamicValueKeySub sub = dynamicValueKeySubMap.get(k); - if (sub.updateValue(tsValue)) { - invalidateFilter = true; - updateDynamicValuesByKey(sub, tsValue); - } - } - - if (invalidateFilter) { - update(); - } } public void fetchData() { @@ -231,104 +100,10 @@ public abstract class TbAbstractDataSubCtx lastUpdateTs && (lastUpdateValue == null || !lastUpdateValue.equals(value.getValue()))) { - this.lastUpdateTs = value.getTs(); - this.lastUpdateValue = value.getValue(); - return true; - } else { - return false; - } - } - } - - private ListenableFuture resolveEntityValue(TenantId tenantId, EntityId entityId, DynamicValueKey key) { - ListenableFuture> entry = attributesService.find(tenantId, entityId, - TbAttributeSubscriptionScope.SERVER_SCOPE.name(), key.getSourceAttribute()); - return Futures.transform(entry, attributeOpt -> { - DynamicValueKeySub sub = new DynamicValueKeySub(key, entityId); - if (attributeOpt.isPresent()) { - AttributeKvEntry attribute = attributeOpt.get(); - sub.setLastUpdateTs(attribute.getLastUpdateTs()); - sub.setLastUpdateValue(attribute.getValueAsString()); - updateDynamicValuesByKey(sub, new TsValue(attribute.getLastUpdateTs(), attribute.getValueAsString())); - } - return sub; - }, MoreExecutors.directExecutor()); - } - - @SuppressWarnings("unchecked") - private void updateDynamicValuesByKey(DynamicValueKeySub sub, TsValue tsValue) { - DynamicValueKey dvk = sub.getKey(); - switch (dvk.getPredicateType()) { - case STRING: - dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(tsValue.getValue())); - break; - case NUMERIC: - try { - Double dValue = Double.parseDouble(tsValue.getValue()); - dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(dValue)); - } catch (NumberFormatException e) { - dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(null)); - } - break; - case BOOLEAN: - Boolean bValue = Boolean.parseBoolean(tsValue.getValue()); - dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(bValue)); - break; - } - } - - @SuppressWarnings("unchecked") - private void registerDynamicValues(KeyFilterPredicate predicate) { - switch (predicate.getType()) { - case STRING: - case NUMERIC: - case BOOLEAN: - Optional value = getDynamicValueFromSimplePredicate((SimpleKeyFilterPredicate) predicate); - if (value.isPresent()) { - DynamicValue dynamicValue = value.get(); - DynamicValueKey key = new DynamicValueKey( - predicate.getType(), - dynamicValue.getSourceType(), - dynamicValue.getSourceAttribute()); - dynamicValues.computeIfAbsent(key, tmp -> new ArrayList<>()).add(dynamicValue); - } - break; - case COMPLEX: - ((ComplexFilterPredicate) predicate).getPredicates().forEach(this::registerDynamicValues); - } - } - - private Optional> getDynamicValueFromSimplePredicate(SimpleKeyFilterPredicate predicate) { - if (predicate.getValue().getUserValue() == null) { - return Optional.ofNullable(predicate.getValue().getDynamicValue()); - } else { - return Optional.empty(); - } - } - - public String getSessionId() { - return sessionRef.getSessionId(); - } - - public TenantId getTenantId() { - return sessionRef.getSecurityCtx().getTenantId(); - } - - public CustomerId getCustomerId() { - return sessionRef.getSecurityCtx().getCustomerId(); - } - - public UserId getUserId() { - return sessionRef.getSecurityCtx().getId(); + @Override + public void clearSubscriptions() { + clearEntitySubscriptions(); + super.clearSubscriptions(); } public void clearEntitySubscriptions() { @@ -340,26 +115,6 @@ public abstract class TbAbstractDataSubCtx task) { - this.refreshTask = task; - } - - public void cancelTasks() { - if (this.refreshTask != null) { - log.trace("[{}][{}] Canceling old refresh task", sessionRef.getSessionId(), cmdId); - this.refreshTask.cancel(true); - } - } - public void createSubscriptions(List keys, boolean resultToLatestValues) { Map> keysByType = getEntityKeyByTypeMap(keys); for (EntityData entityData : data.getData()) { @@ -459,14 +214,4 @@ public abstract class TbAbstractDataSubCtx { + + protected final String serviceId; + protected final SubscriptionServiceStatistics stats; + protected final TelemetryWebSocketService wsService; + protected final EntityService entityService; + protected final TbLocalSubscriptionService localSubscriptionService; + protected final AttributesService attributesService; + protected final TelemetryWebSocketSessionRef sessionRef; + protected final int cmdId; + protected final Set subToDynamicValueKeySet; + @Getter + protected final Map> dynamicValues; + @Getter + @Setter + protected T query; + @Setter + protected volatile ScheduledFuture refreshTask; + + public TbAbstractSubCtx(String serviceId, TelemetryWebSocketService wsService, + EntityService entityService, TbLocalSubscriptionService localSubscriptionService, + AttributesService attributesService, SubscriptionServiceStatistics stats, + TelemetryWebSocketSessionRef sessionRef, int cmdId) { + this.serviceId = serviceId; + this.wsService = wsService; + this.entityService = entityService; + this.localSubscriptionService = localSubscriptionService; + this.attributesService = attributesService; + this.stats = stats; + this.sessionRef = sessionRef; + this.cmdId = cmdId; + this.subToDynamicValueKeySet = ConcurrentHashMap.newKeySet(); + this.dynamicValues = new ConcurrentHashMap<>(); + } + + public void setAndResolveQuery(T query) { + dynamicValues.clear(); + this.query = query; + if (query != null && query.getKeyFilters() != null) { + for (KeyFilter filter : query.getKeyFilters()) { + registerDynamicValues(filter.getPredicate()); + } + } + resolve(getTenantId(), getCustomerId(), getUserId()); + } + + public void resolve(TenantId tenantId, CustomerId customerId, UserId userId) { + List> futures = new ArrayList<>(); + for (DynamicValueKey key : dynamicValues.keySet()) { + switch (key.getSourceType()) { + case CURRENT_TENANT: + futures.add(resolveEntityValue(tenantId, tenantId, key)); + break; + case CURRENT_CUSTOMER: + if (customerId != null && !customerId.isNullUid()) { + futures.add(resolveEntityValue(tenantId, customerId, key)); + } + break; + case CURRENT_USER: + if (userId != null && !userId.isNullUid()) { + futures.add(resolveEntityValue(tenantId, userId, key)); + } + break; + } + } + try { + Map> tmpSubMap = new HashMap<>(); + for (DynamicValueKeySub sub : Futures.successfulAsList(futures).get()) { + tmpSubMap.computeIfAbsent(sub.getEntityId(), tmp -> new HashMap<>()).put(sub.getKey().getSourceAttribute(), sub); + } + for (EntityId entityId : tmpSubMap.keySet()) { + Map keyStates = new HashMap<>(); + Map dynamicValueKeySubMap = tmpSubMap.get(entityId); + dynamicValueKeySubMap.forEach((k, v) -> keyStates.put(k, v.getLastUpdateTs())); + int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet(); + TbAttributeSubscription sub = TbAttributeSubscription.builder() + .serviceId(serviceId) + .sessionId(sessionRef.getSessionId()) + .subscriptionId(subIdx) + .tenantId(sessionRef.getSecurityCtx().getTenantId()) + .entityId(entityId) + .updateConsumer((s, subscriptionUpdate) -> dynamicValueSubUpdate(s, subscriptionUpdate, dynamicValueKeySubMap)) + .allKeys(false) + .keyStates(keyStates) + .scope(TbAttributeSubscriptionScope.SERVER_SCOPE) + .build(); + subToDynamicValueKeySet.add(subIdx); + localSubscriptionService.addSubscription(sub); + } + } catch (InterruptedException | ExecutionException e) { + log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet()); + } + + } + + private void dynamicValueSubUpdate(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, + Map dynamicValueKeySubMap) { + Map latestUpdate = new HashMap<>(); + subscriptionUpdate.getData().forEach((k, v) -> { + Object[] data = (Object[]) v.get(0); + latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1])); + }); + + boolean invalidateFilter = false; + for (Map.Entry entry : latestUpdate.entrySet()) { + String k = entry.getKey(); + TsValue tsValue = entry.getValue(); + DynamicValueKeySub sub = dynamicValueKeySubMap.get(k); + if (sub.updateValue(tsValue)) { + invalidateFilter = true; + updateDynamicValuesByKey(sub, tsValue); + } + } + + if (invalidateFilter) { + update(); + } + } + + public abstract void fetchData(); + + protected abstract void update(); + + public void clearSubscriptions() { + clearDynamicValueSubscriptions(); + } + + @Data + private static class DynamicValueKeySub { + private final DynamicValueKey key; + private final EntityId entityId; + private long lastUpdateTs; + private String lastUpdateValue; + + boolean updateValue(TsValue value) { + if (value.getTs() > lastUpdateTs && (lastUpdateValue == null || !lastUpdateValue.equals(value.getValue()))) { + this.lastUpdateTs = value.getTs(); + this.lastUpdateValue = value.getValue(); + return true; + } else { + return false; + } + } + } + + private ListenableFuture resolveEntityValue(TenantId tenantId, EntityId entityId, DynamicValueKey key) { + ListenableFuture> entry = attributesService.find(tenantId, entityId, + TbAttributeSubscriptionScope.SERVER_SCOPE.name(), key.getSourceAttribute()); + return Futures.transform(entry, attributeOpt -> { + DynamicValueKeySub sub = new DynamicValueKeySub(key, entityId); + if (attributeOpt.isPresent()) { + AttributeKvEntry attribute = attributeOpt.get(); + sub.setLastUpdateTs(attribute.getLastUpdateTs()); + sub.setLastUpdateValue(attribute.getValueAsString()); + updateDynamicValuesByKey(sub, new TsValue(attribute.getLastUpdateTs(), attribute.getValueAsString())); + } + return sub; + }, MoreExecutors.directExecutor()); + } + + @SuppressWarnings("unchecked") + protected void updateDynamicValuesByKey(DynamicValueKeySub sub, TsValue tsValue) { + DynamicValueKey dvk = sub.getKey(); + switch (dvk.getPredicateType()) { + case STRING: + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(tsValue.getValue())); + break; + case NUMERIC: + try { + Double dValue = Double.parseDouble(tsValue.getValue()); + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(dValue)); + } catch (NumberFormatException e) { + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(null)); + } + break; + case BOOLEAN: + Boolean bValue = Boolean.parseBoolean(tsValue.getValue()); + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(bValue)); + break; + } + } + + @SuppressWarnings("unchecked") + private void registerDynamicValues(KeyFilterPredicate predicate) { + switch (predicate.getType()) { + case STRING: + case NUMERIC: + case BOOLEAN: + Optional value = getDynamicValueFromSimplePredicate((SimpleKeyFilterPredicate) predicate); + if (value.isPresent()) { + DynamicValue dynamicValue = value.get(); + DynamicValueKey key = new DynamicValueKey( + predicate.getType(), + dynamicValue.getSourceType(), + dynamicValue.getSourceAttribute()); + dynamicValues.computeIfAbsent(key, tmp -> new ArrayList<>()).add(dynamicValue); + } + break; + case COMPLEX: + ((ComplexFilterPredicate) predicate).getPredicates().forEach(this::registerDynamicValues); + } + } + + private Optional> getDynamicValueFromSimplePredicate(SimpleKeyFilterPredicate predicate) { + if (predicate.getValue().getUserValue() == null) { + return Optional.ofNullable(predicate.getValue().getDynamicValue()); + } else { + return Optional.empty(); + } + } + + public String getSessionId() { + return sessionRef.getSessionId(); + } + + public TenantId getTenantId() { + return sessionRef.getSecurityCtx().getTenantId(); + } + + public CustomerId getCustomerId() { + return sessionRef.getSecurityCtx().getCustomerId(); + } + + public UserId getUserId() { + return sessionRef.getSecurityCtx().getId(); + } + + protected void clearDynamicValueSubscriptions() { + if (subToDynamicValueKeySet != null) { + for (Integer subId : subToDynamicValueKeySet) { + localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), subId); + } + subToDynamicValueKeySet.clear(); + } + } + + public void setRefreshTask(ScheduledFuture task) { + this.refreshTask = task; + } + + public void cancelTasks() { + if (this.refreshTask != null) { + log.trace("[{}][{}] Canceling old refresh task", sessionRef.getSessionId(), cmdId); + this.refreshTask.cancel(true); + } + } + + @Data + public static class DynamicValueKey { + @Getter + private final FilterPredicateType predicateType; + @Getter + private final DynamicValueSourceType sourceType; + @Getter + private final String sourceAttribute; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java index 95f15fcb3f..8af9ba2330 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -90,8 +90,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { AlarmDataUpdate update; if (!entitiesMap.isEmpty()) { long start = System.currentTimeMillis(); - PageData alarms = alarmService.findAlarmDataByQueryForEntities(getTenantId(), getCustomerId(), - query, getOrderedEntityIds()); + PageData alarms = alarmService.findAlarmDataByQueryForEntities(getTenantId(), getCustomerId(), query, getOrderedEntityIds()); long end = System.currentTimeMillis(); stats.getAlarmQueryInvocationCnt().incrementAndGet(); stats.getAlarmQueryTimeSpent().addAndGet(end - start); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java new file mode 100644 index 0000000000..e97892a37d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.subscription; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; + +@Slf4j +public class TbEntityCountSubCtx extends TbAbstractSubCtx { + + private volatile int result; + + public TbEntityCountSubCtx(String serviceId, TelemetryWebSocketService wsService, EntityService entityService, + TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService, + SubscriptionServiceStatistics stats, TelemetryWebSocketSessionRef sessionRef, int cmdId) { + super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); + } + + @Override + public void fetchData() { + result = (int) entityService.countEntitiesByQuery(getTenantId(), getCustomerId(), query); + wsService.sendWsMsg(sessionRef.getSessionId(), new EntityCountUpdate(cmdId, result)); + } + + @Override + protected void update() { + int newCount = (int) entityService.countEntitiesByQuery(getTenantId(), getCustomerId(), query); + if (newCount != result) { + result = newCount; + wsService.sendWsMsg(sessionRef.getSessionId(), new EntityCountUpdate(cmdId, result)); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java index 60abdeaa9c..8760997bfd 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.subscription; import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd; @@ -25,6 +26,8 @@ public interface TbEntityDataSubscriptionService { void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityDataCmd cmd); + void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityCountCmd cmd); + void handleCmd(TelemetryWebSocketSessionRef sessionId, AlarmDataCmd cmd); void cancelSubscription(String sessionId, UnsubscribeCmd subscriptionId); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java index 2efbc2d0a8..eec45a9c8d 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java @@ -51,22 +51,22 @@ import org.thingsboard.server.service.security.ValidationResult; import org.thingsboard.server.service.security.ValidationResultCode; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.subscription.TbAttributeSubscription; +import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService; import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; -import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; -import org.thingsboard.server.service.subscription.TbAttributeSubscription; import org.thingsboard.server.service.subscription.TbTimeseriesSubscription; +import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; import org.thingsboard.server.service.telemetry.cmd.v1.AttributesSubscriptionCmd; import org.thingsboard.server.service.telemetry.cmd.v1.GetHistoryCmd; import org.thingsboard.server.service.telemetry.cmd.v1.SubscriptionCmd; import org.thingsboard.server.service.telemetry.cmd.v1.TelemetryPluginCmd; -import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd; import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUnsubscribeCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.CmdUpdate; import org.thingsboard.server.service.telemetry.cmd.v2.DataUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd; import org.thingsboard.server.service.telemetry.exception.UnauthorizedException; @@ -216,12 +216,18 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi if (cmdsWrapper.getAlarmDataCmds() != null) { cmdsWrapper.getAlarmDataCmds().forEach(cmd -> handleWsAlarmDataCmd(sessionRef, cmd)); } + if (cmdsWrapper.getEntityCountCmds() != null) { + cmdsWrapper.getEntityCountCmds().forEach(cmd -> handleWsEntityCountCmd(sessionRef, cmd)); + } if (cmdsWrapper.getEntityDataUnsubscribeCmds() != null) { cmdsWrapper.getEntityDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd)); } if (cmdsWrapper.getAlarmDataUnsubscribeCmds() != null) { cmdsWrapper.getAlarmDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd)); } + if (cmdsWrapper.getEntityCountUnsubscribeCmds() != null) { + cmdsWrapper.getEntityCountUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd)); + } } } catch (IOException e) { log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e); @@ -239,6 +245,16 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi } } + private void handleWsEntityCountCmd(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) { + String sessionId = sessionRef.getSessionId(); + log.debug("[{}] Processing: {}", sessionId, cmd); + + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId) + && validateSubscriptionCmd(sessionRef, cmd)) { + entityDataSubService.handleCmd(sessionRef, cmd); + } + } + private void handleWsAlarmDataCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { String sessionId = sessionRef.getSessionId(); log.debug("[{}] Processing: {}", sessionId, cmd); @@ -264,7 +280,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi } @Override - public void sendWsMsg(String sessionId, DataUpdate update) { + public void sendWsMsg(String sessionId, CmdUpdate update) { sendWsMsg(sessionId, update.getCmdId(), update); } @@ -679,6 +695,20 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi return true; } + private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) { + if (cmd.getCmdId() < 0) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Cmd id is negative value!"); + sendWsMsg(sessionRef, update); + return false; + } else if (cmd.getQuery() == null) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Query is empty!"); + sendWsMsg(sessionRef, update); + return false; + } + return true; + } + private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { if (cmd.getCmdId() < 0) { TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java index 6834d26b4d..c8cdbd06af 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.telemetry; +import org.thingsboard.server.service.telemetry.cmd.v2.CmdUpdate; import org.thingsboard.server.service.telemetry.cmd.v2.DataUpdate; import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; @@ -29,6 +30,6 @@ public interface TelemetryWebSocketService { void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update); - void sendWsMsg(String sessionId, DataUpdate update); + void sendWsMsg(String sessionId, CmdUpdate update); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java index 1a78a4d29a..351dfec15c 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.service.telemetry.cmd.v2; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public abstract class CmdUpdate { private final int cmdId; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java index 5da682502c..d0658044d0 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java @@ -15,18 +15,12 @@ */ package org.thingsboard.server.service.telemetry.cmd.v2; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.ToString; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; import java.util.List; -@JsonIgnoreProperties(ignoreUnknown = true) public abstract class DataUpdate extends CmdUpdate { @Getter diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java index 299413d9c9..2249940c48 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java @@ -35,16 +35,23 @@ import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EntityCountQuery; 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.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; @@ -243,6 +250,98 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest { Assert.assertEquals(new TsValue(dataPoint4.getTs(), dataPoint4.getValueAsString()), tsValues[0]); } + @Test + public void testEntityCountWsCmd() throws Exception { + Device device = new Device(); + device.setName("Device"); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + device = doPost("/api/device", device, Device.class); + + AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("temperature", 42L)); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Collections.singletonList(dataPoint1)); + + DeviceTypeFilter dtf1 = new DeviceTypeFilter(); + dtf1.setDeviceNameFilter("D"); + dtf1.setDeviceType("default"); + EntityCountQuery edq1 = new EntityCountQuery(dtf1, Collections.emptyList()); + + EntityCountCmd cmd1 = new EntityCountCmd(1, edq1); + + TelemetryPluginCmdsWrapper wrapper1 = new TelemetryPluginCmdsWrapper(); + wrapper1.setEntityCountCmds(Collections.singletonList(cmd1)); + + wsClient.send(mapper.writeValueAsString(wrapper1)); + String msg1 = wsClient.waitForReply(); + EntityCountUpdate update1 = mapper.readValue(msg1, EntityCountUpdate.class); + Assert.assertEquals(1, update1.getCmdId()); + Assert.assertEquals(1, update1.getCount()); + + DeviceTypeFilter dtf2 = new DeviceTypeFilter(); + dtf2.setDeviceNameFilter("D"); + dtf2.setDeviceType("non-existing-device-type"); + EntityCountQuery edq2 = new EntityCountQuery(dtf2, Collections.emptyList()); + + EntityCountCmd cmd2 = new EntityCountCmd(2, edq2); + + TelemetryPluginCmdsWrapper wrapper2 = new TelemetryPluginCmdsWrapper(); + wrapper2.setEntityCountCmds(Collections.singletonList(cmd2)); + wsClient.send(mapper.writeValueAsString(wrapper2)); + + String msg2 = wsClient.waitForReply(); + EntityCountUpdate update2 = mapper.readValue(msg2, EntityCountUpdate.class); + Assert.assertEquals(2, update2.getCmdId()); + Assert.assertEquals(0, update2.getCount()); + + KeyFilter highTemperatureFilter = new KeyFilter(); + highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromDouble(40)); + predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + highTemperatureFilter.setPredicate(predicate); + highTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC); + + DeviceTypeFilter dtf3 = new DeviceTypeFilter(); + dtf3.setDeviceNameFilter("D"); + dtf3.setDeviceType("default"); + EntityCountQuery edq3 = new EntityCountQuery(dtf3, Collections.singletonList(highTemperatureFilter)); + + EntityCountCmd cmd3 = new EntityCountCmd(3, edq3); + + TelemetryPluginCmdsWrapper wrapper3 = new TelemetryPluginCmdsWrapper(); + wrapper3.setEntityCountCmds(Collections.singletonList(cmd3)); + wsClient.send(mapper.writeValueAsString(wrapper3)); + + String msg3 = wsClient.waitForReply(); + EntityCountUpdate update3 = mapper.readValue(msg3, EntityCountUpdate.class); + Assert.assertEquals(3, update3.getCmdId()); + Assert.assertEquals(1, update3.getCount()); + + KeyFilter highTemperatureFilter2 = new KeyFilter(); + highTemperatureFilter2.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + NumericFilterPredicate predicate2 = new NumericFilterPredicate(); + predicate2.setValue(FilterPredicateValue.fromDouble(50)); + predicate2.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + highTemperatureFilter2.setPredicate(predicate2); + highTemperatureFilter2.setValueType(EntityKeyValueType.NUMERIC); + + DeviceTypeFilter dtf4 = new DeviceTypeFilter(); + dtf4.setDeviceNameFilter("D"); + dtf4.setDeviceType("default"); + EntityCountQuery edq4 = new EntityCountQuery(dtf4, Collections.singletonList(highTemperatureFilter2)); + + EntityCountCmd cmd4 = new EntityCountCmd(4, edq4); + + TelemetryPluginCmdsWrapper wrapper4 = new TelemetryPluginCmdsWrapper(); + wrapper4.setEntityCountCmds(Collections.singletonList(cmd4)); + wsClient.send(mapper.writeValueAsString(wrapper4)); + + String msg4 = wsClient.waitForReply(); + EntityCountUpdate update4 = mapper.readValue(msg4, EntityCountUpdate.class); + Assert.assertEquals(4, update4.getCmdId()); + Assert.assertEquals(0, update4.getCount()); + } + @Test public void testEntityDataLatestWidgetFlow() throws Exception { Device device = new Device(); diff --git a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java index 3186765130..a386f18c37 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java @@ -26,9 +26,9 @@ import java.util.Arrays; @RunWith(ClasspathSuite.class) @ClasspathSuite.ClassnameFilters({ -// "org.thingsboard.server.controller.sql.WebsocketApiSqlTest", + "org.thingsboard.server.controller.sql.WebsocketApiSqlTest", // "org.thingsboard.server.controller.sql.TenantProfileControllerSqlTest", - "org.thingsboard.server.controller.sql.*Test", +// "org.thingsboard.server.controller.sql.*Test", }) public class ControllerSqlTestSuite { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 8e8bd6638e..170a2edfa2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -249,18 +249,70 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { EntityType entityType = resolveEntityType(query.getEntityFilter()); QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, customerId, entityType)); - ctx.append("select count(e.id) from "); - ctx.append(addEntityTableQuery(ctx, query.getEntityFilter())); - ctx.append(" e where "); - ctx.append(buildEntityWhere(ctx, query.getEntityFilter(), Collections.emptyList())); - return transactionTemplate.execute(status -> { - long startTs = System.currentTimeMillis(); - try { - return jdbcTemplate.queryForObject(ctx.getQuery(), ctx, Long.class); - } finally { - queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - startTs); + if (query.getKeyFilters() == null || query.getKeyFilters().isEmpty()) { + ctx.append("select count(e.id) from "); + ctx.append(addEntityTableQuery(ctx, query.getEntityFilter())); + ctx.append(" e where "); + ctx.append(buildEntityWhere(ctx, query.getEntityFilter(), Collections.emptyList())); + return transactionTemplate.execute(status -> { + long startTs = System.currentTimeMillis(); + try { + return jdbcTemplate.queryForObject(ctx.getQuery(), ctx, Long.class); + } finally { + queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - startTs); + } + }); + } else { + List mappings = EntityKeyMapping.prepareEntityCountKeyMapping(query); + + List selectionMapping = mappings.stream().filter(EntityKeyMapping::isSelection) + .collect(Collectors.toList()); + List entityFieldsSelectionMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest()) + .collect(Collectors.toList()); + + List filterMapping = mappings.stream().filter(EntityKeyMapping::hasFilter) + .collect(Collectors.toList()); + List entityFieldsFiltersMapping = filterMapping.stream().filter(mapping -> !mapping.isLatest()) + .collect(Collectors.toList()); + + List allLatestMappings = mappings.stream().filter(EntityKeyMapping::isLatest) + .collect(Collectors.toList()); + + + String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), entityFieldsFiltersMapping); + String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true); + String entityFieldsSelection = EntityKeyMapping.buildSelections(entityFieldsSelectionMapping, query.getEntityFilter().getType(), entityType); + String entityTypeStr; + if (query.getEntityFilter().getType().equals(EntityFilterType.RELATIONS_QUERY)) { + entityTypeStr = "e.entity_type"; + } else { + entityTypeStr = "'" + entityType.name() + "'"; } - }); + if (!StringUtils.isEmpty(entityFieldsSelection)) { + entityFieldsSelection = String.format("e.id id, %s entity_type, %s", entityTypeStr, entityFieldsSelection); + } else { + entityFieldsSelection = String.format("e.id id, %s entity_type", entityTypeStr); + } + + String fromClauseCount = String.format("from (select %s from (select %s from %s e where %s) entities %s ) result %s", + "entities.*", + entityFieldsSelection, + addEntityTableQuery(ctx, query.getEntityFilter()), + entityWhereClause, + latestJoinsCnt, + ""); + + String countQuery = String.format("select count(id) %s", fromClauseCount); + + return transactionTemplate.execute(status -> { + long startTs = System.currentTimeMillis(); + try { + return jdbcTemplate.queryForObject(countQuery, ctx, Long.class); + } finally { + queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - startTs); + } + }); + } } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index 0bacdf6349..313a44f4a3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataSortOrder; import org.thingsboard.server.common.data.query.EntityFilter; @@ -380,6 +381,30 @@ public class EntityKeyMapping { return mappings; } + public static List prepareEntityCountKeyMapping(EntityCountQuery query) { + Map> filters = + query.getKeyFilters() != null ? + query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap(); + int index = 2; + List mappings = new ArrayList<>(); + if (!filters.isEmpty()) { + for (EntityKey filterField : filters.keySet()) { + EntityKeyMapping mapping = new EntityKeyMapping(); + mapping.setIndex(index); + mapping.setAlias(String.format("alias%s", index)); + mapping.setKeyFilters(filters.get(filterField)); + mapping.setLatest(!filterField.getType().equals(EntityKeyType.ENTITY_FIELD)); + mapping.setSelection(false); + mapping.setEntityKey(filterField); + mappings.add(mapping); + index += 1; + } + } + + return mappings; + } + + private String buildAttributeSelection() { return buildTimeSeriesOrAttrSelection(true); } From 0b43762a83d7644e416acd234d8df6043f7f0210 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 2 Mar 2021 12:04:45 +0200 Subject: [PATCH 3/5] Introduce entities count datasource. --- ui-ngx/package.json | 1 + ui-ngx/src/app/core/api/alias-controller.ts | 21 ++- .../app/core/api/entity-data-subscription.ts | 79 +++++++- .../src/app/core/api/entity-data.service.ts | 7 +- ui-ngx/src/app/core/http/entity.service.ts | 8 +- .../app/core/services/item-buffer.service.ts | 4 +- ui-ngx/src/app/core/services/utils.service.ts | 3 +- .../core/ws/telemetry-websocket.service.ts | 32 +++- .../attribute/attribute-table.component.ts | 1 + .../dashboard-page.component.ts | 1 + .../widget/data-key-config.component.html | 4 +- .../widget/data-key-config.component.ts | 9 +- .../widget/data-keys.component.html | 10 +- .../components/widget/data-keys.component.ts | 59 +++--- .../entities-hierarchy-widget.component.ts | 14 +- .../lib/entities-hierarchy-widget.models.ts | 11 +- .../widget/widget-config.component.html | 170 ++++++++++-------- .../widget/widget-config.component.scss | 40 ++++- .../widget/widget-config.component.ts | 51 ++++-- .../pages/widget/widget-library.component.ts | 1 + .../mat-chip-draggable.directive.ts | 2 + .../app/shared/models/query/query.models.ts | 9 +- .../models/telemetry/telemetry.models.ts | 111 +++++++++--- ui-ngx/src/app/shared/models/widget.models.ts | 6 +- ui-ngx/src/app/shared/shared.module.ts | 6 + .../assets/locale/locale.constant-en_US.json | 2 + ui-ngx/yarn.lock | 7 + 27 files changed, 491 insertions(+), 178 deletions(-) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 13913d8d97..775a685ae8 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -68,6 +68,7 @@ "ngx-clipboard": "^13.0.1", "ngx-color-picker": "^10.1.0", "ngx-daterangepicker-material": "^4.0.1", + "ngx-drag-drop": "^2.0.0", "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master", "ngx-hm-carousel": "^2.0.0-rc.1", "ngx-sharebuttons": "^8.0.1", diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index 6ca208c506..5f7474a31b 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -16,7 +16,7 @@ import { AliasInfo, IAliasController, StateControllerHolder, StateEntityInfo } from '@core/api/widget-api.models'; import { forkJoin, Observable, of, ReplaySubject, Subject } from 'rxjs'; -import { Datasource, DatasourceType } from '@app/shared/models/widget.models'; +import { Datasource, DatasourceType, datasourceTypeTranslationMap } from '@app/shared/models/widget.models'; import { deepClone, isEqual } from '@core/utils'; import { EntityService } from '@core/http/entity.service'; import { UtilsService } from '@core/services/utils.service'; @@ -27,6 +27,7 @@ import { defaultEntityDataPageLink, Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, updateDatasourceFromEntityInfo } from '@shared/models/query/query.models'; +import { TranslateService } from '@ngx-translate/core'; export class AliasController implements IAliasController { @@ -50,6 +51,7 @@ export class AliasController implements IAliasController { constructor(private utils: UtilsService, private entityService: EntityService, + private translate: TranslateService, private stateControllerHolder: StateControllerHolder, private origEntityAliases: EntityAliases, private origFilters: Filters) { @@ -241,7 +243,7 @@ export class AliasController implements IAliasController { private resolveDatasource(datasource: Datasource, forceFilter = false): Observable { const newDatasource = deepClone(datasource); - if (newDatasource.type === DatasourceType.entity) { + if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount) { if (newDatasource.filterId) { newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId); } @@ -329,16 +331,23 @@ export class AliasController implements IAliasController { return forkJoin(observables).pipe( map((result) => { let functionIndex = 0; + let entityCountIndex = 0; result.forEach((datasource) => { - if (datasource.type === DatasourceType.function) { + if (datasource.type === DatasourceType.function || datasource.type === DatasourceType.entityCount) { let name: string; if (datasource.name && datasource.name.length) { name = datasource.name; } else { - functionIndex++; - name = DatasourceType.function; - if (functionIndex > 1) { + if (datasource.type === DatasourceType.function) { + functionIndex++; + } else { + entityCountIndex++; + } + name = this.translate.instant(datasourceTypeTranslationMap.get(datasource.type)); + if (datasource.type === DatasourceType.function && functionIndex > 1) { name += ' ' + functionIndex; + } else if (datasource.type === DatasourceType.entityCount && entityCountIndex > 1) { + name += ' ' + entityCountIndex; } } datasource.name = name; diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index 6a8e21a389..79c505b07a 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -22,12 +22,14 @@ import { EntityFilter, EntityKey, EntityKeyType, - entityKeyTypeToDataKeyType, entityPageDataChanged, + entityKeyTypeToDataKeyType, + entityPageDataChanged, KeyFilter, TsValue } from '@shared/models/query/query.models'; import { DataKeyType, + EntityCountCmd, EntityDataCmd, SubscriptionData, SubscriptionDataHolder, @@ -42,6 +44,7 @@ import { DataAggregator } from '@core/api/data-aggregator'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { EntityType } from '@shared/models/entity-type.models'; import { Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { EntityId } from '@shared/models/id/entity-id'; import Timeout = NodeJS.Timeout; declare type DataKeyFunction = (time: number, prevValue: any) => any; @@ -82,6 +85,7 @@ export class EntityDataSubscription { private subscriber: TelemetrySubscriber; private dataCommand: EntityDataCmd; private subsCommand: EntityDataCmd; + private countCommand: EntityCountCmd; private attrFields: Array; private tsFields: Array; @@ -91,7 +95,7 @@ export class EntityDataSubscription { private pageData: PageData; private subsTw: SubscriptionTimewindow; private dataAggregators: Array; - private dataKeys: {[key: string]: Array | SubscriptionDataKey} = {} + private dataKeys: {[key: string]: Array | SubscriptionDataKey} = {}; private datasourceData: {[index: number]: {[key: string]: DataSetHolder}}; private datasourceOrigData: {[index: number]: {[key: string]: DataSetHolder}}; private entityIdToDataIndex: {[id: string]: number}; @@ -125,7 +129,8 @@ export class EntityDataSubscription { } } let key: string; - if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount || + this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.datasourceType === DatasourceType.function) { key = `${dataKey.name}_${dataKey.index}_${dataKey.type}`; } else { @@ -159,7 +164,7 @@ export class EntityDataSubscription { if (this.dataAggregators) { this.dataAggregators.forEach((aggregator) => { aggregator.destroy(); - }) + }); this.dataAggregators = null; } this.pageData = null; @@ -291,6 +296,65 @@ export class EntityDataSubscription { totalPages: 1 }; this.onPageData(pageData); + } else if (this.datasourceType === DatasourceType.entityCount) { + this.subscriber = new TelemetrySubscriber(this.telemetryService); + this.countCommand = new EntityCountCmd(); + let keyFilters = this.entityDataSubscriptionOptions.keyFilters; + if (this.entityDataSubscriptionOptions.additionalKeyFilters) { + if (keyFilters) { + keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); + } else { + keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; + } + } + this.countCommand.query = { + entityFilter: this.entityDataSubscriptionOptions.entityFilter, + keyFilters + }; + this.subscriber.subscriptionCommands.push(this.countCommand); + + const entityId: EntityId = { + id: NULL_UUID, + entityType: null + }; + + const entityData: EntityData = { + entityId, + timeseries: {}, + latest: {} + }; + entityData.latest[EntityKeyType.ENTITY_FIELD] = { + name: {ts: Date.now(), value: DatasourceType.entityCount}, + }; + + const countKey = this.entityDataSubscriptionOptions.dataKeys[0]; + + this.subscriber.entityCount$.subscribe( + (entityCountUpdate) => { + if (!entityData.latest[EntityKeyType.COUNT]) { + entityData.latest[EntityKeyType.COUNT] = {}; + entityData.latest[EntityKeyType.COUNT][countKey.name] = { + ts: Date.now(), + value: entityCountUpdate.count + '' + }; + const pageData: PageData = { + data: [entityData], + hasNext: false, + totalElements: 1, + totalPages: 1 + }; + this.onPageData(pageData); + } else { + const update = [deepClone(entityData)]; + update[0].latest[EntityKeyType.COUNT][countKey.name] = { + ts: Date.now(), + value: entityCountUpdate.count + '' + }; + this.onDataUpdate(update); + } + } + ); + this.subscriber.subscribe(); } if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { return of(null); @@ -347,7 +411,7 @@ export class EntityDataSubscription { limit: this.subsTw.aggregation.limit, agg: this.subsTw.aggregation.type, fetchLatestPreviousPoint: this.subsTw.aggregation.stateData - } + }; } } } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { @@ -381,7 +445,7 @@ export class EntityDataSubscription { if (this.dataAggregators) { this.dataAggregators.forEach((aggregator) => { aggregator.destroy(); - }) + }); } this.dataAggregators = []; this.resetData(); @@ -419,7 +483,8 @@ export class EntityDataSubscription { this.datasourceData[dataIndex] = {}; for (const key of Object.keys(this.dataKeys)) { const dataKey = this.dataKeys[key]; - if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount || + this.entityDataSubscriptionOptions.type === widgetType.timeseries) { const dataKeysList = dataKey as Array; for (let index = 0; index < dataKeysList.length; index++) { this.datasourceData[dataIndex][key + '_' + index] = { diff --git a/ui-ngx/src/app/core/api/entity-data.service.ts b/ui-ngx/src/app/core/api/entity-data.service.ts index c02f36a290..7b664417d0 100644 --- a/ui-ngx/src/app/core/api/entity-data.service.ts +++ b/ui-ngx/src/app/core/api/entity-data.service.ts @@ -135,11 +135,14 @@ export class EntityDataService { dataKeys: subscriptionDataKeys, type: subscriptionType }; - if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { + if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity || + entityDataSubscriptionOptions.datasourceType === DatasourceType.entityCount) { entityDataSubscriptionOptions.entityFilter = datasource.entityFilter; - entityDataSubscriptionOptions.pageLink = pageLink; entityDataSubscriptionOptions.keyFilters = keyFilters; entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; + if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { + entityDataSubscriptionOptions.pageLink = pageLink; + } } entityDataSubscriptionOptions.isPaginatedDataSubscription = isPaginatedDataSubscription; return entityDataSubscriptionOptions; diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 1118307d06..17a35b2305 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -1099,10 +1099,10 @@ export class EntityService { dataKeys: [] }; this.prepareEntityFilterFromSubscriptionInfo(datasource, subscriptionInfo); - } else if (subscriptionInfo.type === DatasourceType.function) { + } else if (subscriptionInfo.type === DatasourceType.function || subscriptionInfo.type === DatasourceType.entityCount) { datasource = { type: subscriptionInfo.type, - name: subscriptionInfo.name || DatasourceType.function, + name: subscriptionInfo.name || subscriptionInfo.type, dataKeys: [] }; } @@ -1119,6 +1119,10 @@ export class EntityService { if (subscriptionInfo.alarmFields) { this.createDatasourceKeys(subscriptionInfo.alarmFields, DataKeyType.alarm, datasource); } + if (subscriptionInfo.type === DatasourceType.entityCount) { + const dataKey = this.utils.createKey({ name: 'count'}, DataKeyType.count); + datasource.dataKeys.push(dataKey); + } } return datasource; } diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index f3614e88d0..cabe78ff6e 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -93,7 +93,7 @@ export class ItemBufferService { if (widget.config.datasources) { for (let i = 0; i < widget.config.datasources.length; i++) { const datasource = widget.config.datasources[i]; - if (datasource.type === DatasourceType.entity && datasource.entityAliasId) { + if ((datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) && datasource.entityAliasId) { entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId]; if (entityAlias) { aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias); @@ -119,7 +119,7 @@ export class ItemBufferService { if (widget.config.datasources) { for (let i = 0; i < widget.config.datasources.length; i++) { const datasource = widget.config.datasources[i]; - if (datasource.type === DatasourceType.entity && datasource.filterId) { + if ((datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) && datasource.filterId) { filter = dashboard.configuration.filters[datasource.filterId]; if (filter) { filtersInfo.datasourceFilters[i] = this.prepareFilterInfo(filter); diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 90b2989834..3c1c659e95 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -27,7 +27,8 @@ import { deleteNullProperties, guid, isDefined, - isDefinedAndNotNull, isString, + isDefinedAndNotNull, + isString, isUndefined } from '@core/utils'; import { WindowMessage } from '@shared/models/window-message.model'; diff --git a/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts b/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts index facdd0f60a..a3fc6eb767 100644 --- a/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts +++ b/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts @@ -16,18 +16,27 @@ import { Inject, Injectable, NgZone } from '@angular/core'; import { - AlarmDataCmd, AlarmDataUnsubscribeCmd, + AlarmDataCmd, + AlarmDataUnsubscribeCmd, AlarmDataUpdate, - AttributesSubscriptionCmd, EntityDataCmd, EntityDataUnsubscribeCmd, EntityDataUpdate, - GetHistoryCmd, isAlarmDataUpdateMsg, isEntityDataUpdateMsg, + AttributesSubscriptionCmd, + EntityCountCmd, EntityCountUnsubscribeCmd, + EntityCountUpdate, + EntityDataCmd, + EntityDataUnsubscribeCmd, + EntityDataUpdate, + GetHistoryCmd, + isAlarmDataUpdateMsg, + isEntityCountUpdateMsg, + isEntityDataUpdateMsg, SubscriptionCmd, SubscriptionUpdate, - SubscriptionUpdateMsg, TelemetryFeature, TelemetryPluginCmdsWrapper, TelemetryService, TelemetrySubscriber, - TimeseriesSubscriptionCmd, WebsocketDataMsg + TimeseriesSubscriptionCmd, + WebsocketDataMsg } from '@app/shared/models/telemetry/telemetry.models'; import { select, Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -111,6 +120,8 @@ export class TelemetryWebsocketService implements TelemetryService { this.cmdsWrapper.entityDataCmds.push(subscriptionCommand); } else if (subscriptionCommand instanceof AlarmDataCmd) { this.cmdsWrapper.alarmDataCmds.push(subscriptionCommand); + } else if (subscriptionCommand instanceof EntityCountCmd) { + this.cmdsWrapper.entityCountCmds.push(subscriptionCommand); } } ); @@ -150,6 +161,10 @@ export class TelemetryWebsocketService implements TelemetryService { const alarmDataUnsubscribeCmd = new AlarmDataUnsubscribeCmd(); alarmDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; this.cmdsWrapper.alarmDataUnsubscribeCmds.push(alarmDataUnsubscribeCmd); + } else if (subscriptionCommand instanceof EntityCountCmd) { + const entityCountUnsubscribeCmd = new EntityCountUnsubscribeCmd(); + entityCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; + this.cmdsWrapper.entityCountUnsubscribeCmds.push(entityCountUnsubscribeCmd); } const cmdId = subscriptionCommand.cmdId; if (cmdId) { @@ -236,7 +251,7 @@ export class TelemetryWebsocketService implements TelemetryService { { url: uri, openObserver: { - next: (e: Event) => { + next: () => { this.onOpen(); } }, @@ -294,6 +309,11 @@ export class TelemetryWebsocketService implements TelemetryService { if (subscriber) { subscriber.onAlarmData(new AlarmDataUpdate(message)); } + } else if (isEntityCountUpdateMsg(message)) { + subscriber = this.subscribersMap.get(message.cmdId); + if (subscriber) { + subscriber.onEntityCount(new EntityCountUpdate(message)); + } } else if (message.subscriptionId) { subscriber = this.subscribersMap.get(message.subscriptionId); if (subscriber) { diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index 6456e6d6ed..7e83c67ec6 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -403,6 +403,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI this.aliasController = new AliasController(this.utils, this.entityService, + this.translate, () => stateController, entitiAliases, filters); const dataKeyType: DataKeyType = this.attributeScope === LatestTelemetry.LATEST_TELEMETRY ? diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 5385913842..c154ef2867 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -312,6 +312,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.dashboardCtx.aliasController = new AliasController(this.utils, this.entityService, + this.translate, () => this.dashboardCtx.stateController, this.dashboardConfiguration.entityAliases, this.dashboardConfiguration.filters); diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html index 45cc3a8087..fbc62010da 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html @@ -25,7 +25,7 @@ formControlName="name" required [matAutocomplete]="keyAutocomplete"> - - close + close - + @@ -128,7 +128,7 @@ {{ translate.get('entity.no-key-matching', {key: truncate.transform(searchText, true, 6, '...')}) | async }} - + entity.create-new-key diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts index c3f138cb80..96bd8c702f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts @@ -16,26 +16,26 @@ import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { - AfterViewInit, - Component, - ElementRef, - forwardRef, - Input, - OnChanges, - OnInit, - SimpleChanges, - SkipSelf, - ViewChild + AfterViewInit, + Component, + ElementRef, + forwardRef, + Input, + OnChanges, + OnInit, + SimpleChanges, + SkipSelf, + ViewChild } from '@angular/core'; import { - ControlValueAccessor, - FormBuilder, - FormControl, - FormGroup, - FormGroupDirective, - NG_VALUE_ACCESSOR, - NgForm, - Validators + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NG_VALUE_ACCESSOR, + NgForm, + Validators } from '@angular/forms'; import { Observable, of } from 'rxjs'; import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; @@ -56,8 +56,8 @@ import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { DialogService } from '@core/services/dialog.service'; import { MatDialog } from '@angular/material/dialog'; import { - DataKeyConfigDialogComponent, - DataKeyConfigDialogData + DataKeyConfigDialogComponent, + DataKeyConfigDialogData } from '@home/components/widget/data-key-config-dialog.component'; import { deepClone } from '@core/utils'; import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive'; @@ -94,8 +94,15 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie @Input() datasourceType: DatasourceType; + private maxDataKeysValue: number; + get maxDataKeys(): number { + return this.datasourceType === DatasourceType.entityCount ? 1 : this.maxDataKeysValue; + } + @Input() - maxDataKeys: number; + set maxDataKeys(value: number) { + this.maxDataKeysValue = value; + } @Input() optDataKeys: boolean; @@ -114,7 +121,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie private requiredValue: boolean; get required(): boolean { - return this.requiredValue || !this.optDataKeys; + return this.requiredValue || !this.optDataKeys || this.datasourceType === DatasourceType.entityCount; } @Input() set required(value: boolean) { @@ -210,8 +217,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie if (this.maxDataKeys !== null && this.maxDataKeys > -1) { if (this.datasourceType === DatasourceType.function) { return this.translate.instant('datakey.maximum-function-types', {count: this.maxDataKeys}); - } else { + } else if (this.datasourceType !== DatasourceType.entityCount) { return this.translate.instant('datakey.maximum-timeseries-or-attributes', {count: this.maxDataKeys}); + } else { + return ''; } } else { return ''; @@ -241,6 +250,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie private reset() { if (this.widgetType === widgetType.alarm) { this.keys = this.utils.getDefaultAlarmDataKeys(); + } else if (this.datasourceType === DatasourceType.entityCount) { + this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count)]; } else { this.keys = []; } @@ -426,7 +437,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; fetchObservable = of(targetKeysList); } else { - if (this.entityAliasId) { + if (this.datasourceType !== DatasourceType.entityCount && this.entityAliasId) { const dataKeyTypes = [DataKeyType.timeseries]; if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { dataKeyTypes.push(DataKeyType.attribute); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts index e79f6ef0e4..5fb11d025b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts @@ -84,7 +84,6 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O private widgetConfig: WidgetConfig; private subscription: IWidgetSubscription; private datasources: Array; - private data: Array>; private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {}; private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {}; @@ -121,7 +120,6 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O this.widgetConfig = this.ctx.widgetConfig; this.subscription = this.ctx.defaultSubscription; this.datasources = this.subscription.datasources as Array; - this.data = this.subscription.dataPages[0].data; this.initializeConfig(); this.ctx.updateWidgetParams(); } @@ -252,12 +250,16 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O public loadNodes: LoadNodesCallback = (node, cb) => { if (node.id === '#') { const childNodes: HierarchyNavTreeNode[] = []; + let dataIndex = 0; this.datasources.forEach((childDatasource, index) => { - childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, this.data[index])); + const datasourceData = this.subscription.data.slice(dataIndex); + childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, datasourceData)); + dataIndex += childDatasource.dataKeys.length; }); cb(this.prepareNodes(childNodes)); } else { - if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.nodeCtx.entity.id.entityType !== 'function') { + if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.datasource.type === DatasourceType.entity + && node.data.nodeCtx.entity.id.entityType !== 'function') { this.loadChildren(node, node.data.datasource, cb); /* (error) => { // TODO: let errorText = 'Failed to get relations!'; @@ -369,8 +371,8 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O } private datasourceToNode(datasource: HierarchyNodeDatasource, - data: DatasourceData[], - parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode { + data: DatasourceData[], + parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode { const node: HierarchyNavTreeNode = { id: (++this.nodeIdCounter) + '' }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts index 07ab91fe5d..74166b1742 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts @@ -153,7 +153,16 @@ export const defaultNodeOpenedFunction: NodeOpenedFunction = nodeCtx => { }; export const defaultNodesSortFunction: NodesSortFunction = (nodeCtx1, nodeCtx2) => { - let result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); + let result = 0; + if (!nodeCtx1.entity.id.entityType || !nodeCtx2.entity.id.entityType ) { + if (nodeCtx1.entity.id.entityType) { + result = 1; + } else if (nodeCtx2.entity.id.entityType) { + result = -1; + } + } else { + result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); + } if (result === 0) { result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name); } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index e47d96bebe..8b745c480f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -100,87 +100,115 @@
- +
- widget-config.datasource-type + widget-config.datasource-type widget-config.datasource-parameters
-
- {{$index + 1}}. -
-
- - - - {{ datasourceTypesTranslations.get(datasourceType) | translate }} - - - -
- - - - + + + + +
+
+ + {{$index + 1}}. +
+
+
+ + + + {{ datasourceTypesTranslations.get(datasourceType) | translate }} + + - - -
- - - - +
+ + + + + + + + + + + + + + + +
- -
- - -
- -
-
+ + +
+ +
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss index 39182661f1..6e1e1cabdf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss @@ -23,10 +23,34 @@ .tb-advanced-widget-config { height: 100%; } + .tb-datasources { + + .handle { + cursor: move; + } + + .mat-list { + min-height: 68px; + padding-left: 0; + } + + .mat-list-item { + height: auto; + min-height: 68px; + display: block; + &.dndDraggingSource { + display: none; + } + &.dndPlaceholder { + display: block; + background-color: #ddd; + } + } + } .tb-datasource-type { - min-width: 110px; + min-width: 120px; @media #{$mat-gt-sm} { - max-width: 110px; + max-width: 120px; } } .tb-datasource { @@ -70,13 +94,18 @@ white-space: normal; } .mat-expansion-panel { - &.tb-datasources{ + &.tb-datasources { &.mat-expanded { overflow: visible; } .mat-expansion-panel-body{ padding: 0 12px 16px; } + .mat-list-base .mat-list-item { + .mat-list-item-content { + padding: 0; + } + } } .mat-expansion-panel-content { font: inherit; @@ -110,6 +139,11 @@ } } } + .tb-datasource-name.no-border-top { + .mat-form-field-infix { + border-top: 0; + } + } } .tb-data-keys { @media #{$mat-gt-sm} { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 05c6264a92..523913afe4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -40,7 +40,7 @@ import { Validators } from '@angular/forms'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { deepClone, isDefined, isObject } from '@app/core/utils'; +import { deepClone, isDefined, isObject, isUndefined } from '@app/core/utils'; import { alarmFields, AlarmSearchStatus, @@ -71,6 +71,7 @@ import { Filter, Filters } from '@shared/models/query/query.models'; import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { MatChipInputEvent } from '@angular/material/chips'; +import { DndDropEvent } from 'ngx-drag-drop'; const emptySettingsSchema: JsonSchema = { type: 'object', @@ -141,7 +142,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont widgetType: widgetType; datasourceType = DatasourceType; - datasourceTypes: Array; + datasourceTypes: Array = []; datasourceTypesTranslations = datasourceTypeTranslationMap; widgetConfigCallbacks: WidgetConfigCallbacks = { @@ -186,11 +187,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } ngOnInit(): void { - if (this.functionsOnly) { - this.datasourceTypes = [DatasourceType.function]; - } else { - this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; - } this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); this.alarmSourceSettings = this.fb.group({}); @@ -295,6 +291,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } private buildForms() { + if (this.functionsOnly) { + this.datasourceTypes = [DatasourceType.function]; + } else { + this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; + if (this.widgetType === widgetType.latest) { + this.datasourceTypes.push(DatasourceType.entityCount); + } + } this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); this.alarmSourceSettings = this.fb.group({}); @@ -517,22 +521,28 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } private buildDatasourceForm(datasource?: Datasource): FormGroup { - const dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional; + let dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional + || datasource?.type === DatasourceType.entityCount; const datasourceFormGroup = this.fb.group( { type: [datasource ? datasource.type : null, [Validators.required]], name: [datasource ? datasource.name : null, []], entityAliasId: [datasource ? datasource.entityAliasId : null, - datasource && datasource.type === DatasourceType.entity ? [Validators.required] : []], + datasource && (datasource.type === DatasourceType.entity || + datasource.type === DatasourceType.entityCount) ? [Validators.required] : []], filterId: [datasource ? datasource.filterId : null, []], dataKeys: [datasource ? datasource.dataKeys : null, dataKeysRequired ? [Validators.required] : []] } ); datasourceFormGroup.get('type').valueChanges.subscribe((type: DatasourceType) => { datasourceFormGroup.get('entityAliasId').setValidators( - type === DatasourceType.entity ? [Validators.required] : [] + (type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : [] ); + dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional + || type === DatasourceType.entityCount; + datasourceFormGroup.get('dataKeys').setValidators(dataKeysRequired ? [Validators.required] : []); datasourceFormGroup.get('entityAliasId').updateValueAndValidity(); + datasourceFormGroup.get('dataKeys').updateValueAndValidity(); }); return datasourceFormGroup; } @@ -659,8 +669,22 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont return !!this.modelValue && !!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema; } + public dndDatasourceMoved(index: number) { + this.datasourcesFormArray().removeAt(index); + } + + public onDatasourceDrop(event: DndDropEvent) { + let index = event.index; + if (isUndefined(index)) { + index = this.datasourcesFormArray().length; + } + this.datasourcesFormArray().insert(index, + this.buildDatasourceForm(event.data) + ); + } + public removeDatasource(index: number) { - (this.dataSettings.get('datasources') as FormArray).removeAt(index); + this.datasourcesFormArray().removeAt(index); } public addDatasource() { @@ -673,8 +697,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont dataKeys: [] }; } - const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray; - datasourcesFormArray.push(this.buildDatasourceForm(newDatasource)); + this.datasourcesFormArray().push(this.buildDatasourceForm(newDatasource)); } public generateDataKey(chip: any, type: DataKeyType): DataKey { @@ -704,6 +727,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont if (!result.funcBody) { result.funcBody = 'return prevValue + 1;'; } + } else if (type === DataKeyType.count) { + result.name = 'count'; } if (isDefined(this.modelValue.dataKeySettingsSchema.schema)) { result.settings = this.utils.generateObjectFromJsonSchema(this.modelValue.dataKeySettingsSchema.schema); diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts index 8e99a14016..0be34d5f39 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts @@ -84,6 +84,7 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { aliasController: IAliasController = new AliasController(this.utils, this.entityService, + this.translate, () => { return { getStateParams(): StateParams { return {}; diff --git a/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts b/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts index 246f38b87a..f9ada15d64 100644 --- a/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts +++ b/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts @@ -124,6 +124,7 @@ class DraggableChip { if (this.preventDrag) { event.preventDefault(); } else { + event.stopPropagation(); this.dragging = true; globalDraggingChipListId = this.chipListElement.id; this.chipListElement.classList.add(draggingClassName); @@ -159,6 +160,7 @@ class DraggableChip { } private onDragEnd(event: Event | any) { + event.stopPropagation(); this.dragging = false; globalDraggingChipListId = null; this.chipListElement.classList.remove(draggingClassName); diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index b8dc2564bc..dc21f70907 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -36,7 +36,8 @@ export enum EntityKeyType { TIME_SERIES = 'TIME_SERIES', ENTITY_FIELD = 'ENTITY_FIELD', ALARM_FIELD = 'ALARM_FIELD', - CONSTANT = 'CONSTANT' + CONSTANT = 'CONSTANT', + COUNT = 'COUNT' } export const entityKeyTypeTranslationMap = new Map( @@ -61,6 +62,8 @@ export function entityKeyTypeToDataKeyType(entityKeyType: EntityKeyType): DataKe return DataKeyType.entityField; case EntityKeyType.ALARM_FIELD: return DataKeyType.alarm; + case EntityKeyType.COUNT: + return DataKeyType.count; } } @@ -76,6 +79,8 @@ export function dataKeyTypeToEntityKeyType(dataKeyType: DataKeyType): EntityKeyT return EntityKeyType.ALARM_FIELD; case DataKeyType.entityField: return EntityKeyType.ENTITY_FIELD; + case DataKeyType.count: + return EntityKeyType.COUNT; } } @@ -716,13 +721,13 @@ export const defaultEntityDataPageLink: EntityDataPageLink = createDefaultEntity export interface EntityCountQuery { entityFilter: EntityFilter; + keyFilters?: Array; } export interface AbstractDataQuery extends EntityCountQuery { pageLink: T; entityFields?: Array; latestValues?: Array; - keyFilters?: Array; } export interface EntityDataQuery extends AbstractDataQuery { diff --git a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts index 1e77285667..cfbde53233 100644 --- a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts +++ b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts @@ -23,7 +23,7 @@ import { map } from 'rxjs/operators'; import { NgZone } from '@angular/core'; import { AlarmData, - AlarmDataQuery, + AlarmDataQuery, EntityCountQuery, EntityData, EntityDataQuery, EntityKey, @@ -36,7 +36,8 @@ export enum DataKeyType { attribute = 'attribute', function = 'function', alarm = 'alarm', - entityField = 'entityField' + entityField = 'entityField', + count = 'count' } export enum LatestTelemetry { @@ -181,6 +182,11 @@ export class EntityDataCmd implements WebsocketCmd { } } +export class EntityCountCmd implements WebsocketCmd { + cmdId: number; + query?: EntityCountQuery; +} + export class AlarmDataCmd implements WebsocketCmd { cmdId: number; query?: AlarmDataQuery; @@ -194,6 +200,10 @@ export class EntityDataUnsubscribeCmd implements WebsocketCmd { cmdId: number; } +export class EntityCountUnsubscribeCmd implements WebsocketCmd { + cmdId: number; +} + export class AlarmDataUnsubscribeCmd implements WebsocketCmd { cmdId: number; } @@ -206,6 +216,8 @@ export class TelemetryPluginCmdsWrapper { entityDataUnsubscribeCmds: Array; alarmDataCmds: Array; alarmDataUnsubscribeCmds: Array; + entityCountCmds: Array; + entityCountUnsubscribeCmds: Array; constructor() { this.attrSubCmds = []; @@ -215,6 +227,8 @@ export class TelemetryPluginCmdsWrapper { this.entityDataUnsubscribeCmds = []; this.alarmDataCmds = []; this.alarmDataUnsubscribeCmds = []; + this.entityCountCmds = []; + this.entityCountUnsubscribeCmds = []; } public hasCommands(): boolean { @@ -224,7 +238,9 @@ export class TelemetryPluginCmdsWrapper { this.entityDataCmds.length > 0 || this.entityDataUnsubscribeCmds.length > 0 || this.alarmDataCmds.length > 0 || - this.alarmDataUnsubscribeCmds.length > 0; + this.alarmDataUnsubscribeCmds.length > 0 || + this.entityCountCmds.length > 0 || + this.entityCountUnsubscribeCmds.length > 0; } public clear() { @@ -235,6 +251,8 @@ export class TelemetryPluginCmdsWrapper { this.entityDataUnsubscribeCmds.length = 0; this.alarmDataCmds.length = 0; this.alarmDataUnsubscribeCmds.length = 0; + this.entityCountCmds.length = 0; + this.entityCountUnsubscribeCmds.length = 0; } public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper { @@ -253,6 +271,10 @@ export class TelemetryPluginCmdsWrapper { preparedWrapper.alarmDataCmds = this.popCmds(this.alarmDataCmds, leftCount); leftCount -= preparedWrapper.alarmDataCmds.length; preparedWrapper.alarmDataUnsubscribeCmds = this.popCmds(this.alarmDataUnsubscribeCmds, leftCount); + leftCount -= preparedWrapper.alarmDataUnsubscribeCmds.length; + preparedWrapper.entityCountCmds = this.popCmds(this.entityCountCmds, leftCount); + leftCount -= preparedWrapper.entityCountCmds.length; + preparedWrapper.entityCountUnsubscribeCmds = this.popCmds(this.entityCountUnsubscribeCmds, leftCount); return preparedWrapper; } @@ -280,40 +302,54 @@ export interface SubscriptionUpdateMsg extends SubscriptionDataHolder { errorMsg: string; } -export enum DataUpdateType { +export enum CmdUpdateType { ENTITY_DATA = 'ENTITY_DATA', - ALARM_DATA = 'ALARM_DATA' + ALARM_DATA = 'ALARM_DATA', + COUNT_DATA = 'COUNT_DATA' } -export interface DataUpdateMsg { +export interface CmdUpdateMsg { cmdId: number; - data?: PageData; - update?: Array; errorCode: number; errorMsg: string; - dataUpdateType: DataUpdateType; + cmdUpdateType: CmdUpdateType; +} + +export interface DataUpdateMsg extends CmdUpdateMsg { + data?: PageData; + update?: Array; } export interface EntityDataUpdateMsg extends DataUpdateMsg { - dataUpdateType: DataUpdateType.ENTITY_DATA; + cmdUpdateType: CmdUpdateType.ENTITY_DATA; } export interface AlarmDataUpdateMsg extends DataUpdateMsg { - dataUpdateType: DataUpdateType.ALARM_DATA; + cmdUpdateType: CmdUpdateType.ALARM_DATA; allowedEntities: number; totalEntities: number; } -export type WebsocketDataMsg = AlarmDataUpdateMsg | EntityDataUpdateMsg | SubscriptionUpdateMsg; +export interface EntityCountUpdateMsg extends CmdUpdateMsg { + cmdUpdateType: CmdUpdateType.COUNT_DATA; + count: number; +} + +export type WebsocketDataMsg = AlarmDataUpdateMsg | EntityDataUpdateMsg | EntityCountUpdateMsg | SubscriptionUpdateMsg; export function isEntityDataUpdateMsg(message: WebsocketDataMsg): message is EntityDataUpdateMsg { - const updateMsg = (message as DataUpdateMsg); - return updateMsg.cmdId !== undefined && updateMsg.dataUpdateType === DataUpdateType.ENTITY_DATA; + const updateMsg = (message as CmdUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.ENTITY_DATA; } export function isAlarmDataUpdateMsg(message: WebsocketDataMsg): message is AlarmDataUpdateMsg { - const updateMsg = (message as DataUpdateMsg); - return updateMsg.cmdId !== undefined && updateMsg.dataUpdateType === DataUpdateType.ALARM_DATA; + const updateMsg = (message as CmdUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.ALARM_DATA; +} + +export function isEntityCountUpdateMsg(message: WebsocketDataMsg): message is EntityCountUpdateMsg { + const updateMsg = (message as CmdUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.COUNT_DATA; } export class SubscriptionUpdate implements SubscriptionUpdateMsg { @@ -365,21 +401,28 @@ export class SubscriptionUpdate implements SubscriptionUpdateMsg { } } -export class DataUpdate implements DataUpdateMsg { +export class CmdUpdate implements CmdUpdateMsg { cmdId: number; errorCode: number; errorMsg: string; - data?: PageData; - update?: Array; - dataUpdateType: DataUpdateType; + cmdUpdateType: CmdUpdateType; - constructor(msg: DataUpdateMsg) { + constructor(msg: CmdUpdateMsg) { this.cmdId = msg.cmdId; this.errorCode = msg.errorCode; this.errorMsg = msg.errorMsg; + this.cmdUpdateType = msg.cmdUpdateType; + } +} + +export class DataUpdate extends CmdUpdate implements DataUpdateMsg { + data?: PageData; + update?: Array; + + constructor(msg: DataUpdateMsg) { + super(msg); this.data = msg.data; this.update = msg.update; - this.dataUpdateType = msg.dataUpdateType; } } @@ -400,6 +443,15 @@ export class AlarmDataUpdate extends DataUpdate { } } +export class EntityCountUpdate extends CmdUpdate { + count: number; + + constructor(msg: EntityCountUpdateMsg) { + super(msg); + this.count = msg.count; + } +} + export interface TelemetryService { subscribe(subscriber: TelemetrySubscriber); update(subscriber: TelemetrySubscriber); @@ -411,6 +463,7 @@ export class TelemetrySubscriber { private dataSubject = new ReplaySubject(1); private entityDataSubject = new ReplaySubject(1); private alarmDataSubject = new ReplaySubject(1); + private entityCountSubject = new ReplaySubject(1); private reconnectSubject = new Subject(); private zone: NgZone; @@ -420,6 +473,7 @@ export class TelemetrySubscriber { public data$ = this.dataSubject.asObservable(); public entityData$ = this.entityDataSubject.asObservable(); public alarmData$ = this.alarmDataSubject.asObservable(); + public entityCount$ = this.entityCountSubject.asObservable(); public reconnect$ = this.reconnectSubject.asObservable(); public static createEntityAttributesSubscription(telemetryService: TelemetryService, @@ -464,6 +518,7 @@ export class TelemetrySubscriber { this.dataSubject.complete(); this.entityDataSubject.complete(); this.alarmDataSubject.complete(); + this.entityCountSubject.complete(); this.reconnectSubject.complete(); } @@ -513,6 +568,18 @@ export class TelemetrySubscriber { } } + public onEntityCount(message: EntityCountUpdate) { + if (this.zone) { + this.zone.run( + () => { + this.entityCountSubject.next(message); + } + ); + } else { + this.entityCountSubject.next(message); + } + } + public onReconnected() { this.reconnectSubject.next(); } diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 179a6ff47b..55e6b88e34 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -246,13 +246,15 @@ export interface DataKey extends KeyInfo { export enum DatasourceType { function = 'function', - entity = 'entity' + entity = 'entity', + entityCount = 'entityCount' } export const datasourceTypeTranslationMap = new Map( [ [ DatasourceType.function, 'function.function' ], - [ DatasourceType.entity, 'entity.entity' ] + [ DatasourceType.entity, 'entity.entity' ], + [ DatasourceType.entityCount, 'entity.entities-count' ] ] ); diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index ba56ad72fc..a142a48e90 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -52,6 +52,7 @@ import { MatTableModule } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatListModule } from '@angular/material/list'; import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; import { NgxDaterangepickerMd } from 'ngx-daterangepicker-material'; import { GridsterModule } from 'angular-gridster2'; @@ -132,6 +133,7 @@ import { TbJsonToStringDirective } from '@shared/components/directives/tb-json-t import { JsonObjectEditDialogComponent } from '@shared/components/dialog/json-object-edit-dialog.component'; import { HistorySelectorComponent } from './components/time/history-selector/history-selector.component'; import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component'; +import { DndModule } from 'ngx-drag-drop'; import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; import { ContactComponent } from '@shared/components/contact.component'; import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; @@ -259,6 +261,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select MatStepperModule, MatAutocompleteModule, MatChipsModule, + MatListModule, GridsterModule, ClipboardModule, FlexLayoutModule.withConfig({addFlexToParent: false}), @@ -269,6 +272,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select HotkeyModule, ColorPickerModule, NgxHmCarouselModule, + DndModule, NgxFlowModule, NgxFlowchartModule ], @@ -348,6 +352,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select MatStepperModule, MatAutocompleteModule, MatChipsModule, + MatListModule, GridsterModule, ClipboardModule, FlexLayoutModule, @@ -358,6 +363,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select HotkeyModule, ColorPickerModule, NgxHmCarouselModule, + DndModule, NgxFlowchartModule, ConfirmDialogComponent, AlertDialogComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 216ae2c1b7..50fbe58583 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -800,6 +800,7 @@ "datasource": { "type": "Datasource type", "name": "Name", + "label": "Label", "add-datasource-prompt": "Please add datasource" }, "details": { @@ -1094,6 +1095,7 @@ "entity": { "entity": "Entity", "entities": "Entities", + "entities-count": "Entities count", "aliases": "Entity aliases", "entity-alias": "Entity alias", "unable-delete-entity-alias-title": "Unable to delete entity alias", diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index b993a8dbd4..b1c5167ac9 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -6534,6 +6534,13 @@ ngx-daterangepicker-material@^4.0.1: dependencies: tslib "^1.10.0" +ngx-drag-drop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ngx-drag-drop/-/ngx-drag-drop-2.0.0.tgz#65d970229964803726fb7b9af4aec24005c810c7" + integrity sha512-t+4/eiC8zaXKqU1ruNfFEfGs1GpMNwpffD0baopvZFKjQHCb5rhNqFilJ54wO4T0OwGp4/RnsVhlcxe1mX6UJg== + dependencies: + tslib "^1.9.0" + "ngx-flowchart@git://github.com/thingsboard/ngx-flowchart.git#master": version "0.0.0" resolved "git://github.com/thingsboard/ngx-flowchart.git#078bfd2cedeeab412dee922e8066a19be6da7278" From f8d1fff4cc346f1867a729d1a9c1ed389bf2cfb5 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 2 Mar 2021 12:06:50 +0200 Subject: [PATCH 4/5] New Alias --- .../BaseEntityQueryControllerTest.java | 31 ++++++++++++++++++- .../controller/ControllerSqlTestSuite.java | 6 ++-- .../common/data/asset/AssetSearchQuery.java | 4 +-- .../common/data/device/DeviceSearchQuery.java | 4 +-- .../entityview/EntityViewSearchQuery.java | 4 +-- .../common/data/query/EntityFilter.java | 1 + .../common/data/query/EntityFilterType.java | 1 + .../common/data/query/EntityTypeFilter.java | 30 ++++++++++++++++++ .../data/query/RelationsQueryFilter.java | 5 ++- .../data/relation/EntityRelationsQuery.java | 2 +- ...ter.java => RelationEntityTypeFilter.java} | 2 +- .../dao/relation/BaseRelationService.java | 10 +++--- .../query/DefaultEntityQueryRepository.java | 10 ++++-- .../dao/service/BaseEntityServiceTest.java | 8 ++--- .../dao/service/BaseRelationServiceTest.java | 6 ++-- .../rule/engine/data/RelationsQuery.java | 4 +-- .../TbGetRelatedAttrNodeConfiguration.java | 6 ++-- .../TbChangeOriginatorNodeConfiguration.java | 6 ++-- 18 files changed, 102 insertions(+), 38 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/query/EntityTypeFilter.java rename common/data/src/main/java/org/thingsboard/server/common/data/relation/{EntityTypeFilter.java => RelationEntityTypeFilter.java} (95%) diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java index d4e4b4e0d6..5d234d2691 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java @@ -44,6 +44,7 @@ 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.common.data.query.EntityListFilter; +import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.NumericFilterPredicate; @@ -132,6 +133,14 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); Assert.assertEquals(97, count.longValue()); + + EntityTypeFilter filter2 = new EntityTypeFilter(); + filter2.setEntityType(EntityType.DEVICE); + + EntityCountQuery countQuery2 = new EntityCountQuery(filter2); + + Long count2 = doPostWithResponse("/api/entitiesQuery/count", countQuery2, Long.class); + Assert.assertEquals(97, count2.longValue()); } @Test @@ -198,11 +207,31 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe Assert.assertEquals(11, data.getTotalElements()); Assert.assertEquals("Device19", data.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + + EntityTypeFilter filter2 = new EntityTypeFilter(); + filter2.setEntityType(EntityType.DEVICE); + + EntityDataSortOrder sortOrder2 = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink2 = new EntityDataPageLink(10, 0, null, sortOrder2); + List entityFields2 = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + EntityDataQuery query2 = new EntityDataQuery(filter2, pageLink2, entityFields2, null, null); + + PageData data2 = + doPostWithTypedResponse("/api/entitiesQuery/find", query2, new TypeReference>() { + }); + + Assert.assertEquals(97, data2.getTotalElements()); + Assert.assertEquals(10, data2.getTotalPages()); + Assert.assertTrue(data2.hasNext()); + Assert.assertEquals(10, data2.getData().size()); + } @Test public void testFindEntityDataByQueryWithAttributes() throws Exception { - List devices = new ArrayList<>(); List temperatures = new ArrayList<>(); List highTemperatures = new ArrayList<>(); diff --git a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java index a386f18c37..0f969a848f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java @@ -26,9 +26,9 @@ import java.util.Arrays; @RunWith(ClasspathSuite.class) @ClasspathSuite.ClassnameFilters({ - "org.thingsboard.server.controller.sql.WebsocketApiSqlTest", -// "org.thingsboard.server.controller.sql.TenantProfileControllerSqlTest", -// "org.thingsboard.server.controller.sql.*Test", +// "org.thingsboard.server.controller.sql.WebsocketApiSqlTest", +// "org.thingsboard.server.controller.sql.EntityQueryControllerSqlTest", + "org.thingsboard.server.controller.sql.*Test", }) public class ControllerSqlTestSuite { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetSearchQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetSearchQuery.java index 0fcb942de4..fe916f0eaa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetSearchQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetSearchQuery.java @@ -19,7 +19,7 @@ import lombok.Data; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import java.util.Collections; @@ -39,7 +39,7 @@ public class AssetSearchQuery { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(parameters); query.setFilters( - Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, + Collections.singletonList(new RelationEntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, Collections.singletonList(EntityType.ASSET)))); return query; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceSearchQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceSearchQuery.java index 2423bda7db..9143fdfece 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceSearchQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceSearchQuery.java @@ -19,7 +19,7 @@ import lombok.Data; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import java.util.Collections; @@ -36,7 +36,7 @@ public class DeviceSearchQuery { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(parameters); query.setFilters( - Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, + Collections.singletonList(new RelationEntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, Collections.singletonList(EntityType.DEVICE)))); return query; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java index 363832c5ce..348f7725b0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java @@ -19,7 +19,7 @@ import lombok.Data; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import java.util.Collections; @@ -36,7 +36,7 @@ public class EntityViewSearchQuery { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(parameters); query.setFilters( - Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, + Collections.singletonList(new RelationEntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, Collections.singletonList(EntityType.ENTITY_VIEW)))); return query; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java index 78ef869f56..efdd70ec7b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java @@ -29,6 +29,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = SingleEntityFilter.class, name = "singleEntity"), @JsonSubTypes.Type(value = EntityListFilter.class, name = "entityList"), @JsonSubTypes.Type(value = EntityNameFilter.class, name = "entityName"), + @JsonSubTypes.Type(value = EntityTypeFilter.class, name = "entityType"), @JsonSubTypes.Type(value = AssetTypeFilter.class, name = "assetType"), @JsonSubTypes.Type(value = DeviceTypeFilter.class, name = "deviceType"), @JsonSubTypes.Type(value = EntityViewTypeFilter.class, name = "entityViewType"), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java index 62c7546e8e..6b590c4695 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java @@ -19,6 +19,7 @@ public enum EntityFilterType { SINGLE_ENTITY("singleEntity"), ENTITY_LIST("entityList"), ENTITY_NAME("entityName"), + ENTITY_TYPE("entityType"), ASSET_TYPE("assetType"), DEVICE_TYPE("deviceType"), ENTITY_VIEW_TYPE("entityViewType"), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityTypeFilter.java new file mode 100644 index 0000000000..22c2212a84 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityTypeFilter.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; + +@Data +public class EntityTypeFilter implements EntityFilter { + @Override + public EntityFilterType getType() { + return EntityFilterType.ENTITY_TYPE; + } + + private EntityType entityType; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java index 0890f48c3a..9c113eb793 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java @@ -18,8 +18,7 @@ package org.thingsboard.server.common.data.query; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import java.util.List; @@ -33,7 +32,7 @@ public class RelationsQueryFilter implements EntityFilter { private EntityId rootEntity; private EntitySearchDirection direction; - private List filters; + private List filters; private int maxLevel; private boolean fetchLastLevelOnly; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationsQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationsQuery.java index b673f60462..1a5415d3c7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationsQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationsQuery.java @@ -26,6 +26,6 @@ import java.util.List; public class EntityRelationsQuery { private RelationsSearchParameters parameters; - private List filters; + private List filters; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java similarity index 95% rename from common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityTypeFilter.java rename to common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java index 8b9849d6a1..1e817dda14 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityTypeFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java @@ -26,7 +26,7 @@ import java.util.List; */ @Data @AllArgsConstructor -public class EntityTypeFilter { +public class RelationEntityTypeFilter { private String relationType; diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 1fa9f057b2..44b328fd9f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationInfo; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.dao.entity.EntityService; @@ -457,7 +457,7 @@ public class BaseRelationService implements RelationService { //boolean fetchLastLevelOnly = true; log.trace("Executing findByQuery [{}]", query); RelationsSearchParameters params = query.getParameters(); - final List filters = query.getFilters(); + final List filters = query.getFilters(); if (filters == null || filters.isEmpty()) { log.debug("Filters are not set [{}]", query); } @@ -575,8 +575,8 @@ public class BaseRelationService implements RelationService { }; } - private boolean matchFilters(List filters, EntityRelation relation, EntitySearchDirection direction) { - for (EntityTypeFilter filter : filters) { + private boolean matchFilters(List filters, EntityRelation relation, EntitySearchDirection direction) { + for (RelationEntityTypeFilter filter : filters) { if (match(filter, relation, direction)) { return true; } @@ -584,7 +584,7 @@ public class BaseRelationService implements RelationService { return false; } - private boolean match(EntityTypeFilter filter, EntityRelation relation, EntitySearchDirection direction) { + private boolean match(RelationEntityTypeFilter filter, EntityRelation relation, EntitySearchDirection direction) { if (StringUtils.isEmpty(filter.getRelationType()) || filter.getRelationType().equals(relation.getType())) { if (filter.getEntityTypes() == null || filter.getEntityTypes().isEmpty()) { return true; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 170a2edfa2..4907c03dea 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -40,12 +40,13 @@ import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityListFilter; import org.thingsboard.server.common.data.query.EntityNameFilter; import org.thingsboard.server.common.data.query.EntitySearchQueryFilter; +import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.EntityViewSearchQueryFilter; import org.thingsboard.server.common.data.query.EntityViewTypeFilter; import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import java.util.Arrays; import java.util.Collections; @@ -488,6 +489,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { case ASSET_SEARCH_QUERY: case ENTITY_VIEW_SEARCH_QUERY: case API_USAGE_STATE: + case ENTITY_TYPE: return ""; default: throw new RuntimeException("Not implemented!"); @@ -573,7 +575,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { boolean single = entityFilter.getFilters() != null && entityFilter.getFilters().size() == 1; if (entityFilter.getFilters() != null && !entityFilter.getFilters().isEmpty()) { int entityTypeFilterIdx = 0; - for (EntityTypeFilter etf : entityFilter.getFilters()) { + for (RelationEntityTypeFilter etf : entityFilter.getFilters()) { String etfCondition = buildEtfCondition(ctx, etf, entityFilter.getDirection(), entityTypeFilterIdx++); if (!etfCondition.isEmpty()) { if (noConditions) { @@ -622,7 +624,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { return "( " + selectFields + from + ")"; } - private String buildEtfCondition(QueryContext ctx, EntityTypeFilter etf, EntitySearchDirection direction, int entityTypeFilterIdx) { + private String buildEtfCondition(QueryContext ctx, RelationEntityTypeFilter etf, EntitySearchDirection direction, int entityTypeFilterIdx) { StringBuilder whereFilter = new StringBuilder(); String relationType = etf.getRelationType(); List entityTypes = etf.getEntityTypes(); @@ -728,6 +730,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { return ((EntityListFilter) entityFilter).getEntityType(); case ENTITY_NAME: return ((EntityNameFilter) entityFilter).getEntityType(); + case ENTITY_TYPE: + return ((EntityTypeFilter) entityFilter).getEntityType(); case ASSET_TYPE: case ASSET_SEARCH_QUERY: return EntityType.ASSET; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java index 98934cfaa0..6c899c01c4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java @@ -58,7 +58,7 @@ import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; @@ -160,13 +160,13 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); Assert.assertEquals(30, count); - filter.setFilters(Collections.singletonList(new EntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); + filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); Assert.assertEquals(25, count); filter.setRootEntity(devices.get(0).getId()); filter.setDirection(EntitySearchDirection.TO); - filter.setFilters(Collections.singletonList(new EntityTypeFilter("Manages", Collections.singletonList(EntityType.TENANT)))); + filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Manages", Collections.singletonList(EntityType.TENANT)))); count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); Assert.assertEquals(1, count); @@ -228,7 +228,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { RelationsQueryFilter filter = new RelationsQueryFilter(); filter.setRootEntity(tenantId); filter.setDirection(EntitySearchDirection.FROM); - filter.setFilters(Collections.singletonList(new EntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); + filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); EntityDataSortOrder sortOrder = new EntityDataSortOrder( new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java index 84c0a2975c..6f5ea9acb9 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.dao.exception.DataValidationException; @@ -221,7 +221,7 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, false)); - query.setFilters(Collections.singletonList(new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); + query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); List relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get(); Assert.assertEquals(3, relations.size()); Assert.assertTrue(relations.contains(relationA)); @@ -255,7 +255,7 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, false)); - query.setFilters(Collections.singletonList(new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); + query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); List relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get(); Assert.assertEquals(2, relations.size()); Assert.assertTrue(relations.contains(relationAB)); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java index 295690d445..46e3c97e87 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java @@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.data; import lombok.Data; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import java.util.List; @@ -26,6 +26,6 @@ public class RelationsQuery { private EntitySearchDirection direction; private int maxLevel = 1; - private List filters; + private List filters; private boolean fetchLastLevelOnly = false; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java index 2ae5fd7add..b9a8df3acd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java @@ -19,7 +19,7 @@ import lombok.Data; import org.thingsboard.rule.engine.data.RelationsQuery; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import java.util.Collections; import java.util.HashMap; @@ -41,8 +41,8 @@ public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfig RelationsQuery relationsQuery = new RelationsQuery(); relationsQuery.setDirection(EntitySearchDirection.FROM); relationsQuery.setMaxLevel(1); - EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); - relationsQuery.setFilters(Collections.singletonList(entityTypeFilter)); + RelationEntityTypeFilter relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); + relationsQuery.setFilters(Collections.singletonList(relationEntityTypeFilter)); configuration.setRelationsQuery(relationsQuery); return configuration; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java index 79ed4f5191..6dcc5280ea 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java @@ -20,7 +20,7 @@ import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.data.RelationsQuery; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import java.util.Collections; @@ -39,8 +39,8 @@ public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfigur RelationsQuery relationsQuery = new RelationsQuery(); relationsQuery.setDirection(EntitySearchDirection.FROM); relationsQuery.setMaxLevel(1); - EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); - relationsQuery.setFilters(Collections.singletonList(entityTypeFilter)); + RelationEntityTypeFilter relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); + relationsQuery.setFilters(Collections.singletonList(relationEntityTypeFilter)); configuration.setRelationsQuery(relationsQuery); return configuration; From 99c9c099ba2e61d4a3ce917e2c2464bb81a694e3 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 2 Mar 2021 15:13:21 +0200 Subject: [PATCH 5/5] Add Entity Type alias. Fix Key filter value conditional processing. --- .../app/core/api/entity-data-subscription.ts | 54 ++++++++++++------- ui-ngx/src/app/core/http/entity.service.ts | 7 +++ .../entity/entity-filter-view.component.ts | 4 ++ .../entity/entity-filter.component.html | 7 +++ .../entity/entity-filter.component.ts | 5 ++ .../filter/key-filter-dialog.component.ts | 21 +++++--- .../relation/relation-filters.component.ts | 10 ++-- ui-ngx/src/app/shared/models/alias.models.ts | 12 +++-- .../app/shared/models/query/query.models.ts | 4 +- .../src/app/shared/models/relation.models.ts | 4 +- .../assets/locale/locale.constant-en_US.json | 1 + 11 files changed, 89 insertions(+), 40 deletions(-) diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index 79c505b07a..8a6c2f54e8 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -155,7 +155,7 @@ export class EntityDataSubscription { clearTimeout(this.timer); this.timer = null; } - if (this.datasourceType === DatasourceType.entity) { + if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount) { if (this.subscriber) { this.subscriber.unsubscribe(); this.subscriber = null; @@ -318,24 +318,30 @@ export class EntityDataSubscription { entityType: null }; - const entityData: EntityData = { - entityId, - timeseries: {}, - latest: {} - }; - entityData.latest[EntityKeyType.ENTITY_FIELD] = { - name: {ts: Date.now(), value: DatasourceType.entityCount}, - }; - const countKey = this.entityDataSubscriptionOptions.dataKeys[0]; + let dataReceived = false; + this.subscriber.entityCount$.subscribe( (entityCountUpdate) => { - if (!entityData.latest[EntityKeyType.COUNT]) { - entityData.latest[EntityKeyType.COUNT] = {}; - entityData.latest[EntityKeyType.COUNT][countKey.name] = { - ts: Date.now(), - value: entityCountUpdate.count + '' + if (!dataReceived) { + const entityData: EntityData = { + entityId, + latest: { + [EntityKeyType.ENTITY_FIELD]: { + name: { + ts: Date.now(), + value: DatasourceType.entityCount + } + }, + [EntityKeyType.COUNT]: { + [countKey.name]: { + ts: Date.now(), + value: entityCountUpdate.count + '' + } + } + }, + timeseries: {} }; const pageData: PageData = { data: [entityData], @@ -344,12 +350,20 @@ export class EntityDataSubscription { totalPages: 1 }; this.onPageData(pageData); + dataReceived = true; } else { - const update = [deepClone(entityData)]; - update[0].latest[EntityKeyType.COUNT][countKey.name] = { - ts: Date.now(), - value: entityCountUpdate.count + '' - }; + const update: EntityData[] = [{ + entityId, + latest: { + [EntityKeyType.COUNT]: { + [countKey.name]: { + ts: Date.now(), + value: entityCountUpdate.count + '' + } + } + }, + timeseries: {} + }]; this.onDataUpdate(update); } } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 17a35b2305..0862e10a76 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -481,6 +481,8 @@ export class EntityService { return entityTypes.indexOf(filter.entityType) > -1 ? true : false; case AliasFilterType.entityName: return entityTypes.indexOf(filter.entityType) > -1 ? true : false; + case AliasFilterType.entityType: + return entityTypes.indexOf(filter.entityType) > -1 ? true : false; case AliasFilterType.stateEntity: return true; case AliasFilterType.assetType: @@ -540,6 +542,8 @@ export class EntityService { return true; case AliasFilterType.entityName: return true; + case AliasFilterType.entityType: + return true; case AliasFilterType.stateEntity: return true; case AliasFilterType.assetType: @@ -805,6 +809,9 @@ export class EntityService { case AliasFilterType.entityName: result.entityFilter = deepClone(filter); return of(result); + case AliasFilterType.entityType: + result.entityFilter = deepClone(filter); + return of(result); case AliasFilterType.stateEntity: result.stateEntity = true; if (stateEntityId) { diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts index 42fb943ef3..07cee4ffb2 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts @@ -76,6 +76,10 @@ export class EntityFilterViewComponent implements ControlValueAccessor { this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).nameStartsWith, {prefix}); break; + case AliasFilterType.entityType: + entityType = this.filter.entityType; + this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).typePlural); + break; case AliasFilterType.stateEntity: this.filterDisplayValue = this.translate.instant('alias.filter-type-state-entity-description'); break; diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html index cfd75d7299..5c5ca5ff59 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html @@ -59,6 +59,13 @@ + + + + alias.state-entity-parameter-name diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts index 992ee75f7b..539d6b3ff8 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts @@ -123,6 +123,11 @@ export class EntityFilterComponent implements ControlValueAccessor, OnInit { entityNameFilter: [filter ? filter.entityNameFilter : '', [Validators.required]], }); break; + case AliasFilterType.entityType: + this.filterFormGroup = this.fb.group({ + entityType: [filter ? filter.entityType : null, [Validators.required]] + }); + break; case AliasFilterType.stateEntity: this.filterFormGroup = this.fb.group({ stateEntityParamName: [filter ? filter.stateEntityParamName : null, []], diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts index d3e95cb8a2..6c9c85330a 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts @@ -107,12 +107,15 @@ export class KeyFilterDialogComponent extends key: [this.data.keyFilter.key.key, [Validators.required]] } ), - value: [this.data.keyFilter.value], valueType: [this.data.keyFilter.valueType, [Validators.required]], predicates: [this.data.keyFilter.predicates, [Validators.required]] } ); - + if (this.data.telemetryKeysOnly) { + this.keyFilterFormGroup.addControl( + 'value', this.fb.control(this.data.keyFilter.value) + ); + } if (!this.data.readonly) { this.keyFilterFormGroup.get('valueType').valueChanges.pipe( takeUntil(this.destroy$) @@ -144,12 +147,14 @@ export class KeyFilterDialogComponent extends } else { this.showAutocomplete = false; } - if (type === EntityKeyType.CONSTANT) { - this.keyFilterFormGroup.get('value').setValidators(Validators.required); - this.keyFilterFormGroup.get('value').updateValueAndValidity(); - } else { - this.keyFilterFormGroup.get('value').clearValidators(); - this.keyFilterFormGroup.get('value').updateValueAndValidity(); + if (this.data.telemetryKeysOnly) { + if (type === EntityKeyType.CONSTANT) { + this.keyFilterFormGroup.get('value').setValidators(Validators.required); + this.keyFilterFormGroup.get('value').updateValueAndValidity(); + } else { + this.keyFilterFormGroup.get('value').clearValidators(); + this.keyFilterFormGroup.get('value').updateValueAndValidity(); + } } }); diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts index c9bbf02173..43dbfc45f6 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts @@ -24,7 +24,7 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; -import { EntityTypeFilter } from '@shared/models/relation.models'; +import { RelationEntityTypeFilter } from '@shared/models/relation.models'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -80,7 +80,7 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa this.disabled = isDisabled; } - writeValue(filters: Array): void { + writeValue(filters: Array): void { if (this.valueChangeSubscription) { this.valueChangeSubscription.unsubscribe(); } @@ -102,14 +102,14 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa public addFilter() { const relationFiltersFormArray = this.relationFiltersFormGroup.get('relationFilters') as FormArray; - const filter: EntityTypeFilter = { + const filter: RelationEntityTypeFilter = { relationType: null, entityTypes: [] }; relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter)); } - private createRelationFilterFormGroup(filter: EntityTypeFilter): AbstractControl { + private createRelationFilterFormGroup(filter: RelationEntityTypeFilter): AbstractControl { return this.fb.group({ relationType: [filter ? filter.relationType : null], entityTypes: [filter ? filter.entityTypes : []] @@ -117,7 +117,7 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa } private updateModel() { - const filters: Array = this.relationFiltersFormGroup.get('relationFilters').value; + const filters: Array = this.relationFiltersFormGroup.get('relationFilters').value; this.propagateChange(filters); } } diff --git a/ui-ngx/src/app/shared/models/alias.models.ts b/ui-ngx/src/app/shared/models/alias.models.ts index df80125ae4..5c02089b04 100644 --- a/ui-ngx/src/app/shared/models/alias.models.ts +++ b/ui-ngx/src/app/shared/models/alias.models.ts @@ -16,14 +16,14 @@ import { EntityType } from '@shared/models/entity-type.models'; import { EntityId } from '@shared/models/id/entity-id'; -import { EntitySearchDirection, EntityTypeFilter } from '@shared/models/relation.models'; -import { EntityInfo } from './entity.models'; +import { EntitySearchDirection, RelationEntityTypeFilter } from '@shared/models/relation.models'; import { EntityFilter } from '@shared/models/query/query.models'; export enum AliasFilterType { singleEntity = 'singleEntity', entityList = 'entityList', entityName = 'entityName', + entityType = 'entityType', stateEntity = 'stateEntity', assetType = 'assetType', deviceType = 'deviceType', @@ -40,6 +40,7 @@ export const aliasFilterTypeTranslationMap = new Map( [ AliasFilterType.singleEntity, 'alias.filter-type-single-entity' ], [ AliasFilterType.entityList, 'alias.filter-type-entity-list' ], [ AliasFilterType.entityName, 'alias.filter-type-entity-name' ], + [ AliasFilterType.entityType, 'alias.filter-type-entity-type' ], [ AliasFilterType.stateEntity, 'alias.filter-type-state-entity' ], [ AliasFilterType.assetType, 'alias.filter-type-asset-type' ], [ AliasFilterType.deviceType, 'alias.filter-type-device-type' ], @@ -66,6 +67,10 @@ export interface EntityNameFilter { entityNameFilter?: string; } +export interface EntityTypeFilter { + entityType?: EntityType; +} + export interface StateEntityFilter { stateEntityParamName?: string; defaultStateEntity?: EntityId; @@ -92,7 +97,7 @@ export interface RelationsQueryFilter { defaultStateEntity?: EntityId; rootEntity?: EntityId; direction?: EntitySearchDirection; - filters?: Array; + filters?: Array; maxLevel?: number; fetchLastLevelOnly?: boolean; } @@ -129,6 +134,7 @@ export type EntityFilters = SingleEntityFilter & EntityListFilter & EntityNameFilter & + EntityTypeFilter & StateEntityFilter & AssetTypeFilter & DeviceTypeFilter & diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index dc21f70907..25efe437d7 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -351,14 +351,14 @@ export interface KeyFilterPredicateInfo { export interface KeyFilter { key: EntityKey; valueType: EntityKeyValueType; - value: string | number | boolean; + value?: string | number | boolean; predicate: KeyFilterPredicate; } export interface KeyFilterInfo { key: EntityKey; valueType: EntityKeyValueType; - value: string | number | boolean; + value?: string | number | boolean; predicates: Array; } diff --git a/ui-ngx/src/app/shared/models/relation.models.ts b/ui-ngx/src/app/shared/models/relation.models.ts index 2f0f2b456f..c9037a5dab 100644 --- a/ui-ngx/src/app/shared/models/relation.models.ts +++ b/ui-ngx/src/app/shared/models/relation.models.ts @@ -52,7 +52,7 @@ export const directionTypeTranslations = new Map( ] ); -export interface EntityTypeFilter { +export interface RelationEntityTypeFilter { relationType: string; entityTypes: Array; } @@ -68,7 +68,7 @@ export interface RelationsSearchParameters { export interface EntityRelationsQuery { parameters: RelationsSearchParameters; - filters: Array; + filters: Array; } export interface EntitySearchQuery { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 50fbe58583..7b62f680a8 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -299,6 +299,7 @@ "filter-type-single-entity": "Single entity", "filter-type-entity-list": "Entity list", "filter-type-entity-name": "Entity name", + "filter-type-entity-type": "Entity type", "filter-type-state-entity": "Entity from dashboard state", "filter-type-state-entity-description": "Entity taken from dashboard state parameters", "filter-type-asset-type": "Asset type",