You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ri...@apache.org on 2026/06/15 19:36:14 UTC
(streampipes) branch dev updated: feat: Extend visualization options (#4561)
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 a04db10d02 feat: Extend visualization options (#4561)
a04db10d02 is described below
commit a04db10d029db220a9d4679d0078376292088fbe
Author: Dominik Riemer <do...@gmail.com>
AuthorDate: Mon Jun 15 21:36:09 2026 +0200
feat: Extend visualization options (#4561)
---
ui/deployment/i18n/de.json | 29 +-
ui/deployment/i18n/en.json | 27 +-
ui/deployment/i18n/pl.json | 29 +-
.../model/datalake/data-lake-query-config.model.ts | 1 +
.../field-order-config.component.html | 90 +++++
.../field-order-config.component.scss} | 49 +--
.../field-order-config.component.ts | 105 ++++++
.../time-series-item-config.component.html | 5 +-
.../time-series-item-config.component.ts | 26 +-
.../config/gauge-widget-config.component.html | 7 +-
.../gauge/config/gauge-widget-config.component.ts | 50 +++
.../charts/gauge/gauge-renderer.service.ts | 17 +-
.../progress-bar-appearance-config.component.html | 46 +++
.../progress-bar-appearance-config.component.scss | 34 ++
.../progress-bar-appearance-config.component.ts | 88 +++++
.../progress-bar-widget-config.component.html | 159 +++++++++
.../config/progress-bar-widget-config.component.ts | 154 +++++++++
.../model/progress-bar-widget.model.ts | 54 +++
.../progress-bar-widget.component.html | 112 ++++++
.../progress-bar-widget.component.scss | 107 ++++++
.../progress-bar/progress-bar-widget.component.ts | 376 +++++++++++++++++++++
.../config/table-widget-config.component.html | 88 +----
.../config/table-widget-config.component.scss | 53 ---
.../table/config/table-widget-config.component.ts | 69 +---
.../charts/table/table-widget.component.ts | 58 +++-
.../sp-timeseries-renderer.service.ts | 13 +-
.../value-card-appearance-config.component.html | 50 +++
.../value-card-appearance-config.component.ts | 84 +++++
.../config/value-card-widget-config.component.html | 86 +++++
.../config/value-card-widget-config.component.ts | 133 ++++++++
.../value-card/model/value-card-widget.model.ts | 45 +++
.../value-card/value-card-widget.component.html | 91 +++++
.../value-card/value-card-widget.component.scss | 118 +++++++
.../value-card/value-card-widget.component.ts | 371 ++++++++++++++++++++
.../registry/chart-registry.service.ts | 30 ++
.../chart-shared/services/result-label.service.ts | 70 ++++
.../chart-shared/services/widget-render.utils.ts | 79 +++++
.../chart-data-settings.component.html | 5 +
.../data-settings/chart-data-settings.component.ts | 3 +
.../result-label-configuration.component.html | 49 +++
.../result-label-configuration.component.scss | 17 +
.../result-label-configuration.component.ts | 93 +++++
42 files changed, 2902 insertions(+), 268 deletions(-)
diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index e0aada136d..56d3b9f2c0 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -146,10 +146,12 @@
"Backend Version": "Backend-Version",
"Background": "Hintergrund",
"Bar": "Balken",
+ "Bar settings": "Balkeneinstellungen",
"Basic": "Allgemein",
"Basic Settings": "Basiseinstellungen",
"Basic settings": "Grundeinstellungen",
"Basics": "Allgemein",
+ "Behavior": "Verhalten",
"Behaviour in case of missing values": "Verhalten im Falle von fehlenden Daten",
"Boolean": "Boolean",
"Boolean Value": "Boolescher Wert",
@@ -161,6 +163,7 @@
"CSV configuration": "CSV-Konfiguration",
"Cancel": "Abbrechen",
"Car": "Auto",
+ "Card settings": "Karteneinstellungen",
"Caution when changing data types": "Vorsicht bei Änderung des Datentyps",
"Caution when loading total count": "Vorsicht beim Laden der Gesamtanzahl",
"Certificate Details": "Zertifikat-Details",
@@ -186,6 +189,7 @@
"Choose existing file": "Vorhandene Datei auswählen",
"Choose target dataset": "Ziel-Dataset auswählen",
"Choose template": "Vorlage auswählen",
+ "Clamp to 0-100%": "Auf 0-100 % begrenzen",
"Clean code": "Code aufräumen",
"Clear": "Zurücksetzen",
"Clear & use simple filters": "Löschen und einfache Filter verwenden",
@@ -209,7 +213,6 @@
"Color used above the high threshold.": "Farbe, die oberhalb des hohen Schwellenwerts verwendet wird.",
"Color used between low and high thresholds.": "Farbe, die zwischen niedrigem und hohem Schwellenwert verwendet wird.",
"Color used from min up to the low threshold.": "Farbe, die von Min bis zum niedrigen Schwellenwert verwendet wird.",
- "Column Order": "Spaltenreihenfolge",
"Compatible Elements": "Kompatible Elemente",
"Configuration": "Konfiguration",
"Configuration required": "Konfiguration erforderlich",
@@ -254,11 +257,13 @@
"Creating adapter {{adapterName}}": "Adapter erstellen {{adapterName}}",
"Creating pipeline to persist data stream": "Erstellen einer Pipeline zum Speichern der Daten",
"Critical datatype changes": "Kritische Datentypänderungen",
+ "Current / Target": "Aktuell / Ziel",
"Current Warning Range: ": "Aktueller Warnbereich: ",
"Current day": "Aktueller Tag",
"Current hour": "Aktuelle Stunde",
"Current month": "Aktueller Monat",
"Current password": "Aktuelles Passwort",
+ "Current value field": "Feld für aktuellen Wert",
"Current week": "Aktuelle Woche",
"Current year": "Aktuelles Jahr",
"Currently configured query": "Aktuell konfigurierte Anfrage",
@@ -344,7 +349,10 @@
"Discard changes": "Änderungen verwerfen",
"Display Name": "Anzeigename",
"Display an image": "Ein Bild anzeigen",
+ "Display mode": "Anzeigemodus",
"Display points on an x/y plane": "Punkte in einer x/y-Ebene anzeigen",
+ "Display the latest values of multiple selected fields in a single card": "Die neuesten Werte mehrerer ausgewählter Felder in einer einzelnen Karte anzeigen",
+ "Display the progress of a numeric value against a target": "Den Fortschritt eines numerischen Werts im Verhältnis zu einem Ziel anzeigen",
"Do you really want to delete adapter ": "Wollen Sie den Adapter wirklich löschen",
"Do you really want to delete pipeline": "Wollen Sie die Pipeline wirklich löschen?",
"Do you really want to delete the dataset {{index}}?": "Möchten Sie den Datensatz {{index}} wirklich löschen?",
@@ -459,6 +467,7 @@
"First": "Erster",
"First row contains headers": "Erste Zeile enthält Überschriften",
"First row index to append data": "Erster Zeilenindex an denen Daten angehängt werden sollen",
+ "Fixed value": "Fester Wert",
"Focus map on last event": "Karte auf letztes Ereignis fokussieren",
"Font Style": "Schriftstil",
"For tile server, use placeholders for x, y and z coordinates": "Für Tile-Server Platzhalter für x-, y- und z-Koordinaten verwenden",
@@ -552,6 +561,7 @@
"Invalid URI format.": "Ungültiges URI-Format.",
"Invalid configuration for": "Ungültige Konfiguration für",
"Invalid pipeline: Modify to fix problems": "Ungültige Pipeline: Ändern, um Probleme zu beheben",
+ "Invert progress semantics": "Fortschrittslogik umkehren",
"Issuer": "Aussteller",
"Items per page": "Einträge pro Seite",
"JWT Signature": "JWT-Signature",
@@ -571,6 +581,7 @@
"Label Content": "Beschriftungsinhalt",
"Label Line": "Beschriftungslinie",
"Label Position": "Beschriftungsposition",
+ "Label font size": "Schriftgröße der Bezeichnung",
"Label shown below the current gauge value.": "Beschriftung, die unter dem aktuellen Tacho-Wert angezeigt wird.",
"Labels": "Labels",
"Labels & custom fields": "Label und benutzerdefinierte Felder",
@@ -713,7 +724,6 @@
"No datasets match your search.": "Keine Datensätze entsprechen deiner Suche.",
"No entries available.": "Keine Einträge vorhanden.",
"No export providers found": "Keine Exportanbieter gefunden",
- "No fields selected.": "Keine Felder ausgewählt.",
"No groups configured": "Keine Gruppen konfiguriert",
"No labels available - Click 'Manage Labels'": "Keine Labels verfügbar - Klicken Sie auf 'Labels verwalten'",
"No linked resources": "Keine verknüpften Ressourcen",
@@ -760,6 +770,8 @@
"Passwords does not match.": "Die Passwörter stimmen nicht überein.",
"Path": "Pfad",
"Percent": "Prozent",
+ "Percent + Current / Target": "Prozent + Aktuell / Ziel",
+ "Percent only": "Nur Prozent",
"Permissions": "Berechtigungen",
"Persist events": "Daten speichern",
"Pie": "Kreisdiagramm",
@@ -803,6 +815,8 @@
"Probable cause": "Wahrscheinliche Ursache",
"Problems reading the following nodes": "Probleme beim Lesen der folgenden Knoten",
"Profile": "Profil",
+ "Progress Bar": "Fortschrittsbalken",
+ "Progress color": "Fortschrittsfarbe",
"Protocol": "Protokoll",
"Provider Settings": "Anbieter-Einstellungen",
"Provider Type": "Anbieter Typ",
@@ -906,8 +920,8 @@
"Schema fields": "Schemafelder",
"Script": "Skript",
"Search": "Suche",
- "Search datasets": "Datensätze suchen",
"Search charts": "Diagramme suchen",
+ "Search datasets": "Datensätze suchen",
"Searching for available extensions, please wait...": "Nach verfügbaren Extensions suchen, bitte warten...",
"Second": "Sekunde",
"Secret Key": "Secret Key",
@@ -971,9 +985,11 @@
"Show documentation link in user menu": "Link zur Dokumentation im Benutzermenü anzeigen",
"Show documentation link on login page": "Link zur Dokumentation auf Anmeldeseite anzeigen",
"Show input fields": "Eingabefelder anzeigen",
+ "Show latest time above card": "Neueste Zeit über der Karte anzeigen",
"Show legend": "Legende anzeigen",
"Show only recommended settings": "Nur empfohlene Einstellungen anzeigen",
"Show pipeline configuration as code": "Pipeline-Konfiguration als Code anzeigen",
+ "Show progress label": "Fortschrittsbeschriftung anzeigen",
"Show raw data from your data source.": "Zeigen Sie Rohdaten aus Ihrer Datenquelle an.",
"Show single data entry.": "Einzelne Dateneinträge anzeigen.",
"Show subassets": "Teilassets anzeigen",
@@ -1044,6 +1060,9 @@
"System Info": "Systeminformationen",
"Table": "Tabelle",
"Tags": "Tags",
+ "Target field": "Zielfeld",
+ "Target source": "Zielquelle",
+ "Target value": "Zielwert",
"Template": "Vorlage",
"Template description": "Beschreibung der Vorlage",
"Template name": "Name der Vorlage",
@@ -1124,6 +1143,7 @@
"Topics": "Themen",
"Total Memory": "Gesamtspeicher",
"Trace": "Spur",
+ "Track color": "Hintergrundfarbe",
"Traffic Light": "Ampel",
"Transformation": "Transformation",
"Transport": "Transport",
@@ -1195,10 +1215,13 @@
"Uses proxy": "Verwendet Proxy",
"Validation Error": "Validierungsfehler",
"Value": "Wert",
+ "Value Card": "Wertkarte",
"Value Distribution Heatmap": "Wertverteilung-Heatmap",
"Value Highlighting": "Wert-Hervorhebung",
+ "Value font size": "Schriftgröße des Werts",
"Value where the low color range ends.": "Wert, bei dem der niedrige Farbbereich endet.",
"Value where the medium color range ends and high begins.": "Wert, bei dem der mittlere Farbbereich endet und der hohe beginnt.",
+ "Values": "Werte",
"Vector": "Vektor",
"Versions": "Versionen",
"View API Docs": "API-Doku anzeigen",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index c81b30c8a5..e58a1a807e 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -146,10 +146,12 @@
"Backend Version": null,
"Background": null,
"Bar": null,
+ "Bar settings": null,
"Basic": null,
"Basic Settings": null,
"Basic settings": null,
"Basics": null,
+ "Behavior": null,
"Behaviour in case of missing values": null,
"Boolean": null,
"Boolean Value": null,
@@ -161,6 +163,7 @@
"CSV configuration": null,
"Cancel": null,
"Car": null,
+ "Card settings": null,
"Caution when changing data types": null,
"Caution when loading total count": null,
"Certificate Details": null,
@@ -186,6 +189,7 @@
"Choose existing file": null,
"Choose target dataset": null,
"Choose template": null,
+ "Clamp to 0-100%": null,
"Clean code": null,
"Clear": null,
"Clear & use simple filters": null,
@@ -209,7 +213,6 @@
"Color used above the high threshold.": null,
"Color used between low and high thresholds.": null,
"Color used from min up to the low threshold.": null,
- "Column Order": null,
"Compatible Elements": null,
"Configuration": null,
"Configuration required": null,
@@ -254,11 +257,13 @@
"Creating adapter {{adapterName}}": "Creating adapter {{adapterName}}",
"Creating pipeline to persist data stream": null,
"Critical datatype changes": null,
+ "Current / Target": null,
"Current Warning Range: ": null,
"Current day": null,
"Current hour": null,
"Current month": null,
"Current password": null,
+ "Current value field": null,
"Current week": null,
"Current year": null,
"Currently configured query": null,
@@ -344,7 +349,10 @@
"Discard changes": null,
"Display Name": null,
"Display an image": null,
+ "Display mode": null,
"Display points on an x/y plane": null,
+ "Display the latest values of multiple selected fields in a single card": null,
+ "Display the progress of a numeric value against a target": null,
"Do you really want to delete adapter ": null,
"Do you really want to delete pipeline": null,
"Do you really want to delete the dataset {{index}}?": "Do you really want to delete the dataset {{index}}?",
@@ -459,6 +467,7 @@
"First": null,
"First row contains headers": null,
"First row index to append data": null,
+ "Fixed value": null,
"Focus map on last event": null,
"Font Style": null,
"For tile server, use placeholders for x, y and z coordinates": null,
@@ -552,6 +561,7 @@
"Invalid URI format.": null,
"Invalid configuration for": null,
"Invalid pipeline: Modify to fix problems": null,
+ "Invert progress semantics": null,
"Issuer": null,
"Items per page": null,
"JWT Signature": null,
@@ -571,6 +581,7 @@
"Label Content": null,
"Label Line": null,
"Label Position": null,
+ "Label font size": null,
"Label shown below the current gauge value.": null,
"Labels": null,
"Labels & custom fields": null,
@@ -713,7 +724,6 @@
"No datasets match your search.": null,
"No entries available.": null,
"No export providers found": null,
- "No fields selected.": null,
"No groups configured": null,
"No labels available - Click 'Manage Labels'": null,
"No linked resources": null,
@@ -760,6 +770,8 @@
"Passwords does not match.": null,
"Path": null,
"Percent": null,
+ "Percent + Current / Target": null,
+ "Percent only": null,
"Permissions": null,
"Persist events": null,
"Pie": null,
@@ -803,6 +815,8 @@
"Probable cause": null,
"Problems reading the following nodes": null,
"Profile": null,
+ "Progress Bar": null,
+ "Progress color": null,
"Protocol": null,
"Provider Settings": null,
"Provider Type": null,
@@ -971,9 +985,11 @@
"Show documentation link in user menu": null,
"Show documentation link on login page": null,
"Show input fields": null,
+ "Show latest time above card": null,
"Show legend": null,
"Show only recommended settings": null,
"Show pipeline configuration as code": null,
+ "Show progress label": null,
"Show raw data from your data source.": null,
"Show single data entry.": null,
"Show subassets": null,
@@ -1044,6 +1060,9 @@
"System Info": null,
"Table": null,
"Tags": null,
+ "Target field": null,
+ "Target source": null,
+ "Target value": null,
"Template": null,
"Template description": null,
"Template name": null,
@@ -1124,6 +1143,7 @@
"Topics": null,
"Total Memory": null,
"Trace": null,
+ "Track color": null,
"Traffic Light": null,
"Transformation": null,
"Transport": null,
@@ -1195,10 +1215,13 @@
"Uses proxy": null,
"Validation Error": null,
"Value": null,
+ "Value Card": null,
"Value Distribution Heatmap": null,
"Value Highlighting": null,
+ "Value font size": null,
"Value where the low color range ends.": null,
"Value where the medium color range ends and high begins.": null,
+ "Values": null,
"Vector": null,
"Versions": null,
"View API Docs": null,
diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json
index 506b51c71c..b9062bcd9f 100644
--- a/ui/deployment/i18n/pl.json
+++ b/ui/deployment/i18n/pl.json
@@ -146,10 +146,12 @@
"Backend Version": "Wersja backendu",
"Background": "Tło",
"Bar": "Słupkowy",
+ "Bar settings": "Ustawienia paska",
"Basic": "Ogólne",
"Basic Settings": "Ustawienia podstawowe",
"Basic settings": "Ustawienia podstawowe",
"Basics": "Podstawy",
+ "Behavior": "Zachowanie",
"Behaviour in case of missing values": "Zachowanie w przypadku brakujących wartości",
"Boolean": "Logiczny",
"Boolean Value": "Wartość logiczna",
@@ -161,6 +163,7 @@
"CSV configuration": "Konfiguracja CSV",
"Cancel": "Anuluj",
"Car": "Samochód",
+ "Card settings": "Ustawienia karty",
"Caution when changing data types": "Uwaga przy zmianie typów danych",
"Caution when loading total count": "Uwaga przy wczytywaniu łącznej liczby",
"Certificate Details": "Szczegóły certyfikatu",
@@ -186,6 +189,7 @@
"Choose existing file": "Wybierz istniejący plik",
"Choose target dataset": "Wybierz docelowy zbiór danych",
"Choose template": "Wybierz szablon",
+ "Clamp to 0-100%": "Ogranicz do 0-100%",
"Clean code": "Wyczyść kod",
"Clear": "Wyczyść",
"Clear & use simple filters": "Wyczyść i użyj prostych filtrów",
@@ -209,7 +213,6 @@
"Color used above the high threshold.": "Kolor używany powyżej wysokiego progu.",
"Color used between low and high thresholds.": "Kolor używany między niskim a wysokim progiem.",
"Color used from min up to the low threshold.": "Kolor używany od minimum do niskiego progu.",
- "Column Order": "Kolejność kolumn",
"Compatible Elements": "Kompatybilne elementy",
"Configuration": "Konfiguracja",
"Configuration required": "Wymagana konfiguracja",
@@ -254,11 +257,13 @@
"Creating adapter {{adapterName}}": "Tworzenie adaptera {{adapterName}}",
"Creating pipeline to persist data stream": "Tworzenie strumienia do utrwalenia strumienia danych",
"Critical datatype changes": "Krytyczne zmiany typów danych",
+ "Current / Target": "Bieżąca / Docelowa",
"Current Warning Range: ": "Bieżący zakres ostrzegawczy: ",
"Current day": "Bieżący dzień",
"Current hour": "Bieżąca godzina",
"Current month": "Bieżący miesiąc",
"Current password": "Obecne hasło",
+ "Current value field": "Pole bieżącej wartości",
"Current week": "Bieżący tydzień",
"Current year": "Bieżący rok",
"Currently configured query": "Aktualnie skonfigurowane zapytanie",
@@ -344,7 +349,10 @@
"Discard changes": "Odrzuć zmiany",
"Display Name": "Nazwa wyświetlana",
"Display an image": "Wyświetl obraz",
+ "Display mode": "Tryb wyświetlania",
"Display points on an x/y plane": "Wyświetl punkty na płaszczyźnie x/y",
+ "Display the latest values of multiple selected fields in a single card": "Wyświetl najnowsze wartości wielu wybranych pól na jednej karcie",
+ "Display the progress of a numeric value against a target": "Wyświetl postęp wartości liczbowej względem celu",
"Do you really want to delete adapter ": "Czy na pewno chcesz usunąć adapter ",
"Do you really want to delete pipeline": "Czy na pewno chcesz usunąć strumień?",
"Do you really want to delete the dataset {{index}}?": "Czy na pewno chcesz usunąć zbiór danych {{index}}?",
@@ -459,6 +467,7 @@
"First": "Pierwszy",
"First row contains headers": "Pierwszy wiersz zawiera nagłówki",
"First row index to append data": "Indeks pierwszego wiersza, do którego dodać dane",
+ "Fixed value": "Wartość stała",
"Focus map on last event": "Skup mapę na ostatnim zdarzeniu",
"Font Style": "Styl czcionki",
"For tile server, use placeholders for x, y and z coordinates": "W przypadku serwera kafelków(serwer map cyfrowych) użyj placeholderów dla współrzędnych x, y i z",
@@ -552,6 +561,7 @@
"Invalid URI format.": "Nieprawidłowy format URI.",
"Invalid configuration for": "Nieprawidłowa konfiguracja dla",
"Invalid pipeline: Modify to fix problems": "Nieprawidłowy strumień: zmodyfikuj, aby naprawić problemy",
+ "Invert progress semantics": "Odwróć logikę postępu",
"Issuer": "Wystawca",
"Items per page": "Elementów na stronę",
"JWT Signature": "Podpis JWT",
@@ -571,6 +581,7 @@
"Label Content": "Zawartość etykiety",
"Label Line": "Linia etykiety",
"Label Position": "Pozycja etykiety",
+ "Label font size": "Rozmiar czcionki etykiety",
"Label shown below the current gauge value.": "Etykieta wyświetlana poniżej bieżącej wartości wskaźnika.",
"Labels": "Etykiety",
"Labels & custom fields": "Etykiety i pola niestandardowe",
@@ -713,7 +724,6 @@
"No datasets match your search.": "Żadne zbiory danych nie pasują do wyszukiwania.",
"No entries available.": "Brak dostępnych wpisów.",
"No export providers found": "Nie znaleziono exportera",
- "No fields selected.": "Nie wybrano żadnych pól.",
"No groups configured": "Nie skonfigurowano grup",
"No labels available - Click 'Manage Labels'": "Brak dostępnych etykiet - kliknij 'Zarządzaj etykietami'",
"No linked resources": "Brak powiązanych zasobów",
@@ -760,6 +770,8 @@
"Passwords does not match.": "Hasła nie są zgodne.",
"Path": "Ścieżka",
"Percent": "Procent",
+ "Percent + Current / Target": "Procent + Bieżąca / Docelowa",
+ "Percent only": "Tylko procent",
"Permissions": "Uprawnienia",
"Persist events": "Trwałe przechowywanie zdarzeń",
"Pie": "Wykres kołowy",
@@ -803,6 +815,8 @@
"Probable cause": "Prawdopodobna przyczyna",
"Problems reading the following nodes": "Problemy z odczytem następujących węzłów",
"Profile": "Profil",
+ "Progress Bar": "Pasek postępu",
+ "Progress color": "Kolor postępu",
"Protocol": "Protokół",
"Provider Settings": "Ustawienia dostawcy",
"Provider Type": "Typ dostawcy",
@@ -906,8 +920,8 @@
"Schema fields": "Pola schematu",
"Script": "Skrypt",
"Search": "Szukaj",
- "Search datasets": "Szukaj zbiorów danych",
"Search charts": "Szukaj wykresów",
+ "Search datasets": "Szukaj zbiorów danych",
"Searching for available extensions, please wait...": "Wyszukiwanie dostępnych rozszerzeń, proszę czekać...",
"Second": "Sekunda",
"Secret Key": "Klucz tajny",
@@ -971,9 +985,11 @@
"Show documentation link in user menu": "Pokaż link do dokumentacji w menu użytkownika",
"Show documentation link on login page": "Pokaż link do dokumentacji na stronie logowania",
"Show input fields": "Pokaż pola wejściowe",
+ "Show latest time above card": "Pokaż najnowszy czas nad kartą",
"Show legend": "Pokaż legendę",
"Show only recommended settings": "Pokaż tylko zalecane ustawienia",
"Show pipeline configuration as code": "Pokaż konfigurację strumienia jako kod",
+ "Show progress label": "Pokaż etykietę postępu",
"Show raw data from your data source.": "Pokaż surowe dane ze źródła.",
"Show single data entry.": "Pokaż pojedynczy wpis danych.",
"Show subassets": "Pokaż podzasoby",
@@ -1044,6 +1060,9 @@
"System Info": "Informacje o systemie",
"Table": "Tabela",
"Tags": "Tagi",
+ "Target field": "Pole docelowe",
+ "Target source": "Źródło celu",
+ "Target value": "Wartość docelowa",
"Template": "Szablon",
"Template description": "Opis szablonu",
"Template name": "Nazwa szablonu",
@@ -1124,6 +1143,7 @@
"Topics": "Tematy",
"Total Memory": "Calkowita pamiec",
"Trace": "Ślad",
+ "Track color": "Kolor tła paska",
"Traffic Light": "Sygnalizator",
"Transformation": "Transformacja",
"Transport": "Transport",
@@ -1195,10 +1215,13 @@
"Uses proxy": "Używa proxy",
"Validation Error": "Błąd walidacji",
"Value": "Wartość",
+ "Value Card": "Karta wartości",
"Value Distribution Heatmap": "Mapa cieplna rozkładu wartości",
"Value Highlighting": "Wyróżnianie wartości",
+ "Value font size": "Rozmiar czcionki wartości",
"Value where the low color range ends.": "Wartość, przy której kończy się niski zakres kolorów.",
"Value where the medium color range ends and high begins.": "Wartość, przy której kończy się średni zakres kolorów, a zaczyna wysoki.",
+ "Values": "Wartości",
"Vector": "Wektor",
"Versions": "Wersje",
"View API Docs": "Zobacz dokumentację API",
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 60a3957286..fa3f72f93e 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
@@ -69,6 +69,7 @@ export interface QueryConfig {
selectedFilters: SelectedFilter[];
filterExpression?: FilterExpressionGroup;
fields?: FieldConfig[];
+ resultLabelOverrides?: Record<string, string>;
groupBy?: FieldConfig[];
limit?: number;
page?: number;
diff --git a/ui/src/app/chart-shared/components/chart-config/field-order-config/field-order-config.component.html b/ui/src/app/chart-shared/components/chart-config/field-order-config/field-order-config.component.html
new file mode 100644
index 0000000000..561fe4b053
--- /dev/null
+++ b/ui/src/app/chart-shared/components/chart-config/field-order-config/field-order-config.component.html
@@ -0,0 +1,90 @@
+<!--
+ ~ 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.
+ ~
+ -->
+
+<sp-split-section [level]="3" [title]="sectionTitle | translate">
+ <div
+ fxLayout="column"
+ class="selected-field-list"
+ cdkDropList
+ [cdkDropListData]="selectedFields"
+ (cdkDropListDropped)="dropSelectedField($event)"
+ >
+ @for (field of selectedFields; track fieldKey(field); let i = $index) {
+ <div
+ class="selected-field-row"
+ fxLayout="row"
+ fxLayoutAlign="space-between center"
+ cdkDrag
+ >
+ <div
+ class="selected-field-main"
+ fxLayout="row"
+ fxLayoutAlign="start center"
+ >
+ <button
+ mat-icon-button
+ type="button"
+ class="selected-field-drag-handle"
+ cdkDragHandle
+ [attr.aria-label]="
+ 'Reorder ' + field.runtimeName | translate
+ "
+ >
+ <mat-icon>drag_indicator</mat-icon>
+ </button>
+ <div class="selected-field-label">
+ {{ selectedFieldLabel(field) }}
+ </div>
+ </div>
+ <div
+ class="selected-field-actions"
+ fxLayout="row"
+ fxLayoutAlign="end center"
+ >
+ <button
+ mat-icon-button
+ type="button"
+ [disabled]="!canMoveSelectedFieldUp(i)"
+ [attr.aria-label]="
+ 'Move ' + field.runtimeName + ' up' | translate
+ "
+ (click)="moveSelectedField(i, -1)"
+ >
+ <mat-icon>arrow_upward</mat-icon>
+ </button>
+ <button
+ mat-icon-button
+ type="button"
+ [disabled]="!canMoveSelectedFieldDown(i)"
+ [attr.aria-label]="
+ 'Move ' + field.runtimeName + ' down' | translate
+ "
+ (click)="moveSelectedField(i, 1)"
+ >
+ <mat-icon>arrow_downward</mat-icon>
+ </button>
+ </div>
+ </div>
+ } @empty {
+ <sp-alert-banner
+ type="info"
+ [title]="emptyMessage | translate"
+ ></sp-alert-banner>
+ }
+ </div>
+</sp-split-section>
diff --git a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.scss b/ui/src/app/chart-shared/components/chart-config/field-order-config/field-order-config.component.scss
similarity index 65%
copy from ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.scss
copy to ui/src/app/chart-shared/components/chart-config/field-order-config/field-order-config.component.scss
index e926be0642..94f2bc6b41 100644
--- a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.scss
+++ b/ui/src/app/chart-shared/components/chart-config/field-order-config/field-order-config.component.scss
@@ -16,11 +16,11 @@
*
*/
-.selected-column-list {
+.selected-field-list {
gap: var(--space-sm);
}
-.selected-column-row {
+.selected-field-row {
gap: var(--space-sm);
padding: var(--space-xs) var(--space-sm);
border: 1px solid var(--color-border-subtle, var(--color-bg-3));
@@ -28,22 +28,22 @@
background: var(--color-bg-1, var(--color-bg-0));
}
-.selected-column-main {
+.selected-field-main {
min-width: 0;
gap: var(--space-xs);
flex: 1;
}
-.selected-column-drag-handle {
+.selected-field-drag-handle {
cursor: grab;
color: var(--color-secondary-text);
}
-.selected-column-drag-handle:active {
+.selected-field-drag-handle:active {
cursor: grabbing;
}
-.selected-column-label {
+.selected-field-label {
min-width: 0;
flex: 1;
font-size: var(--font-size-sm);
@@ -52,7 +52,7 @@
white-space: nowrap;
}
-.selected-column-actions {
+.selected-field-actions {
flex-shrink: 0;
gap: var(--space-xxs);
}
@@ -68,38 +68,3 @@
.cdk-drag-placeholder {
opacity: 0.35;
}
-
-.highlight-color-list {
- gap: var(--space-sm);
-}
-
-.highlight-color-label {
- display: flex;
- flex-direction: column;
- min-width: 0;
- font-size: var(--font-size-sm);
-}
-
-.highlight-color-measure {
- color: var(--color-secondary-text);
- font-size: var(--font-size-xs);
-}
-
-.highlight-color-picker {
- margin-left: auto;
- display: block;
- width: calc(var(--space-md) * 2.4);
- height: calc(var(--space-md) * 2.4);
- padding: 0;
- border: 1px solid var(--color-tab-border);
- border-radius: var(--button-border-radius);
- cursor: pointer;
- appearance: none;
- -webkit-appearance: none;
- background-clip: padding-box;
-}
-
-.highlight-color-picker:disabled {
- cursor: default;
- opacity: 0.45;
-}
diff --git a/ui/src/app/chart-shared/components/chart-config/field-order-config/field-order-config.component.ts b/ui/src/app/chart-shared/components/chart-config/field-order-config/field-order-config.component.ts
new file mode 100644
index 0000000000..b9ce496a2c
--- /dev/null
+++ b/ui/src/app/chart-shared/components/chart-config/field-order-config/field-order-config.component.ts
@@ -0,0 +1,105 @@
+/*
+ * 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, Output } from '@angular/core';
+import { DataExplorerField } from '@streampipes/platform-services';
+import {
+ CdkDrag,
+ CdkDragDrop,
+ CdkDragHandle,
+ CdkDropList,
+ moveItemInArray,
+} from '@angular/cdk/drag-drop';
+import {
+ LayoutAlignDirective,
+ LayoutDirective,
+} from '@ngbracket/ngx-layout/flex';
+import { MatIcon } from '@angular/material/icon';
+import { MatIconButton } from '@angular/material/button';
+import { SpAlertBannerComponent } from '@streampipes/shared-ui';
+import { TranslatePipe } from '@ngx-translate/core';
+import { SplitSectionComponent } from '@streampipes/shared-ui';
+
+@Component({
+ selector: 'sp-field-order-config',
+ templateUrl: './field-order-config.component.html',
+ styleUrls: ['./field-order-config.component.scss'],
+ imports: [
+ SplitSectionComponent,
+ CdkDropList,
+ CdkDrag,
+ CdkDragHandle,
+ LayoutDirective,
+ LayoutAlignDirective,
+ MatIcon,
+ MatIconButton,
+ SpAlertBannerComponent,
+ TranslatePipe,
+ ],
+})
+export class FieldOrderConfigComponent {
+ @Input() selectedFields: DataExplorerField[] = [];
+ @Input() sectionTitle = 'Field Order';
+ @Input() emptyMessage = 'No fields selected.';
+
+ @Output() selectedFieldsChange = new EventEmitter<DataExplorerField[]>();
+
+ dropSelectedField(event: CdkDragDrop<DataExplorerField[]>): void {
+ if (event.previousIndex === event.currentIndex) {
+ return;
+ }
+
+ const fields = [...(this.selectedFields ?? [])];
+ moveItemInArray(fields, event.previousIndex, event.currentIndex);
+ this.selectedFieldsChange.emit(fields);
+ }
+
+ moveSelectedField(fromIndex: number, offset: number): void {
+ const fields = [...(this.selectedFields ?? [])];
+ const targetIndex = fromIndex + offset;
+
+ if (
+ fromIndex < 0 ||
+ targetIndex < 0 ||
+ fromIndex >= fields.length ||
+ targetIndex >= fields.length
+ ) {
+ return;
+ }
+
+ const [movedField] = fields.splice(fromIndex, 1);
+ fields.splice(targetIndex, 0, movedField);
+ this.selectedFieldsChange.emit(fields);
+ }
+
+ canMoveSelectedFieldUp(index: number): boolean {
+ return index > 0;
+ }
+
+ canMoveSelectedFieldDown(index: number): boolean {
+ return index < (this.selectedFields?.length ?? 0) - 1;
+ }
+
+ selectedFieldLabel(field: DataExplorerField): string {
+ return `${field.runtimeName} (${field.measure})`;
+ }
+
+ fieldKey(field: DataExplorerField): string {
+ return `${field.fullDbName}:${field.sourceIndex}`;
+ }
+}
diff --git a/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.html b/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.html
index 1a64d5092d..ba578876af 100644
--- a/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.html
+++ b/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.html
@@ -91,10 +91,7 @@
class="w-100"
>
<input
- [(ngModel)]="
- currentlyConfiguredWidget.visualizationConfig
- .displayName[getFieldKey(field)]
- "
+ [ngModel]="getDisplayName(field)"
matInput
(input)="
onDisplayNameChange($event.target.value, field)
diff --git a/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.ts b/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.ts
index fc23ef2c40..30ea20f873 100644
--- a/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.ts
+++ b/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.ts
@@ -41,6 +41,7 @@ import { MatOption, MatSelect } from '@angular/material/select';
import { TranslatePipe } from '@ngx-translate/core';
import { ColorMappingOptionsConfigComponent } from '../../color-mapping-options-config/color-mapping-options-config.component';
import { LayoutDirective } from '@ngbracket/ngx-layout';
+import { ResultLabelService } from '../../../../services/result-label.service';
@Component({
selector: 'sp-time-series-item-config',
@@ -68,6 +69,8 @@ import { LayoutDirective } from '@ngbracket/ngx-layout';
],
})
export class SpTimeseriesItemConfigComponent {
+ constructor(private resultLabelService: ResultLabelService) {}
+
@Input()
field: DataExplorerField;
@@ -124,9 +127,14 @@ export class SpTimeseriesItemConfigComponent {
}
onDisplayNameChange(searchValue: string, field: DataExplorerField): void {
- this.currentlyConfiguredWidget.visualizationConfig.displayName[
- field.fullDbName + field.sourceIndex.toString()
- ] = searchValue;
+ this.resultLabelService.setOverride(
+ this.currentlyConfiguredWidget.dataConfig.sourceConfigs[
+ field.sourceIndex
+ ].queryConfig,
+ field,
+ searchValue,
+ field.fullDbName,
+ );
this.viewRefreshEmitter.emit();
}
@@ -134,6 +142,18 @@ export class SpTimeseriesItemConfigComponent {
return field.fullDbName + field.sourceIndex.toString();
}
+ getDisplayName(field: DataExplorerField): string {
+ return this.resultLabelService.resolveLabel(
+ this.currentlyConfiguredWidget.dataConfig.sourceConfigs[
+ field.sourceIndex
+ ].queryConfig,
+ field,
+ this.currentlyConfiguredWidget.visualizationConfig.displayName[
+ this.getFieldKey(field)
+ ],
+ );
+ }
+
hasGrouping(field: DataExplorerField): boolean {
return (
this.currentlyConfiguredWidget.dataConfig.sourceConfigs[
diff --git a/ui/src/app/chart-shared/components/charts/gauge/config/gauge-widget-config.component.html b/ui/src/app/chart-shared/components/charts/gauge/config/gauge-widget-config.component.html
index 389465b05c..70b38520ec 100644
--- a/ui/src/app/chart-shared/components/charts/gauge/config/gauge-widget-config.component.html
+++ b/ui/src/app/chart-shared/components/charts/gauge/config/gauge-widget-config.component.html
@@ -83,11 +83,8 @@
<input
matInput
type="text"
- [(ngModel)]="
- currentlyConfiguredWidget.visualizationConfig
- .displayName
- "
- (ngModelChange)="triggerViewRefresh()"
+ [ngModel]="getDisplayName()"
+ (ngModelChange)="onDisplayNameChange($event)"
/>
</mat-form-field>
</sp-form-field>
diff --git a/ui/src/app/chart-shared/components/charts/gauge/config/gauge-widget-config.component.ts b/ui/src/app/chart-shared/components/charts/gauge/config/gauge-widget-config.component.ts
index 5fc0916abc..db4057351a 100644
--- a/ui/src/app/chart-shared/components/charts/gauge/config/gauge-widget-config.component.ts
+++ b/ui/src/app/chart-shared/components/charts/gauge/config/gauge-widget-config.component.ts
@@ -32,6 +32,7 @@ import { MatInput } from '@angular/material/input';
import { FormsModule } from '@angular/forms';
import { TranslatePipe } from '@ngx-translate/core';
import { MatCheckbox } from '@angular/material/checkbox';
+import { ResultLabelService } from '../../../../services/result-label.service';
@Component({
selector: 'sp-data-explorer-gauge-widget-config',
@@ -53,9 +54,58 @@ export class GaugeWidgetConfigComponent extends BaseWidgetConfig<
GaugeWidgetModel,
GaugeVisConfig
> {
+ constructor(private resultLabelService: ResultLabelService) {
+ super();
+ }
+
setSelectedProperty(field: DataExplorerField) {
this.currentlyConfiguredWidget.visualizationConfig.selectedProperty =
field;
+ this.currentlyConfiguredWidget.visualizationConfig.displayName =
+ this.resultLabelService.getOverride(
+ this.currentlyConfiguredWidget.dataConfig.sourceConfigs[
+ field.sourceIndex
+ ].queryConfig,
+ field,
+ ) ??
+ field.runtimeName ??
+ field.fullDbName;
+ this.triggerViewRefresh();
+ }
+
+ getDisplayName(): string {
+ const selectedProperty =
+ this.currentlyConfiguredWidget.visualizationConfig.selectedProperty;
+
+ if (!selectedProperty) {
+ return '';
+ }
+
+ return this.resultLabelService.resolveLabel(
+ this.currentlyConfiguredWidget.dataConfig.sourceConfigs[
+ selectedProperty.sourceIndex
+ ].queryConfig,
+ selectedProperty,
+ this.currentlyConfiguredWidget.visualizationConfig.displayName,
+ );
+ }
+
+ onDisplayNameChange(label: string): void {
+ const selectedProperty =
+ this.currentlyConfiguredWidget.visualizationConfig.selectedProperty;
+
+ if (!selectedProperty) {
+ return;
+ }
+
+ this.resultLabelService.setOverride(
+ this.currentlyConfiguredWidget.dataConfig.sourceConfigs[
+ selectedProperty.sourceIndex
+ ].queryConfig,
+ selectedProperty,
+ label,
+ selectedProperty.fullDbName,
+ );
this.triggerViewRefresh();
}
diff --git a/ui/src/app/chart-shared/components/charts/gauge/gauge-renderer.service.ts b/ui/src/app/chart-shared/components/charts/gauge/gauge-renderer.service.ts
index b833ceba59..619c9c3b0d 100644
--- a/ui/src/app/chart-shared/components/charts/gauge/gauge-renderer.service.ts
+++ b/ui/src/app/chart-shared/components/charts/gauge/gauge-renderer.service.ts
@@ -31,6 +31,7 @@ import {
import { WidgetSize } from '../../../models/dataset.model';
import { EchartsBasicOptionsGeneratorService } from '../../../echarts-renderer/echarts-basic-options-generator.service';
import { SpFieldUpdateService } from '../../../services/field-update.service';
+import { ResultLabelService } from '../../../services/result-label.service';
@Injectable({ providedIn: 'root' })
export class SpGaugeRendererService implements SpEchartsRenderer<GaugeWidgetModel> {
@@ -38,10 +39,11 @@ export class SpGaugeRendererService implements SpEchartsRenderer<GaugeWidgetMode
protected echartsBaseOptionsGenerator = inject(
EchartsBasicOptionsGeneratorService,
);
+ protected resultLabelService = inject(ResultLabelService);
makeSeriesItem(
seriesName: string,
- fieldName: string,
+ selectedField: DataExplorerField,
value: number,
decimals: number | undefined,
widgetConfig: GaugeWidgetModel,
@@ -51,9 +53,14 @@ export class SpGaugeRendererService implements SpEchartsRenderer<GaugeWidgetMode
const visConfig = widgetConfig.visualizationConfig;
const clamp = this.getSizeClamp(widgetSize);
const useThresholdColors = !!visConfig.enableThresholdColors;
- const displayName = this.makeDisplayName(
- visConfig.displayName,
- fieldName,
+ const displayName = this.resultLabelService.resolveLabel(
+ widgetConfig.dataConfig.sourceConfigs[selectedField.sourceIndex]
+ .queryConfig,
+ selectedField,
+ this.makeDisplayName(
+ visConfig.displayName,
+ selectedField.fullDbName,
+ ),
);
const series: GaugeSeriesOption = {
@@ -172,7 +179,7 @@ export class SpGaugeRendererService implements SpEchartsRenderer<GaugeWidgetMode
},
series: this.makeSeriesItem(
'',
- selectedField.fullDbName,
+ selectedField,
data,
decimals,
widgetConfig,
diff --git a/ui/src/app/chart-shared/components/charts/progress-bar/appearance-config/progress-bar-appearance-config.component.html b/ui/src/app/chart-shared/components/charts/progress-bar/appearance-config/progress-bar-appearance-config.component.html
new file mode 100644
index 0000000000..1abed96706
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/progress-bar/appearance-config/progress-bar-appearance-config.component.html
@@ -0,0 +1,46 @@
+<!--
+ ~ 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.
+ ~
+ -->
+
+<sp-split-section [level]="3" [title]="'Bar settings' | translate">
+ <div fxLayout="column" class="progress-color-list">
+ <sp-form-field [level]="3" [label]="'Progress color' | translate">
+ <input
+ class="progress-color-picker"
+ [colorPicker]="config.progressColor"
+ [style.background]="config.progressColor"
+ [cpPosition]="'auto'"
+ [cpPresetColors]="presetColors"
+ (colorPickerChange)="setColor('progressColor', $event)"
+ />
+ </sp-form-field>
+
+ <sp-form-field [level]="3" [label]="'Track color' | translate">
+ <input
+ class="progress-color-picker"
+ [colorPicker]="config.trackColor"
+ [style.background]="config.trackColor"
+ [cpPosition]="'auto'"
+ [cpPresetColors]="presetColors"
+ (colorPickerChange)="setColor('trackColor', $event)"
+ />
+ </sp-form-field>
+ </div>
+
+ <sp-number-format-config [appearanceConfig]="appearanceConfig">
+ </sp-number-format-config>
+</sp-split-section>
diff --git a/ui/src/app/chart-shared/components/charts/progress-bar/appearance-config/progress-bar-appearance-config.component.scss b/ui/src/app/chart-shared/components/charts/progress-bar/appearance-config/progress-bar-appearance-config.component.scss
new file mode 100644
index 0000000000..ddc3d05f1e
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/progress-bar/appearance-config/progress-bar-appearance-config.component.scss
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ *
+ */
+
+.progress-color-list {
+ gap: var(--space-sm);
+}
+
+.progress-color-picker {
+ display: block;
+ width: calc(var(--space-md) * 2.4);
+ height: calc(var(--space-md) * 2.4);
+ padding: 0;
+ border: 1px solid var(--color-tab-border);
+ border-radius: var(--button-border-radius);
+ cursor: pointer;
+ appearance: none;
+ -webkit-appearance: none;
+ background-clip: padding-box;
+}
diff --git a/ui/src/app/chart-shared/components/charts/progress-bar/appearance-config/progress-bar-appearance-config.component.ts b/ui/src/app/chart-shared/components/charts/progress-bar/appearance-config/progress-bar-appearance-config.component.ts
new file mode 100644
index 0000000000..606fb8b18e
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/progress-bar/appearance-config/progress-bar-appearance-config.component.ts
@@ -0,0 +1,88 @@
+/*
+ * 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 {
+ FormFieldComponent,
+ SplitSectionComponent,
+} from '@streampipes/shared-ui';
+import { TranslatePipe } from '@ngx-translate/core';
+import { SpNumberFormatConfigComponent } from '../../../chart-config/number-format-config/number-format-config.component';
+import { ProgressBarAppearanceConfig } from '../model/progress-bar-widget.model';
+import { ChartConfigurationService } from '../../../../services/chart-configuration.service';
+import { ColorPickerDirective } from 'ngx-color-picker';
+import { FlexDirective } from '@ngbracket/ngx-layout/flex';
+
+@Component({
+ selector: 'sp-progress-bar-widget-appearance-config',
+ templateUrl: './progress-bar-appearance-config.component.html',
+ styleUrls: ['./progress-bar-appearance-config.component.scss'],
+ imports: [
+ SplitSectionComponent,
+ FormFieldComponent,
+ TranslatePipe,
+ SpNumberFormatConfigComponent,
+ ColorPickerDirective,
+ FlexDirective,
+ ],
+})
+export class ProgressBarWidgetAppearanceConfigComponent implements OnInit {
+ private widgetConfigurationService = inject(ChartConfigurationService);
+
+ @Input()
+ appearanceConfig: ProgressBarAppearanceConfig;
+
+ readonly presetColors = [
+ '#2563EB',
+ '#16A34A',
+ '#F59E0B',
+ '#DC2626',
+ '#0F766E',
+ '#9333EA',
+ ];
+
+ ngOnInit(): void {
+ this.ensureAppearanceConfig();
+ }
+
+ get config(): ProgressBarAppearanceConfig {
+ return this.ensureAppearanceConfig();
+ }
+
+ setColor(key: 'progressColor' | 'trackColor', color: string): void {
+ this.config[key] = color;
+ this.widgetConfigurationService.notify({
+ refreshView: true,
+ refreshData: false,
+ });
+ }
+
+ private ensureAppearanceConfig(): ProgressBarAppearanceConfig {
+ this.appearanceConfig ??= {
+ backgroundColor: 'var(--color-bg-0)',
+ textColor: 'var(--color-default-text)',
+ widgetTitle: '',
+ numberFormat: {},
+ };
+
+ this.appearanceConfig.progressColor ??= '#2563EB';
+ this.appearanceConfig.trackColor ??= 'rgba(148, 163, 184, 0.24)';
+
+ return this.appearanceConfig;
+ }
+}
diff --git a/ui/src/app/chart-shared/components/charts/progress-bar/config/progress-bar-widget-config.component.html b/ui/src/app/chart-shared/components/charts/progress-bar/config/progress-bar-widget-config.component.html
new file mode 100644
index 0000000000..07eef853c4
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/progress-bar/config/progress-bar-widget-config.component.html
@@ -0,0 +1,159 @@
+<!--
+ ~ 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.
+ ~
+ -->
+
+<sp-visualization-config-outer
+ [configurationValid]="
+ currentlyConfiguredWidget.visualizationConfig.configurationValid
+ "
+>
+ <sp-split-section [level]="3" [title]="'Values' | translate">
+ <sp-form-field [level]="3" [label]="'Current value field' | translate">
+ <sp-select-single-property-config
+ [availableProperties]="availableNumericFields"
+ [selectedProperty]="
+ currentlyConfiguredWidget.visualizationConfig
+ .currentValueField
+ "
+ (changeSelectedProperty)="setCurrentValueField($event)"
+ >
+ </sp-select-single-property-config>
+ </sp-form-field>
+
+ <sp-form-field [level]="3" [label]="'Target source' | translate">
+ <mat-radio-group
+ [ngModel]="
+ currentlyConfiguredWidget.visualizationConfig.targetSource
+ "
+ (ngModelChange)="setTargetSource($event)"
+ >
+ <mat-radio-button [value]="'fixed'">
+ {{ 'Fixed value' | translate }}
+ </mat-radio-button>
+ <mat-radio-button [value]="'field'">
+ {{ 'Field' | translate }}
+ </mat-radio-button>
+ </mat-radio-group>
+ </sp-form-field>
+
+ @if (
+ currentlyConfiguredWidget.visualizationConfig.targetSource ===
+ 'field'
+ ) {
+ <sp-form-field [level]="3" [label]="'Target field' | translate">
+ <sp-select-single-property-config
+ [availableProperties]="availableNumericFields"
+ [selectedProperty]="
+ currentlyConfiguredWidget.visualizationConfig
+ .targetField
+ "
+ (changeSelectedProperty)="setTargetField($event)"
+ >
+ </sp-select-single-property-config>
+ </sp-form-field>
+ } @else {
+ <sp-form-field [level]="3" [label]="'Target value' | translate">
+ <mat-form-field fxFlex="100">
+ <input
+ matInput
+ type="number"
+ [ngModel]="
+ currentlyConfiguredWidget.visualizationConfig
+ .targetValue
+ "
+ (ngModelChange)="setTargetValue($event)"
+ />
+ </mat-form-field>
+ </sp-form-field>
+ }
+ </sp-split-section>
+
+ <sp-split-section [level]="3" [title]="'Behavior' | translate">
+ <sp-form-field [level]="3" [label]="'Display mode' | translate">
+ <mat-radio-group
+ [ngModel]="
+ currentlyConfiguredWidget.visualizationConfig.displayMode
+ "
+ (ngModelChange)="setDisplayMode($event)"
+ >
+ <mat-radio-button [value]="'percent'">
+ {{ 'Percent only' | translate }}
+ </mat-radio-button>
+ <mat-radio-button [value]="'value'">
+ {{ 'Current / Target' | translate }}
+ </mat-radio-button>
+ <mat-radio-button [value]="'percent-and-value'">
+ {{ 'Percent + Current / Target' | translate }}
+ </mat-radio-button>
+ </mat-radio-group>
+ </sp-form-field>
+
+ <mat-checkbox
+ [checked]="currentlyConfiguredWidget.visualizationConfig.showLabel"
+ (change)="setShowLabel($event.checked)"
+ >
+ {{ 'Show progress label' | translate }}
+ </mat-checkbox>
+
+ <mat-checkbox
+ [checked]="
+ currentlyConfiguredWidget.visualizationConfig.invertProgress
+ "
+ (change)="setInvertProgress($event.checked)"
+ >
+ {{ 'Invert progress semantics' | translate }}
+ </mat-checkbox>
+
+ <mat-checkbox
+ [checked]="
+ currentlyConfiguredWidget.visualizationConfig.clampProgress
+ "
+ (change)="setClampProgress($event.checked)"
+ >
+ {{ 'Clamp to 0-100%' | translate }}
+ </mat-checkbox>
+ </sp-split-section>
+
+ <sp-split-section [level]="3" [title]="'Content' | translate">
+ <sp-form-field [level]="3" [label]="'Title' | translate">
+ <mat-form-field fxFlex="100">
+ <input
+ matInput
+ type="text"
+ [(ngModel)]="
+ currentlyConfiguredWidget.visualizationConfig.title
+ "
+ (ngModelChange)="triggerViewRefresh()"
+ />
+ </mat-form-field>
+ </sp-form-field>
+
+ <sp-form-field [level]="3" [label]="'Description' | translate">
+ <mat-form-field fxFlex="100">
+ <textarea
+ matInput
+ rows="3"
+ [(ngModel)]="
+ currentlyConfiguredWidget.visualizationConfig
+ .description
+ "
+ (ngModelChange)="triggerViewRefresh()"
+ ></textarea>
+ </mat-form-field>
+ </sp-form-field>
+ </sp-split-section>
+</sp-visualization-config-outer>
diff --git a/ui/src/app/chart-shared/components/charts/progress-bar/config/progress-bar-widget-config.component.ts b/ui/src/app/chart-shared/components/charts/progress-bar/config/progress-bar-widget-config.component.ts
new file mode 100644
index 0000000000..bea77b375f
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/progress-bar/config/progress-bar-widget-config.component.ts
@@ -0,0 +1,154 @@
+/*
+ * 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 } from '@angular/core';
+import { BaseWidgetConfig } from '../../base/base-widget-config';
+import {
+ ProgressBarDisplayMode,
+ ProgressBarTargetSource,
+ ProgressBarVisConfig,
+ ProgressBarWidgetModel,
+} from '../model/progress-bar-widget.model';
+import { DataExplorerField } from '@streampipes/platform-services';
+import { SpVisualizationConfigOuterComponent } from '../../../chart-config/visualization-config-outer/visualization-config-outer.component';
+import {
+ FormFieldComponent,
+ SplitSectionComponent,
+} from '@streampipes/shared-ui';
+import { SelectSinglePropertyConfigComponent } from '../../../chart-config/select-single-property-config/select-single-property-config.component';
+import { MatFormField } from '@angular/material/form-field';
+import { MatInput } from '@angular/material/input';
+import { FormsModule } from '@angular/forms';
+import { MatCheckbox } from '@angular/material/checkbox';
+import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
+import { FlexDirective } from '@ngbracket/ngx-layout/flex';
+import { TranslatePipe } from '@ngx-translate/core';
+
+@Component({
+ selector: 'sp-data-explorer-progress-bar-widget-config',
+ templateUrl: './progress-bar-widget-config.component.html',
+ imports: [
+ SpVisualizationConfigOuterComponent,
+ SplitSectionComponent,
+ FormFieldComponent,
+ SelectSinglePropertyConfigComponent,
+ MatFormField,
+ MatInput,
+ MatCheckbox,
+ MatRadioGroup,
+ MatRadioButton,
+ FlexDirective,
+ FormsModule,
+ TranslatePipe,
+ ],
+})
+export class ProgressBarWidgetConfigComponent extends BaseWidgetConfig<
+ ProgressBarWidgetModel,
+ ProgressBarVisConfig
+> {
+ get availableNumericFields(): DataExplorerField[] {
+ const primarySourceFields = this.fieldProvider.numericFields.filter(
+ field => field.sourceIndex === 0,
+ );
+ return primarySourceFields.length > 0
+ ? primarySourceFields
+ : this.fieldProvider.numericFields;
+ }
+
+ setCurrentValueField(field: DataExplorerField): void {
+ this.currentlyConfiguredWidget.visualizationConfig.currentValueField =
+ field;
+ this.triggerViewRefresh();
+ }
+
+ setTargetSource(targetSource: ProgressBarTargetSource): void {
+ this.currentlyConfiguredWidget.visualizationConfig.targetSource =
+ targetSource;
+
+ if (
+ targetSource === 'field' &&
+ !this.currentlyConfiguredWidget.visualizationConfig.targetField
+ ) {
+ this.currentlyConfiguredWidget.visualizationConfig.targetField =
+ this.availableNumericFields[0];
+ }
+
+ this.triggerViewRefresh();
+ }
+
+ setTargetField(field: DataExplorerField): void {
+ this.currentlyConfiguredWidget.visualizationConfig.targetField = field;
+ this.triggerViewRefresh();
+ }
+
+ setTargetValue(targetValue: string | number): void {
+ const numericValue = Number(String(targetValue).replace(',', '.'));
+ this.currentlyConfiguredWidget.visualizationConfig.targetValue =
+ Number.isFinite(numericValue) ? numericValue : undefined;
+ this.triggerViewRefresh();
+ }
+
+ setInvertProgress(invertProgress: boolean): void {
+ this.currentlyConfiguredWidget.visualizationConfig.invertProgress =
+ invertProgress;
+ this.triggerViewRefresh();
+ }
+
+ setClampProgress(clampProgress: boolean): void {
+ this.currentlyConfiguredWidget.visualizationConfig.clampProgress =
+ clampProgress;
+ this.triggerViewRefresh();
+ }
+
+ setDisplayMode(displayMode: ProgressBarDisplayMode): void {
+ this.currentlyConfiguredWidget.visualizationConfig.displayMode =
+ displayMode;
+ this.triggerViewRefresh();
+ }
+
+ setShowLabel(showLabel: boolean): void {
+ this.currentlyConfiguredWidget.visualizationConfig.showLabel =
+ showLabel;
+ this.triggerViewRefresh();
+ }
+
+ protected applyWidgetConfig(config: ProgressBarVisConfig): void {
+ config.currentValueField = this.fieldService.getSelectedField(
+ config.currentValueField,
+ this.availableNumericFields,
+ () => this.availableNumericFields[0],
+ );
+ config.targetSource ??= 'fixed';
+ config.targetField = this.fieldService.getSelectedField(
+ config.targetField,
+ this.availableNumericFields,
+ () => this.availableNumericFields[0],
+ );
+ config.targetValue ??= 100;
+ config.invertProgress ??= false;
+ config.clampProgress ??= true;
+ config.displayMode ??= 'percent-and-value';
+ config.showLabel ??= true;
+ config.title ??= '';
+ config.description ??= '';
+ }
+
+ protected requiredFieldsForChartPresent(): boolean {
+ return this.availableNumericFields.length > 0;
+ }
+}
diff --git a/ui/src/app/chart-shared/components/charts/progress-bar/model/progress-bar-widget.model.ts b/ui/src/app/chart-shared/components/charts/progress-bar/model/progress-bar-widget.model.ts
new file mode 100644
index 0000000000..678980565e
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/progress-bar/model/progress-bar-widget.model.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 {
+ DataExplorerDataConfig,
+ DataExplorerField,
+ DataExplorerWidgetModel,
+} from '@streampipes/platform-services';
+import {
+ DataExplorerVisConfig,
+ WidgetNumberAppearanceConfig,
+} from '../../../../models/dataview-dashboard.model';
+
+export type ProgressBarTargetSource = 'fixed' | 'field';
+export type ProgressBarDisplayMode = 'percent' | 'value' | 'percent-and-value';
+
+export interface ProgressBarVisConfig extends DataExplorerVisConfig {
+ currentValueField?: DataExplorerField;
+ targetSource?: ProgressBarTargetSource;
+ targetField?: DataExplorerField;
+ targetValue?: number;
+ invertProgress?: boolean;
+ clampProgress?: boolean;
+ displayMode?: ProgressBarDisplayMode;
+ showLabel?: boolean;
+ title?: string;
+ description?: string;
+}
+
+export interface ProgressBarAppearanceConfig extends WidgetNumberAppearanceConfig {
+ progressColor?: string;
+ trackColor?: string;
+}
+
+export interface ProgressBarWidgetModel extends DataExplorerWidgetModel {
+ dataConfig: DataExplorerDataConfig;
+ visualizationConfig: ProgressBarVisConfig;
+ baseAppearanceConfig: ProgressBarAppearanceConfig;
+}
diff --git a/ui/src/app/chart-shared/components/charts/progress-bar/progress-bar-widget.component.html b/ui/src/app/chart-shared/components/charts/progress-bar/progress-bar-widget.component.html
new file mode 100644
index 0000000000..e5dc11771b
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/progress-bar/progress-bar-widget.component.html
@@ -0,0 +1,112 @@
+<!--
+ ~ 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="progress-widget"
+ fxLayout="column"
+ fxFlex="100"
+ [ngStyle]="widgetStyles"
+ data-cy="progress-bar-widget"
+>
+ @if (showNoDataInDateRange) {
+ <sp-no-data-in-date-range [viewDateRange]="timeSettings" class="h-100">
+ </sp-no-data-in-date-range>
+ }
+
+ @if (showTooMuchData) {
+ <sp-too-much-data
+ [amountOfEvents]="amountOfTooMuchEvents"
+ (loadDataWithTooManyEventsEmitter)="loadDataWithTooManyEvents()"
+ class="h-100"
+ >
+ </sp-too-much-data>
+ }
+
+ @if (showInvalidConfiguration) {
+ <sp-invalid-configuration
+ [widgetTypeLabel]="widgetTypeLabel"
+ class="h-100"
+ >
+ </sp-invalid-configuration>
+ }
+
+ @if (showData && progressView) {
+ <div class="progress-shell" fxLayout="column" fxFlex="100">
+ @if (titleText || descriptionText) {
+ <div class="progress-copy" fxLayout="column">
+ @if (titleText) {
+ <div class="progress-title">
+ {{ titleText }}
+ </div>
+ }
+
+ @if (descriptionText) {
+ <div class="progress-description">
+ {{ descriptionText }}
+ </div>
+ }
+ </div>
+ }
+
+ <div
+ class="progress-meta"
+ fxLayout="row"
+ fxLayoutAlign="space-between center"
+ >
+ <span class="progress-status">{{
+ progressView.statusLabel
+ }}</span>
+ @if (showLabel) {
+ <span class="progress-percent">{{
+ progressView.percentLabel
+ }}</span>
+ }
+ </div>
+
+ <div class="progress-track">
+ <div class="progress-fill" [style.width]="progressWidth"></div>
+ </div>
+
+ @if (showLabel) {
+ <div class="progress-labels" fxLayout="column">
+ @if (
+ dataExplorerWidget.visualizationConfig.displayMode ===
+ 'percent' ||
+ dataExplorerWidget.visualizationConfig.displayMode ===
+ 'percent-and-value'
+ ) {
+ <div class="progress-primary-label">
+ {{ progressView.percentLabel }}
+ </div>
+ }
+
+ @if (
+ dataExplorerWidget.visualizationConfig.displayMode ===
+ 'value' ||
+ dataExplorerWidget.visualizationConfig.displayMode ===
+ 'percent-and-value'
+ ) {
+ <div class="progress-secondary-label">
+ {{ progressView.valueLabel }}
+ </div>
+ }
+ </div>
+ }
+ </div>
+ }
+</div>
diff --git a/ui/src/app/chart-shared/components/charts/progress-bar/progress-bar-widget.component.scss b/ui/src/app/chart-shared/components/charts/progress-bar/progress-bar-widget.component.scss
new file mode 100644
index 0000000000..b25c8eee61
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/progress-bar/progress-bar-widget.component.scss
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ *
+ */
+
+:host {
+ display: block;
+ height: 100%;
+}
+
+.h-100 {
+ height: 100%;
+}
+
+.progress-widget {
+ box-sizing: border-box;
+ height: 100%;
+ min-height: 0;
+ overflow: hidden;
+ padding: var(--progress-widget-padding);
+}
+
+.progress-shell {
+ gap: var(--progress-shell-gap);
+ min-height: 0;
+}
+
+.progress-copy {
+ gap: 0.35rem;
+}
+
+.progress-title {
+ font-size: var(--progress-title-size);
+ font-weight: 700;
+ letter-spacing: -0.03em;
+ line-height: 1.08;
+}
+
+.progress-description {
+ color: color-mix(in srgb, currentColor 70%, transparent);
+ font-size: var(--progress-description-size);
+ line-height: 1.35;
+ max-width: 48ch;
+}
+
+.progress-meta {
+ column-gap: 1rem;
+}
+
+.progress-status {
+ color: color-mix(in srgb, currentColor 68%, transparent);
+ font-size: var(--progress-percent-size);
+ font-weight: 700;
+}
+
+.progress-percent {
+ font-size: var(--progress-percent-size);
+ font-weight: 700;
+}
+
+.progress-track {
+ background: var(--progress-bar-track-color);
+ border-radius: 999px;
+ flex-shrink: 0;
+ height: var(--progress-track-height);
+ overflow: hidden;
+ position: relative;
+}
+
+.progress-fill {
+ background: var(--progress-bar-fill-color);
+ border-radius: inherit;
+ height: 100%;
+ min-width: 0;
+ transition: width 220ms ease-out;
+}
+
+.progress-labels {
+ gap: 0.2rem;
+}
+
+.progress-primary-label {
+ font-size: var(--progress-primary-label-size);
+ font-weight: 700;
+ letter-spacing: -0.04em;
+ line-height: 1;
+}
+
+.progress-secondary-label {
+ color: color-mix(in srgb, currentColor 70%, transparent);
+ font-size: var(--progress-secondary-label-size);
+ font-weight: 600;
+ line-height: 1.3;
+}
diff --git a/ui/src/app/chart-shared/components/charts/progress-bar/progress-bar-widget.component.ts b/ui/src/app/chart-shared/components/charts/progress-bar/progress-bar-widget.component.ts
new file mode 100644
index 0000000000..4d42c12d6a
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/progress-bar/progress-bar-widget.component.ts
@@ -0,0 +1,376 @@
+/*
+ * 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 { NgStyle } from '@angular/common';
+import { Component, LOCALE_ID, OnInit, inject } from '@angular/core';
+import {
+ DataExplorerField,
+ SpQueryResult,
+} from '@streampipes/platform-services';
+import { FlexDirective, LayoutDirective } from '@ngbracket/ngx-layout/flex';
+import { BaseDataExplorerWidgetDirective } from '../base/base-data-explorer-widget.directive';
+import { NoDataInDateRangeComponent } from '../base/no-data/no-data-in-date-range.component';
+import { TooMuchDataComponent } from '../base/too-much-data/too-much-data.component';
+import { SpInvalidConfigurationComponent } from '../base/invalid-configuration/invalid-configuration.component';
+import { ChartRegistry } from '../../../registry/chart-registry.service';
+import {
+ ProgressBarAppearanceConfig,
+ ProgressBarTargetSource,
+ ProgressBarWidgetModel,
+} from './model/progress-bar-widget.model';
+import {
+ clampValue,
+ formatWidgetNumber,
+ scaleResponsiveValue,
+} from '../../../services/widget-render.utils';
+
+interface ProgressBarViewModel {
+ ratio: number;
+ percentLabel: string;
+ valueLabel: string;
+ statusLabel: string;
+}
+
+@Component({
+ selector: 'sp-data-explorer-progress-bar-widget',
+ templateUrl: './progress-bar-widget.component.html',
+ styleUrls: ['./progress-bar-widget.component.scss'],
+ imports: [
+ LayoutDirective,
+ FlexDirective,
+ NgStyle,
+ NoDataInDateRangeComponent,
+ TooMuchDataComponent,
+ SpInvalidConfigurationComponent,
+ ],
+})
+export class ProgressBarWidgetComponent
+ extends BaseDataExplorerWidgetDirective<ProgressBarWidgetModel>
+ implements OnInit
+{
+ widgetTypeLabel: string;
+ progressView?: ProgressBarViewModel;
+
+ private hasReceivedData = false;
+ private latestData: SpQueryResult[] = [];
+ private readonly locale = inject(LOCALE_ID);
+ private readonly chartRegistry = inject(ChartRegistry);
+
+ ngOnInit(): void {
+ super.ngOnInit();
+ this.widgetTypeLabel = this.chartRegistry.getChartTemplate(
+ this.dataExplorerWidget.widgetType,
+ ).label;
+ }
+
+ get appearanceConfig(): ProgressBarAppearanceConfig {
+ this.dataExplorerWidget.baseAppearanceConfig ??= {
+ backgroundColor: 'var(--color-bg-0)',
+ textColor: 'var(--color-default-text)',
+ widgetTitle: '',
+ numberFormat: {},
+ progressColor: '#2563EB',
+ trackColor: 'rgba(148, 163, 184, 0.24)',
+ };
+
+ return this.dataExplorerWidget
+ .baseAppearanceConfig as ProgressBarAppearanceConfig;
+ }
+
+ get widgetStyles(): Record<string, string> {
+ const compactMode =
+ (this.currentWidth ?? 0) < 520 || (this.currentHeight ?? 0) < 220;
+ const fontScale = this.getFontScale();
+
+ return {
+ 'background': this.appearanceConfig.backgroundColor,
+ 'color': this.appearanceConfig.textColor,
+ '--progress-widget-padding': compactMode
+ ? '0.75rem 0.85rem'
+ : '1rem 1.1rem',
+ '--progress-shell-gap': `${scaleResponsiveValue(
+ 8,
+ 16,
+ fontScale,
+ 0.55,
+ 1.35,
+ )}px`,
+ '--progress-title-size': `${scaleResponsiveValue(
+ 16,
+ 20,
+ fontScale,
+ 0.55,
+ 1.35,
+ )}px`,
+ '--progress-description-size': `${scaleResponsiveValue(
+ 12,
+ 14,
+ fontScale,
+ 0.55,
+ 1.35,
+ )}px`,
+ '--progress-status-size': `${scaleResponsiveValue(
+ 10,
+ 13,
+ fontScale,
+ 0.55,
+ 1.35,
+ )}px`,
+ '--progress-percent-size': `${scaleResponsiveValue(
+ 12,
+ 15,
+ fontScale,
+ 0.55,
+ 1.35,
+ )}px`,
+ '--progress-track-height': `${scaleResponsiveValue(
+ 10,
+ 16,
+ fontScale,
+ 0.55,
+ 1.35,
+ )}px`,
+ '--progress-primary-label-size': `${scaleResponsiveValue(
+ 18,
+ 38,
+ fontScale,
+ 0.55,
+ 1.35,
+ )}px`,
+ '--progress-secondary-label-size': `${scaleResponsiveValue(
+ 12,
+ 15,
+ fontScale,
+ 0.55,
+ 1.35,
+ )}px`,
+ '--progress-bar-fill-color':
+ this.appearanceConfig.progressColor ?? '#2563EB',
+ '--progress-bar-track-color':
+ this.appearanceConfig.trackColor ?? 'rgba(148, 163, 184, 0.24)',
+ };
+ }
+
+ get titleText(): string {
+ return this.dataExplorerWidget.visualizationConfig.title?.trim() ?? '';
+ }
+
+ get descriptionText(): string {
+ return (
+ this.dataExplorerWidget.visualizationConfig.description?.trim() ??
+ ''
+ );
+ }
+
+ get showLabel(): boolean {
+ return !!this.dataExplorerWidget.visualizationConfig.showLabel;
+ }
+
+ get progressWidth(): string {
+ return `${clampValue((this.progressView?.ratio ?? 0) * 100, 0, 100)}%`;
+ }
+
+ beforeDataFetched(): void {
+ if (this.progressView) {
+ this.setShownComponents(false, true, false, false);
+ return;
+ }
+
+ this.setShownComponents(false, false, true, false);
+ }
+
+ onDataReceived(spQueryResults: SpQueryResult[]): void {
+ this.hasReceivedData = true;
+ this.latestData = spQueryResults;
+ this.refreshView();
+ }
+
+ onResize(_width: number, _height: number): void {
+ this.refreshView();
+ }
+
+ refreshView(): void {
+ this.updateProgressView();
+ }
+
+ handleUpdatedFields(
+ addedFields: DataExplorerField[],
+ removedFields: DataExplorerField[],
+ ): void {
+ const fieldUpdateInfo = {
+ addedFields,
+ removedFields,
+ fieldProvider: this.fieldProvider,
+ };
+
+ this.dataExplorerWidget.visualizationConfig.currentValueField =
+ this.fieldUpdateService.updateNumericField(
+ this.dataExplorerWidget.visualizationConfig.currentValueField,
+ fieldUpdateInfo,
+ );
+ this.dataExplorerWidget.visualizationConfig.targetField =
+ this.fieldUpdateService.updateNumericField(
+ this.dataExplorerWidget.visualizationConfig.targetField,
+ fieldUpdateInfo,
+ );
+ this.refreshView();
+ }
+
+ private updateProgressView(): void {
+ if (!this.isConfigurationValid()) {
+ this.showInvalidConfiguration = true;
+ this.progressView = undefined;
+ this.setShownComponents(false, false, false, false);
+ return;
+ }
+
+ this.showInvalidConfiguration = false;
+ if (!this.hasReceivedData && this.latestData.length === 0) {
+ return;
+ }
+
+ const firstQueryResult = this.latestData[0];
+ const firstRow = this.getFirstRow(firstQueryResult);
+
+ if (!firstQueryResult?.headers?.length || !firstRow) {
+ this.progressView = undefined;
+ this.setShownComponents(true, false, false, false);
+ return;
+ }
+
+ const currentValue = this.getFieldValue(
+ firstQueryResult,
+ firstRow,
+ this.dataExplorerWidget.visualizationConfig.currentValueField,
+ );
+ const targetValue = this.getTargetValue(firstQueryResult, firstRow);
+
+ if (
+ currentValue === undefined ||
+ targetValue === undefined ||
+ !Number.isFinite(currentValue) ||
+ !Number.isFinite(targetValue) ||
+ targetValue <= 0
+ ) {
+ this.showInvalidConfiguration = true;
+ this.progressView = undefined;
+ this.setShownComponents(false, false, false, false);
+ return;
+ }
+
+ const rawRatio = currentValue / targetValue;
+ const inverted =
+ this.dataExplorerWidget.visualizationConfig.invertProgress;
+ const unclampedRatio = inverted ? 1 - rawRatio : rawRatio;
+ const ratio = this.dataExplorerWidget.visualizationConfig.clampProgress
+ ? clampValue(unclampedRatio, 0, 1)
+ : unclampedRatio;
+
+ this.progressView = {
+ ratio,
+ percentLabel: `${formatWidgetNumber(
+ ratio * 100,
+ this.locale,
+ this.appearanceConfig.numberFormat?.decimals,
+ )}%`,
+ valueLabel: `${formatWidgetNumber(
+ currentValue,
+ this.locale,
+ this.appearanceConfig.numberFormat?.decimals,
+ )} / ${formatWidgetNumber(
+ targetValue,
+ this.locale,
+ this.appearanceConfig.numberFormat?.decimals,
+ )}`,
+ statusLabel: inverted ? 'Remaining' : 'Completed',
+ };
+
+ this.setShownComponents(false, true, false, false);
+ }
+
+ private isConfigurationValid(): boolean {
+ const config = this.dataExplorerWidget.visualizationConfig;
+
+ if (!config.currentValueField) {
+ return false;
+ }
+
+ if (config.targetSource === 'field') {
+ return !!config.targetField;
+ }
+
+ return config.targetValue !== undefined && config.targetValue !== null;
+ }
+
+ private getTargetValue(
+ queryResult: SpQueryResult,
+ row: unknown[],
+ ): number | undefined {
+ const targetSource = this.dataExplorerWidget.visualizationConfig
+ .targetSource as ProgressBarTargetSource;
+
+ if (targetSource === 'field') {
+ return this.getFieldValue(
+ queryResult,
+ row,
+ this.dataExplorerWidget.visualizationConfig.targetField,
+ );
+ }
+
+ return this.dataExplorerWidget.visualizationConfig.targetValue;
+ }
+
+ private getFieldValue(
+ queryResult: SpQueryResult,
+ row: unknown[],
+ field?: DataExplorerField,
+ ): number | undefined {
+ if (!field) {
+ return undefined;
+ }
+
+ const fieldIndex = queryResult.headers.findIndex(
+ header => header === field.fullDbName,
+ );
+ if (fieldIndex < 0) {
+ return undefined;
+ }
+
+ const value = Number(row[fieldIndex]);
+ return Number.isFinite(value) ? value : undefined;
+ }
+
+ private getFirstRow(queryResult?: SpQueryResult): unknown[] | undefined {
+ return queryResult?.allDataSeries.find(series => series.rows.length > 0)
+ ?.rows[0];
+ }
+
+ private getFontScale(): number {
+ const width = this.currentWidth ?? 0;
+ const height = this.currentHeight ?? 0;
+
+ if (width <= 0 || height <= 0) {
+ return 0.85;
+ }
+
+ const widthScale = clampValue(width / 420, 0.6, 1.35);
+ const heightScale = clampValue(height / 260, 0.55, 1.35);
+
+ return clampValue(Math.min(widthScale, heightScale), 0.55, 1.35);
+ }
+}
diff --git a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.html b/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.html
index fa8808c9e9..7b0a9e1772 100644
--- a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.html
+++ b/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.html
@@ -31,86 +31,14 @@
"
>
</sp-select-properties-config>
- <sp-split-section [level]="3" [title]="'Column Order' | translate">
- <div
- fxLayout="column"
- class="selected-column-list"
- cdkDropList
- [cdkDropListData]="
- currentlyConfiguredWidget.visualizationConfig.selectedColumns
- "
- (cdkDropListDropped)="dropSelectedColumn($event)"
- >
- @for (
- field of currentlyConfiguredWidget.visualizationConfig
- .selectedColumns;
- track colorKey(field);
- let i = $index
- ) {
- <div
- class="selected-column-row"
- fxLayout="row"
- fxLayoutAlign="space-between center"
- cdkDrag
- >
- <div
- class="selected-column-main"
- fxLayout="row"
- fxLayoutAlign="start center"
- >
- <button
- mat-icon-button
- type="button"
- class="selected-column-drag-handle"
- cdkDragHandle
- [attr.aria-label]="
- 'Reorder ' + field.runtimeName | translate
- "
- >
- <mat-icon>drag_indicator</mat-icon>
- </button>
- <div class="selected-column-label">
- {{ selectedColumnLabel(field) }}
- </div>
- </div>
- <div
- class="selected-column-actions"
- fxLayout="row"
- fxLayoutAlign="end center"
- >
- <button
- mat-icon-button
- type="button"
- [disabled]="!canMoveSelectedColumnUp(i)"
- [attr.aria-label]="
- 'Move ' + field.runtimeName + ' up' | translate
- "
- (click)="moveSelectedColumn(i, -1)"
- >
- <mat-icon>arrow_upward</mat-icon>
- </button>
- <button
- mat-icon-button
- type="button"
- [disabled]="!canMoveSelectedColumnDown(i)"
- [attr.aria-label]="
- 'Move ' + field.runtimeName + ' down'
- | translate
- "
- (click)="moveSelectedColumn(i, 1)"
- >
- <mat-icon>arrow_downward</mat-icon>
- </button>
- </div>
- </div>
- } @empty {
- <sp-alert-banner
- type="info"
- [title]="'No fields selected.' | translate"
- ></sp-alert-banner>
- }
- </div>
- </sp-split-section>
+ <sp-field-order-config
+ [sectionTitle]="'Column Order'"
+ [selectedFields]="
+ currentlyConfiguredWidget.visualizationConfig.selectedColumns
+ "
+ (selectedFieldsChange)="setSelectedColumnOrder($event)"
+ >
+ </sp-field-order-config>
<sp-split-section [level]="3" [title]="'Settings' | translate">
<sp-form-field [level]="3" [label]="'Filter' | translate">
<mat-form-field fxFlex="100">
diff --git a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.scss b/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.scss
index e926be0642..2bc4d58126 100644
--- a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.scss
+++ b/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.scss
@@ -16,59 +16,6 @@
*
*/
-.selected-column-list {
- gap: var(--space-sm);
-}
-
-.selected-column-row {
- gap: var(--space-sm);
- padding: var(--space-xs) var(--space-sm);
- border: 1px solid var(--color-border-subtle, var(--color-bg-3));
- border-radius: var(--button-border-radius);
- background: var(--color-bg-1, var(--color-bg-0));
-}
-
-.selected-column-main {
- min-width: 0;
- gap: var(--space-xs);
- flex: 1;
-}
-
-.selected-column-drag-handle {
- cursor: grab;
- color: var(--color-secondary-text);
-}
-
-.selected-column-drag-handle:active {
- cursor: grabbing;
-}
-
-.selected-column-label {
- min-width: 0;
- flex: 1;
- font-size: var(--font-size-sm);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.selected-column-actions {
- flex-shrink: 0;
- gap: var(--space-xxs);
-}
-
-.cdk-drag-preview {
- box-sizing: border-box;
- border-radius: var(--button-border-radius);
- border: 1px solid var(--color-border-subtle, var(--color-bg-3));
- background: var(--color-bg-1, var(--color-bg-0));
- box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.18);
-}
-
-.cdk-drag-placeholder {
- opacity: 0.35;
-}
-
.highlight-color-list {
gap: var(--space-sm);
}
diff --git a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.ts b/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.ts
index 957db58dfc..6b31df9c59 100644
--- a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.ts
+++ b/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.ts
@@ -39,15 +39,7 @@ import { TranslatePipe } from '@ngx-translate/core';
import { MatOption, MatSelect } from '@angular/material/select';
import { ColorPickerDirective } from 'ngx-color-picker';
import { MatCheckbox } from '@angular/material/checkbox';
-import {
- CdkDrag,
- CdkDragHandle,
- CdkDropList,
- CdkDragDrop,
- moveItemInArray,
-} from '@angular/cdk/drag-drop';
-import { MatIcon } from '@angular/material/icon';
-import { MatIconButton } from '@angular/material/button';
+import { FieldOrderConfigComponent } from '../../../chart-config/field-order-config/field-order-config.component';
@Component({
selector: 'sp-data-explorer-table-widget-config',
@@ -67,11 +59,7 @@ import { MatIconButton } from '@angular/material/button';
LayoutDirective,
LayoutAlignDirective,
MatCheckbox,
- CdkDropList,
- CdkDrag,
- CdkDragHandle,
- MatIcon,
- MatIconButton,
+ FieldOrderConfigComponent,
TranslatePipe,
FormFieldComponent,
SpAlertBannerComponent,
@@ -104,41 +92,9 @@ export class TableWidgetConfigComponent extends BaseWidgetConfig<
this.triggerViewRefresh();
}
- moveSelectedColumn(fromIndex: number, offset: number): void {
- const columns = [
- ...(this.currentlyConfiguredWidget.visualizationConfig
- .selectedColumns ?? []),
- ];
- const targetIndex = fromIndex + offset;
-
- if (
- fromIndex < 0 ||
- targetIndex < 0 ||
- fromIndex >= columns.length ||
- targetIndex >= columns.length
- ) {
- return;
- }
-
- const [movedColumn] = columns.splice(fromIndex, 1);
- columns.splice(targetIndex, 0, movedColumn);
- this.currentlyConfiguredWidget.visualizationConfig.selectedColumns =
- columns;
- this.triggerViewRefresh();
- }
-
- dropSelectedColumn(event: CdkDragDrop<DataExplorerField[]>): void {
- if (event.previousIndex === event.currentIndex) {
- return;
- }
-
- const columns = [
- ...(this.currentlyConfiguredWidget.visualizationConfig
- .selectedColumns ?? []),
- ];
- moveItemInArray(columns, event.previousIndex, event.currentIndex);
+ setSelectedColumnOrder(selectedColumns: DataExplorerField[]): void {
this.currentlyConfiguredWidget.visualizationConfig.selectedColumns =
- columns;
+ selectedColumns;
this.triggerViewRefresh();
}
@@ -247,23 +203,6 @@ export class TableWidgetConfigComponent extends BaseWidgetConfig<
return true;
}
- canMoveSelectedColumnUp(index: number): boolean {
- return index > 0;
- }
-
- canMoveSelectedColumnDown(index: number): boolean {
- return (
- index <
- (this.currentlyConfiguredWidget.visualizationConfig.selectedColumns
- ?.length ?? 0) -
- 1
- );
- }
-
- selectedColumnLabel(field: DataExplorerField): string {
- return `${field.runtimeName} (${field.measure})`;
- }
-
private syncHighlightColorMap(): void {
const activeColorKeys = new Set(
(
diff --git a/ui/src/app/chart-shared/components/charts/table/table-widget.component.ts b/ui/src/app/chart-shared/components/charts/table/table-widget.component.ts
index 5a176f73f9..a6061a67d6 100644
--- a/ui/src/app/chart-shared/components/charts/table/table-widget.component.ts
+++ b/ui/src/app/chart-shared/components/charts/table/table-widget.component.ts
@@ -46,6 +46,7 @@ import { NoDataInDateRangeComponent } from '../base/no-data/no-data-in-date-rang
import { TooMuchDataComponent } from '../base/too-much-data/too-much-data.component';
import { TranslatePipe } from '@ngx-translate/core';
import { WidgetNumberAppearanceConfig } from '../../../models/dataview-dashboard.model';
+import { ResultLabelService } from '../../../services/result-label.service';
type SortDirection = 'asc' | 'desc' | '';
@@ -120,6 +121,7 @@ const TIMESTAMP_MASK = 'yyyy-mm-dd HH:mm:ss.SSS';
})
export class TableWidgetComponent extends BaseDataExplorerWidgetDirective<TableWidgetModel> {
private elRef = inject(ElementRef);
+ private resultLabelService = inject(ResultLabelService);
private static readonly DEFAULT_PAGE_SIZE = 20;
@@ -865,8 +867,22 @@ export class TableWidgetComponent extends BaseDataExplorerWidgetDirective<TableW
return Math.max(0, normalizedIndex - firstReorderableIndex);
}
- headerLabel = (column: string): string =>
- column === 'time' ? 'Time' : column;
+ headerLabel = (column: string): string => {
+ if (column === 'time') {
+ return 'Time';
+ }
+
+ const field = this.getFieldByColumnName(column);
+ if (!field) {
+ return column;
+ }
+
+ return this.resultLabelService.resolveLabel(
+ this.dataExplorerWidget.dataConfig.sourceConfigs[field.sourceIndex]
+ .queryConfig,
+ field,
+ );
+ };
formatCellValue(column: string, value: unknown): unknown {
if (column === 'time') {
@@ -1203,6 +1219,44 @@ export class TableWidgetComponent extends BaseDataExplorerWidgetDirective<TableW
: (n - stats.min) / (stats.max - stats.min);
}
+ private getFieldByColumnName(
+ column: string,
+ ): DataExplorerField | undefined {
+ const selectedField = (
+ this.dataExplorerWidget.visualizationConfig.selectedColumns ?? []
+ ).find(f => f.fullDbName === column);
+
+ if (selectedField) {
+ return selectedField;
+ }
+
+ for (const [
+ sourceIndex,
+ sourceConfig,
+ ] of this.dataExplorerWidget.dataConfig.sourceConfigs.entries()) {
+ const groupedField = sourceConfig.queryConfig.groupBy
+ ?.filter(groupBy => groupBy.selected)
+ .find(groupBy => groupBy.runtimeName === column);
+
+ if (groupedField) {
+ return {
+ runtimeName: groupedField.runtimeName,
+ measure: sourceConfig.measureName,
+ fullDbName: groupedField.runtimeName,
+ sourceIndex,
+ fieldCharacteristics: {
+ dimension: true,
+ numeric: false,
+ binary: false,
+ semanticTypes: [],
+ },
+ };
+ }
+ }
+
+ return undefined;
+ }
+
private toNumber(value: unknown): number | undefined {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string' && value.trim() !== '') {
diff --git a/ui/src/app/chart-shared/components/charts/time-series-chart/sp-timeseries-renderer.service.ts b/ui/src/app/chart-shared/components/charts/time-series-chart/sp-timeseries-renderer.service.ts
index ebd417bfa5..b094faedd8 100644
--- a/ui/src/app/chart-shared/components/charts/time-series-chart/sp-timeseries-renderer.service.ts
+++ b/ui/src/app/chart-shared/components/charts/time-series-chart/sp-timeseries-renderer.service.ts
@@ -27,6 +27,7 @@ import { Injectable } from '@angular/core';
import { TimeSeriesChartWidgetModel } from './model/time-series-chart-widget.model';
import { DataExplorerField } from '@streampipes/platform-services';
import { SpBaseEchartsRenderer } from '../../../echarts-renderer/base-echarts-renderer';
+import { ResultLabelService } from '../../../services/result-label.service';
import {
GeneratedDataset,
TagValue,
@@ -44,6 +45,10 @@ import type { FieldUpdateInfo } from '../../../models/field-update.model';
@Injectable({ providedIn: 'root' })
export class SpTimeseriesRendererService extends SpBaseEchartsRenderer<TimeSeriesChartWidgetModel> {
+ constructor(private resultLabelService: ResultLabelService) {
+ super();
+ }
+
applyOptions(
generatedDataset: GeneratedDataset,
options: EChartsOption,
@@ -71,10 +76,16 @@ export class SpTimeseriesRendererService extends SpBaseEchartsRenderer<TimeSerie
const rawDatasetDimensions = dataset.rawDataset.dimensions;
const groupIndex = i - dataset.meta.preparedDataStartIndex;
const tag = dataset.tagValues[groupIndex];
- const displayName =
+ const legacyDisplayName =
widgetConfig.visualizationConfig.displayName[
field.fullDbName + field.sourceIndex
];
+ const displayName = this.resultLabelService.resolveLabel(
+ widgetConfig.dataConfig.sourceConfigs[field.sourceIndex]
+ .queryConfig,
+ field,
+ legacyDisplayName,
+ );
const mappedGroupLabel = this.colorizationService.findLabel(
widgetConfig.visualizationConfig,
field,
diff --git a/ui/src/app/chart-shared/components/charts/value-card/appearance-config/value-card-appearance-config.component.html b/ui/src/app/chart-shared/components/charts/value-card/appearance-config/value-card-appearance-config.component.html
new file mode 100644
index 0000000000..1e2c42bd62
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/value-card/appearance-config/value-card-appearance-config.component.html
@@ -0,0 +1,50 @@
+<!--
+ ~ 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.
+ ~
+ -->
+
+<sp-split-section [level]="3" [title]="'Card settings' | translate">
+ <sp-number-format-config [appearanceConfig]="appearanceConfig">
+ </sp-number-format-config>
+</sp-split-section>
+
+<sp-split-section [level]="3" [title]="'Typography' | translate">
+ <sp-form-field [level]="3" [label]="'Label font size' | translate">
+ <mat-form-field appearance="outline" color="accent" fxFlex="100">
+ <input
+ matInput
+ type="number"
+ min="1"
+ [ngModel]="config.labelFontSize"
+ (ngModelChange)="updateFontSize('labelFontSize', $event)"
+ placeholder="{{ 'Auto' | translate }}"
+ />
+ </mat-form-field>
+ </sp-form-field>
+
+ <sp-form-field [level]="3" [label]="'Value font size' | translate">
+ <mat-form-field appearance="outline" color="accent" fxFlex="100">
+ <input
+ matInput
+ type="number"
+ min="1"
+ [ngModel]="config.valueFontSize"
+ (ngModelChange)="updateFontSize('valueFontSize', $event)"
+ placeholder="{{ 'Auto' | translate }}"
+ />
+ </mat-form-field>
+ </sp-form-field>
+</sp-split-section>
diff --git a/ui/src/app/chart-shared/components/charts/value-card/appearance-config/value-card-appearance-config.component.ts b/ui/src/app/chart-shared/components/charts/value-card/appearance-config/value-card-appearance-config.component.ts
new file mode 100644
index 0000000000..388438ace4
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/value-card/appearance-config/value-card-appearance-config.component.ts
@@ -0,0 +1,84 @@
+/*
+ * 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 { SplitSectionComponent } from '@streampipes/shared-ui';
+import { TranslatePipe } from '@ngx-translate/core';
+import { SpNumberFormatConfigComponent } from '../../../chart-config/number-format-config/number-format-config.component';
+import { ValueCardAppearanceConfig } from '../model/value-card-widget.model';
+import { FormFieldComponent } from '@streampipes/shared-ui';
+import { MatFormField } from '@angular/material/form-field';
+import { MatInput } from '@angular/material/input';
+import { FormsModule } from '@angular/forms';
+import { FlexDirective } from '@ngbracket/ngx-layout/flex';
+import { ChartConfigurationService } from '../../../../services/chart-configuration.service';
+
+@Component({
+ selector: 'sp-value-card-widget-appearance-config',
+ templateUrl: './value-card-appearance-config.component.html',
+ imports: [
+ SplitSectionComponent,
+ TranslatePipe,
+ SpNumberFormatConfigComponent,
+ FormFieldComponent,
+ MatFormField,
+ MatInput,
+ FormsModule,
+ FlexDirective,
+ ],
+})
+export class ValueCardWidgetAppearanceConfigComponent implements OnInit {
+ private widgetConfigurationService = inject(ChartConfigurationService);
+
+ @Input()
+ appearanceConfig: ValueCardAppearanceConfig;
+
+ ngOnInit(): void {
+ this.ensureAppearanceConfig();
+ }
+
+ get config(): ValueCardAppearanceConfig {
+ return this.ensureAppearanceConfig();
+ }
+
+ updateFontSize(
+ key: 'labelFontSize' | 'valueFontSize',
+ value: number | null,
+ ): void {
+ this.config[key] =
+ value === null || value === undefined || value <= 0
+ ? undefined
+ : value;
+
+ this.widgetConfigurationService.notify({
+ refreshView: true,
+ refreshData: false,
+ });
+ }
+
+ private ensureAppearanceConfig(): ValueCardAppearanceConfig {
+ this.appearanceConfig ??= {
+ backgroundColor: 'var(--color-bg-0)',
+ textColor: 'var(--color-default-text)',
+ widgetTitle: '',
+ numberFormat: {},
+ };
+
+ return this.appearanceConfig;
+ }
+}
diff --git a/ui/src/app/chart-shared/components/charts/value-card/config/value-card-widget-config.component.html b/ui/src/app/chart-shared/components/charts/value-card/config/value-card-widget-config.component.html
new file mode 100644
index 0000000000..6f5acf1961
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/value-card/config/value-card-widget-config.component.html
@@ -0,0 +1,86 @@
+<!--
+ ~ 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.
+ ~
+ -->
+
+<sp-visualization-config-outer
+ [configurationValid]="
+ currentlyConfiguredWidget.visualizationConfig.configurationValid
+ "
+>
+ <sp-select-properties-config
+ [title]="'Fields' | translate"
+ [availableProperties]="fieldProvider.allFields"
+ [selectedProperties]="
+ currentlyConfiguredWidget.visualizationConfig.selectedFields
+ "
+ (changeSelectedProperties)="updateSelectedFields($event)"
+ >
+ </sp-select-properties-config>
+
+ <sp-field-order-config
+ [sectionTitle]="'Field Order'"
+ [selectedFields]="
+ currentlyConfiguredWidget.visualizationConfig.selectedFields
+ "
+ (selectedFieldsChange)="setSelectedFieldOrder($event)"
+ >
+ </sp-field-order-config>
+
+ <sp-split-section [level]="3" [title]="'Content' | translate">
+ <sp-form-field [level]="3" [label]="'Timestamp' | translate">
+ <mat-checkbox
+ [checked]="
+ currentlyConfiguredWidget.visualizationConfig.showTimestamp
+ "
+ (change)="
+ currentlyConfiguredWidget.visualizationConfig.showTimestamp =
+ $event.checked;
+ triggerViewRefresh()
+ "
+ >
+ {{ 'Show latest time above card' | translate }}
+ </mat-checkbox>
+ </sp-form-field>
+
+ <sp-form-field [level]="3" [label]="'Title' | translate">
+ <mat-form-field fxFlex="100">
+ <input
+ matInput
+ type="text"
+ [(ngModel)]="
+ currentlyConfiguredWidget.visualizationConfig.title
+ "
+ (ngModelChange)="triggerViewRefresh()"
+ />
+ </mat-form-field>
+ </sp-form-field>
+
+ <sp-form-field [level]="3" [label]="'Description' | translate">
+ <mat-form-field fxFlex="100">
+ <textarea
+ matInput
+ rows="3"
+ [(ngModel)]="
+ currentlyConfiguredWidget.visualizationConfig
+ .description
+ "
+ (ngModelChange)="triggerViewRefresh()"
+ ></textarea>
+ </mat-form-field>
+ </sp-form-field>
+ </sp-split-section>
+</sp-visualization-config-outer>
diff --git a/ui/src/app/chart-shared/components/charts/value-card/config/value-card-widget-config.component.ts b/ui/src/app/chart-shared/components/charts/value-card/config/value-card-widget-config.component.ts
new file mode 100644
index 0000000000..3fec8dee87
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/value-card/config/value-card-widget-config.component.ts
@@ -0,0 +1,133 @@
+/*
+ * 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 } from '@angular/core';
+import { BaseWidgetConfig } from '../../base/base-widget-config';
+import {
+ ValueCardVisConfig,
+ ValueCardWidgetModel,
+} from '../model/value-card-widget.model';
+import { DataExplorerField } from '@streampipes/platform-services';
+import { SpVisualizationConfigOuterComponent } from '../../../chart-config/visualization-config-outer/visualization-config-outer.component';
+import { SelectMultiplePropertiesConfigComponent } from '../../../chart-config/select-multiple-properties-config/select-multiple-properties-config.component';
+import {
+ FormFieldComponent,
+ SplitSectionComponent,
+} from '@streampipes/shared-ui';
+import { MatFormField } from '@angular/material/form-field';
+import { MatInput } from '@angular/material/input';
+import { FormsModule } from '@angular/forms';
+import { FlexDirective } from '@ngbracket/ngx-layout/flex';
+import { TranslatePipe } from '@ngx-translate/core';
+import { FieldOrderConfigComponent } from '../../../chart-config/field-order-config/field-order-config.component';
+import { MatCheckbox } from '@angular/material/checkbox';
+
+@Component({
+ selector: 'sp-data-explorer-value-card-widget-config',
+ templateUrl: './value-card-widget-config.component.html',
+ imports: [
+ SpVisualizationConfigOuterComponent,
+ SplitSectionComponent,
+ SelectMultiplePropertiesConfigComponent,
+ FormFieldComponent,
+ FieldOrderConfigComponent,
+ MatFormField,
+ MatInput,
+ MatCheckbox,
+ FlexDirective,
+ FormsModule,
+ TranslatePipe,
+ ],
+})
+export class ValueCardWidgetConfigComponent extends BaseWidgetConfig<
+ ValueCardWidgetModel,
+ ValueCardVisConfig
+> {
+ updateSelectedFields(fields: DataExplorerField[]): void {
+ this.currentlyConfiguredWidget.visualizationConfig.selectedFields =
+ this.mergeSelectedFieldOrder(fields);
+ this.triggerViewRefresh();
+ }
+
+ setSelectedFieldOrder(fields: DataExplorerField[]): void {
+ this.currentlyConfiguredWidget.visualizationConfig.selectedFields =
+ fields;
+ this.triggerViewRefresh();
+ }
+
+ protected applyWidgetConfig(config: ValueCardVisConfig): void {
+ config.selectedFields = this.getCompatibleSelectedFields(
+ config.selectedFields,
+ );
+ config.title ??= '';
+ config.description ??= '';
+ config.showTimestamp ??= false;
+ }
+
+ protected requiredFieldsForChartPresent(): boolean {
+ return this.fieldProvider.allFields.length > 0;
+ }
+
+ private getCompatibleSelectedFields(
+ selectedFields?: DataExplorerField[],
+ ): DataExplorerField[] {
+ const availableFields = this.fieldProvider.allFields;
+
+ if (!selectedFields?.length) {
+ return availableFields.slice(
+ 0,
+ Math.min(4, availableFields.length),
+ );
+ }
+
+ return selectedFields.filter(selectedField =>
+ availableFields.find(
+ availableField =>
+ availableField.fullDbName === selectedField.fullDbName &&
+ availableField.sourceIndex === selectedField.sourceIndex,
+ ),
+ );
+ }
+
+ private mergeSelectedFieldOrder(
+ nextSelectedFields: DataExplorerField[],
+ ): DataExplorerField[] {
+ const currentSelectedFields =
+ this.currentlyConfiguredWidget.visualizationConfig.selectedFields ??
+ [];
+
+ const retainedFields = currentSelectedFields.filter(currentField =>
+ nextSelectedFields.some(nextField =>
+ this.isSameField(currentField, nextField),
+ ),
+ );
+
+ const newlyAddedFields = nextSelectedFields.filter(
+ nextField =>
+ !currentSelectedFields.some(currentField =>
+ this.isSameField(currentField, nextField),
+ ),
+ );
+
+ return [...retainedFields, ...newlyAddedFields];
+ }
+
+ private isSameField(a: DataExplorerField, b: DataExplorerField): boolean {
+ return a.fullDbName === b.fullDbName && a.sourceIndex === b.sourceIndex;
+ }
+}
diff --git a/ui/src/app/chart-shared/components/charts/value-card/model/value-card-widget.model.ts b/ui/src/app/chart-shared/components/charts/value-card/model/value-card-widget.model.ts
new file mode 100644
index 0000000000..d75be279f3
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/value-card/model/value-card-widget.model.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 {
+ DataExplorerDataConfig,
+ DataExplorerField,
+ DataExplorerWidgetModel,
+} from '@streampipes/platform-services';
+import {
+ DataExplorerVisConfig,
+ WidgetNumberAppearanceConfig,
+} from '../../../../models/dataview-dashboard.model';
+
+export interface ValueCardVisConfig extends DataExplorerVisConfig {
+ selectedFields?: DataExplorerField[];
+ title?: string;
+ description?: string;
+ showTimestamp?: boolean;
+}
+
+export interface ValueCardAppearanceConfig extends WidgetNumberAppearanceConfig {
+ labelFontSize?: number;
+ valueFontSize?: number;
+}
+
+export interface ValueCardWidgetModel extends DataExplorerWidgetModel {
+ dataConfig: DataExplorerDataConfig;
+ visualizationConfig: ValueCardVisConfig;
+ baseAppearanceConfig: ValueCardAppearanceConfig;
+}
diff --git a/ui/src/app/chart-shared/components/charts/value-card/value-card-widget.component.html b/ui/src/app/chart-shared/components/charts/value-card/value-card-widget.component.html
new file mode 100644
index 0000000000..943033f6e4
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/value-card/value-card-widget.component.html
@@ -0,0 +1,91 @@
+<!--
+ ~ 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="value-card-widget"
+ fxLayout="column"
+ fxFlex="100"
+ [ngStyle]="widgetStyles"
+ data-cy="value-card-widget"
+>
+ @if (showNoDataInDateRange) {
+ <sp-no-data-in-date-range [viewDateRange]="timeSettings" class="h-100">
+ </sp-no-data-in-date-range>
+ }
+
+ @if (showTooMuchData) {
+ <sp-too-much-data
+ [amountOfEvents]="amountOfTooMuchEvents"
+ (loadDataWithTooManyEventsEmitter)="loadDataWithTooManyEvents()"
+ class="h-100"
+ >
+ </sp-too-much-data>
+ }
+
+ @if (showInvalidConfiguration) {
+ <sp-invalid-configuration
+ [widgetTypeLabel]="widgetTypeLabel"
+ class="h-100"
+ >
+ </sp-invalid-configuration>
+ }
+
+ @if (showData) {
+ <div class="value-card-shell" fxLayout="column" fxFlex="100">
+ @if (titleText || descriptionText) {
+ <div class="value-card-copy" fxLayout="column">
+ @if (titleText) {
+ <div class="value-card-title">
+ {{ titleText }}
+ </div>
+ }
+
+ @if (descriptionText) {
+ <div class="value-card-description">
+ {{ descriptionText }}
+ </div>
+ }
+ </div>
+ }
+
+ @if (showTimestamp) {
+ <div class="value-card-timestamp">
+ {{ latestTimestampLabel }}
+ </div>
+ }
+
+ <article class="value-card-panel" fxFlex>
+ <div
+ class="value-card-list"
+ [style.--value-card-list-columns]="valueListColumns"
+ >
+ @for (card of valueCards; track trackCard($index, card)) {
+ <section class="value-card-item">
+ <div class="value-card-item-label">
+ {{ card.label }}
+ </div>
+ <div class="value-card-item-value">
+ {{ card.value }}
+ </div>
+ </section>
+ }
+ </div>
+ </article>
+ </div>
+ }
+</div>
diff --git a/ui/src/app/chart-shared/components/charts/value-card/value-card-widget.component.scss b/ui/src/app/chart-shared/components/charts/value-card/value-card-widget.component.scss
new file mode 100644
index 0000000000..703a33ca15
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/value-card/value-card-widget.component.scss
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ *
+ */
+
+:host {
+ display: block;
+ height: 100%;
+}
+
+.h-100 {
+ height: 100%;
+}
+
+.value-card-widget {
+ box-sizing: border-box;
+ height: 100%;
+ min-height: 0;
+ overflow: hidden;
+ padding: var(--value-card-padding);
+}
+
+.value-card-shell {
+ gap: var(--value-card-gap);
+ min-height: 0;
+}
+
+.value-card-copy {
+ gap: calc(var(--value-card-gap) * 0.45);
+}
+
+.value-card-title {
+ font-size: var(--value-card-title-size);
+ font-weight: 700;
+ letter-spacing: -0.03em;
+ line-height: 1.08;
+}
+
+.value-card-description {
+ color: var(--value-card-muted-color);
+ font-size: var(--value-card-description-size);
+ line-height: 1.35;
+ max-width: 48ch;
+}
+
+.value-card-timestamp {
+ color: var(--value-card-muted-color);
+ font-size: 0.82rem;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+ line-height: 1.2;
+}
+
+.value-card-panel {
+ background: var(--value-card-panel-background);
+ border: 1px solid var(--value-card-border-color);
+ border-radius: 5px;
+ min-height: 0;
+ overflow: hidden;
+ padding: 0 1.2rem;
+}
+
+.value-card-list {
+ display: grid;
+ align-content: start;
+ gap: 0;
+ grid-template-columns: repeat(
+ var(--value-card-list-columns),
+ minmax(0, 1fr)
+ );
+ min-height: 0;
+}
+
+.value-card-item {
+ align-items: start;
+ border-bottom: 1px solid var(--value-card-divider-color);
+ display: flex;
+ flex-direction: column;
+ gap: var(--value-card-row-gap);
+ min-height: 0;
+ padding: var(--value-card-row-padding) 0;
+}
+
+.value-card-item-label {
+ color: var(--value-card-muted-color);
+ font-size: var(--value-card-label-size);
+ font-weight: 600;
+ line-height: 1.3;
+ min-width: 0;
+ overflow-wrap: anywhere;
+}
+
+.value-card-item-value {
+ font-size: var(--value-card-value-size);
+ font-weight: 700;
+ letter-spacing: -0.04em;
+ line-height: 0.98;
+ min-width: 0;
+ overflow-wrap: anywhere;
+ text-align: left;
+}
+
+.value-card-item:last-child {
+ border-bottom: 0;
+}
diff --git a/ui/src/app/chart-shared/components/charts/value-card/value-card-widget.component.ts b/ui/src/app/chart-shared/components/charts/value-card/value-card-widget.component.ts
new file mode 100644
index 0000000000..380ce0dad6
--- /dev/null
+++ b/ui/src/app/chart-shared/components/charts/value-card/value-card-widget.component.ts
@@ -0,0 +1,371 @@
+/*
+ * 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 { NgStyle } from '@angular/common';
+import { Component, LOCALE_ID, OnInit, inject } from '@angular/core';
+import {
+ DataExplorerField,
+ SpQueryResult,
+} from '@streampipes/platform-services';
+import { FlexDirective, LayoutDirective } from '@ngbracket/ngx-layout/flex';
+import { BaseDataExplorerWidgetDirective } from '../base/base-data-explorer-widget.directive';
+import { NoDataInDateRangeComponent } from '../base/no-data/no-data-in-date-range.component';
+import { TooMuchDataComponent } from '../base/too-much-data/too-much-data.component';
+import { SpInvalidConfigurationComponent } from '../base/invalid-configuration/invalid-configuration.component';
+import { ChartRegistry } from '../../../registry/chart-registry.service';
+import {
+ ValueCardAppearanceConfig,
+ ValueCardVisConfig,
+ ValueCardWidgetModel,
+} from './model/value-card-widget.model';
+import { ResultLabelService } from '../../../services/result-label.service';
+import {
+ clampValue,
+ formatWidgetNumber,
+ resolveResponsiveFontSize,
+ scaleResponsiveValue,
+} from '../../../services/widget-render.utils';
+
+interface ValueCardView {
+ id: string;
+ label: string;
+ value: string;
+}
+
+@Component({
+ selector: 'sp-data-explorer-value-card-widget',
+ templateUrl: './value-card-widget.component.html',
+ styleUrls: ['./value-card-widget.component.scss'],
+ imports: [
+ LayoutDirective,
+ FlexDirective,
+ NgStyle,
+ NoDataInDateRangeComponent,
+ TooMuchDataComponent,
+ SpInvalidConfigurationComponent,
+ ],
+})
+export class ValueCardWidgetComponent
+ extends BaseDataExplorerWidgetDirective<ValueCardWidgetModel>
+ implements OnInit
+{
+ valueCards: ValueCardView[] = [];
+ widgetTypeLabel: string;
+ latestTimestampLabel?: string;
+
+ private hasReceivedData = false;
+ private latestData: SpQueryResult[] = [];
+ private readonly locale = inject(LOCALE_ID);
+ private readonly chartRegistry = inject(ChartRegistry);
+ private readonly resultLabelService = inject(ResultLabelService);
+
+ ngOnInit(): void {
+ super.ngOnInit();
+ this.widgetTypeLabel = this.chartRegistry.getChartTemplate(
+ this.dataExplorerWidget.widgetType,
+ ).label;
+ }
+
+ get appearanceConfig(): ValueCardAppearanceConfig {
+ this.dataExplorerWidget.baseAppearanceConfig ??= {
+ backgroundColor: 'var(--color-bg-0)',
+ textColor: 'var(--color-default-text)',
+ widgetTitle: '',
+ numberFormat: {},
+ };
+
+ return this.dataExplorerWidget
+ .baseAppearanceConfig as ValueCardAppearanceConfig;
+ }
+
+ get widgetStyles(): Record<string, string> {
+ const compactMode =
+ (this.currentWidth ?? 0) < 640 || this.hasManyValues;
+ const fontScale = this.getFontScale();
+
+ return {
+ 'background': this.appearanceConfig.backgroundColor,
+ 'color': this.appearanceConfig.textColor,
+ '--value-card-padding': compactMode ? '14px' : '20px',
+ '--value-card-gap': compactMode ? '12px' : '18px',
+ '--value-card-title-size': compactMode ? '18px' : '24px',
+ '--value-card-description-size': compactMode ? '12px' : '14px',
+ '--value-card-row-padding': `${scaleResponsiveValue(
+ 12,
+ 22,
+ fontScale,
+ 0.7,
+ 1.45,
+ )}px`,
+ '--value-card-label-size': `${resolveResponsiveFontSize(
+ this.appearanceConfig.labelFontSize,
+ 11,
+ 16,
+ fontScale,
+ 0.7,
+ 1.45,
+ )}px`,
+ '--value-card-value-size': `${resolveResponsiveFontSize(
+ this.appearanceConfig.valueFontSize,
+ 20,
+ 42,
+ fontScale,
+ 0.7,
+ 1.45,
+ )}px`,
+ '--value-card-row-gap': `${scaleResponsiveValue(
+ 4,
+ 10,
+ fontScale,
+ 0.7,
+ 1.45,
+ )}px`,
+ '--value-card-border-color':
+ 'color-mix(in srgb, currentColor 10%, transparent)',
+ '--value-card-divider-color':
+ 'color-mix(in srgb, currentColor 8%, transparent)',
+ '--value-card-muted-color':
+ 'color-mix(in srgb, currentColor 68%, transparent)',
+ '--value-card-panel-background':
+ 'color-mix(in srgb, currentColor 6%, transparent)',
+ };
+ }
+
+ get content(): ValueCardVisConfig {
+ return this.dataExplorerWidget.visualizationConfig;
+ }
+
+ get titleText(): string {
+ return this.content.title?.trim() ?? '';
+ }
+
+ get descriptionText(): string {
+ return this.content.description?.trim() ?? '';
+ }
+
+ get showTimestamp(): boolean {
+ return !!this.content.showTimestamp;
+ }
+
+ get hasManyValues(): boolean {
+ return this.valueCards.length > 3;
+ }
+
+ beforeDataFetched(): void {
+ if (this.valueCards.length > 0) {
+ this.setShownComponents(false, true, false, false);
+ return;
+ }
+
+ this.setShownComponents(false, false, true, false);
+ }
+
+ onDataReceived(spQueryResults: SpQueryResult[]): void {
+ this.hasReceivedData = true;
+ this.latestData = spQueryResults;
+ this.refreshView();
+ }
+
+ onResize(_width: number, _height: number): void {
+ this.refreshView();
+ }
+
+ refreshView(): void {
+ this.updateCards();
+ }
+
+ handleUpdatedFields(
+ addedFields: DataExplorerField[],
+ removedFields: DataExplorerField[],
+ ): void {
+ const currentSelection = this.content.selectedFields ?? [];
+ this.content.selectedFields = currentSelection
+ .filter(
+ field =>
+ !removedFields.find(
+ removedField =>
+ removedField.fullDbName === field.fullDbName &&
+ removedField.sourceIndex === field.sourceIndex,
+ ),
+ )
+ .concat(
+ addedFields.filter(
+ field =>
+ !currentSelection.find(
+ currentField =>
+ currentField.fullDbName === field.fullDbName &&
+ currentField.sourceIndex === field.sourceIndex,
+ ),
+ ),
+ );
+ this.refreshView();
+ }
+
+ trackCard(_index: number, card: ValueCardView): string {
+ return card.id;
+ }
+
+ private updateCards(): void {
+ if (this.content.configurationValid === false) {
+ this.showInvalidConfiguration = true;
+ this.valueCards = [];
+ this.setShownComponents(false, false, false, false);
+ return;
+ }
+
+ this.showInvalidConfiguration = false;
+ if (!this.hasReceivedData && this.latestData.length === 0) {
+ return;
+ }
+
+ this.latestTimestampLabel = this.getLatestTimestampLabel();
+ const selectedFields = this.content.selectedFields ?? [];
+ this.valueCards = selectedFields
+ .map(field => this.makeCard(field))
+ .filter((card): card is ValueCardView => !!card);
+
+ if (this.valueCards.length === 0) {
+ this.setShownComponents(true, false, false, false);
+ return;
+ }
+
+ this.setShownComponents(false, true, false, false);
+ }
+
+ private makeCard(field: DataExplorerField): ValueCardView | undefined {
+ const sourceResult = this.latestData[field.sourceIndex];
+
+ if (!sourceResult?.headers?.length) {
+ return undefined;
+ }
+
+ const fieldIndex = sourceResult.headers.findIndex(
+ header => header === field.fullDbName,
+ );
+
+ if (fieldIndex < 0) {
+ return undefined;
+ }
+
+ const firstRow = this.getFirstRow(sourceResult);
+ if (!firstRow) {
+ return undefined;
+ }
+
+ const queryConfig =
+ this.dataExplorerWidget.dataConfig.sourceConfigs[field.sourceIndex]
+ ?.queryConfig;
+
+ return {
+ id: `${field.sourceIndex}-${field.fullDbName}`,
+ label: queryConfig
+ ? this.resultLabelService.resolveLabel(queryConfig, field)
+ : (field.runtimeName ?? field.fullDbName),
+ value: this.formatValue(firstRow[fieldIndex]),
+ };
+ }
+
+ private getFirstRow(queryResult: SpQueryResult): unknown[] | undefined {
+ return queryResult.allDataSeries.find(series => series.rows.length > 0)
+ ?.rows[0];
+ }
+
+ private formatValue(value: unknown): string {
+ if (value === null || value === undefined || value === '') {
+ return '-';
+ }
+
+ if (typeof value === 'number' && Number.isFinite(value)) {
+ return formatWidgetNumber(
+ value,
+ this.locale,
+ this.appearanceConfig.numberFormat?.decimals,
+ );
+ }
+
+ if (typeof value === 'boolean') {
+ return value ? 'True' : 'False';
+ }
+
+ return String(value);
+ }
+
+ private getLatestTimestampLabel(): string | undefined {
+ const firstQueryResult = this.latestData.find(
+ queryResult => !!this.getFirstRow(queryResult),
+ );
+
+ if (!firstQueryResult) {
+ return undefined;
+ }
+
+ const firstTimestamp = this.getFirstTimestamp(firstQueryResult);
+ if (firstTimestamp === undefined) {
+ return undefined;
+ }
+
+ return new Intl.DateTimeFormat(this.locale, {
+ dateStyle: 'medium',
+ timeStyle: 'medium',
+ }).format(new Date(firstTimestamp));
+ }
+
+ private getFirstTimestamp(queryResult: SpQueryResult): number | undefined {
+ const timeIndex = queryResult.headers.findIndex(
+ header => header === 'time',
+ );
+
+ if (timeIndex < 0) {
+ return undefined;
+ }
+
+ const firstRow = this.getFirstRow(queryResult);
+ if (!firstRow) {
+ return undefined;
+ }
+
+ const firstTimestamp = new Date(
+ firstRow[timeIndex] as string,
+ ).getTime();
+ return Number.isFinite(firstTimestamp) ? firstTimestamp : undefined;
+ }
+
+ private getFontScale(): number {
+ const width = this.currentWidth ?? 0;
+ const height = this.currentHeight ?? 0;
+ const rowCount = Math.max(this.valueCards.length, 1);
+
+ if (width <= 0 || height <= 0) {
+ return 0.8;
+ }
+
+ const widthScale = clampValue(width / 640, 0.7, 1.45);
+ const availableHeight = Math.max(height - 110, 120);
+ const heightPerRowScale = clampValue(
+ availableHeight / (rowCount * 84),
+ 0.7,
+ 1.45,
+ );
+
+ return clampValue(Math.min(widthScale, heightPerRowScale), 0.7, 1.45);
+ }
+
+ get valueListColumns(): number {
+ return 1;
+ }
+}
diff --git a/ui/src/app/chart-shared/registry/chart-registry.service.ts b/ui/src/app/chart-shared/registry/chart-registry.service.ts
index 87165d5c2f..d1d32dd65d 100644
--- a/ui/src/app/chart-shared/registry/chart-registry.service.ts
+++ b/ui/src/app/chart-shared/registry/chart-registry.service.ts
@@ -61,6 +61,12 @@ import { StatusWidgetConfigComponent } from '../components/charts/status/config/
import { StatusWidgetComponent } from '../components/charts/status/status-widget.component';
import { IndicatorWidgetComponent } from '../components/charts/indicator/indicator-widget.component';
import { TranslateService } from '@ngx-translate/core';
+import { ValueCardWidgetComponent } from '../components/charts/value-card/value-card-widget.component';
+import { ValueCardWidgetConfigComponent } from '../components/charts/value-card/config/value-card-widget-config.component';
+import { ValueCardWidgetAppearanceConfigComponent } from '../components/charts/value-card/appearance-config/value-card-appearance-config.component';
+import { ProgressBarWidgetComponent } from '../components/charts/progress-bar/progress-bar-widget.component';
+import { ProgressBarWidgetConfigComponent } from '../components/charts/progress-bar/config/progress-bar-widget-config.component';
+import { ProgressBarWidgetAppearanceConfigComponent } from '../components/charts/progress-bar/appearance-config/progress-bar-appearance-config.component';
@Injectable({ providedIn: 'root' })
export class ChartRegistry {
@@ -185,6 +191,30 @@ export class ChartRegistry {
icon: 'image',
description: this.translateService.instant('Display an image'),
},
+ {
+ id: 'value-card',
+ label: this.translateService.instant('Value Card'),
+ widgetAppearanceConfigurationComponent:
+ ValueCardWidgetAppearanceConfigComponent,
+ widgetConfigurationComponent: ValueCardWidgetConfigComponent,
+ widgetComponent: ValueCardWidgetComponent,
+ icon: 'view_agenda',
+ description: this.translateService.instant(
+ 'Display the latest values of multiple selected fields in a single card',
+ ),
+ },
+ {
+ id: 'progress-bar',
+ label: this.translateService.instant('Progress Bar'),
+ widgetAppearanceConfigurationComponent:
+ ProgressBarWidgetAppearanceConfigComponent,
+ widgetConfigurationComponent: ProgressBarWidgetConfigComponent,
+ widgetComponent: ProgressBarWidgetComponent,
+ icon: 'linear_scale',
+ description: this.translateService.instant(
+ 'Display the progress of a numeric value against a target',
+ ),
+ },
{
id: 'indicator-chart',
label: this.translateService.instant('Indicator'),
diff --git a/ui/src/app/chart-shared/services/result-label.service.ts b/ui/src/app/chart-shared/services/result-label.service.ts
new file mode 100644
index 0000000000..875ebe2b94
--- /dev/null
+++ b/ui/src/app/chart-shared/services/result-label.service.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 { DataExplorerField, QueryConfig } from '@streampipes/platform-services';
+
+@Injectable({ providedIn: 'root' })
+export class ResultLabelService {
+ makeFieldKey(field: Pick<DataExplorerField, 'fullDbName' | 'sourceIndex'>) {
+ return `${field.fullDbName}:${field.sourceIndex}`;
+ }
+
+ getOverrides(queryConfig: QueryConfig): Record<string, string> {
+ queryConfig.resultLabelOverrides ??= {};
+ return queryConfig.resultLabelOverrides;
+ }
+
+ getOverride(
+ queryConfig: QueryConfig,
+ field: Pick<DataExplorerField, 'fullDbName' | 'sourceIndex'>,
+ ): string | undefined {
+ return this.getOverrides(queryConfig)[this.makeFieldKey(field)];
+ }
+
+ resolveLabel(
+ queryConfig: QueryConfig,
+ field: Pick<DataExplorerField, 'fullDbName' | 'sourceIndex'>,
+ legacyLabel?: string,
+ ): string {
+ return (
+ this.getOverride(queryConfig, field) ??
+ legacyLabel ??
+ field.fullDbName
+ );
+ }
+
+ setOverride(
+ queryConfig: QueryConfig,
+ field: Pick<DataExplorerField, 'fullDbName' | 'sourceIndex'>,
+ label: string,
+ fallbackLabel?: string,
+ ): void {
+ const normalizedLabel = label.trim();
+ const fallback = fallbackLabel ?? field.fullDbName;
+ const overrides = this.getOverrides(queryConfig);
+ const key = this.makeFieldKey(field);
+
+ if (!normalizedLabel || normalizedLabel === fallback) {
+ delete overrides[key];
+ return;
+ }
+
+ overrides[key] = normalizedLabel;
+ }
+}
diff --git a/ui/src/app/chart-shared/services/widget-render.utils.ts b/ui/src/app/chart-shared/services/widget-render.utils.ts
new file mode 100644
index 0000000000..02d8acb9dc
--- /dev/null
+++ b/ui/src/app/chart-shared/services/widget-render.utils.ts
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ *
+ */
+
+export function clampValue(value: number, min: number, max: number): number {
+ return Math.min(Math.max(value, min), max);
+}
+
+export function normalizeWidgetDecimals(decimals: unknown): number | undefined {
+ if (decimals === null || decimals === undefined || decimals === '') {
+ return undefined;
+ }
+
+ const parsedValue = Number(decimals);
+ if (!Number.isFinite(parsedValue)) {
+ return undefined;
+ }
+
+ return Math.min(10, Math.max(0, Math.round(parsedValue)));
+}
+
+export function formatWidgetNumber(
+ value: number,
+ locale: string,
+ decimals: unknown,
+): string {
+ const normalizedDecimals = normalizeWidgetDecimals(decimals);
+
+ return new Intl.NumberFormat(locale, {
+ maximumFractionDigits: normalizedDecimals ?? 20,
+ minimumFractionDigits: normalizedDecimals ?? 0,
+ }).format(value);
+}
+
+export function scaleResponsiveValue(
+ min: number,
+ max: number,
+ scale: number,
+ scaleMin: number,
+ scaleMax: number,
+): number {
+ const normalizedScale = (scale - scaleMin) / (scaleMax - scaleMin);
+ const scaledValue = min + (max - min) * normalizedScale;
+ return Math.round(clampValue(scaledValue, min, max));
+}
+
+export function resolveResponsiveFontSize(
+ manualSize: number | undefined,
+ min: number,
+ max: number,
+ scale: number,
+ scaleMin: number,
+ scaleMax: number,
+): number {
+ if (
+ manualSize === undefined ||
+ manualSize === null ||
+ Number.isNaN(Number(manualSize)) ||
+ manualSize <= 0
+ ) {
+ return scaleResponsiveValue(min, max, scale, scaleMin, scaleMax);
+ }
+
+ return Math.round(Math.max(1, manualSize) * scale * 10) / 10;
+}
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 af89a9f414..bb64ea35d8 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
@@ -410,6 +410,11 @@
#groupSelectionPanel
[sourceConfig]="sourceConfig"
></sp-group-selection-panel>
+ <sp-result-label-configuration
+ [sourceConfig]="sourceConfig"
+ [sourceIndex]="0"
+ >
+ </sp-result-label-configuration>
<sp-order-selection-panel
[sourceConfig]="sourceConfig"
>
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 5502cff7be..2875af0354 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
@@ -77,6 +77,7 @@ import { AggregateConfigurationComponent } from './aggregate-configuration/aggre
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 { ResultLabelConfigurationComponent } from './result-label-configuration/result-label-configuration.component';
import { TranslatePipe } from '@ngx-translate/core';
import {
MatAutocomplete,
@@ -122,6 +123,7 @@ import {
FilterSelectionPanelComponent,
GroupSelectionPanelComponent,
OrderSelectionPanelComponent,
+ ResultLabelConfigurationComponent,
TranslatePipe,
],
})
@@ -330,6 +332,7 @@ export class ChartDataSettingsComponent implements OnInit {
measureName,
queryConfig: {
selectedFilters: [],
+ resultLabelOverrides: {},
limit: 100,
page: 1,
aggregationTimeUnit: 'd',
diff --git a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/result-label-configuration/result-label-configuration.component.html b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/result-label-configuration/result-label-configuration.component.html
new file mode 100644
index 0000000000..f4dbb538c0
--- /dev/null
+++ b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/result-label-configuration/result-label-configuration.component.html
@@ -0,0 +1,49 @@
+<!--
+~ 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.
+~
+-->
+
+@if (selectedResultFields.length > 0) {
+ <sp-split-section [level]="3" [title]="'Result Labels' | translate">
+ <div fxLayout="column" fxLayoutGap="10px">
+ @for (field of selectedResultFields; track field.fullDbName) {
+ <div
+ fxLayout="row"
+ fxLayoutAlign="start center"
+ fxLayoutGap="10px"
+ >
+ <div fxFlex="35" class="text-xs">
+ {{ field.fullDbName }}
+ </div>
+ <div fxFlex="65">
+ <mat-form-field
+ appearance="outline"
+ color="accent"
+ class="w-100 form-field-small"
+ >
+ <input
+ matInput
+ [ngModel]="getLabel(field)"
+ (ngModelChange)="updateLabel(field, $event)"
+ [placeholder]="field.fullDbName"
+ />
+ </mat-form-field>
+ </div>
+ </div>
+ }
+ </div>
+ </sp-split-section>
+}
diff --git a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/result-label-configuration/result-label-configuration.component.scss b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/result-label-configuration/result-label-configuration.component.scss
new file mode 100644
index 0000000000..13cbc4aacb
--- /dev/null
+++ b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/result-label-configuration/result-label-configuration.component.scss
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ *
+ */
diff --git a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/result-label-configuration/result-label-configuration.component.ts b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/result-label-configuration/result-label-configuration.component.ts
new file mode 100644
index 0000000000..9bb57c0ce2
--- /dev/null
+++ b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/result-label-configuration/result-label-configuration.component.ts
@@ -0,0 +1,93 @@
+/*
+ * 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, inject } from '@angular/core';
+import {
+ DataExplorerField,
+ SourceConfig,
+} from '@streampipes/platform-services';
+import { ChartFieldProviderService } from '../../../../../../chart-shared/services/chart-field-provider.service';
+import { ResultLabelService } from '../../../../../../chart-shared/services/result-label.service';
+import { ChartConfigurationService } from '../../../../../../chart-shared/services/chart-configuration.service';
+import { SplitSectionComponent } from '@streampipes/shared-ui';
+import {
+ FlexDirective,
+ LayoutAlignDirective,
+ LayoutDirective,
+ LayoutGapDirective,
+} from '@ngbracket/ngx-layout/flex';
+import { MatFormField } from '@angular/material/form-field';
+import { MatInput } from '@angular/material/input';
+import { FormsModule } from '@angular/forms';
+import { TranslatePipe } from '@ngx-translate/core';
+
+@Component({
+ selector: 'sp-result-label-configuration',
+ templateUrl: './result-label-configuration.component.html',
+ styleUrls: ['./result-label-configuration.component.scss'],
+ imports: [
+ SplitSectionComponent,
+ LayoutDirective,
+ LayoutAlignDirective,
+ LayoutGapDirective,
+ FlexDirective,
+ MatFormField,
+ MatInput,
+ FormsModule,
+ TranslatePipe,
+ ],
+})
+export class ResultLabelConfigurationComponent {
+ private fieldProvider = inject(ChartFieldProviderService);
+ private resultLabelService = inject(ResultLabelService);
+ private widgetConfigService = inject(ChartConfigurationService);
+
+ @Input() sourceConfig: SourceConfig;
+ @Input() sourceIndex = 0;
+
+ get selectedResultFields(): DataExplorerField[] {
+ if (!this.sourceConfig?.measure) {
+ return [];
+ }
+
+ return this.fieldProvider
+ .generateFieldLists([this.sourceConfig])
+ .allFields.sort((a, b) => a.fullDbName.localeCompare(b.fullDbName));
+ }
+
+ getLabel(field: DataExplorerField): string {
+ return this.resultLabelService.resolveLabel(
+ this.sourceConfig.queryConfig,
+ { ...field, sourceIndex: this.sourceIndex },
+ );
+ }
+
+ updateLabel(field: DataExplorerField, label: string): void {
+ this.resultLabelService.setOverride(
+ this.sourceConfig.queryConfig,
+ { ...field, sourceIndex: this.sourceIndex },
+ label,
+ field.fullDbName,
+ );
+
+ this.widgetConfigService.notify({
+ refreshData: false,
+ refreshView: true,
+ });
+ }
+}