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 2bed46a63b..ea9f031de5 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 @@ -40,5 +40,6 @@ public class RelationsQueryFilter implements EntityFilter { private List filters; private int maxLevel; private boolean fetchLastLevelOnly; + private boolean negate; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java index 6bac4c9a0f..18786250a5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java @@ -17,7 +17,6 @@ package org.thingsboard.server.common.data.relation; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; -import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.server.common.data.EntityType; @@ -27,13 +26,25 @@ import java.util.List; * Created by ashvayka on 02.05.17. */ @Data -@AllArgsConstructor @ApiModel public class RelationEntityTypeFilter { + public RelationEntityTypeFilter(String relationType, List entityTypes) { + this(relationType, entityTypes, false); + } + + public RelationEntityTypeFilter(String relationType, List entityTypes, boolean negate) { + this.relationType = relationType; + this.entityTypes = entityTypes; + this.negate = negate; + } + @ApiModelProperty(position = 1, value = "Type of the relation between root entity and other entity (e.g. 'Contains' or 'Manages').", example = "Contains") private String relationType; @ApiModelProperty(position = 2, value = "Array of entity types to filter the related entities (e.g. 'DEVICE', 'ASSET').") private List entityTypes; + + @ApiModelProperty(position = 3, value = "Negate relation type between root entity and other entity.") + private boolean negate; } 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 1963190aa5..be91d78704 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 @@ -704,10 +704,16 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { .append(entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "to" : "from") .append("_type in (:where_entity_types").append(")"); ctx.addStringListParameter("where_entity_types", Arrays.stream(RELATION_QUERY_ENTITY_TYPES).map(EntityType::name).collect(Collectors.toList())); - } - - if (!noConditions && !single) { - whereFilter = new StringBuilder().append("(").append(whereFilter).append(")"); + } else { + if (!single) { + whereFilter = new StringBuilder() + .append(entityFilter.isNegate() ? " NOT (" : "(") + .append(whereFilter).append(")"); + } else if (entityFilter.isNegate()) { + whereFilter = new StringBuilder() + .append(" NOT ") + .append(whereFilter).append(")"); + } } if (entityFilter.isFetchLastLevelOnly()) { @@ -743,8 +749,11 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { boolean hasRelationType = !StringUtils.isEmpty(relationType); if (hasRelationType) { ctx.addStringParameter("where_relation_type" + entityTypeFilterIdx, relationType); - whereFilter - .append("re.relation_type = :where_relation_type").append(entityTypeFilterIdx); + if (etf.isNegate()) { + whereFilter.append("re.relation_type != :where_relation_type").append(entityTypeFilterIdx); + } else { + whereFilter.append("re.relation_type = :where_relation_type").append(entityTypeFilterIdx); + } } if (!whereEntityTypes.isEmpty()) { if (hasRelationType) { 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 6f588f34e5..85ac9febf5 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 @@ -185,6 +185,9 @@ formControlName="maxLevel"> + + {{ 'relation.negate-all-relations' | translate }} +
relation.relation-filters
{ this.filterFormGroup.get('rootEntity').setValidators(rootStateEntity ? [] : [Validators.required]); diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html index 1f5d6273ec..095cfe6eba 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html @@ -28,24 +28,29 @@
-
- - - - - +
+
+ + + + + +
+ + {{ 'relation.negate-relation' | translate }} +
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 911aa4fe30..71d4350b4f 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 @@ -104,7 +104,8 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa const relationFiltersFormArray = this.relationFiltersFormGroup.get('relationFilters') as FormArray; const filter: RelationEntityTypeFilter = { relationType: null, - entityTypes: [] + entityTypes: [], + negate: false }; relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter)); } @@ -112,7 +113,8 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa private createRelationFilterFormGroup(filter: RelationEntityTypeFilter): AbstractControl { return this.fb.group({ relationType: [filter ? filter.relationType : null], - entityTypes: [filter ? filter.entityTypes : []] + entityTypes: [filter ? filter.entityTypes : []], + negate: [filter ? filter.negate : false], }); } diff --git a/ui-ngx/src/app/shared/models/alias.models.ts b/ui-ngx/src/app/shared/models/alias.models.ts index f67c0d50a7..c3322cda16 100644 --- a/ui-ngx/src/app/shared/models/alias.models.ts +++ b/ui-ngx/src/app/shared/models/alias.models.ts @@ -114,6 +114,7 @@ export interface RelationsQueryFilter { filters?: Array; maxLevel?: number; fetchLastLevelOnly?: boolean; + negate?: boolean; } export interface EntitySearchQueryFilter { diff --git a/ui-ngx/src/app/shared/models/relation.models.ts b/ui-ngx/src/app/shared/models/relation.models.ts index e42b915e9e..27920f275a 100644 --- a/ui-ngx/src/app/shared/models/relation.models.ts +++ b/ui-ngx/src/app/shared/models/relation.models.ts @@ -55,6 +55,7 @@ export const directionTypeTranslations = new Map( export interface RelationEntityTypeFilter { relationType: string; entityTypes: Array; + negate?: boolean; } export interface RelationsSearchParameters { 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 e8c322c650..b053cd83a6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2840,7 +2840,9 @@ "relation-filters": "Relation filters", "additional-info": "Additional info (JSON)", "invalid-additional-info": "Unable to parse additional info json.", - "no-relations-text": "No relations found" + "no-relations-text": "No relations found", + "negate-all-relations": "Negate all relations (NOT condition)", + "negate-relation": "Negate this relation (NOT condition)" }, "resource": { "add": "Add Resource",