This is an automated email from the ASF dual-hosted git repository.
dominikriemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git
The following commit(s) were added to refs/heads/dev by this push:
new 4e2a24c510 feat: Improve chart query builder (#4557)
4e2a24c510 is described below
commit 4e2a24c5104ea7890ee56ca44727f529f5f9891c
Author: Dominik Riemer <[email protected]>
AuthorDate: Sun Jun 14 19:00:59 2026 +0200
feat: Improve chart query builder (#4557)
---
.../influx/DataLakeInfluxQueryBuilder.java | 13 ++-
.../influx/DataLakeQueryBuilderTest.java | 25 +++++
.../dataexplorer/influx/SelectQueryParamsTest.java | 72 +++++++++++++
.../utils/ProvidedQueryParameterBuilder.java | 6 ++
.../param/ProvidedRestQueryParamConverter.java | 2 +-
.../dataexplorer/param/model/FillClauseParams.java | 15 ++-
.../param/model/InfluxQueryParameterValidator.java | 22 ++++
.../datalake/param/SupportedRestQueryParams.java | 2 +
ui/deployment/i18n/de.json | 7 ++
ui/deployment/i18n/en.json | 9 +-
ui/deployment/i18n/pl.json | 7 ++
.../lib/model/datalake/DatalakeQueryParameters.ts | 1 +
.../model/datalake/data-lake-query-config.model.ts | 1 +
.../src/lib/query/DatalakeQueryParameterBuilder.ts | 6 ++
.../lib/query/data-view-query-generator.service.ts | 4 +
.../aggregate-configuration.component.html | 15 ++-
.../aggregate-configuration.component.ts | 10 +-
.../chart-data-settings.component.html | 8 ++
.../data-settings/chart-data-settings.component.ts | 3 +
.../fill-configuration.component.html | 68 ++++++++++++
.../fill-configuration.component.scss | 21 ----
.../fill-configuration.component.ts | 115 +++++++++++++++++++++
22 files changed, 392 insertions(+), 40 deletions(-)
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 74ffa2428a..30111765d4 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
@@ -99,14 +99,15 @@ public class DataLakeInfluxQueryBuilder implements
IDataLakeQueryBuilder<Query>
AggregationFunction
aggregationFunction,
String aliasName) {
- this.selectionQuery.function(aggregationFunction.toDbName(),
columnName).as(aliasName);
+ this.selectionQuery.function(aggregationFunction.toDbName(),
escapeIdentifier(columnName))
+ .as(escapeIdentifier(aliasName));
return this;
}
@Override
public IDataLakeQueryBuilder<Query> withAggregatedColumn(String columnName,
AggregationFunction aggregationFunction) {
- this.selectionQuery.function(aggregationFunction.toDbName(), columnName);
+ this.selectionQuery.function(aggregationFunction.toDbName(),
escapeIdentifier(columnName));
return this;
}
@@ -320,4 +321,12 @@ public class DataLakeInfluxQueryBuilder implements
IDataLakeQueryBuilder<Query>
private String escapeIndex(String index) {
return "\"" + index + "\"";
}
+
+ private String escapeIdentifier(String identifier) {
+ if (identifier.matches("[A-Za-z0-9_]+")) {
+ return identifier;
+ }
+
+ return "\"" + identifier.replace("\"", "\\\"") + "\"";
+ }
}
diff --git
a/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/DataLakeQueryBuilderTest.java
b/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/DataLakeQueryBuilderTest.java
index 48caeaa516..524e12c2fa 100644
---
a/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/DataLakeQueryBuilderTest.java
+++
b/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/DataLakeQueryBuilderTest.java
@@ -18,6 +18,8 @@
package org.apache.streampipes.dataexplorer.influx;
+import org.apache.streampipes.model.datalake.AggregationFunction;
+
import org.junit.jupiter.api.Test;
import java.util.List;
@@ -37,4 +39,27 @@ public class DataLakeQueryBuilderTest {
var expected = String.format("SELECT one,two FROM \"%s\";", MEASUREMENT);
assertEquals(expected , result.getCommand());
}
+
+ @Test
+ public void withAggregatedColumnEscapesDottedFieldAndAliasTest() {
+ var result = DataLakeInfluxQueryBuilder
+ .create(MEASUREMENT)
+ .withAggregatedColumn("temperature.a", AggregationFunction.MEAN,
"temperature.a")
+ .build();
+
+ var expected = String.format("SELECT MEAN(\"temperature.a\") AS
\"temperature.a\" FROM \"%s\";", MEASUREMENT);
+ assertEquals(expected, result.getCommand());
+ }
+
+ @Test
+ public void withGroupByEscapesDottedFieldTest() {
+ var result = DataLakeInfluxQueryBuilder
+ .create(MEASUREMENT)
+ .withSimpleColumn("value")
+ .withGroupBy("temperature.a")
+ .build();
+
+ var expected = String.format("SELECT value FROM \"%s\" GROUP BY
\"temperature.a\";", MEASUREMENT);
+ assertEquals(expected, result.getCommand());
+ }
}
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 b79f55d98d..6513f2a7c3 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
@@ -236,6 +236,66 @@ public class SelectQueryParamsTest {
assertEquals("SELECT value FROM \"abc\" GROUP BY time(1ms),\"sensorId\"
fill(none);", query);
}
+ @Test
+ public void testGroupByTimeWithPreviousFill() {
+ var params = ProvidedQueryParameterBuilder.create("abc")
+ .withSimpleColumns(List.of("value"))
+ .withTimeInterval("1h")
+ .withFill("previous")
+ .build();
+
+ SelectQueryParams qp =
ProvidedRestQueryParamConverter.getSelectQueryParams(params);
+
+ String query =
qp.toQuery(DataLakeInfluxQueryBuilder.create("abc")).getCommand();
+
+ assertEquals("SELECT value FROM \"abc\" GROUP BY time(1h)
fill(previous);", query);
+ }
+
+ @Test
+ public void testGroupByTimeWithLinearFill() {
+ var params = ProvidedQueryParameterBuilder.create("abc")
+ .withSimpleColumns(List.of("value"))
+ .withTimeInterval("1h")
+ .withFill("linear")
+ .build();
+
+ SelectQueryParams qp =
ProvidedRestQueryParamConverter.getSelectQueryParams(params);
+
+ String query =
qp.toQuery(DataLakeInfluxQueryBuilder.create("abc")).getCommand();
+
+ assertEquals("SELECT value FROM \"abc\" GROUP BY time(1h) fill(linear);",
query);
+ }
+
+ @Test
+ public void testGroupByTimeWithNullFill() {
+ var params = ProvidedQueryParameterBuilder.create("abc")
+ .withSimpleColumns(List.of("value"))
+ .withTimeInterval("1h")
+ .withFill("null")
+ .build();
+
+ SelectQueryParams qp =
ProvidedRestQueryParamConverter.getSelectQueryParams(params);
+
+ String query =
qp.toQuery(DataLakeInfluxQueryBuilder.create("abc")).getCommand();
+
+ assertEquals("SELECT value FROM \"abc\" GROUP BY time(1h) fill(null);",
query);
+ }
+
+ @Test
+ public void testGroupByTimeWithNumericFill() {
+ var params = ProvidedQueryParameterBuilder.create("abc")
+ .withSimpleColumns(List.of("value"))
+ .withTimeInterval("1h")
+ .withFill("12.5")
+ .build();
+
+ SelectQueryParams qp =
ProvidedRestQueryParamConverter.getSelectQueryParams(params);
+
+ String query =
qp.toQuery(DataLakeInfluxQueryBuilder.create("abc")).getCommand();
+
+ assertEquals("SELECT value FROM \"abc\" GROUP BY time(1h) fill(12.5);",
query);
+ }
+
@Test
public void testGroupByTimeRejectsUnsafeInterval() {
var params = ProvidedQueryParameterBuilder.create("abc")
@@ -247,6 +307,18 @@ public class SelectQueryParamsTest {
() -> ProvidedRestQueryParamConverter.getSelectQueryParams(params));
}
+ @Test
+ public void testGroupByTimeRejectsUnsafeFill() {
+ var params = ProvidedQueryParameterBuilder.create("abc")
+ .withSimpleColumns(List.of("value"))
+ .withTimeInterval("1h")
+ .withFill("previous); DROP MEASUREMENT foo --")
+ .build();
+
+ assertThrows(IllegalArgumentException.class,
+ () -> ProvidedRestQueryParamConverter.getSelectQueryParams(params));
+ }
+
@Test
public void testGroupByRejectsUnsafeIdentifier() {
var params = ProvidedQueryParameterBuilder.create("abc")
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 944b4f6a66..3b28051000 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 withFill(String fill) {
+ this.queryParams.put(SupportedRestQueryParams.QP_FILL, fill);
+
+ return this;
+ }
+
public ProvidedQueryParameterBuilder withFilter(String filter) {
this.queryParams.put(SupportedRestQueryParams.QP_FILTER, filter);
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 90569a26d2..e6926308c2 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
@@ -78,7 +78,7 @@ public class ProvidedRestQueryParamConverter {
if (params.has(SupportedRestQueryParams.QP_TIME_INTERVAL)) {
String timeInterval =
params.getAsString(SupportedRestQueryParams.QP_TIME_INTERVAL);
queryParameters.withGroupByTimeParams(GroupByTimeClauseParams.from(timeInterval));
- queryParameters.withFillParams(FillClauseParams.from());
+
queryParameters.withFillParams(FillClauseParams.from(params.getAsString(SupportedRestQueryParams.QP_FILL)));
}
if (params.has(SupportedRestQueryParams.QP_GROUP_BY)) {
diff --git
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/FillClauseParams.java
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/FillClauseParams.java
index ccece054ba..f4d133d4b9 100644
---
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/FillClauseParams.java
+++
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/FillClauseParams.java
@@ -22,15 +22,28 @@ import
org.apache.streampipes.dataexplorer.api.IDataLakeQueryBuilder;
import org.apache.streampipes.dataexplorer.api.IQueryStatement;
public class FillClauseParams implements IQueryStatement {
- String fill = "none";
+ private final Object fill;
protected FillClauseParams() {
+ this.fill = "none";
}
public static FillClauseParams from() {
return new FillClauseParams();
}
+ protected FillClauseParams(String fill) {
+ this.fill = InfluxQueryParameterValidator.requireValidFill(fill);
+ }
+
+ public static FillClauseParams from(String fill) {
+ if (fill == null || fill.isBlank()) {
+ return from();
+ }
+
+ return new FillClauseParams(fill);
+ }
+
@Override
public void buildStatement(IDataLakeQueryBuilder<?> builder) {
builder.withFill(fill);
diff --git
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/InfluxQueryParameterValidator.java
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/InfluxQueryParameterValidator.java
index 750d1b3ac8..6eaaba1bd1 100644
---
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/InfluxQueryParameterValidator.java
+++
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/InfluxQueryParameterValidator.java
@@ -26,6 +26,7 @@ final class InfluxQueryParameterValidator {
private static final Pattern SAFE_TIME_INTERVAL =
Pattern.compile("^\\d+(ms|s|m|h|d|w)$");
private static final Pattern SAFE_IDENTIFIER =
Pattern.compile("^[^\\s,;()\"']+$");
+ private static final Pattern NUMERIC_FILL =
Pattern.compile("^-?\\d+(\\.\\d+)?$");
private InfluxQueryParameterValidator() {
}
@@ -47,4 +48,25 @@ final class InfluxQueryParameterValidator {
return identifier;
}
+
+ static Object requireValidFill(String fill) {
+ if (fill == null || fill.isBlank()) {
+ return "none";
+ }
+
+ String normalized = fill.trim().toLowerCase();
+
+ if (normalized.equals("none")
+ || normalized.equals("null")
+ || normalized.equals("previous")
+ || normalized.equals("linear")) {
+ return normalized;
+ }
+
+ if (NUMERIC_FILL.matcher(normalized).matches()) {
+ return Double.parseDouble(normalized);
+ }
+
+ throw new IllegalArgumentException("Invalid fill parameter: " + fill);
+ }
}
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 667d02291f..2eea5c1877 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
@@ -32,6 +32,7 @@ public class SupportedRestQueryParams {
public static final String QP_ORDER = "order";
public static final String QP_AGGREGATION_FUNCTION = "aggregationFunction";
public static final String QP_TIME_INTERVAL = "timeInterval";
+ public static final String QP_FILL = "fill";
public static final String QP_FORMAT = "format";
public static final String QP_CSV_DELIMITER = "delimiter";
@@ -57,6 +58,7 @@ public class SupportedRestQueryParams {
QP_ORDER,
QP_AGGREGATION_FUNCTION,
QP_TIME_INTERVAL,
+ QP_FILL,
QP_FORMAT,
QP_CSV_DELIMITER,
QP_COUNT_ONLY,
diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index 2eea78a5da..e0aada136d 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -263,6 +263,7 @@
"Current year": "Aktuelles Jahr",
"Currently configured query": "Aktuell konfigurierte Anfrage",
"Custom": "Benutzerdefiniert",
+ "Custom Value": "Benutzerdefinierter Wert",
"Custom color mapping": "Benutzerdefiniertes Farb-Mapping",
"Custom fields": "Benutzerdefinierte Felder",
"Dark mode": "Dunkel",
@@ -448,6 +449,8 @@
"Filename": "Dateiname",
"Files": "Dateien",
"Filetype": "Dateityp",
+ "Fill": "Auffüllen",
+ "Fill Value": "Auffüllwert",
"Filter": "Filter",
"Filter Preview": "Filtervorschau",
"Filter assets": "Assets filtern",
@@ -495,6 +498,7 @@
"Grouped": "Gruppiert",
"Grouped colors": "Farben für Gruppierung",
"Groups": "Gruppen",
+ "Handle empty values": "Leere Werte behandeln",
"Header": "Header",
"Header column name": "Spaltenname",
"Help": "Hilfe",
@@ -593,6 +597,7 @@
"Light mode": "Hell",
"Limit": "Limit",
"Line": "Linien",
+ "Linear Interpolation": "Lineare Interpolation",
"Link Type": "Link Typ",
"Linked Resources": "Verknüpfte Ressourcen",
"Links": "Links",
@@ -792,6 +797,7 @@
"Preview is being prepared from the current CSV configuration.": "Die
Vorschau wird aus der aktuellen CSV-Konfiguration erstellt.",
"Preview is currently unavailable.": "Die Vorschau ist derzeit nicht
verfügbar.",
"Previous": "Zurück",
+ "Previous Value": "Vorheriger Wert",
"Previous page": "Vorherige Seite",
"Privileges": "Rechte",
"Probable cause": "Wahrscheinliche Ursache",
@@ -859,6 +865,7 @@
"Restore password": "Passwort wiederherstellen",
"Restrict to service tags": "Auf Service-Tags beschränken",
"Result": "Ergebnis",
+ "Result Labels": "Ergebnisbeschriftungen",
"Retention": "Aufbewahrung",
"Retention Log": "Speicherprotokoll",
"Right": "Rechts",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index 0c4193aaa4..c81b30c8a5 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -263,6 +263,7 @@
"Current year": null,
"Currently configured query": null,
"Custom": null,
+ "Custom Value": null,
"Custom color mapping": null,
"Custom fields": null,
"Dark mode": null,
@@ -448,6 +449,8 @@
"Filename": null,
"Files": null,
"Filetype": null,
+ "Fill": null,
+ "Fill Value": null,
"Filter": null,
"Filter Preview": null,
"Filter assets": null,
@@ -495,6 +498,7 @@
"Grouped": null,
"Grouped colors": null,
"Groups": null,
+ "Handle empty values": null,
"Header": null,
"Header column name": null,
"Help": null,
@@ -593,6 +597,7 @@
"Light mode": null,
"Limit": null,
"Line": null,
+ "Linear Interpolation": null,
"Link Type": null,
"Linked Resources": null,
"Links": null,
@@ -792,6 +797,7 @@
"Preview is being prepared from the current CSV configuration.": null,
"Preview is currently unavailable.": null,
"Previous": null,
+ "Previous Value": null,
"Previous page": null,
"Privileges": null,
"Probable cause": null,
@@ -859,6 +865,7 @@
"Restore password": null,
"Restrict to service tags": null,
"Result": null,
+ "Result Labels": null,
"Retention": null,
"Retention Log": null,
"Right": null,
@@ -899,8 +906,8 @@
"Schema fields": null,
"Script": null,
"Search": null,
- "Search datasets": null,
"Search charts": null,
+ "Search datasets": null,
"Searching for available extensions, please wait...": null,
"Second": null,
"Secret Key": null,
diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json
index a53abc89a1..506b51c71c 100644
--- a/ui/deployment/i18n/pl.json
+++ b/ui/deployment/i18n/pl.json
@@ -263,6 +263,7 @@
"Current year": "Bieżący rok",
"Currently configured query": "Aktualnie skonfigurowane zapytanie",
"Custom": "Niestandardowe",
+ "Custom Value": "Wartość niestandardowa",
"Custom color mapping": "Własne mapowanie kolorów",
"Custom fields": "Pola niestandardowe",
"Dark mode": "Ciemny",
@@ -448,6 +449,8 @@
"Filename": "Nazwa pliku",
"Files": "Pliki",
"Filetype": "Typ pliku",
+ "Fill": "Wypełnij",
+ "Fill Value": "Wartość wypełnienia",
"Filter": "Filtr",
"Filter Preview": "Podgląd filtra",
"Filter assets": "Filtruj zasoby",
@@ -495,6 +498,7 @@
"Grouped": "Zgrupowane",
"Grouped colors": "Kolory grupowania",
"Groups": "Grupy",
+ "Handle empty values": "Obsłuż puste wartości",
"Header": "Nagłówek",
"Header column name": "Nazwa kolumny nagłówka",
"Help": "Pomoc",
@@ -593,6 +597,7 @@
"Light mode": "Jasny",
"Limit": "Limit",
"Line": "Liniowy",
+ "Linear Interpolation": "Interpolacja liniowa",
"Link Type": "Typ linku",
"Linked Resources": "Powiązane zasoby",
"Links": "Linki",
@@ -792,6 +797,7 @@
"Preview is being prepared from the current CSV configuration.": "Podgląd
jest przygotowywany na podstawie bieżącej konfiguracji CSV.",
"Preview is currently unavailable.": "Podgląd jest obecnie niedostępny.",
"Previous": "Wstecz",
+ "Previous Value": "Poprzednia wartość",
"Previous page": "Poprzednia strona",
"Privileges": "Uprawnienia",
"Probable cause": "Prawdopodobna przyczyna",
@@ -859,6 +865,7 @@
"Restore password": "Przywróć hasło",
"Restrict to service tags": "Ogranicz do tagów usług",
"Result": "Wynik",
+ "Result Labels": "Etykiety wyników",
"Retention": "Retencja",
"Retention Log": "Dziennik retencji",
"Right": "Prawa",
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 eaf8627e11..e288b47fe5 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
@@ -29,6 +29,7 @@ export interface DatalakeQueryParameters {
order?: string;
aggregationFunction?: string;
timeInterval?: string;
+ fill?: string | number;
countOnly?: boolean;
autoAggregate?: boolean;
filter?: string;
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 6b648c48f8..60a3957286 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
@@ -77,6 +77,7 @@ export interface QueryConfig {
aggregationValue?: number;
aggregationTimeUnit?: string;
aggregationFunction?: string;
+ fill?: string | number;
}
export interface SourceConfig {
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 f8ab65e7c9..1c7d78b2bf 100644
---
a/ui/projects/streampipes/platform-services/src/lib/query/DatalakeQueryParameterBuilder.ts
+++
b/ui/projects/streampipes/platform-services/src/lib/query/DatalakeQueryParameterBuilder.ts
@@ -80,6 +80,12 @@ export class DatalakeQueryParameterBuilder {
return this;
}
+ public withFill(fill: string | number): DatalakeQueryParameterBuilder {
+ this.queryParams.fill = fill;
+
+ return this;
+ }
+
public withGrouping(groupBy: FieldConfig[]): DatalakeQueryParameterBuilder
{
const groupByRuntimeNames = groupBy.map(
property => property.runtimeName,
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 ccb3e33bcc..ef1c5cf9f4 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
@@ -138,6 +138,10 @@ export class DataViewQueryGeneratorService {
queryConfig.aggregationValue,
);
}
+
+ if (queryConfig.fill !== undefined) {
+ queryBuilder.withFill(queryConfig.fill);
+ }
}
if (ignoreEventsWithMissingValues) {
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.html
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.html
index 9fd6a3014f..706df8aba1 100644
---
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.html
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.html
@@ -16,7 +16,7 @@
~
-->
-<div fxLayout="column" fxLayoutAlign="stretch" fxFlex="100">
+<div fxLayout="column" fxFlex="100">
<div
fxFlex="100"
fxLayout="column"
@@ -24,12 +24,7 @@
class="ml-0 mr-5 form-field-small"
>
<sp-form-field [level]="3" [label]="'Unit' | translate" class="w-100">
- <mat-form-field
- appearance="outline"
- fxFlex="100"
- class="w-100"
- color="accent"
- >
+ <mat-form-field appearance="outline" fxFlex="100" class="w-100">
<mat-select
[(value)]="queryConfig.aggregationTimeUnit"
class="w-100"
@@ -43,7 +38,11 @@
</mat-select>
</mat-form-field>
</sp-form-field>
- <sp-form-field [level]="3" [label]="'Value' | translate" class="w-100">
+ <sp-form-field
+ [level]="3"
+ [label]="'Value' | translate"
+ class="w-100 mb-10"
+ >
<mat-form-field
appearance="outline"
fxFlex="100"
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.ts
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.ts
index 77ee49957f..c8514ddaf3 100644
---
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.ts
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.ts
@@ -66,11 +66,9 @@ export class AggregateConfigurationComponent {
];
triggerDataRefresh() {
- if (this.widgetId) {
- this.widgetConfigService.notify({
- refreshData: true,
- refreshView: true,
- });
- }
+ this.widgetConfigService.notify({
+ refreshData: true,
+ refreshView: true,
+ });
}
}
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.html
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.html
index dc988a4145..af89a9f414 100644
---
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.html
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.html
@@ -351,6 +351,14 @@
>
</sp-aggregate-configuration>
}
+ <sp-fill-configuration
+ class="mt-10"
+ [queryConfig]="
+
sourceConfig.queryConfig
+ "
+ [widgetId]="widgetId"
+ >
+ </sp-fill-configuration>
</div>
}
</div>
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
index 71477ca0ac..5502cff7be 100644
---
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
@@ -74,6 +74,7 @@ import { ClassDirective } from
'@ngbracket/ngx-layout/extended';
import { MatInput } from '@angular/material/input';
import { MatCheckbox } from '@angular/material/checkbox';
import { AggregateConfigurationComponent } from
'./aggregate-configuration/aggregate-configuration.component';
+import { FillConfigurationComponent } from
'./fill-configuration/fill-configuration.component';
import { FilterSelectionPanelComponent } from
'./filter-selection-panel/filter-selection-panel.component';
import { OrderSelectionPanelComponent } from
'./order-selection-panel/order-selection-panel.component';
import { TranslatePipe } from '@ngx-translate/core';
@@ -116,6 +117,7 @@ import {
MatAutocomplete,
MatAutocompleteTrigger,
AggregateConfigurationComponent,
+ FillConfigurationComponent,
FieldSelectionPanelComponent,
FilterSelectionPanelComponent,
GroupSelectionPanelComponent,
@@ -332,6 +334,7 @@ export class ChartDataSettingsComponent implements OnInit {
page: 1,
aggregationTimeUnit: 'd',
aggregationValue: 1,
+ fill: 'none',
},
queryType: 'raw',
};
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.html
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.html
new file mode 100644
index 0000000000..43b4750a6f
--- /dev/null
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.html
@@ -0,0 +1,68 @@
+<!--
+~ 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 fxLayout="column" fxFlex="100">
+ <div
+ fxFlex="100"
+ fxLayout="column"
+ fxLayoutAlign="start center"
+ class="ml-0 mr-5 form-field-small"
+ >
+ <sp-form-field
+ [level]="3"
+ [label]="'Fill' | translate"
+ class="w-100"
+ [description]="'Handle empty values' | translate"
+ >
+ <mat-form-field appearance="outline" fxFlex="100" class="w-100">
+ <mat-select
+ [(ngModel)]="fillMode"
+ class="w-100"
+ (selectionChange)="updateFillMode(fillMode)"
+ >
+ @for (option of fillOptions; track option.value) {
+ <mat-option [value]="option.value">{{
+ option.label | translate
+ }}</mat-option>
+ }
+ </mat-select>
+ </mat-form-field>
+ </sp-form-field>
+ @if (fillMode === 'number') {
+ <sp-form-field
+ [level]="3"
+ [label]="'Fill Value' | translate"
+ class="w-100"
+ >
+ <mat-form-field
+ appearance="outline"
+ fxFlex="100"
+ class="w-100"
+ color="accent"
+ >
+ <input
+ matInput
+ type="number"
+ [(ngModel)]="customFillValue"
+ (ngModelChange)="updateCustomFillValue()"
+ />
+ </mat-form-field>
+ </sp-form-field>
+ }
+ </div>
+</div>
diff --git
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/FillClauseParams.java
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.scss
similarity index 61%
copy from
streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/FillClauseParams.java
copy to
ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.scss
index ccece054ba..13cbc4aacb 100644
---
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/FillClauseParams.java
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.scss
@@ -15,24 +15,3 @@
* limitations under the License.
*
*/
-
-package org.apache.streampipes.dataexplorer.param.model;
-
-import org.apache.streampipes.dataexplorer.api.IDataLakeQueryBuilder;
-import org.apache.streampipes.dataexplorer.api.IQueryStatement;
-
-public class FillClauseParams implements IQueryStatement {
- String fill = "none";
-
- protected FillClauseParams() {
- }
-
- public static FillClauseParams from() {
- return new FillClauseParams();
- }
-
- @Override
- public void buildStatement(IDataLakeQueryBuilder<?> builder) {
- builder.withFill(fill);
- }
-}
diff --git
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.ts
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.ts
new file mode 100644
index 0000000000..9b0ab2a203
--- /dev/null
+++
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.ts
@@ -0,0 +1,115 @@
+/*
+ * 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, Input, OnInit, inject } from '@angular/core';
+import { QueryConfig } from '@streampipes/platform-services';
+import { ChartConfigurationService } from
'../../../../../../chart-shared/services/chart-configuration.service';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import {
+ FlexDirective,
+ LayoutAlignDirective,
+ LayoutDirective,
+} from '@ngbracket/ngx-layout/flex';
+import { FormFieldComponent } from '@streampipes/shared-ui';
+import { MatFormField } from '@angular/material/form-field';
+import { MatOption, MatSelect } from '@angular/material/select';
+import { MatInput } from '@angular/material/input';
+import { FormsModule } from '@angular/forms';
+
+type FillMode = 'none' | 'previous' | 'linear' | 'null' | 'number';
+
+@Component({
+ selector: 'sp-fill-configuration',
+ templateUrl: './fill-configuration.component.html',
+ styleUrls: ['./fill-configuration.component.scss'],
+ imports: [
+ LayoutDirective,
+ LayoutAlignDirective,
+ FlexDirective,
+ FormFieldComponent,
+ MatFormField,
+ MatSelect,
+ MatOption,
+ MatInput,
+ FormsModule,
+ TranslatePipe,
+ ],
+})
+export class FillConfigurationComponent implements OnInit {
+ private widgetConfigService = inject(ChartConfigurationService);
+ private translate = inject(TranslateService);
+
+ @Input() queryConfig: QueryConfig;
+ @Input() widgetId: string;
+
+ fillMode: FillMode = 'none';
+ customFillValue = 0;
+
+ fillOptions: Array<{ value: FillMode; label: string }> = [
+ { value: 'none', label: this.translate.instant('None') },
+ { value: 'previous', label: this.translate.instant('Previous Value') },
+ {
+ value: 'linear',
+ label: this.translate.instant('Linear Interpolation'),
+ },
+ { value: 'null', label: 'Null' },
+ { value: 'number', label: this.translate.instant('Custom Value') },
+ ];
+
+ ngOnInit(): void {
+ if (
+ typeof this.queryConfig.fill === 'number' &&
+ !Number.isNaN(this.queryConfig.fill)
+ ) {
+ this.fillMode = 'number';
+ this.customFillValue = this.queryConfig.fill;
+ return;
+ }
+
+ const configuredMode = this.queryConfig.fill;
+ if (
+ configuredMode === 'none' ||
+ configuredMode === 'previous' ||
+ configuredMode === 'linear' ||
+ configuredMode === 'null'
+ ) {
+ this.fillMode = configuredMode;
+ return;
+ }
+
+ this.queryConfig.fill = 'none';
+ }
+
+ updateFillMode(mode: FillMode): void {
+ this.fillMode = mode;
+ this.queryConfig.fill = mode === 'number' ? this.customFillValue :
mode;
+ this.triggerDataRefresh();
+ }
+
+ updateCustomFillValue(): void {
+ this.queryConfig.fill = this.customFillValue;
+ this.triggerDataRefresh();
+ }
+
+ triggerDataRefresh(): void {
+ this.widgetConfigService.notify({
+ refreshData: true,
+ refreshView: true,
+ });
+ }
+}