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,
+        }));
+    }
 }

Reply via email to