This is an automated email from the ASF dual-hosted git repository.
riemer pushed a commit to branch 4188-support-complex-filters-in-data-explorer
in repository https://gitbox.apache.org/repos/asf/streampipes.git
The following commit(s) were added to
refs/heads/4188-support-complex-filters-in-data-explorer by this push:
new 7fb54795e4 feat(#4188): Add support for nested filters in data explorer
7fb54795e4 is described below
commit 7fb54795e43c7640f8fa997c68608f78cb6824be
Author: Dominik Riemer <[email protected]>
AuthorDate: Sun Feb 22 19:42:16 2026 +0100
feat(#4188): Add support for nested filters in data explorer
---
.../dataexplorer/api/IDataLakeQueryBuilder.java | 3 +
.../influx/DataLakeInfluxQueryBuilder.java | 41 +++
.../dataexplorer/influx/SelectQueryParamsTest.java | 83 ++++++
.../utils/ProvidedQueryParameterBuilder.java | 6 +
.../param/ProvidedRestQueryParamConverter.java | 30 ++-
.../param/model/WhereClauseParams.java | 72 ++++-
.../model/datalake/FilterExpressionCondition.java | 24 +-
.../model/datalake/FilterExpressionGroup.java | 25 +-
.../datalake/FilterExpressionLogicalOperator.java | 26 +-
.../model/datalake/FilterExpressionNode.java | 30 +--
.../datalake/param/SupportedRestQueryParams.java | 2 +
.../rest/impl/datalake/DataLakeResource.java | 3 +
.../lib/model/datalake/DatalakeQueryParameters.ts | 1 +
.../model/datalake/data-lake-query-config.model.ts | 17 ++
.../src/lib/query/DatalakeQueryParameterBuilder.ts | 148 ++++++++++-
.../lib/query/data-view-query-generator.service.ts | 4 +-
.../advanced-filter-condition-row.component.html | 52 ++++
.../advanced-filter-condition-row.component.ts | 102 +++++++
.../advanced-filter-dialog.component.html | 145 ++++++++++
.../advanced-filter-dialog.component.scss | 36 +++
.../advanced-filter-dialog.component.ts | 292 +++++++++++++++++++++
.../filter-expression-preview.service.ts | 61 +++++
.../filter-selection-panel-row.component.html | 22 ++
.../filter-selection-panel-row.component.ts | 8 +
.../filter-selection-panel.component.html | 38 ++-
.../filter-selection-panel.component.ts | 113 +++++++-
26 files changed, 1270 insertions(+), 114 deletions(-)
diff --git
a/streampipes-data-explorer-api/src/main/java/org/apache/streampipes/dataexplorer/api/IDataLakeQueryBuilder.java
b/streampipes-data-explorer-api/src/main/java/org/apache/streampipes/dataexplorer/api/IDataLakeQueryBuilder.java
index 372635a69c..c91f056742 100644
---
a/streampipes-data-explorer-api/src/main/java/org/apache/streampipes/dataexplorer/api/IDataLakeQueryBuilder.java
+++
b/streampipes-data-explorer-api/src/main/java/org/apache/streampipes/dataexplorer/api/IDataLakeQueryBuilder.java
@@ -23,6 +23,7 @@ package org.apache.streampipes.dataexplorer.api;
import org.apache.streampipes.model.datalake.AggregationFunction;
import org.apache.streampipes.model.datalake.DataLakeQueryOrdering;
import org.apache.streampipes.model.datalake.FilterCondition;
+import org.apache.streampipes.model.datalake.FilterExpressionGroup;
import java.util.List;
@@ -65,6 +66,8 @@ public interface IDataLakeQueryBuilder<T> {
IDataLakeQueryBuilder<T> withInclusiveFilter(List<FilterCondition>
filterConditions);
+ IDataLakeQueryBuilder<T> withFilterExpression(FilterExpressionGroup
filterExpression);
+
IDataLakeQueryBuilder<T> withGroupByTime(String timeInterval);
IDataLakeQueryBuilder<T> withGroupByTime(String timeInterval,
diff --git
a/streampipes-data-explorer-influx/src/main/java/org/apache/streampipes/dataexplorer/influx/DataLakeInfluxQueryBuilder.java
b/streampipes-data-explorer-influx/src/main/java/org/apache/streampipes/dataexplorer/influx/DataLakeInfluxQueryBuilder.java
index 24f552ea24..07eede930a 100644
---
a/streampipes-data-explorer-influx/src/main/java/org/apache/streampipes/dataexplorer/influx/DataLakeInfluxQueryBuilder.java
+++
b/streampipes-data-explorer-influx/src/main/java/org/apache/streampipes/dataexplorer/influx/DataLakeInfluxQueryBuilder.java
@@ -23,6 +23,10 @@ import
org.apache.streampipes.commons.environment.Environments;
import org.apache.streampipes.model.datalake.AggregationFunction;
import org.apache.streampipes.model.datalake.DataLakeQueryOrdering;
import org.apache.streampipes.model.datalake.FilterCondition;
+import org.apache.streampipes.model.datalake.FilterExpressionCondition;
+import org.apache.streampipes.model.datalake.FilterExpressionGroup;
+import org.apache.streampipes.model.datalake.FilterExpressionLogicalOperator;
+import org.apache.streampipes.model.datalake.FilterExpressionNode;
import org.apache.streampipes.dataexplorer.api.IDataLakeQueryBuilder;
import org.influxdb.dto.Query;
@@ -177,6 +181,17 @@ public class DataLakeInfluxQueryBuilder implements
IDataLakeQueryBuilder<Query>
return this;
}
+ @Override
+ public IDataLakeQueryBuilder<Query>
withFilterExpression(FilterExpressionGroup filterExpression) {
+ if (Objects.nonNull(filterExpression)
+ && Objects.nonNull(filterExpression.children())
+ && !filterExpression.children().isEmpty()) {
+ this.whereClauses.add(toNestedClause(filterExpression));
+ }
+
+ return this;
+ }
+
@Override
public DataLakeInfluxQueryBuilder withGroupByTime(String timeInterval) {
@@ -276,6 +291,32 @@ public class DataLakeInfluxQueryBuilder implements
IDataLakeQueryBuilder<Query>
this.whereClauses.add(nestedClause);
}
+ private NestedClause toNestedClause(FilterExpressionGroup group) {
+ List<ConjunctionClause> conjunctionClauses = new ArrayList<>();
+ var conjunction = Objects.nonNull(group.operator()) ? group.operator() :
FilterExpressionLogicalOperator.AND;
+ group.children().forEach(node ->
conjunctionClauses.add(makeConjunction(node, conjunction)));
+ return new NestedClause(conjunctionClauses);
+ }
+
+ private ConjunctionClause makeConjunction(FilterExpressionNode node,
FilterExpressionLogicalOperator conjunction) {
+ Clause clause = toClause(node);
+ return conjunction == FilterExpressionLogicalOperator.OR
+ ? new OrConjunction(clause)
+ : new AndConjunction(clause);
+ }
+
+ private Clause toClause(FilterExpressionNode node) {
+ if (node instanceof FilterExpressionCondition condition) {
+ return new SimpleClause(condition.field(), condition.operator(),
condition.condition());
+ }
+
+ if (node instanceof FilterExpressionGroup group) {
+ return toNestedClause(group);
+ }
+
+ throw new IllegalArgumentException("Unsupported filter expression node: "
+ node);
+ }
+
private String escapeIndex(String index) {
return "\"" + index + "\"";
}
diff --git
a/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/SelectQueryParamsTest.java
b/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/SelectQueryParamsTest.java
index 828ed2304c..26eedcccbc 100644
---
a/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/SelectQueryParamsTest.java
+++
b/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/SelectQueryParamsTest.java
@@ -206,4 +206,87 @@ public class SelectQueryParamsTest {
+ " time > 1000000) GROUP BY sensorId,sensorId2;", query);
}
+ @Test
+ public void testFilterExpressionOr() {
+ var params = ProvidedQueryParameterBuilder.create("abc")
+ .withStartDate(1)
+ .withEndDate(2)
+ .withSimpleColumns(Arrays.asList("p1", "p2"))
+ .withFilterExpression(
+ "{\"type\":\"group\",\"operator\":\"OR\",\"children\":["
+ +
"{\"type\":\"condition\",\"field\":\"p1\",\"operator\":\"=\",\"condition\":1},"
+ +
"{\"type\":\"condition\",\"field\":\"p2\",\"operator\":\"=\",\"condition\":2}"
+ + "]}")
+ .build();
+
+ SelectQueryParams qp =
ProvidedRestQueryParamConverter.getSelectQueryParams(params);
+
+ String query =
qp.toQuery(DataLakeInfluxQueryBuilder.create("abc")).getCommand();
+
+ assertEquals("SELECT p1,p2 FROM \"abc\" WHERE (time < 2000000 AND time >
1000000) "
+ + "AND (p1 = 1 OR p2 = 2);", query);
+ }
+
+ @Test
+ public void testFilterExpressionTakesPrecedenceOverLegacyFilter() {
+ var params = ProvidedQueryParameterBuilder.create("abc")
+ .withStartDate(1)
+ .withEndDate(2)
+ .withSimpleColumns(Arrays.asList("p1", "p2"))
+ .withFilter("[p1;=;1]")
+ .withFilterExpression(
+ "{\"type\":\"group\",\"operator\":\"OR\",\"children\":["
+ +
"{\"type\":\"condition\",\"field\":\"p1\",\"operator\":\"=\",\"condition\":1},"
+ +
"{\"type\":\"condition\",\"field\":\"p2\",\"operator\":\"=\",\"condition\":2}"
+ + "]}")
+ .build();
+
+ SelectQueryParams qp =
ProvidedRestQueryParamConverter.getSelectQueryParams(params);
+
+ String query =
qp.toQuery(DataLakeInfluxQueryBuilder.create("abc")).getCommand();
+
+ assertEquals("SELECT p1,p2 FROM \"abc\" WHERE (time < 2000000 AND time >
1000000) "
+ + "AND (p1 = 1 OR p2 = 2);", query);
+ }
+
+ @Test
+ public void testFilterExpressionStringBooleanIsParsedAsBoolean() {
+ var params = ProvidedQueryParameterBuilder.create("abc")
+ .withStartDate(1)
+ .withEndDate(2)
+ .withSimpleColumns(Arrays.asList("p1", "p2"))
+ .withFilterExpression(
+ "{\"type\":\"group\",\"operator\":\"AND\",\"children\":["
+ +
"{\"type\":\"condition\",\"field\":\"p1\",\"operator\":\"=\",\"condition\":\"true\"}"
+ + "]}")
+ .build();
+
+ SelectQueryParams qp =
ProvidedRestQueryParamConverter.getSelectQueryParams(params);
+
+ String query =
qp.toQuery(DataLakeInfluxQueryBuilder.create("abc")).getCommand();
+
+ assertEquals("SELECT p1,p2 FROM \"abc\" WHERE (time < 2000000 AND time >
1000000) "
+ + "AND (p1 = true);", query);
+ }
+
+ @Test
+ public void testFilterExpressionStringNumberIsParsedAsNumber() {
+ var params = ProvidedQueryParameterBuilder.create("abc")
+ .withStartDate(1)
+ .withEndDate(2)
+ .withSimpleColumns(Arrays.asList("p1", "p2"))
+ .withFilterExpression(
+ "{\"type\":\"group\",\"operator\":\"AND\",\"children\":["
+ +
"{\"type\":\"condition\",\"field\":\"p1\",\"operator\":\"=\",\"condition\":\"1\"}"
+ + "]}")
+ .build();
+
+ SelectQueryParams qp =
ProvidedRestQueryParamConverter.getSelectQueryParams(params);
+
+ String query =
qp.toQuery(DataLakeInfluxQueryBuilder.create("abc")).getCommand();
+
+ assertEquals("SELECT p1,p2 FROM \"abc\" WHERE (time < 2000000 AND time >
1000000) "
+ + "AND (p1 = 1.0);", query);
+ }
+
}
diff --git
a/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/utils/ProvidedQueryParameterBuilder.java
b/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/utils/ProvidedQueryParameterBuilder.java
index 8f55f57987..c5ad9bd2d2 100644
---
a/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/utils/ProvidedQueryParameterBuilder.java
+++
b/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/utils/ProvidedQueryParameterBuilder.java
@@ -74,6 +74,12 @@ public class ProvidedQueryParameterBuilder {
return this;
}
+ public ProvidedQueryParameterBuilder withFilterExpression(String
filterExpression) {
+ this.queryParams.put(SupportedRestQueryParams.QP_FILTER_EXPRESSION,
filterExpression);
+
+ return this;
+ }
+
public ProvidedQueryParameterBuilder withPage(int page) {
this.queryParams.put(SupportedRestQueryParams.QP_PAGE,
String.valueOf(page));
diff --git
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/ProvidedRestQueryParamConverter.java
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/ProvidedRestQueryParamConverter.java
index 5628b5f992..f767d1b79b 100644
---
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/ProvidedRestQueryParamConverter.java
+++
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/ProvidedRestQueryParamConverter.java
@@ -26,9 +26,13 @@ import
org.apache.streampipes.dataexplorer.param.model.OffsetClauseParams;
import org.apache.streampipes.dataexplorer.param.model.OrderByClauseParams;
import org.apache.streampipes.dataexplorer.param.model.SelectClauseParams;
import org.apache.streampipes.dataexplorer.param.model.WhereClauseParams;
+import org.apache.streampipes.model.datalake.FilterExpressionGroup;
import org.apache.streampipes.model.datalake.param.ProvidedRestQueryParams;
import org.apache.streampipes.model.datalake.param.SupportedRestQueryParams;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -40,6 +44,7 @@ public class ProvidedRestQueryParamConverter {
public static final String BRACKET_CLOSE = "\\]";
public static final String ORDER_DESCENDING = "DESC";
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static SelectQueryParams getSelectQueryParams(ProvidedRestQueryParams
params) {
SelectQueryParams queryParameters = new
SelectQueryParams(params.getMeasurementId());
@@ -54,15 +59,20 @@ public class ProvidedRestQueryParamConverter {
params.getAsString(SupportedRestQueryParams.QP_AGGREGATION_FUNCTION)));
}
- String filterConditions =
params.getAsString(SupportedRestQueryParams.QP_FILTER);
+ String filterExpression =
params.getAsString(SupportedRestQueryParams.QP_FILTER_EXPRESSION);
+ FilterExpressionGroup parsedFilterExpression =
parseFilterExpression(filterExpression);
+ String filterConditions = parsedFilterExpression == null
+ ? params.getAsString(SupportedRestQueryParams.QP_FILTER)
+ : null;
if (hasTimeParams(params)) {
queryParameters.withWhereParams(WhereClauseParams.from(
params.getAsLong(SupportedRestQueryParams.QP_START_DATE),
params.getAsLong(SupportedRestQueryParams.QP_END_DATE),
- filterConditions));
- } else if (filterConditions != null) {
-
queryParameters.withWhereParams(WhereClauseParams.from(filterConditions));
+ filterConditions,
+ parsedFilterExpression));
+ } else if (filterConditions != null || parsedFilterExpression != null) {
+ queryParameters.withWhereParams(WhereClauseParams.from(filterConditions,
parsedFilterExpression));
}
if (params.has(SupportedRestQueryParams.QP_TIME_INTERVAL)) {
@@ -133,4 +143,16 @@ public class ProvidedRestQueryParamConverter {
.replaceAll(BRACKET_CLOSE, "")
.split(";");
}
+
+ private static FilterExpressionGroup parseFilterExpression(String
filterExpression) {
+ if (filterExpression == null || filterExpression.isBlank()) {
+ return null;
+ }
+
+ try {
+ return OBJECT_MAPPER.readValue(filterExpression,
FilterExpressionGroup.class);
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Invalid filter expression provided",
e);
+ }
+ }
}
diff --git
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/WhereClauseParams.java
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/WhereClauseParams.java
index 01613487a5..cc4aaabd37 100644
---
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/WhereClauseParams.java
+++
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/WhereClauseParams.java
@@ -21,6 +21,9 @@ import
org.apache.streampipes.dataexplorer.api.IDataLakeQueryBuilder;
import org.apache.streampipes.dataexplorer.api.IQueryStatement;
import
org.apache.streampipes.dataexplorer.param.ProvidedRestQueryParamConverter;
import org.apache.streampipes.model.datalake.FilterCondition;
+import org.apache.streampipes.model.datalake.FilterExpressionCondition;
+import org.apache.streampipes.model.datalake.FilterExpressionGroup;
+import org.apache.streampipes.model.datalake.FilterExpressionNode;
import org.apache.commons.lang3.math.NumberUtils;
@@ -33,13 +36,17 @@ public class WhereClauseParams implements IQueryStatement {
private static final String LT = "<";
private final List<FilterCondition> filterConditions;
+ private final FilterExpressionGroup filterExpression;
private WhereClauseParams(
Long startTime,
Long endTime,
- String whereConditions
+ String whereConditions,
+ FilterExpressionGroup filterExpression
) {
- this(startTime, endTime);
+ this.filterConditions = new ArrayList<>();
+ this.filterExpression = normalizeFilterExpression(filterExpression);
+ this.buildTimeConditions(startTime, endTime);
if (whereConditions != null) {
buildConditions(whereConditions);
}
@@ -50,11 +57,13 @@ public class WhereClauseParams implements IQueryStatement {
Long endTime
) {
this.filterConditions = new ArrayList<>();
+ this.filterExpression = null;
this.buildTimeConditions(startTime, endTime);
}
- private WhereClauseParams(String whereConditions) {
+ private WhereClauseParams(String whereConditions, FilterExpressionGroup
filterExpression) {
this.filterConditions = new ArrayList<>();
+ this.filterExpression = normalizeFilterExpression(filterExpression);
if (whereConditions != null) {
buildConditions(whereConditions);
}
@@ -68,7 +77,14 @@ public class WhereClauseParams implements IQueryStatement {
}
public static WhereClauseParams from(String whereConditions) {
- return new WhereClauseParams(whereConditions);
+ return new WhereClauseParams(whereConditions, null);
+ }
+
+ public static WhereClauseParams from(
+ String whereConditions,
+ FilterExpressionGroup filterExpression
+ ) {
+ return new WhereClauseParams(whereConditions, filterExpression);
}
public static WhereClauseParams from(
@@ -76,7 +92,16 @@ public class WhereClauseParams implements IQueryStatement {
Long endTime,
String whereConditions
) {
- return new WhereClauseParams(startTime, endTime, whereConditions);
+ return new WhereClauseParams(startTime, endTime, whereConditions, null);
+ }
+
+ public static WhereClauseParams from(
+ Long startTime,
+ Long endTime,
+ String whereConditions,
+ FilterExpressionGroup filterExpression
+ ) {
+ return new WhereClauseParams(startTime, endTime, whereConditions,
filterExpression);
}
private void buildTimeConditions(
@@ -119,6 +144,35 @@ public class WhereClauseParams implements IQueryStatement {
}
}
+ private FilterExpressionGroup
normalizeFilterExpression(FilterExpressionGroup input) {
+ if (input == null) {
+ return null;
+ }
+
+ var children = input.children().stream()
+ .map(this::normalizeFilterExpressionNode)
+ .toList();
+
+ return new FilterExpressionGroup(input.operator(), children);
+ }
+
+ private FilterExpressionNode
normalizeFilterExpressionNode(FilterExpressionNode node) {
+ if (node instanceof FilterExpressionGroup group) {
+ return normalizeFilterExpression(group);
+ }
+
+ if (node instanceof FilterExpressionCondition condition) {
+ Object normalizedCondition = condition.condition();
+ if (normalizedCondition instanceof String conditionAsString) {
+ normalizedCondition = returnCondition(conditionAsString);
+ }
+
+ return new FilterExpressionCondition(condition.field(),
condition.operator(), normalizedCondition);
+ }
+
+ return node;
+ }
+
private boolean isQuotedString(String input) {
if (input.startsWith("\"") && input.endsWith("\"")) {
String content = removeQuotes(input);
@@ -141,6 +195,12 @@ public class WhereClauseParams implements IQueryStatement {
@Override
public void buildStatement(IDataLakeQueryBuilder<?> builder) {
- builder.withInclusiveFilter(filterConditions);
+ if (!filterConditions.isEmpty()) {
+ builder.withInclusiveFilter(filterConditions);
+ }
+
+ if (filterExpression != null) {
+ builder.withFilterExpression(filterExpression);
+ }
}
}
diff --git
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/FilterExpressionCondition.java
similarity index 57%
copy from
ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
copy to
streampipes-model/src/main/java/org/apache/streampipes/model/datalake/FilterExpressionCondition.java
index a835f2b297..fbd8045e19 100644
---
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
+++
b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/FilterExpressionCondition.java
@@ -15,27 +15,7 @@
* limitations under the License.
*
*/
+package org.apache.streampipes.model.datalake;
-import { MissingValueBehaviour } from './data-lake-query-config.model';
-
-export interface DatalakeQueryParameters {
- columns?: string;
- startDate?: number;
- endDate?: number;
- page?: number;
- limit?: number;
- offset?: number;
- groupBy?: string;
- order?: string;
- aggregationFunction?: string;
- timeInterval?: string;
- countOnly?: boolean;
- autoAggregate?: boolean;
- filter?: string;
- missingValueBehaviour?: MissingValueBehaviour;
- maximumAmountOfEvents?: number;
-
- // should be only used for multi-query requests
- measureName?: string;
- forId?: string;
+public record FilterExpressionCondition(String field, String operator, Object
condition) implements FilterExpressionNode {
}
diff --git
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/FilterExpressionGroup.java
similarity index 57%
copy from
ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
copy to
streampipes-model/src/main/java/org/apache/streampipes/model/datalake/FilterExpressionGroup.java
index a835f2b297..dfaf9f0e32 100644
---
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
+++
b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/FilterExpressionGroup.java
@@ -15,27 +15,10 @@
* limitations under the License.
*
*/
+package org.apache.streampipes.model.datalake;
-import { MissingValueBehaviour } from './data-lake-query-config.model';
+import java.util.List;
-export interface DatalakeQueryParameters {
- columns?: string;
- startDate?: number;
- endDate?: number;
- page?: number;
- limit?: number;
- offset?: number;
- groupBy?: string;
- order?: string;
- aggregationFunction?: string;
- timeInterval?: string;
- countOnly?: boolean;
- autoAggregate?: boolean;
- filter?: string;
- missingValueBehaviour?: MissingValueBehaviour;
- maximumAmountOfEvents?: number;
-
- // should be only used for multi-query requests
- measureName?: string;
- forId?: string;
+public record FilterExpressionGroup(FilterExpressionLogicalOperator operator,
+ List<FilterExpressionNode> children)
implements FilterExpressionNode {
}
diff --git
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/FilterExpressionLogicalOperator.java
similarity index 57%
copy from
ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
copy to
streampipes-model/src/main/java/org/apache/streampipes/model/datalake/FilterExpressionLogicalOperator.java
index a835f2b297..63ee40b2c1 100644
---
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
+++
b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/FilterExpressionLogicalOperator.java
@@ -15,27 +15,9 @@
* limitations under the License.
*
*/
+package org.apache.streampipes.model.datalake;
-import { MissingValueBehaviour } from './data-lake-query-config.model';
-
-export interface DatalakeQueryParameters {
- columns?: string;
- startDate?: number;
- endDate?: number;
- page?: number;
- limit?: number;
- offset?: number;
- groupBy?: string;
- order?: string;
- aggregationFunction?: string;
- timeInterval?: string;
- countOnly?: boolean;
- autoAggregate?: boolean;
- filter?: string;
- missingValueBehaviour?: MissingValueBehaviour;
- maximumAmountOfEvents?: number;
-
- // should be only used for multi-query requests
- measureName?: string;
- forId?: string;
+public enum FilterExpressionLogicalOperator {
+ AND,
+ OR
}
diff --git
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/FilterExpressionNode.java
similarity index 57%
copy from
ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
copy to
streampipes-model/src/main/java/org/apache/streampipes/model/datalake/FilterExpressionNode.java
index a835f2b297..fc67193910 100644
---
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
+++
b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/FilterExpressionNode.java
@@ -15,27 +15,15 @@
* limitations under the License.
*
*/
+package org.apache.streampipes.model.datalake;
-import { MissingValueBehaviour } from './data-lake-query-config.model';
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
-export interface DatalakeQueryParameters {
- columns?: string;
- startDate?: number;
- endDate?: number;
- page?: number;
- limit?: number;
- offset?: number;
- groupBy?: string;
- order?: string;
- aggregationFunction?: string;
- timeInterval?: string;
- countOnly?: boolean;
- autoAggregate?: boolean;
- filter?: string;
- missingValueBehaviour?: MissingValueBehaviour;
- maximumAmountOfEvents?: number;
-
- // should be only used for multi-query requests
- measureName?: string;
- forId?: string;
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY,
property = "type")
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = FilterExpressionGroup.class, name = "group"),
+ @JsonSubTypes.Type(value = FilterExpressionCondition.class, name =
"condition")
+})
+public interface FilterExpressionNode {
}
diff --git
a/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/param/SupportedRestQueryParams.java
b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/param/SupportedRestQueryParams.java
index 4ec3a83894..667d02291f 100644
---
a/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/param/SupportedRestQueryParams.java
+++
b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/param/SupportedRestQueryParams.java
@@ -39,6 +39,7 @@ public class SupportedRestQueryParams {
public static final String QP_COUNT_ONLY = "countOnly";
public static final String QP_AUTO_AGGREGATE = "autoAggregate";
public static final String QP_FILTER = "filter";
+ public static final String QP_FILTER_EXPRESSION = "filterExpression";
public static final String QP_MAXIMUM_AMOUNT_OF_EVENTS =
"maximumAmountOfEvents";
public static final String QP_XLSX_USE_TEMPLATE = "useTemplate";
public static final String QP_XLSX_TEMPLATE_ID = "templateId";
@@ -62,6 +63,7 @@ public class SupportedRestQueryParams {
QP_AUTO_AGGREGATE,
QP_MISSING_VALUE_BEHAVIOUR,
QP_FILTER,
+ QP_FILTER_EXPRESSION,
QP_MAXIMUM_AMOUNT_OF_EVENTS,
QP_XLSX_START_ROW,
QP_XLSX_TEMPLATE_ID,
diff --git
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeResource.java
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeResource.java
index 51d2ea14be..df2ff38dee 100644
---
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeResource.java
+++
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeResource.java
@@ -70,6 +70,7 @@ import static
org.apache.streampipes.model.datalake.param.SupportedRestQueryPara
import static
org.apache.streampipes.model.datalake.param.SupportedRestQueryParams.QP_CSV_DELIMITER;
import static
org.apache.streampipes.model.datalake.param.SupportedRestQueryParams.QP_END_DATE;
import static
org.apache.streampipes.model.datalake.param.SupportedRestQueryParams.QP_FILTER;
+import static
org.apache.streampipes.model.datalake.param.SupportedRestQueryParams.QP_FILTER_EXPRESSION;
import static
org.apache.streampipes.model.datalake.param.SupportedRestQueryParams.QP_FORMAT;
import static
org.apache.streampipes.model.datalake.param.SupportedRestQueryParams.QP_GROUP_BY;
import static
org.apache.streampipes.model.datalake.param.SupportedRestQueryParams.QP_HEADER_COLUMN_NAME;
@@ -194,6 +195,7 @@ public class DataLakeResource extends
AbstractDataLakeResource {
@Parameter(in = ParameterIn.QUERY, description = "auto-aggregate the
number of results to avoid browser overload") @RequestParam(value =
QP_AUTO_AGGREGATE, required = false) boolean autoAggregate,
@Parameter(in = ParameterIn.QUERY, description = "filter conditions (a
comma-separated list of filter conditions"
+ "such as [field,operator,condition])") @RequestParam(value =
QP_FILTER, required = false) String filter,
+ @Parameter(in = ParameterIn.QUERY, description = "JSON encoded nested
filter expression") @RequestParam(value = QP_FILTER_EXPRESSION, required =
false) String filterExpression,
@Parameter(in = ParameterIn.QUERY, description = "missingValueBehaviour
(ignore or empty)") @RequestParam(value = QP_MISSING_VALUE_BEHAVIOUR, required
= false) String missingValueBehaviour,
@Parameter(in = ParameterIn.QUERY, description = "the maximum amount of
resulting events,"
+ "when too high the query status is set to TOO_MUCH_DATA")
@RequestParam(value = QP_MAXIMUM_AMOUNT_OF_EVENTS, required = false) Integer
maximumAmountOfResults,
@@ -248,6 +250,7 @@ public class DataLakeResource extends
AbstractDataLakeResource {
@Parameter(in = ParameterIn.QUERY, description = "missingValueBehaviour
(ignore or empty)") @RequestParam(value = QP_MISSING_VALUE_BEHAVIOUR, required
= false) String missingValueBehaviour,
@Parameter(in = ParameterIn.QUERY, description = "filter conditions (a
comma-separated list of filter conditions"
+ "such as [field,operator,condition])") @RequestParam(value =
QP_FILTER, required = false) String filter,
+ @Parameter(in = ParameterIn.QUERY, description = "JSON encoded nested
filter expression") @RequestParam(value = QP_FILTER_EXPRESSION, required =
false) String filterExpression,
@Parameter(in = ParameterIn.QUERY, description = "Excel export with
template") @RequestParam(value = QP_XLSX_USE_TEMPLATE, required = false)
boolean useTemplate,
@Parameter(in = ParameterIn.QUERY, description = "ID of the excel
template file to use") @RequestParam(value = QP_XLSX_TEMPLATE_ID, required =
false) String templateId,
@Parameter(in = ParameterIn.QUERY, description = "The first row in the
excel file where data should be written") @RequestParam(value =
QP_XLSX_START_ROW, required = false) Integer startRow,
diff --git
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
b/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
index a835f2b297..eaf8627e11 100644
---
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
+++
b/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
@@ -32,6 +32,7 @@ export interface DatalakeQueryParameters {
countOnly?: boolean;
autoAggregate?: boolean;
filter?: string;
+ filterExpression?: string;
missingValueBehaviour?: MissingValueBehaviour;
maximumAmountOfEvents?: number;
diff --git
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/data-lake-query-config.model.ts
b/ui/projects/streampipes/platform-services/src/lib/model/datalake/data-lake-query-config.model.ts
index f2bc4c9e39..6b648c48f8 100644
---
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/data-lake-query-config.model.ts
+++
b/ui/projects/streampipes/platform-services/src/lib/model/datalake/data-lake-query-config.model.ts
@@ -47,10 +47,27 @@ export interface SelectedFilter {
field?: DataExplorerField;
operator: string;
value: any;
+ chainingOperator?: LogicalOperator;
+}
+
+export type LogicalOperator = 'AND' | 'OR';
+
+export interface FilterExpressionCondition {
+ type: 'condition';
+ field: string;
+ operator: string;
+ condition: any;
+}
+
+export interface FilterExpressionGroup {
+ type: 'group';
+ operator: LogicalOperator;
+ children: Array<FilterExpressionCondition | FilterExpressionGroup>;
}
export interface QueryConfig {
selectedFilters: SelectedFilter[];
+ filterExpression?: FilterExpressionGroup;
fields?: FieldConfig[];
groupBy?: FieldConfig[];
limit?: number;
diff --git
a/ui/projects/streampipes/platform-services/src/lib/query/DatalakeQueryParameterBuilder.ts
b/ui/projects/streampipes/platform-services/src/lib/query/DatalakeQueryParameterBuilder.ts
index 0ef687c835..f8ab65e7c9 100644
---
a/ui/projects/streampipes/platform-services/src/lib/query/DatalakeQueryParameterBuilder.ts
+++
b/ui/projects/streampipes/platform-services/src/lib/query/DatalakeQueryParameterBuilder.ts
@@ -18,6 +18,8 @@
import {
FieldConfig,
+ FilterExpressionCondition,
+ FilterExpressionGroup,
MissingValueBehaviour,
SelectedFilter,
} from '../model/datalake/data-lake-query-config.model';
@@ -155,20 +157,31 @@ export class DatalakeQueryParameterBuilder {
public withFilters(
filterConditions: SelectedFilter[],
): DatalakeQueryParameterBuilder {
- const filters = [];
- filterConditions.forEach(filter => {
- if (filter.field && filter.value && filter.operator) {
- filters.push(
- '[' +
- filter.field.runtimeName +
- ';' +
- filter.operator +
- ';' +
- filter.value +
- ']',
- );
- }
- });
+ const validFilters = filterConditions.filter(filter =>
+ this.isValidFilter(filter),
+ );
+ const hasOrConnector = validFilters.some(
+ (filter, index) => index > 0 && filter.chainingOperator === 'OR',
+ );
+
+ if (hasOrConnector) {
+ this.queryParams.filterExpression = JSON.stringify(
+ this.buildChainedExpression(validFilters),
+ );
+ delete this.queryParams.filter;
+ return this;
+ }
+
+ const filters = validFilters.map(
+ filter =>
+ '[' +
+ filter.field!.runtimeName +
+ ';' +
+ filter.operator +
+ ';' +
+ filter.value +
+ ']',
+ );
if (filters.length > 0) {
this.queryParams.filter = filters.toString();
@@ -177,6 +190,113 @@ export class DatalakeQueryParameterBuilder {
return this;
}
+ public withFilterExpression(
+ filterExpression: FilterExpressionGroup,
+ ): DatalakeQueryParameterBuilder {
+ this.queryParams.filterExpression = JSON.stringify(
+ this.normalizeExpressionGroup(filterExpression),
+ );
+ delete this.queryParams.filter;
+
+ return this;
+ }
+
+ private buildChainedExpression(
+ filterConditions: SelectedFilter[],
+ ): FilterExpressionGroup {
+ let expression: FilterExpressionCondition | FilterExpressionGroup =
+ this.toExpressionCondition(filterConditions[0]);
+
+ for (let i = 1; i < filterConditions.length; i++) {
+ const currentFilter = filterConditions[i];
+ expression = {
+ type: 'group',
+ operator: currentFilter.chainingOperator ?? 'AND',
+ children: [
+ expression,
+ this.toExpressionCondition(currentFilter),
+ ],
+ };
+ }
+
+ if (expression.type === 'condition') {
+ return {
+ type: 'group',
+ operator: 'AND',
+ children: [expression],
+ };
+ }
+
+ return expression;
+ }
+
+ private toExpressionCondition(
+ filter: SelectedFilter,
+ ): FilterExpressionCondition {
+ return {
+ type: 'condition',
+ field: filter.field!.runtimeName,
+ operator: filter.operator,
+ condition: this.normalizeConditionValue(filter.value),
+ };
+ }
+
+ private normalizeConditionValue(value: any): any {
+ if (
+ typeof value === 'string' &&
+ value.startsWith('"') &&
+ value.endsWith('"') &&
+ value.length >= 2
+ ) {
+ return value.substring(1, value.length - 1);
+ }
+
+ if (typeof value === 'string') {
+ if (value.toLowerCase() === 'true') {
+ return true;
+ }
+
+ if (value.toLowerCase() === 'false') {
+ return false;
+ }
+
+ if (value !== '' && !Number.isNaN(Number(value))) {
+ return Number(value);
+ }
+ }
+
+ return value;
+ }
+
+ private normalizeExpressionGroup(
+ group: FilterExpressionGroup,
+ ): FilterExpressionGroup {
+ return {
+ type: 'group',
+ operator: group.operator ?? 'AND',
+ children: group.children.map(child =>
+ child.type === 'group'
+ ? this.normalizeExpressionGroup(child)
+ : {
+ type: 'condition',
+ field: child.field,
+ operator: child.operator,
+ condition: this.normalizeConditionValue(
+ child.condition,
+ ),
+ },
+ ),
+ };
+ }
+
+ private isValidFilter(filter: SelectedFilter): boolean {
+ const hasValue =
+ filter.value !== undefined &&
+ filter.value !== null &&
+ filter.value !== '';
+ return !!filter.field && !!filter.operator && hasValue;
+ }
+
public withMissingValueBehaviour(
missingValueBehaviour: MissingValueBehaviour,
): DatalakeQueryParameterBuilder {
diff --git
a/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts
b/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts
index deb9c14e1b..8a08e535d0 100644
---
a/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts
+++
b/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts
@@ -113,7 +113,9 @@ export class DataViewQueryGeneratorService {
}
}
- if (queryConfig.selectedFilters.length > 0) {
+ if (queryConfig.filterExpression) {
+ queryBuilder.withFilterExpression(queryConfig.filterExpression);
+ } else if (queryConfig.selectedFilters.length > 0) {
queryBuilder.withFilters(queryConfig.selectedFilters);
}
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-condition-row/advanced-filter-condition-row.component.html
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-condition-row/advanced-filter-condition-row.component.html
new file mode 100644
index 0000000000..9e4b9c800f
--- /dev/null
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-condition-row/advanced-filter-condition-row.component.html
@@ -0,0 +1,52 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You 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.
+ ~
+ -->
+
+<div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap">
+ <sp-filter-selection-panel-row-property-selection
+ [filter]="filterModel"
+ [possibleFields]="possibleFields"
+ (update)="onUpdate()"
+ >
+ </sp-filter-selection-panel-row-property-selection>
+
+ <sp-filter-selection-panel-row-operation-selection
+ [filter]="filterModel"
+ (update)="onUpdate()"
+ >
+ </sp-filter-selection-panel-row-operation-selection>
+
+ @if (!filterModel.field || !tagValues.has(filterModel.field.runtimeName)) {
+ <sp-filter-selection-panel-row-value-input
+ [filter]="filterModel"
+ [tagValues]="tagValues"
+ (update)="onUpdate()"
+ >
+ </sp-filter-selection-panel-row-value-input>
+ } @else {
+ <sp-filter-selection-panel-row-value-autocomplete
+ [filter]="filterModel"
+ [tagValues]="tagValues"
+ (update)="onUpdate()"
+ >
+ </sp-filter-selection-panel-row-value-autocomplete>
+ }
+
+ <button mat-icon-button color="accent" (click)="onRemove()">
+ <i class="material-icons">remove</i>
+ </button>
+</div>
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-condition-row/advanced-filter-condition-row.component.ts
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-condition-row/advanced-filter-condition-row.component.ts
new file mode 100644
index 0000000000..33a4998489
--- /dev/null
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-condition-row/advanced-filter-condition-row.component.ts
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ *
+ */
+
+import {
+ Component,
+ EventEmitter,
+ Input,
+ OnChanges,
+ Output,
+ SimpleChanges,
+} from '@angular/core';
+import {
+ FieldConfig,
+ FilterExpressionCondition,
+ SelectedFilter,
+} from '@streampipes/platform-services';
+import { FilterSelectionPanelRowPropertySelectionComponent } from
'../../filter-selection-panel-row/panel-row-property-selection/filter-selection-panel-row-property-selection.component';
+import { FilterSelectionPanelRowOperationSelectionComponent } from
'../../filter-selection-panel-row/panel-row-operation-selection/filter-selection-panel-row-operation-selection.component';
+import { FilterSelectionPanelRowValueInputComponent } from
'../../filter-selection-panel-row/panel-row-value-input/filter-selection-panel-row-value-input.component';
+import { FilterSelectionPanelRowValueAutocompleteComponent } from
'../../filter-selection-panel-row/panel-row-value-input-autocomplete/filter-selection-panel-row-value-autocomplete.component';
+import { MatIconButton } from '@angular/material/button';
+
+@Component({
+ selector: 'sp-advanced-filter-condition-row',
+ templateUrl: './advanced-filter-condition-row.component.html',
+ imports: [
+ FilterSelectionPanelRowPropertySelectionComponent,
+ FilterSelectionPanelRowOperationSelectionComponent,
+ FilterSelectionPanelRowValueInputComponent,
+ FilterSelectionPanelRowValueAutocompleteComponent,
+ MatIconButton,
+ ],
+})
+export class AdvancedFilterConditionRowComponent implements OnChanges {
+ @Input()
+ condition: FilterExpressionCondition;
+
+ @Input()
+ possibleFields: FieldConfig[] = [];
+
+ @Input()
+ tagValues: Map<string, string[]> = new Map<string, string[]>();
+
+ @Output()
+ update = new EventEmitter<void>();
+
+ @Output()
+ remove = new EventEmitter<void>();
+
+ filterModel: SelectedFilter = {
+ operator: '=',
+ value: '',
+ };
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['condition'] || changes['possibleFields']) {
+ this.syncFromCondition();
+ }
+ }
+
+ onUpdate(): void {
+ this.syncToCondition();
+ this.update.emit();
+ }
+
+ onRemove(): void {
+ this.remove.emit();
+ }
+
+ private syncFromCondition(): void {
+ const selectedField = this.possibleFields.find(
+ field => field.runtimeName === this.condition?.field,
+ );
+
+ this.filterModel = {
+ field: selectedField as any,
+ operator: this.condition?.operator ?? '=',
+ value: this.condition?.condition ?? '',
+ };
+ }
+
+ private syncToCondition(): void {
+ this.condition.field = this.filterModel.field?.runtimeName;
+ this.condition.operator = this.filterModel.operator;
+ this.condition.condition = this.filterModel.value;
+ }
+}
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-dialog.component.html
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-dialog.component.html
new file mode 100644
index 0000000000..6e12a4a48b
--- /dev/null
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-dialog.component.html
@@ -0,0 +1,145 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You 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.
+ ~
+ -->
+
+<div class="sp-dialog-container">
+ <div class="sp-dialog-content p-15">
+ <div fxFlex="100" fxLayout="column">
+ <sp-split-section
+ [level]="3"
+ [title]="'Advanced filter' | translate"
+ [subtitle]="
+ 'Build nested AND/OR expressions with groups.' | translate
+ "
+ >
+ @if (hasPreview()) {
+ <sp-alert-banner
+ type="info"
+ [title]="'Filter Preview' | translate"
+ [description]="previewSummary()"
+ >
+ </sp-alert-banner>
+ }
+
+ <ng-container
+ *ngTemplateOutlet="
+ groupEditor;
+ context: { $implicit: expression, depth: 0 }
+ "
+ ></ng-container>
+
+ @if (validationMessage) {
+ <sp-alert-banner
+ type="error"
+ [title]="'Validation Error' | translate"
+ [description]="validationMessage"
+ >
+ </sp-alert-banner>
+ }
+ </sp-split-section>
+ </div>
+ </div>
+ <mat-divider></mat-divider>
+ <div class="sp-dialog-actions" fxLayoutGap="10px">
+ <button mat-flat-button (click)="save()">
+ {{ 'Apply' | translate }}
+ </button>
+ <button
+ mat-flat-button
+ class="mat-basic"
+ (click)="clearAdvancedFilter()"
+ >
+ {{ 'Use simple filters' | translate }}
+ </button>
+ <button mat-flat-button class="mat-basic" (click)="close()">
+ {{ 'Cancel' | translate }}
+ </button>
+ </div>
+</div>
+
+<ng-template #groupEditor let-group let-depth="depth">
+ <div class="advanced-filter-group" [style.marginLeft.px]="depth * 16">
+ <div class="advanced-filter-group-header">
+ <div class="advanced-filter-group-title">
+ {{ formatGroupLabel(depth) }}
+ </div>
+
+ <mat-form-field
+ appearance="outline"
+ class="advanced-filter-operator"
+ >
+ <mat-select
+ [(value)]="group.operator"
+ (selectionChange)="onExpressionChanged()"
+ >
+ <mat-option [value]="'AND'">AND</mat-option>
+ <mat-option [value]="'OR'">OR</mat-option>
+ </mat-select>
+ </mat-form-field>
+
+ <button
+ mat-flat-button
+ class="mat-basic"
+ (click)="addCondition(group)"
+ >
+ {{ 'Add condition' | translate }}
+ </button>
+ <button mat-flat-button class="mat-basic"
(click)="addGroup(group)">
+ {{ 'Add group' | translate }}
+ </button>
+ </div>
+
+ <div class="advanced-filter-group-children">
+ @for (child of group.children; track child; let i = $index) {
+ @if (child.type === 'group') {
+ <div
+ fxLayout="row"
+ fxLayoutGap="10px"
+ fxLayoutAlign="start center"
+ >
+ <button
+ mat-icon-button
+ color="accent"
+ (click)="removeChild(group, i)"
+ class="advanced-filter-remove-group"
+ >
+ <i class="material-icons">remove</i>
+ </button>
+
+ <ng-container
+ *ngTemplateOutlet="
+ groupEditor;
+ context: { $implicit: child, depth: depth + 1 }
+ "
+ ></ng-container>
+ </div>
+ } @else {
+ <div class="advanced-filter-condition">
+ <sp-advanced-filter-condition-row
+ [condition]="child"
+ [possibleFields]="possibleFields"
+ [tagValues]="tagValues"
+ (update)="onExpressionChanged()"
+ (remove)="removeChild(group, i)"
+ >
+ </sp-advanced-filter-condition-row>
+ </div>
+ }
+ }
+ </div>
+ </div>
+</ng-template>
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-dialog.component.scss
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-dialog.component.scss
new file mode 100644
index 0000000000..a93e07616c
--- /dev/null
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-dialog.component.scss
@@ -0,0 +1,36 @@
+.advanced-filter-group {
+ border: 1px solid var(--color-bg-3);
+ border-radius: 6px;
+ padding: 10px;
+ background: #fff;
+}
+
+.advanced-filter-group-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.advanced-filter-group-title {
+ font-weight: 600;
+ min-width: 70px;
+}
+
+.advanced-filter-operator {
+ width: 110px;
+}
+
+.advanced-filter-group-children {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.advanced-filter-remove-group {
+ margin-top: 6px;
+}
+
+.advanced-filter-condition {
+ padding: 4px 0;
+}
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-dialog.component.ts
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-dialog.component.ts
new file mode 100644
index 0000000000..68623b61ae
--- /dev/null
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/advanced-filter-dialog/advanced-filter-dialog.component.ts
@@ -0,0 +1,292 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ *
+ */
+
+import { Component, inject, Input, OnInit } from '@angular/core';
+import {
+ DialogRef,
+ SpAlertBannerComponent,
+ SplitSectionComponent,
+} from '@streampipes/shared-ui';
+import {
+ FieldConfig,
+ FilterExpressionCondition,
+ FilterExpressionGroup,
+ SelectedFilter,
+} from '@streampipes/platform-services';
+import { MatButton, MatIconButton } from '@angular/material/button';
+import { MatFormField } from '@angular/material/form-field';
+import { MatOption, MatSelect } from '@angular/material/select';
+import { NgTemplateOutlet } from '@angular/common';
+import { AdvancedFilterConditionRowComponent } from
'./advanced-filter-condition-row/advanced-filter-condition-row.component';
+import { TranslatePipe } from '@ngx-translate/core';
+import {
+ FlexDirective,
+ LayoutAlignDirective,
+ LayoutDirective,
+ LayoutGapDirective,
+} from '@ngbracket/ngx-layout';
+import { MatDivider } from '@angular/material/list';
+import { FilterExpressionPreviewService } from
'../filter-expression-preview.service';
+
+export interface AdvancedFilterDialogResult {
+ action: 'save' | 'clear';
+ expression?: FilterExpressionGroup;
+}
+
+@Component({
+ selector: 'sp-advanced-filter-dialog',
+ templateUrl: './advanced-filter-dialog.component.html',
+ styleUrls: ['./advanced-filter-dialog.component.scss'],
+ imports: [
+ SplitSectionComponent,
+ MatButton,
+ MatIconButton,
+ MatFormField,
+ MatSelect,
+ MatOption,
+ NgTemplateOutlet,
+ AdvancedFilterConditionRowComponent,
+ TranslatePipe,
+ SpAlertBannerComponent,
+ LayoutGapDirective,
+ MatDivider,
+ FlexDirective,
+ LayoutDirective,
+ LayoutAlignDirective,
+ ],
+})
+export class AdvancedFilterDialogComponent implements OnInit {
+ @Input()
+ existingExpression?: FilterExpressionGroup;
+
+ @Input()
+ selectedFilters: SelectedFilter[] = [];
+
+ @Input()
+ possibleFields: FieldConfig[] = [];
+
+ @Input()
+ tagValues: Map<string, string[]> = new Map<string, string[]>();
+
+ private dialogRef = inject(DialogRef<AdvancedFilterDialogComponent>);
+ private filterExpressionPreviewService = inject(
+ FilterExpressionPreviewService,
+ );
+
+ expression: FilterExpressionGroup = this.createEmptyGroup();
+ validationMessage?: string;
+
+ ngOnInit(): void {
+ if (this.existingExpression) {
+ this.expression = this.cloneGroup(this.existingExpression);
+ return;
+ }
+
+ const expressionFromSimple = this.buildExpressionFromSimpleFilters(
+ this.selectedFilters,
+ );
+ this.expression = expressionFromSimple ?? this.createEmptyGroup();
+ }
+
+ addCondition(group: FilterExpressionGroup): void {
+ group.children.push(this.createEmptyCondition());
+ this.validationMessage = undefined;
+ }
+
+ addGroup(group: FilterExpressionGroup): void {
+ group.children.push(this.createEmptyGroup());
+ this.validationMessage = undefined;
+ }
+
+ removeChild(group: FilterExpressionGroup, index: number): void {
+ group.children.splice(index, 1);
+ }
+
+ close(): void {
+ this.dialogRef.close();
+ }
+
+ clearAdvancedFilter(): void {
+ this.dialogRef.close({ action: 'clear' } as
AdvancedFilterDialogResult);
+ }
+
+ save(): void {
+ const validationError = this.validateGroup(this.expression);
+ if (validationError) {
+ this.validationMessage = validationError;
+ return;
+ }
+
+ this.dialogRef.close({
+ action: 'save',
+ expression: this.cloneGroup(this.expression),
+ } as AdvancedFilterDialogResult);
+ }
+
+ onExpressionChanged(): void {
+ this.validationMessage = undefined;
+ }
+
+ hasPreview(): boolean {
+ return this.expression?.children?.length > 0;
+ }
+
+ previewSummary(): string {
+ if (!this.hasPreview()) {
+ return '';
+ }
+
+ return this.filterExpressionPreviewService.format(this.expression);
+ }
+
+ formatGroupLabel(depth: number): string {
+ return depth === 0 ? 'Root group' : 'Group';
+ }
+
+ private createEmptyGroup(): FilterExpressionGroup {
+ return {
+ type: 'group',
+ operator: 'AND',
+ children: [],
+ };
+ }
+
+ private createEmptyCondition(): FilterExpressionCondition {
+ return {
+ type: 'condition',
+ field: '',
+ operator: '=',
+ condition: '',
+ };
+ }
+
+ private cloneGroup(group: FilterExpressionGroup): FilterExpressionGroup {
+ return {
+ type: 'group',
+ operator: group.operator ?? 'AND',
+ children: group.children.map(child =>
+ child.type === 'group'
+ ? this.cloneGroup(child)
+ : {
+ type: 'condition',
+ field: child.field,
+ operator: child.operator,
+ condition: child.condition,
+ },
+ ),
+ };
+ }
+
+ private buildExpressionFromSimpleFilters(
+ filters: SelectedFilter[],
+ ): FilterExpressionGroup | undefined {
+ const validFilters = filters.filter(filter =>
+ this.isValidSimpleFilter(filter),
+ );
+ if (validFilters.length === 0) {
+ return undefined;
+ }
+
+ const root: FilterExpressionGroup = {
+ type: 'group',
+ operator: 'AND',
+ children: [],
+ };
+
+ validFilters.forEach((filter, index) => {
+ if (index > 0 && filter.chainingOperator === 'OR') {
+ // Preserve flat OR chains when opening the advanced editor.
+ root.operator = 'OR';
+ }
+
+ root.children.push({
+ type: 'condition',
+ field: filter.field?.runtimeName ?? '',
+ operator: filter.operator,
+ condition: filter.value,
+ });
+ });
+
+ // For mixed AND/OR flat chains, preserve left-associative semantics.
+ if (
+ validFilters.some(
+ (filter, index) =>
+ index > 0 && filter.chainingOperator === 'OR',
+ ) &&
+ validFilters.some(
+ (filter, index) =>
+ index > 0 && filter.chainingOperator !== 'OR',
+ )
+ ) {
+ let expression: FilterExpressionCondition | FilterExpressionGroup =
+ root.children[0] as FilterExpressionCondition;
+
+ for (let i = 1; i < validFilters.length; i++) {
+ const current = validFilters[i];
+ expression = {
+ type: 'group',
+ operator: current.chainingOperator ?? 'AND',
+ children: [expression, root.children[i]],
+ };
+ }
+
+ return expression.type === 'group'
+ ? expression
+ : {
+ type: 'group',
+ operator: 'AND',
+ children: [expression],
+ };
+ }
+
+ return root;
+ }
+
+ private isValidSimpleFilter(filter: SelectedFilter): boolean {
+ const hasValue =
+ filter.value !== undefined &&
+ filter.value !== null &&
+ filter.value !== '';
+ return !!filter.field && !!filter.operator && hasValue;
+ }
+
+ private validateGroup(group: FilterExpressionGroup): string | undefined {
+ if (!group.children?.length) {
+ return 'Every group must contain at least one condition or
sub-group.';
+ }
+
+ for (const child of group.children) {
+ if (child.type === 'group') {
+ const nestedError = this.validateGroup(child);
+ if (nestedError) {
+ return nestedError;
+ }
+ } else {
+ const hasConditionValue =
+ child.condition !== undefined &&
+ child.condition !== null &&
+ child.condition !== '';
+ if (!child.field || !child.operator || !hasConditionValue) {
+ return 'Please complete all fields, operators and values
before applying the advanced filter.';
+ }
+ }
+ }
+
+ return undefined;
+ }
+}
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-expression-preview.service.ts
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-expression-preview.service.ts
new file mode 100644
index 0000000000..39e3c20c5d
--- /dev/null
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-expression-preview.service.ts
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ *
+ */
+
+import { Injectable } from '@angular/core';
+import { FilterExpressionGroup } from '@streampipes/platform-services';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class FilterExpressionPreviewService {
+ format(group?: FilterExpressionGroup): string {
+ if (!group) {
+ return '';
+ }
+
+ return this.formatExpressionGroup(group);
+ }
+
+ private formatExpressionGroup(group: FilterExpressionGroup): string {
+ const formattedChildren = group.children.map(child =>
+ child.type === 'group'
+ ? this.formatExpressionGroup(child)
+ : this.formatCondition(
+ child.field,
+ child.operator,
+ child.condition,
+ ),
+ );
+
+ if (formattedChildren.length === 0) {
+ return '()';
+ }
+
+ return '(' + formattedChildren.join(` ${group.operator} `) + ')';
+ }
+
+ private formatCondition(
+ field: string,
+ operator: string,
+ condition: any,
+ ): string {
+ const displayValue =
+ typeof condition === 'string' ? `${condition}` : String(condition);
+ return `${field} ${operator} ${displayValue}`;
+ }
+}
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel-row/filter-selection-panel-row.component.html
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel-row/filter-selection-panel-row.component.html
index 4401ad9ccf..a2ae21f5d4 100644
---
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel-row/filter-selection-panel-row.component.html
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel-row/filter-selection-panel-row.component.html
@@ -18,6 +18,28 @@
<div fxFlex="100" fxLayout="column" class="form-field-small">
<div fxFlex="100" fxLayout="row" fxLayoutAlign="start center">
+ @if (index > 0) {
+ <mat-form-field
+ color="accent"
+ class="w-80-px mr-5"
+ appearance="outline"
+ >
+ <mat-select
+ [(value)]="filter.chainingOperator"
+ panelClass="form-field-small min-w-100"
+ (selectionChange)="updateParentComponent()"
+ data-cy="design-panel-data-settings-filter-connector"
+ >
+ <mat-option [value]="'AND'">
+ <span class="pipeline-name">AND</span>
+ </mat-option>
+ <mat-option [value]="'OR'">
+ <span class="pipeline-name">OR</span>
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ }
+
<sp-filter-selection-panel-row-property-selection
[filter]="filter"
[possibleFields]="possibleFields"
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel-row/filter-selection-panel-row.component.ts
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel-row/filter-selection-panel-row.component.ts
index 4c847fa816..192851129f 100644
---
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel-row/filter-selection-panel-row.component.ts
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel-row/filter-selection-panel-row.component.ts
@@ -28,6 +28,8 @@ import { FilterSelectionPanelRowOperationSelectionComponent }
from './panel-row-
import { FilterSelectionPanelRowValueInputComponent } from
'./panel-row-value-input/filter-selection-panel-row-value-input.component';
import { FilterSelectionPanelRowValueAutocompleteComponent } from
'./panel-row-value-input-autocomplete/filter-selection-panel-row-value-autocomplete.component';
import { MatIconButton } from '@angular/material/button';
+import { MatFormField } from '@angular/material/form-field';
+import { MatOption, MatSelect } from '@angular/material/select';
@Component({
selector: 'sp-filter-selection-panel-row',
@@ -41,6 +43,9 @@ import { MatIconButton } from '@angular/material/button';
FilterSelectionPanelRowValueInputComponent,
FilterSelectionPanelRowValueAutocompleteComponent,
MatIconButton,
+ MatFormField,
+ MatSelect,
+ MatOption,
],
})
export class FilterSelectionPanelRowComponent {
@@ -50,6 +55,9 @@ export class FilterSelectionPanelRowComponent {
@Input()
public possibleFields: FieldConfig[];
+ @Input()
+ public index: number;
+
@Input()
public tagValues: Map<string, string[]>;
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.html
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.html
index 0912e88d98..c274a4b408 100644
---
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.html
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.html
@@ -19,7 +19,6 @@
<sp-split-section [level]="3" [title]="'Filter' | translate">
<div section-actions fxLayoutAlign="start center" fxFlex="100">
<button
- mat-button
mat-flat-button
color="accent"
class="small-button"
@@ -29,8 +28,44 @@
>
{{ 'Add Filter' | translate }}
</button>
+
+ <button
+ mat-flat-button
+ class="mat-basic small-button"
+ data-cy="design-panel-data-settings-advanced-filter"
+ (click)="openAdvancedFilterDialog()"
+ >
+ {{ 'Advanced Filter' | translate }}
+ </button>
</div>
<div fxLayout="column">
+ @if (hasAdvancedFilterExpression()) {
+ <sp-alert-banner
+ type="info"
+ [title]="'Advanced filter active' | translate"
+ [description]="advancedFilterSummary()"
+ style="margin-bottom: 10px"
+ >
+ <div
+ style="font-size: 12px; margin-top: 6px; margin-bottom:
6px"
+ >
+ {{
+ 'Simple filters below are kept for fallback, but the
advanced filter is currently used for queries.'
+ | translate
+ }}
+ </div>
+ <div>
+ <button
+ mat-flat-button
+ class="small-button"
+ (click)="disableAdvancedFilter()"
+ >
+ {{ 'Clear & use simple filters' | translate }}
+ </button>
+ </div>
+ </sp-alert-banner>
+ }
+
@for (
filter of sourceConfig.queryConfig.selectedFilters;
track filter;
@@ -38,6 +73,7 @@
) {
<sp-filter-selection-panel-row
[filter]="filter"
+ [index]="i"
[possibleFields]="sourceConfig.queryConfig.fields"
[tagValues]="tagValues"
(update)="updateWidget()"
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.ts
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.ts
index 23c9680ef7..d4d464d804 100644
---
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.ts
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.ts
@@ -19,12 +19,18 @@
import { Component, Input, OnInit } from '@angular/core';
import {
DatalakeRestService,
+ FilterExpressionGroup,
SelectedFilter,
SourceConfig,
} from '@streampipes/platform-services';
import { ChartConfigurationService } from
'../../../../../../chart-shared/services/chart-configuration.service';
import { ChartFieldProviderService } from
'../../../../../../chart-shared/services/chart-field-provider.service';
-import { SplitSectionComponent } from '@streampipes/shared-ui';
+import {
+ DialogService,
+ PanelType,
+ SpAlertBannerComponent,
+ SplitSectionComponent,
+} from '@streampipes/shared-ui';
import {
FlexDirective,
LayoutAlignDirective,
@@ -33,6 +39,11 @@ import {
import { MatButton } from '@angular/material/button';
import { FilterSelectionPanelRowComponent } from
'./filter-selection-panel-row/filter-selection-panel-row.component';
import { TranslatePipe } from '@ngx-translate/core';
+import {
+ AdvancedFilterDialogComponent,
+ AdvancedFilterDialogResult,
+} from './advanced-filter-dialog/advanced-filter-dialog.component';
+import { FilterExpressionPreviewService } from
'./filter-expression-preview.service';
@Component({
selector: 'sp-filter-selection-panel',
@@ -45,6 +56,7 @@ import { TranslatePipe } from '@ngx-translate/core';
LayoutDirective,
FilterSelectionPanelRowComponent,
TranslatePipe,
+ SpAlertBannerComponent,
],
})
export class FilterSelectionPanelComponent implements OnInit {
@@ -56,9 +68,15 @@ export class FilterSelectionPanelComponent implements OnInit
{
private widgetConfigService: ChartConfigurationService,
private fieldProviderService: ChartFieldProviderService,
private dataLakeRestService: DatalakeRestService,
+ private dialogService: DialogService,
+ private filterExpressionPreviewService: FilterExpressionPreviewService,
) {}
ngOnInit(): void {
+ this.sourceConfig.queryConfig.selectedFilters.forEach(filter => {
+ filter.chainingOperator ??= 'AND';
+ });
+
this.sourceConfig.queryConfig.fields.forEach(f => {
this.tagValues.set(f.runtimeName, []);
});
@@ -96,6 +114,7 @@ export class FilterSelectionPanelComponent implements OnInit
{
const newFilter: SelectedFilter = {
operator: '=',
value: '',
+ chainingOperator: 'AND',
};
this.sourceConfig.queryConfig.selectedFilters.push(newFilter);
this.widgetConfigService.notify({
@@ -115,10 +134,74 @@ export class FilterSelectionPanelComponent implements
OnInit {
this.updateWidget();
}
+ openAdvancedFilterDialog(): void {
+ const dialogRef = this.dialogService.open(
+ AdvancedFilterDialogComponent,
+ {
+ panelType: PanelType.SLIDE_IN_PANEL,
+ title: 'Advanced Filter',
+ width: '60vw',
+ data: {
+ existingExpression:
+ this.sourceConfig.queryConfig.filterExpression,
+ selectedFilters: this.cloneSelectedFilters(
+ this.sourceConfig.queryConfig.selectedFilters,
+ ),
+ possibleFields: this.sourceConfig.queryConfig.fields,
+ tagValues: this.tagValues,
+ },
+ },
+ );
+
+ dialogRef
+ .afterClosed()
+ .subscribe((result?: AdvancedFilterDialogResult) => {
+ if (!result) {
+ return;
+ }
+
+ if (result.action === 'clear') {
+ delete this.sourceConfig.queryConfig.filterExpression;
+ } else if (result.action === 'save' && result.expression) {
+ this.sourceConfig.queryConfig.filterExpression =
+ this.cloneExpression(result.expression);
+ }
+
+ this.updateWidget();
+ });
+ }
+
+ hasAdvancedFilterExpression(): boolean {
+ return !!this.sourceConfig.queryConfig.filterExpression;
+ }
+
+ disableAdvancedFilter(): void {
+ delete this.sourceConfig.queryConfig.filterExpression;
+ this.updateWidget();
+ }
+
+ advancedFilterSummary(): string {
+ return this.filterExpressionPreviewService.format(
+ this.sourceConfig.queryConfig.filterExpression,
+ );
+ }
+
updateWidget() {
+ if (this.sourceConfig.queryConfig.filterExpression) {
+ this.widgetConfigService.notify({
+ refreshData: true,
+ refreshView: true,
+ });
+ return;
+ }
+
let update = true;
this.sourceConfig.queryConfig.selectedFilters.forEach(filter => {
- if (!filter.field || !filter.value || !filter.operator) {
+ const hasValue =
+ filter.value !== undefined &&
+ filter.value !== null &&
+ filter.value !== '';
+ if (!filter.field || !hasValue || !filter.operator) {
update = false;
}
});
@@ -130,4 +213,30 @@ export class FilterSelectionPanelComponent implements
OnInit {
});
}
}
+
+ private cloneExpression(
+ expression: FilterExpressionGroup,
+ ): FilterExpressionGroup {
+ return {
+ type: 'group',
+ operator: expression.operator ?? 'AND',
+ children: expression.children.map(child =>
+ child.type === 'group'
+ ? this.cloneExpression(child)
+ : {
+ type: 'condition',
+ field: child.field,
+ operator: child.operator,
+ condition: child.condition,
+ },
+ ),
+ };
+ }
+
+ private cloneSelectedFilters(filters: SelectedFilter[]): SelectedFilter[] {
+ return filters.map(filter => ({
+ ...filter,
+ field: filter.field ? { ...(filter.field as any) } : undefined,
+ }));
+ }
}