mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 18:56:02 +08:00
commit
558352e43b
148
README.de-de.md
148
README.de-de.md
@ -5,7 +5,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/frontend?label=Downloads"> </a>
|
||||
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/query-service?label=Downloads"> </a>
|
||||
<img alt="GitHub issues" src="https://img.shields.io/github/issues/signoz/signoz"> </a>
|
||||
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability">
|
||||
<img alt="tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"> </a>
|
||||
@ -14,27 +14,62 @@
|
||||
|
||||
<h3 align="center">
|
||||
<a href="https://signoz.io/docs"><b>Dokumentation</b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/develop/README.md"><b>Readme auf Englisch </b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/develop/README.zh-cn.md"><b>ReadMe auf Chinesisch</b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/develop/README.pt-br.md"><b>ReadMe auf Portugiesisch</b></a> •
|
||||
<a href="https://signoz.io/slack"><b>Slack Community</b></a> •
|
||||
<a href="https://twitter.com/SigNozHQ"><b>Twitter</b></a>
|
||||
<a href="https://twitter.com/SigNozHq"><b>Twitter</b></a>
|
||||
</h3>
|
||||
|
||||
##
|
||||
|
||||
SigNoz hilft Entwicklern, Anwendungen zu überwachen und Probleme in ihren bereitgestellten Anwendungen zu beheben. SigNoz benutzt verteilte Einzelschritt-Fehlersuchen, um Einblick in deinen Software-Stack zu bekommen.
|
||||
SigNoz hilft Entwicklern, Anwendungen zu überwachen und Probleme in ihren bereitgestellten Anwendungen zu beheben. Mit SigNoz können Sie Folgendes tun:
|
||||
|
||||
👉 Du kannst Werte wie die P99-Latenz und die Fehler Häufigkeit von deinen Services, externen API Aufrufen und einzelnen Endpunkten sehen.
|
||||
👉 Visualisieren Sie Metriken, Traces und Logs in einer einzigen Oberfläche.
|
||||
|
||||
👉 Du kannst die Ursache des Problems finden, indem du zu dem Einzelschritt gehst, der das Problem verursacht und dir detaillierte Flamegraphs von einzelnen Abfragefehlersuchen anzeigen lassen.
|
||||
👉 Sie können Metriken wie die p99-Latenz, Fehlerquoten für Ihre Dienste, externe API-Aufrufe und individuelle Endpunkte anzeigen.
|
||||
|
||||
👉 Erstelle Aggregate auf Basis von Fehlersuche Daten, um geschäftsrelevante Metriken zu erhalten.
|
||||
👉 Sie können die Ursache des Problems ermitteln, indem Sie zu den genauen Traces gehen, die das Problem verursachen, und detaillierte Flammenbilder einzelner Anfragetraces anzeigen.
|
||||
|
||||
👉 Führen Sie Aggregationen auf Trace-Daten durch, um geschäftsrelevante Metriken zu erhalten.
|
||||
|
||||
👉 Filtern und Abfragen von Logs, Erstellen von Dashboards und Benachrichtigungen basierend auf Attributen in den Logs.
|
||||
|
||||
👉 Automatische Aufzeichnung von Ausnahmen in Python, Java, Ruby und Javascript.
|
||||
|
||||
👉 Einfache Einrichtung von Benachrichtigungen mit dem selbst erstellbaren Abfrage-Builder.
|
||||
|
||||
##
|
||||
### Anwendung Metriken
|
||||
|
||||

|
||||
|
||||
|
||||
### Verteiltes Tracing
|
||||
<img width="2068" alt="distributed_tracing_2 2" src="https://user-images.githubusercontent.com/83692067/226536447-bae58321-6a22-4ed3-af80-e3e964cb3489.png">
|
||||
|
||||
<img width="2068" alt="distributed_tracing_1" src="https://user-images.githubusercontent.com/83692067/226536462-939745b6-4f9d-45a6-8016-814837e7f7b4.png">
|
||||
|
||||
### Log Verwaltung
|
||||
|
||||
<img width="2068" alt="logs_management" src="https://user-images.githubusercontent.com/83692067/226536482-b8a5c4af-b69c-43d5-969c-338bd5eaf1a5.png">
|
||||
|
||||
### Infrastruktur Überwachung
|
||||
|
||||
<img width="2068" alt="infrastructure_monitoring" src="https://user-images.githubusercontent.com/83692067/226536496-f38c4dbf-e03c-4158-8be0-32d4a61158c7.png">
|
||||
|
||||
### Exceptions Monitoring
|
||||
|
||||

|
||||
|
||||
|
||||
### Alarme
|
||||
|
||||
<img width="2068" alt="alerts_management" src="https://user-images.githubusercontent.com/83692067/226536548-2c81e2e8-c12d-47e8-bad7-c6be79055def.png">
|
||||
|
||||

|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributing.svg" width="50px" />
|
||||
|
||||
## Werde Teil unserer Slack Community
|
||||
|
||||
@ -42,20 +77,22 @@ Sag Hi zu uns auf [Slack](https://signoz.io/slack) 👋
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Features.svg" width="50px" />
|
||||
|
||||
## Funktionen:
|
||||
|
||||
- Übersichtsmetriken deiner Anwendung wie RPS, 50tes/90tes/99tes Quantil Latenzen und Fehler Häufigkeiten.
|
||||
- Übersicht der langsamsten Endpunkte deiner Anwendung.
|
||||
- Sieh dir die genaue Einzelschritt-Fehlersuche deiner Abfrage an, um Fehler in nachgelagerten Diensten, langsamen Datenbank Abfragen und Aufrufen von Drittanbieter Diensten wie Zahlungsportalen, etc. zu finden.
|
||||
- Filtere Einzelschritt-Fehlersuchen nach Dienstname, Latenz, Fehler, Stichworten/ Anmerkungen.
|
||||
- Führe Aggregate auf Basis von Einzelschritt-Fehlersuche Daten (Ereignisse/Abstände) aus, um geschäftsrelevante Metriken zu erhalten. Du kannst dir z. B. die Fehlerrate und 99tes Quantil Latenz von `customer_type: gold`, `deployment_version: v2` oder `external_call: paypal` ausgeben lassen.
|
||||
- Einheitliche Benutzeroberfläche für Metriken und Einzelschritt-Fehlersuchen. Du musst nicht zwischen Prometheus und Jaeger hin und her wechseln, um Fehler zu beheben.
|
||||
- Einheitliche Benutzeroberfläche für Metriken, Traces und Logs. Keine Notwendigkeit, zwischen Prometheus und Jaeger zu wechseln, um Probleme zu debuggen oder ein separates Log-Tool wie Elastic neben Ihrer Metriken- und Traces-Stack zu verwenden.
|
||||
- Überblick über Anwendungsmetriken wie RPS, Latenzzeiten des 50tes/90tes/99tes Perzentils und Fehlerquoten.
|
||||
- Langsamste Endpunkte in Ihrer Anwendung.
|
||||
- Zeigen Sie genaue Anfragetraces an, um Probleme in nachgelagerten Diensten, langsamen Datenbankabfragen oder Aufrufen von Drittanbieterdiensten wie Zahlungsgateways zu identifizieren.
|
||||
- Filtern Sie Traces nach Dienstname, Operation, Latenz, Fehler, Tags/Annotationen.
|
||||
- Führen Sie Aggregationen auf Trace-Daten (Ereignisse/Spans) durch, um geschäftsrelevante Metriken zu erhalten. Beispielsweise können Sie die Fehlerquote und die 99tes Perzentillatenz für `customer_type: gold` oder `deployment_version: v2` oder `external_call: paypal` erhalten.
|
||||
- Native Unterstützung für OpenTelemetry-Logs, erweiterten Log-Abfrage-Builder und automatische Log-Sammlung aus dem Kubernetes-Cluster.
|
||||
- Blitzschnelle Log-Analytik ([Logs Perf. Benchmark](https://signoz.io/blog/logs-performance-benchmark/))
|
||||
- End-to-End-Sichtbarkeit der Infrastrukturleistung, Aufnahme von Metriken aus allen Arten von Host-Umgebungen.
|
||||
- Einfache Einrichtung von Benachrichtigungen mit dem selbst erstellbaren Abfrage-Builder.
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/WhatsCool.svg" width="50px" />
|
||||
|
||||
## Wieso SigNoz?
|
||||
|
||||
@ -65,24 +102,28 @@ Wir wollten eine selbst gehostete, Open Source Variante von Lösungen wie DataDo
|
||||
|
||||
Open Source gibt dir außerdem die totale Kontrolle über deine Konfiguration, Stichprobenentnahme und Betriebszeit. Du kannst des Weiteren neue Module auf Basis von SigNoz bauen, die erweiterte, geschäftsspezifische Funktionen anbieten.
|
||||
|
||||
### Unterstützte Programmiersprachen:
|
||||
### Languages supported:
|
||||
|
||||
Wir unterstützen [OpenTelemetry](https://opentelemetry.io) als die Software Library, die du nutzen kannst um deine Anwendungen auszuführen. Jedes Framework und jede Sprache die von OpenTelemetry unterstützt wird, wird auch von SigNoz unterstützt. Einige der unterstützten, größeren Programmiersprachen sind:
|
||||
Wir unterstützen [OpenTelemetry](https://opentelemetry.io) als Bibliothek, mit der Sie Ihre Anwendungen instrumentieren können. Daher wird jedes von OpenTelemetry unterstützte Framework und jede Sprache auch von SignNoz unterstützt. Einige der wichtigsten unterstützten Sprachen sind:
|
||||
|
||||
- Java
|
||||
- Python
|
||||
- NodeJS
|
||||
- Go
|
||||
- PHP
|
||||
- .NET
|
||||
- Ruby
|
||||
- Elixir
|
||||
- Rust
|
||||
|
||||
|
||||
Hier findest du die vollständige Liste von unterstützten Programmiersprachen - https://opentelemetry.io/docs/
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Philosophy.svg" width="50px" />
|
||||
|
||||
## Erste Schritte mit SigNoz
|
||||
|
||||
|
||||
### Bereitstellung mit Docker
|
||||
|
||||
Bitte folge den [hier](https://signoz.io/docs/install/docker/) aufgelisteten Schritten um deine Anwendung mit Docker bereitzustellen.
|
||||
@ -91,19 +132,16 @@ Die [Anleitungen zur Fehlerbehebung](https://signoz.io/docs/install/troubleshoot
|
||||
|
||||
<p>  </p>
|
||||
|
||||
|
||||
### Bereitstellung mit Kubernetes und Helm
|
||||
### Deploy in Kubernetes using Helm
|
||||
|
||||
Bitte folge den [hier](https://signoz.io/docs/deployment/helm_chart) aufgelisteten Schritten, um deine Anwendung mit Helm Charts bereitzustellen.
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/UseSigNoz.svg" width="50px" />
|
||||
|
||||
## Vergleiche mit anderen Lösungen
|
||||
## Vergleiche mit bekannten Tools
|
||||
|
||||
### SigNoz vs. Prometheus
|
||||
### SigNoz vs Prometheus
|
||||
|
||||
Prometheus ist gut, falls du dich nur für Metriken interessierst. Wenn du eine nahtlose Integration von Metriken und Einzelschritt-Fehlersuchen haben möchtest, ist die Kombination aus Prometheus und Jaeger nicht das Richtige für dich.
|
||||
|
||||
@ -111,49 +149,79 @@ Unser Ziel ist es, eine integrierte Benutzeroberfläche aus Metriken und Einzels
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### SigNoz vs. Jaeger
|
||||
### SigNoz vs Jaeger
|
||||
|
||||
Jaeger kümmert sich nur um verteilte Einzelschritt-Fehlersuche. SigNoz erstellt sowohl Metriken als auch Einzelschritt-Fehlersuche, daneben haben wir auch Protokoll Verwaltung auf unserem Plan.
|
||||
|
||||
Außerdem hat SigNoz noch mehr spezielle Funktionen im Vergleich zu Jaeger:
|
||||
|
||||
- Jaeger UI zeigt keine Metriken für Einzelschritt-Fehlersuchen oder für gefilterte Einzelschritt-Fehlersuchen an
|
||||
- Jaeger erstellt keine Aggregate für gefilterte Einzelschritt-Fehlersuchen, z. B. die P99 Latenz von Abfragen mit dem Tag - customer_type='premium', was hingegen mit SigNoz leicht umsetzbar ist.
|
||||
- Jaeger UI zeigt keine Metriken für Einzelschritt-Fehlersuchen oder für gefilterte Einzelschritt-Fehlersuchen an.
|
||||
- Jaeger erstellt keine Aggregate für gefilterte Einzelschritt-Fehlersuchen, z. B. die P99 Latenz von Abfragen mit dem Tag `customer_type=premium`, was hingegen mit SigNoz leicht umsetzbar ist.
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### SigNoz vs Elastic
|
||||
|
||||
- Die Verwaltung von SigNoz-Protokollen basiert auf 'ClickHouse', einem spaltenbasierten OLAP-Datenspeicher, der aggregierte Protokollanalyseabfragen wesentlich effizienter macht.
|
||||
- 50 % geringerer Ressourcenbedarf im Vergleich zu Elastic während der Aufnahme.
|
||||
|
||||
Wir haben Benchmarks veröffentlicht, die Elastic mit SignNoz vergleichen. Schauen Sie es sich [hier](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### SigNoz vs Loki
|
||||
|
||||
- SigNoz unterstützt Aggregationen von Daten mit hoher Kardinalität über ein großes Volumen, Loki hingegen nicht.
|
||||
- SigNoz unterstützt Indizes über Daten mit hoher Kardinalität und hat keine Beschränkungen hinsichtlich der Anzahl der Indizes, während Loki maximale Streams erreicht, wenn ein paar Indizes hinzugefügt werden.
|
||||
- Das Durchsuchen großer Datenmengen ist in Loki im Vergleich zu SigNoz schwierig und langsam.
|
||||
|
||||
Wir haben Benchmarks veröffentlicht, die Loki mit SigNoz vergleichen. Schauen Sie es sich [hier](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributors.svg" width="50px" />
|
||||
|
||||
## Zum Projekt beitragen
|
||||
|
||||
Wir ❤️ Beiträge zum Projekt, egal ob große oder kleine. Bitte lies dir zuerst die [CONTRIBUTING.md](CONTRIBUTING.md), durch, bevor du anfängst, Beiträge zu SigNoz zu machen.
|
||||
Du bist dir nicht sicher, wie du anfangen sollst? Schreib uns einfach auf dem #contributing Kanal in unserer [slack community](https://signoz.io/slack)
|
||||
|
||||
Wir ❤️ Beiträge zum Projekt, egal ob große oder kleine. Bitte lies dir zuerst die [CONTRIBUTING.md](CONTRIBUTING.md) durch, bevor du anfängst, Beiträge zu SigNoz zu machen.
|
||||
### Unsere Projektbetreuer
|
||||
|
||||
Du bist dir nicht sicher, wie du anfangen sollst? Schreib uns einfach auf dem `#contributing` Kanal in unserer [Slack Community](https://signoz.io/slack).
|
||||
#### Backend
|
||||
|
||||
- [Ankit Nayan](https://github.com/ankitnayan)
|
||||
- [Nityananda Gohain](https://github.com/nityanandagohain)
|
||||
- [Srikanth Chekuri](https://github.com/srikanthccv)
|
||||
- [Vishal Sharma](https://github.com/makeavish)
|
||||
|
||||
#### Frontend
|
||||
|
||||
- [Palash Gupta](https://github.com/palashgdev)
|
||||
|
||||
#### DevOps
|
||||
|
||||
- [Prashant Shahi](https://github.com/prashant-shahi)
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/DevelopingLocally.svg" width="50px" />
|
||||
|
||||
## Dokumentation
|
||||
|
||||
Du findest unsere Dokumentation unter https://signoz.io/docs/. Falls etwas unverständlich ist oder fehlt, öffne gerne ein Github Issue mit dem Label `documentation` oder schreib uns über den Community Slack Channel.
|
||||
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributing.svg" width="50px" />
|
||||
|
||||
## Community
|
||||
## Gemeinschaft
|
||||
|
||||
Werde Teil der [Slack Community](https://signoz.io/slack) um mehr über verteilte Einzelschritt-Fehlersuche, Messung von Systemzuständen oder SigNoz zu erfahren und sich mit anderen Nutzern und Mitwirkenden in Verbindung zu setzen.
|
||||
Werde Teil der [slack community](https://signoz.io/slack) um mehr über verteilte Einzelschritt-Fehlersuche, Messung von Systemzuständen oder SigNoz zu erfahren und sich mit anderen Nutzern und Mitwirkenden in Verbindung zu setzen.
|
||||
|
||||
Falls du irgendwelche Ideen, Fragen oder Feedback hast, kannst du sie gerne über unsere [Github Discussions](https://github.com/SigNoz/signoz/discussions) mit uns teilen.
|
||||
|
||||
Wie immer, danke an unsere großartigen Unterstützer!
|
||||
Wie immer, Dank an unsere großartigen Mitwirkenden!
|
||||
|
||||
<a href="https://github.com/signoz/signoz/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=signoz/signoz" />
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
|
@ -137,7 +137,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.25.0
|
||||
image: signoz/query-service:0.25.1
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
@ -166,7 +166,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.25.0
|
||||
image: signoz/frontend:0.25.1
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
@ -153,7 +153,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.25.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.25.1}
|
||||
container_name: query-service
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
@ -181,7 +181,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.25.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.25.1}
|
||||
container_name: frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"go.signoz.io/signoz/ee/query-service/interfaces"
|
||||
"go.signoz.io/signoz/ee/query-service/license"
|
||||
baseapp "go.signoz.io/signoz/pkg/query-service/app"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
rules "go.signoz.io/signoz/pkg/query-service/rules"
|
||||
@ -15,14 +16,15 @@ import (
|
||||
)
|
||||
|
||||
type APIHandlerOptions struct {
|
||||
DataConnector interfaces.DataConnector
|
||||
SkipConfig *basemodel.SkipConfig
|
||||
PreferDelta bool
|
||||
PreferSpanMetrics bool
|
||||
AppDao dao.ModelDao
|
||||
RulesManager *rules.Manager
|
||||
FeatureFlags baseint.FeatureLookup
|
||||
LicenseManager *license.Manager
|
||||
DataConnector interfaces.DataConnector
|
||||
SkipConfig *basemodel.SkipConfig
|
||||
PreferDelta bool
|
||||
PreferSpanMetrics bool
|
||||
AppDao dao.ModelDao
|
||||
RulesManager *rules.Manager
|
||||
FeatureFlags baseint.FeatureLookup
|
||||
LicenseManager *license.Manager
|
||||
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
|
||||
}
|
||||
|
||||
type APIHandler struct {
|
||||
@ -34,13 +36,15 @@ type APIHandler struct {
|
||||
func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
|
||||
|
||||
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
||||
Reader: opts.DataConnector,
|
||||
SkipConfig: opts.SkipConfig,
|
||||
PerferDelta: opts.PreferDelta,
|
||||
PreferSpanMetrics: opts.PreferSpanMetrics,
|
||||
AppDao: opts.AppDao,
|
||||
RuleManager: opts.RulesManager,
|
||||
FeatureFlags: opts.FeatureFlags})
|
||||
Reader: opts.DataConnector,
|
||||
SkipConfig: opts.SkipConfig,
|
||||
PerferDelta: opts.PreferDelta,
|
||||
PreferSpanMetrics: opts.PreferSpanMetrics,
|
||||
AppDao: opts.AppDao,
|
||||
RuleManager: opts.RulesManager,
|
||||
FeatureFlags: opts.FeatureFlags,
|
||||
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -31,6 +31,7 @@ import (
|
||||
baseapp "go.signoz.io/signoz/pkg/query-service/app"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
baseexplorer "go.signoz.io/signoz/pkg/query-service/app/explorer"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/opamp"
|
||||
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
|
||||
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
||||
@ -78,6 +79,9 @@ type Server struct {
|
||||
// feature flags
|
||||
featureLookup baseint.FeatureLookup
|
||||
|
||||
// Usage manager
|
||||
usageManager *usage.Manager
|
||||
|
||||
unavailableChannel chan healthcheck.Status
|
||||
}
|
||||
|
||||
@ -157,6 +161,12 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ingestion pipelines manager
|
||||
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(localDB, "sqlite")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// start the usagemanager
|
||||
usageManager, err := usage.New("sqlite", localDB, lm.GetRepo(), reader.GetConn())
|
||||
if err != nil {
|
||||
@ -170,14 +180,15 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
telemetry.GetInstance().SetReader(reader)
|
||||
|
||||
apiOpts := api.APIHandlerOptions{
|
||||
DataConnector: reader,
|
||||
SkipConfig: skipConfig,
|
||||
PreferDelta: serverOptions.PreferDelta,
|
||||
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
|
||||
AppDao: modelDao,
|
||||
RulesManager: rm,
|
||||
FeatureFlags: lm,
|
||||
LicenseManager: lm,
|
||||
DataConnector: reader,
|
||||
SkipConfig: skipConfig,
|
||||
PreferDelta: serverOptions.PreferDelta,
|
||||
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
|
||||
AppDao: modelDao,
|
||||
RulesManager: rm,
|
||||
FeatureFlags: lm,
|
||||
LicenseManager: lm,
|
||||
LogsParsingPipelineController: logParsingPipelineController,
|
||||
}
|
||||
|
||||
apiHandler, err := api.NewAPIHandler(apiOpts)
|
||||
@ -191,6 +202,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
ruleManager: rm,
|
||||
serverOptions: serverOptions,
|
||||
unavailableChannel: make(chan healthcheck.Status),
|
||||
usageManager: usageManager,
|
||||
}
|
||||
|
||||
httpServer, err := s.createPublicServer(apiHandler)
|
||||
@ -552,6 +564,9 @@ func (s *Server) Stop() error {
|
||||
s.ruleManager.Stop()
|
||||
}
|
||||
|
||||
// stop usage manager
|
||||
s.usageManager.Stop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -144,6 +144,7 @@ func main() {
|
||||
logger.Info("Received HealthCheck status: ", zap.Int("status", int(status)))
|
||||
case <-signalsChannel:
|
||||
logger.Fatal("Received OS Interrupt Signal ... ")
|
||||
server.Stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/go-co-op/gocron"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
@ -28,9 +29,6 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// send usage every 24 hour
|
||||
uploadFrequency = 24 * time.Hour
|
||||
|
||||
locker = stateUnlocked
|
||||
)
|
||||
|
||||
@ -39,12 +37,7 @@ type Manager struct {
|
||||
|
||||
licenseRepo *license.Repo
|
||||
|
||||
// end the usage routine, this is important to gracefully
|
||||
// stopping usage reporting and protect in-consistent updates
|
||||
done chan struct{}
|
||||
|
||||
// terminated waits for the UsageExporter go routine to end
|
||||
terminated chan struct{}
|
||||
scheduler *gocron.Scheduler
|
||||
}
|
||||
|
||||
func New(dbType string, db *sqlx.DB, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn) (*Manager, error) {
|
||||
@ -53,6 +46,7 @@ func New(dbType string, db *sqlx.DB, licenseRepo *license.Repo, clickhouseConn c
|
||||
// repository: repo,
|
||||
clickhouseConn: clickhouseConn,
|
||||
licenseRepo: licenseRepo,
|
||||
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@ -64,37 +58,30 @@ func (lm *Manager) Start() error {
|
||||
return fmt.Errorf("usage exporter is locked")
|
||||
}
|
||||
|
||||
go lm.UsageExporter(context.Background())
|
||||
_, err := lm.scheduler.Do(func() { lm.UploadUsage() })
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// upload usage once when starting the service
|
||||
lm.UploadUsage()
|
||||
|
||||
lm.scheduler.StartAsync()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lm *Manager) UsageExporter(ctx context.Context) {
|
||||
defer close(lm.terminated)
|
||||
|
||||
uploadTicker := time.NewTicker(uploadFrequency)
|
||||
defer uploadTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-lm.done:
|
||||
return
|
||||
case <-uploadTicker.C:
|
||||
lm.UploadUsage(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (lm *Manager) UploadUsage(ctx context.Context) error {
|
||||
func (lm *Manager) UploadUsage() {
|
||||
ctx := context.Background()
|
||||
// check if license is present or not
|
||||
license, err := lm.licenseRepo.GetActiveLicense(context.Background())
|
||||
license, err := lm.licenseRepo.GetActiveLicense(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get active license")
|
||||
zap.S().Errorf("failed to get active license: %v", zap.Error(err))
|
||||
return
|
||||
}
|
||||
if license == nil {
|
||||
// we will not start the usage reporting if license is not present.
|
||||
zap.S().Info("no license present, skipping usage reporting")
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
usages := []model.UsageDB{}
|
||||
@ -120,7 +107,8 @@ func (lm *Manager) UploadUsage(ctx context.Context) error {
|
||||
dbusages := []model.UsageDB{}
|
||||
err := lm.clickhouseConn.Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour)))
|
||||
if err != nil && !strings.Contains(err.Error(), "doesn't exist") {
|
||||
return err
|
||||
zap.S().Errorf("failed to get usage from clickhouse: %v", zap.Error(err))
|
||||
return
|
||||
}
|
||||
for _, u := range dbusages {
|
||||
u.Type = db
|
||||
@ -130,7 +118,7 @@ func (lm *Manager) UploadUsage(ctx context.Context) error {
|
||||
|
||||
if len(usages) <= 0 {
|
||||
zap.S().Info("no snapshots to upload, skipping.")
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
zap.S().Info("uploading usage data")
|
||||
@ -139,13 +127,15 @@ func (lm *Manager) UploadUsage(ctx context.Context) error {
|
||||
for _, usage := range usages {
|
||||
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
|
||||
if err != nil {
|
||||
return err
|
||||
zap.S().Errorf("error while decrypting usage data: %v", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
usageData := model.Usage{}
|
||||
err = json.Unmarshal(usageDataBytes, &usageData)
|
||||
if err != nil {
|
||||
return err
|
||||
zap.S().Errorf("error while unmarshalling usage data: %v", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
usageData.CollectorID = usage.CollectorID
|
||||
@ -160,20 +150,16 @@ func (lm *Manager) UploadUsage(ctx context.Context) error {
|
||||
LicenseKey: key,
|
||||
Usage: usagesPayload,
|
||||
}
|
||||
err = lm.UploadUsageWithExponentalBackOff(ctx, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
lm.UploadUsageWithExponentalBackOff(ctx, payload)
|
||||
}
|
||||
|
||||
func (lm *Manager) UploadUsageWithExponentalBackOff(ctx context.Context, payload model.UsagePayload) error {
|
||||
func (lm *Manager) UploadUsageWithExponentalBackOff(ctx context.Context, payload model.UsagePayload) {
|
||||
for i := 1; i <= MaxRetries; i++ {
|
||||
apiErr := licenseserver.SendUsage(ctx, payload)
|
||||
if apiErr != nil && i == MaxRetries {
|
||||
zap.S().Errorf("retries stopped : %v", zap.Error(apiErr))
|
||||
// not returning error here since it is captured in the failed count
|
||||
return nil
|
||||
return
|
||||
} else if apiErr != nil {
|
||||
// sleeping for exponential backoff
|
||||
sleepDuration := RetryInterval * time.Duration(i)
|
||||
@ -183,11 +169,14 @@ func (lm *Manager) UploadUsageWithExponentalBackOff(ctx context.Context, payload
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lm *Manager) Stop() {
|
||||
close(lm.done)
|
||||
lm.scheduler.Stop()
|
||||
|
||||
zap.S().Debug("sending usage data before shutting down")
|
||||
// send usage before shutting down
|
||||
lm.UploadUsage()
|
||||
|
||||
atomic.StoreUint32(&locker, stateUnlocked)
|
||||
<-lm.terminated
|
||||
}
|
||||
|
@ -21,7 +21,9 @@ const config: Config.InitialOptions = {
|
||||
'^.+\\.(ts|tsx)?$': 'ts-jest',
|
||||
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!(lodash-es)/)'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend)/)',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||
moduleDirectories: ['node_modules', 'src'],
|
||||
|
@ -46,6 +46,7 @@
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"chartjs-plugin-annotation": "^1.4.0",
|
||||
"color": "^4.2.1",
|
||||
"color-alpha": "1.1.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "4.3.0",
|
||||
"css-minimizer-webpack-plugin": "^3.2.0",
|
||||
@ -53,6 +54,7 @@
|
||||
"dompurify": "3.0.0",
|
||||
"dotenv": "8.2.0",
|
||||
"event-source-polyfill": "1.0.31",
|
||||
"eventemitter3": "5.0.1",
|
||||
"file-loader": "6.1.1",
|
||||
"fontfaceobserver": "2.3.0",
|
||||
"history": "4.10.1",
|
||||
@ -68,10 +70,14 @@
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"papaparse": "5.4.1",
|
||||
"react": "18.2.0",
|
||||
"react-addons-update": "15.6.3",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-drag-listview": "2.0.0",
|
||||
"react-force-graph": "^1.41.0",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-helmet-async": "1.3.0",
|
||||
"react-i18next": "^11.16.1",
|
||||
"react-intersection-observer": "9.4.1",
|
||||
"react-query": "^3.34.19",
|
||||
@ -133,8 +139,10 @@
|
||||
"@types/node": "^16.10.3",
|
||||
"@types/papaparse": "5.3.7",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-addons-update": "0.14.21",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"@types/react-grid-layout": "^1.1.2",
|
||||
"@types/react-helmet-async": "1.0.3",
|
||||
"@types/react-redux": "^7.1.11",
|
||||
"@types/react-resizable": "3.0.3",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
|
@ -5,5 +5,8 @@
|
||||
"my_settings": "My Settings",
|
||||
"overview_metrics": "Overview Metrics",
|
||||
"dbcall_metrics": "Database Calls",
|
||||
"external_metrics": "External Calls"
|
||||
"external_metrics": "External Calls",
|
||||
"pipelines": "Pipelines",
|
||||
"archives": "Archives",
|
||||
"logs_to_metrics": "Logs To Metrics"
|
||||
}
|
||||
|
36
frontend/public/locales/en-GB/titles.json
Normal file
36
frontend/public/locales/en-GB/titles.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"SIGN_UP": "SigNoz | Sign Up",
|
||||
"LOGIN": "SigNoz | Login",
|
||||
"SERVICE_METRICS": "SigNoz | Service Metrics",
|
||||
"SERVICE_MAP": "SigNoz | Service Map",
|
||||
"TRACE": "SigNoz | Trace",
|
||||
"TRACE_DETAIL": "SigNoz | Trace Detail",
|
||||
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
|
||||
"SETTINGS": "SigNoz | Settings",
|
||||
"INSTRUMENTATION": "SigNoz | Get Started",
|
||||
"USAGE_EXPLORER": "SigNoz | Usage Explorer",
|
||||
"APPLICATION": "SigNoz | Home",
|
||||
"ALL_DASHBOARD": "SigNoz | All Dashboards",
|
||||
"DASHBOARD": "SigNoz | Dashboard",
|
||||
"DASHBOARD_WIDGET": "SigNoz | Dashboard Widget",
|
||||
"EDIT_ALERTS": "SigNoz | Edit Alerts",
|
||||
"LIST_ALL_ALERT": "SigNoz | All Alerts",
|
||||
"ALERTS_NEW": "SigNoz | New Alert",
|
||||
"ALL_CHANNELS": "SigNoz | All Channels",
|
||||
"CHANNELS_NEW": "SigNoz | New Channel",
|
||||
"CHANNELS_EDIT": "SigNoz | Edit Channel",
|
||||
"ALL_ERROR": "SigNoz | All Errors",
|
||||
"ERROR_DETAIL": "SigNoz | Error Detail",
|
||||
"VERSION": "SigNoz | Version",
|
||||
"MY_SETTINGS": "SigNoz | My Settings",
|
||||
"ORG_SETTINGS": "SigNoz | Organization Settings",
|
||||
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
|
||||
"UN_AUTHORIZED": "SigNoz | Unauthorized",
|
||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||
"LOGS": "SigNoz | Logs",
|
||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz"
|
||||
}
|
@ -12,6 +12,7 @@
|
||||
"routes": {
|
||||
"general": "General",
|
||||
"alert_channels": "Alert Channels",
|
||||
"all_errors": "All Exceptions"
|
||||
"all_errors": "All Exceptions",
|
||||
"pipelines": "Pipelines"
|
||||
}
|
||||
}
|
||||
|
44
frontend/public/locales/en/pipeline.json
Normal file
44
frontend/public/locales/en/pipeline.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"delete": "Delete",
|
||||
"filter": "Filter",
|
||||
"update": "Update",
|
||||
"create": "Create",
|
||||
"reorder": "Reorder",
|
||||
"cancel": "Cancel",
|
||||
"reorder_pipeline": "Do you want to reorder pipeline?",
|
||||
"reorder_pipeline_description": "Logs are processed sequentially in processors and pipelines. Reordering it may change how data is processed by them.",
|
||||
"delete_pipeline": "Do you want to delete pipeline",
|
||||
"delete_pipeline_description": "Logs are processed sequentially in processors and pipelines. Deleting a pipeline may change content of data processed by other pipelines & processors",
|
||||
"add_new_pipeline": "Add a New Pipeline",
|
||||
"new_pipeline": "New Pipeline",
|
||||
"enter_edit_mode": "Enter Edit Mode",
|
||||
"save_configuration": "Save Configuration",
|
||||
"edit_pipeline": "Edit Pipeline",
|
||||
"create_pipeline": "Create New Pipeline",
|
||||
"add_new_processor": "Add Processor",
|
||||
"edit_processor": "Edit Processor",
|
||||
"create_processor": "Create New Processor",
|
||||
"processor_type": "Select Processor Type",
|
||||
"reorder_processor": "Do you want to reorder processor?",
|
||||
"reorder_processor_description": "Logs are processed sequentially in processors. Reordering it may change how data is processed by them.",
|
||||
"delete_processor": "Do you want to delete processor",
|
||||
"delete_processor_description": "Logs are processed sequentially in processors. Deleting a processor may change content of data processed by other processors",
|
||||
"search_pipeline_placeholder": "Filter Pipelines",
|
||||
"pipeline_name_placeholder": "Name",
|
||||
"pipeline_tags_placeholder": "Tags",
|
||||
"pipeline_description_placeholder": "Enter description for your pipeline",
|
||||
"processor_name_placeholder": "Name",
|
||||
"processor_regex_placeholder": "Regex",
|
||||
"processor_parsefrom_placeholder": "Parse From",
|
||||
"processor_parseto_placeholder": "Parse From",
|
||||
"processor_onerror_placeholder": "on Error",
|
||||
"processor_pattern_placeholder": "Pattern",
|
||||
"processor_field_placeholder": "Field",
|
||||
"processor_value_placeholder": "Value",
|
||||
"processor_description_placeholder": "example rule: %{word:first}",
|
||||
"processor_trace_id_placeholder": "Trace Id Parce From",
|
||||
"processor_span_id_placeholder": "Span id Parse From",
|
||||
"processor_trace_flags_placeholder": "Trace flags parse from",
|
||||
"processor_from_placeholder": "From",
|
||||
"processor_to_placeholder": "To"
|
||||
}
|
@ -5,5 +5,8 @@
|
||||
"my_settings": "My Settings",
|
||||
"overview_metrics": "Overview Metrics",
|
||||
"dbcall_metrics": "Database Calls",
|
||||
"external_metrics": "External Calls"
|
||||
"external_metrics": "External Calls",
|
||||
"pipelines": "Pipelines",
|
||||
"archives": "Archives",
|
||||
"logs_to_metrics": "Logs To Metrics"
|
||||
}
|
||||
|
36
frontend/public/locales/en/titles.json
Normal file
36
frontend/public/locales/en/titles.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"SIGN_UP": "SigNoz | Sign Up",
|
||||
"LOGIN": "SigNoz | Login",
|
||||
"SERVICE_METRICS": "SigNoz | Service Metrics",
|
||||
"SERVICE_MAP": "SigNoz | Service Map",
|
||||
"TRACE": "SigNoz | Trace",
|
||||
"TRACE_DETAIL": "SigNoz | Trace Detail",
|
||||
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
|
||||
"SETTINGS": "SigNoz | Settings",
|
||||
"INSTRUMENTATION": "SigNoz | Get Started",
|
||||
"USAGE_EXPLORER": "SigNoz | Usage Explorer",
|
||||
"APPLICATION": "SigNoz | Home",
|
||||
"ALL_DASHBOARD": "SigNoz | All Dashboards",
|
||||
"DASHBOARD": "SigNoz | Dashboard",
|
||||
"DASHBOARD_WIDGET": "SigNoz | Dashboard Widget",
|
||||
"EDIT_ALERTS": "SigNoz | Edit Alerts",
|
||||
"LIST_ALL_ALERT": "SigNoz | All Alerts",
|
||||
"ALERTS_NEW": "SigNoz | New Alert",
|
||||
"ALL_CHANNELS": "SigNoz | All Channels",
|
||||
"CHANNELS_NEW": "SigNoz | New Channel",
|
||||
"CHANNELS_EDIT": "SigNoz | Edit Channel",
|
||||
"ALL_ERROR": "SigNoz | All Errors",
|
||||
"ERROR_DETAIL": "SigNoz | Error Detail",
|
||||
"VERSION": "SigNoz | Version",
|
||||
"MY_SETTINGS": "SigNoz | My Settings",
|
||||
"ORG_SETTINGS": "SigNoz | Organization Settings",
|
||||
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
|
||||
"UN_AUTHORIZED": "SigNoz | Unauthorized",
|
||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||
"LOGS": "SigNoz | Logs",
|
||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz"
|
||||
}
|
@ -12,6 +12,7 @@
|
||||
"routes": {
|
||||
"general": "General",
|
||||
"alert_channels": "Alert Channels",
|
||||
"all_errors": "All Exceptions"
|
||||
"all_errors": "All Exceptions",
|
||||
"pipelines": "Pipelines"
|
||||
}
|
||||
}
|
||||
|
@ -132,3 +132,7 @@ export const SomethingWentWrong = Loadable(
|
||||
export const LicensePage = Loadable(
|
||||
() => import(/* webpackChunkName: "All Channels" */ 'pages/License'),
|
||||
);
|
||||
|
||||
export const PipelinePage = Loadable(
|
||||
() => import(/* webpackChunkName: "Pipelines" */ 'pages/Pipelines'),
|
||||
);
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
NewDashboardPage,
|
||||
OrganizationSettings,
|
||||
PasswordReset,
|
||||
PipelinePage,
|
||||
ServiceMapPage,
|
||||
ServiceMetricsPage,
|
||||
ServicesTablePage,
|
||||
@ -253,6 +254,13 @@ const routes: AppRoutes[] = [
|
||||
key: 'SOMETHING_WENT_WRONG',
|
||||
isPrivate: false,
|
||||
},
|
||||
{
|
||||
path: ROUTES.PIPELINES,
|
||||
exact: true,
|
||||
component: PipelinePage,
|
||||
key: 'PIPELINES',
|
||||
isPrivate: true,
|
||||
},
|
||||
];
|
||||
|
||||
export interface AppRoutes {
|
||||
|
@ -18,6 +18,7 @@ export const getMetricsQueryRange = async (
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
params: props,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
|
25
frontend/src/api/pipeline/get.ts
Normal file
25
frontend/src/api/pipeline/get.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Pipeline } from 'types/api/pipeline/def';
|
||||
import { Props } from 'types/api/pipeline/get';
|
||||
|
||||
const get = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<Pipeline> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`/logs/pipelines/${props.version}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response?.data?.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default get;
|
25
frontend/src/api/pipeline/post.ts
Normal file
25
frontend/src/api/pipeline/post.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Pipeline } from 'types/api/pipeline/def';
|
||||
import { Props } from 'types/api/pipeline/post';
|
||||
|
||||
const post = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<Pipeline> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/logs/pipelines', props.data);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default post;
|
54
frontend/src/components/DraggableTableRow/index.tsx
Normal file
54
frontend/src/components/DraggableTableRow/index.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
|
||||
import { dragHandler, dropHandler } from './utils';
|
||||
|
||||
const type = 'DraggableTableRow';
|
||||
|
||||
function DraggableTableRow({
|
||||
index,
|
||||
moveRow,
|
||||
className,
|
||||
style,
|
||||
...restProps
|
||||
}: DraggableTableRowProps): JSX.Element {
|
||||
const ref = useRef<HTMLTableRowElement>(null);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(item: { index: number }) => {
|
||||
if (moveRow) moveRow(item.index, index);
|
||||
},
|
||||
[moveRow, index],
|
||||
);
|
||||
|
||||
const [, drop] = useDrop({
|
||||
accept: type,
|
||||
collect: dropHandler,
|
||||
drop: handleDrop,
|
||||
});
|
||||
|
||||
const [, drag] = useDrag({
|
||||
type,
|
||||
item: { index },
|
||||
collect: dragHandler,
|
||||
});
|
||||
drop(drag(ref));
|
||||
|
||||
return (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={className}
|
||||
style={{ ...style }}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface DraggableTableRowProps
|
||||
extends React.HTMLAttributes<HTMLTableRowElement> {
|
||||
index: number;
|
||||
moveRow: (dragIndex: number, hoverIndex: number) => void;
|
||||
}
|
||||
|
||||
export default DraggableTableRow;
|
@ -0,0 +1,38 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { Table } from 'antd';
|
||||
import { matchMedia } from 'container/PipelinePage/tests/AddNewPipeline.test';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
import i18n from 'ReactI18';
|
||||
import store from 'store';
|
||||
|
||||
import DraggableTableRow from '..';
|
||||
|
||||
beforeAll(() => {
|
||||
matchMedia();
|
||||
});
|
||||
|
||||
jest.mock('react-dnd', () => ({
|
||||
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
|
||||
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
|
||||
}));
|
||||
|
||||
describe('DraggableTableRow Snapshot test', () => {
|
||||
it('should render DraggableTableRow', async () => {
|
||||
const { asFragment } = render(
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Table
|
||||
components={{
|
||||
body: {
|
||||
row: DraggableTableRow,
|
||||
},
|
||||
}}
|
||||
pagination={false}
|
||||
/>
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -0,0 +1,103 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="ant-table-wrapper css-dev-only-do-not-override-1i536d8"
|
||||
>
|
||||
<div
|
||||
class="ant-spin-nested-loading css-dev-only-do-not-override-1i536d8"
|
||||
>
|
||||
<div
|
||||
class="ant-spin-container"
|
||||
>
|
||||
<div
|
||||
class="ant-table ant-table-empty"
|
||||
>
|
||||
<div
|
||||
class="ant-table-container"
|
||||
>
|
||||
<div
|
||||
class="ant-table-content"
|
||||
>
|
||||
<table
|
||||
style="table-layout: auto;"
|
||||
>
|
||||
<colgroup />
|
||||
<thead
|
||||
class="ant-table-thead"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
class="ant-table-cell"
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="ant-table-tbody"
|
||||
>
|
||||
<tr
|
||||
class="ant-table-placeholder"
|
||||
>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div
|
||||
class="css-dev-only-do-not-override-1i536d8 ant-empty ant-empty-normal"
|
||||
>
|
||||
<div
|
||||
class="ant-empty-image"
|
||||
>
|
||||
<svg
|
||||
height="41"
|
||||
viewBox="0 0 64 41"
|
||||
width="64"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
transform="translate(0 1)"
|
||||
>
|
||||
<ellipse
|
||||
cx="32"
|
||||
cy="33"
|
||||
fill="#f5f5f5"
|
||||
rx="32"
|
||||
ry="7"
|
||||
/>
|
||||
<g
|
||||
fill-rule="nonzero"
|
||||
stroke="#d9d9d9"
|
||||
>
|
||||
<path
|
||||
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
|
||||
/>
|
||||
<path
|
||||
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
|
||||
fill="#fafafa"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`PipelinePage container test should render AddNewPipeline section 1`] = `<DocumentFragment />`;
|
@ -0,0 +1,44 @@
|
||||
import { dragHandler, dropHandler } from '../utils';
|
||||
|
||||
jest.mock('react-dnd', () => ({
|
||||
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
|
||||
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
|
||||
}));
|
||||
|
||||
describe('Utils testing of DraggableTableRow component', () => {
|
||||
test('Should dropHandler return true', () => {
|
||||
const monitor = {
|
||||
isOver: jest.fn().mockReturnValueOnce(true),
|
||||
} as never;
|
||||
const dropDataTruthy = dropHandler(monitor);
|
||||
|
||||
expect(dropDataTruthy).toEqual({ isOver: true });
|
||||
});
|
||||
|
||||
test('Should dropHandler return false', () => {
|
||||
const monitor = {
|
||||
isOver: jest.fn().mockReturnValueOnce(false),
|
||||
} as never;
|
||||
const dropDataFalsy = dropHandler(monitor);
|
||||
|
||||
expect(dropDataFalsy).toEqual({ isOver: false });
|
||||
});
|
||||
|
||||
test('Should dragHandler return true', () => {
|
||||
const monitor = {
|
||||
isDragging: jest.fn().mockReturnValueOnce(true),
|
||||
} as never;
|
||||
const dragDataTruthy = dragHandler(monitor);
|
||||
|
||||
expect(dragDataTruthy).toEqual({ isDragging: true });
|
||||
});
|
||||
|
||||
test('Should dragHandler return false', () => {
|
||||
const monitor = {
|
||||
isDragging: jest.fn().mockReturnValueOnce(false),
|
||||
} as never;
|
||||
const dragDataFalsy = dragHandler(monitor);
|
||||
|
||||
expect(dragDataFalsy).toEqual({ isDragging: false });
|
||||
});
|
||||
});
|
15
frontend/src/components/DraggableTableRow/utils.ts
Normal file
15
frontend/src/components/DraggableTableRow/utils.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { DragSourceMonitor, DropTargetMonitor } from 'react-dnd';
|
||||
|
||||
export function dropHandler(monitor: DropTargetMonitor): { isOver: boolean } {
|
||||
return {
|
||||
isOver: monitor.isOver(),
|
||||
};
|
||||
}
|
||||
|
||||
export function dragHandler(
|
||||
monitor: DragSourceMonitor,
|
||||
): { isDragging: boolean } {
|
||||
return {
|
||||
isDragging: monitor.isDragging(),
|
||||
};
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import { Chart, ChartType, Plugin } from 'chart.js';
|
||||
import { Events } from 'constants/events';
|
||||
import { colors } from 'lib/getRandomColor';
|
||||
import { get } from 'lodash-es';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
|
||||
const getOrCreateLegendList = (
|
||||
chart: Chart,
|
||||
@ -74,6 +76,10 @@ export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => ({
|
||||
item.datasetIndex,
|
||||
!chart.isDatasetVisible(item.datasetIndex),
|
||||
);
|
||||
eventEmitter.emit(Events.UPDATE_GRAPH_MANAGER_TABLE, {
|
||||
name: id,
|
||||
index: item.datasetIndex,
|
||||
});
|
||||
}
|
||||
chart.update();
|
||||
};
|
||||
|
@ -1,12 +1,8 @@
|
||||
import {
|
||||
ActiveElement,
|
||||
BarController,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
Chart,
|
||||
ChartData,
|
||||
ChartEvent,
|
||||
ChartOptions,
|
||||
ChartType,
|
||||
Decimation,
|
||||
Filler,
|
||||
@ -21,33 +17,28 @@ import {
|
||||
Title,
|
||||
Tooltip,
|
||||
} from 'chart.js';
|
||||
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import { hasData } from './hasData';
|
||||
import { getAxisLabelColor } from './helpers';
|
||||
import { legend } from './Plugin';
|
||||
import {
|
||||
createDragSelectPlugin,
|
||||
createDragSelectPluginOptions,
|
||||
dragSelectPluginId,
|
||||
DragSelectPluginOptions,
|
||||
} from './Plugin/DragSelect';
|
||||
import { createDragSelectPlugin } from './Plugin/DragSelect';
|
||||
import { emptyGraph } from './Plugin/EmptyGraph';
|
||||
import {
|
||||
createIntersectionCursorPlugin,
|
||||
createIntersectionCursorPluginOptions,
|
||||
intersectionCursorPluginId,
|
||||
IntersectionCursorPluginOptions,
|
||||
} from './Plugin/IntersectionCursor';
|
||||
import { createIntersectionCursorPlugin } from './Plugin/IntersectionCursor';
|
||||
import { TooltipPosition as TooltipPositionHandler } from './Plugin/Tooltip';
|
||||
import { LegendsContainer } from './styles';
|
||||
import { CustomChartOptions, GraphProps, ToggleGraphProps } from './types';
|
||||
import { getGraphOptions, toggleGraph } from './utils';
|
||||
import { useXAxisTimeUnit } from './xAxisConfig';
|
||||
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
|
||||
|
||||
Chart.register(
|
||||
LineElement,
|
||||
@ -70,265 +61,125 @@ Chart.register(
|
||||
|
||||
Tooltip.positioners.custom = TooltipPositionHandler;
|
||||
|
||||
function Graph({
|
||||
animate = true,
|
||||
data,
|
||||
type,
|
||||
title,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit = 'short',
|
||||
forceReRender,
|
||||
staticLine,
|
||||
containerHeight,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
}: GraphProps): JSX.Element {
|
||||
const nearestDatasetIndex = useRef<null | number>(null);
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
||||
(
|
||||
{
|
||||
animate = true,
|
||||
data,
|
||||
type,
|
||||
title,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit = 'short',
|
||||
forceReRender,
|
||||
staticLine,
|
||||
containerHeight,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
},
|
||||
ref,
|
||||
): JSX.Element => {
|
||||
const nearestDatasetIndex = useRef<null | number>(null);
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const currentTheme = isDarkMode ? 'dark' : 'light';
|
||||
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
|
||||
const currentTheme = isDarkMode ? 'dark' : 'light';
|
||||
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
|
||||
|
||||
const lineChartRef = useRef<Chart>();
|
||||
const getGridColor = useCallback(() => {
|
||||
if (currentTheme === undefined) {
|
||||
return 'rgba(231,233,237,0.1)';
|
||||
}
|
||||
const lineChartRef = useRef<Chart>();
|
||||
|
||||
if (currentTheme === 'dark') {
|
||||
return 'rgba(231,233,237,0.1)';
|
||||
}
|
||||
|
||||
return 'rgba(231,233,237,0.8)';
|
||||
}, [currentTheme]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const buildChart = useCallback(() => {
|
||||
if (lineChartRef.current !== undefined) {
|
||||
lineChartRef.current.destroy();
|
||||
}
|
||||
|
||||
if (chartRef.current !== null) {
|
||||
const options: CustomChartOptions = {
|
||||
animation: {
|
||||
duration: animate ? 200 : 0,
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
(): ToggleGraphProps => ({
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void {
|
||||
toggleGraph(graphIndex, isVisible, lineChartRef);
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
annotation: staticLine
|
||||
? {
|
||||
annotations: [
|
||||
{
|
||||
type: 'line',
|
||||
yMin: staticLine.yMin,
|
||||
yMax: staticLine.yMax,
|
||||
borderColor: staticLine.borderColor,
|
||||
borderWidth: staticLine.borderWidth,
|
||||
label: {
|
||||
content: staticLine.lineText,
|
||||
enabled: true,
|
||||
font: {
|
||||
size: 10,
|
||||
},
|
||||
borderWidth: 0,
|
||||
position: 'start',
|
||||
backgroundColor: 'transparent',
|
||||
color: staticLine.textColor,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
title: {
|
||||
display: title !== undefined,
|
||||
text: title,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title(context) {
|
||||
const date = dayjs(context[0].parsed.x);
|
||||
return date.format('MMM DD, YYYY, HH:mm:ss');
|
||||
},
|
||||
label(context) {
|
||||
let label = context.dataset.label || '';
|
||||
}),
|
||||
);
|
||||
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += getToolTipValue(context.parsed.y.toString(), yAxisUnit);
|
||||
}
|
||||
|
||||
return label;
|
||||
},
|
||||
labelTextColor(labelData) {
|
||||
if (labelData.datasetIndex === nearestDatasetIndex.current) {
|
||||
return 'rgba(255, 255, 255, 1)';
|
||||
}
|
||||
|
||||
return 'rgba(255, 255, 255, 0.75)';
|
||||
},
|
||||
},
|
||||
position: 'custom',
|
||||
itemSort(item1, item2) {
|
||||
return item2.parsed.y - item1.parsed.y;
|
||||
},
|
||||
},
|
||||
[dragSelectPluginId]: createDragSelectPluginOptions(
|
||||
!!onDragSelect,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
),
|
||||
[intersectionCursorPluginId]: createIntersectionCursorPluginOptions(
|
||||
!!onDragSelect,
|
||||
currentTheme === 'dark' ? 'white' : 'black',
|
||||
),
|
||||
},
|
||||
layout: {
|
||||
padding: 0,
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: true,
|
||||
color: getGridColor(),
|
||||
drawTicks: true,
|
||||
},
|
||||
adapters: {
|
||||
date: chartjsAdapter,
|
||||
},
|
||||
time: {
|
||||
unit: xAxisTimeUnit?.unitName || 'minute',
|
||||
stepSize: xAxisTimeUnit?.stepSize || 1,
|
||||
displayFormats: {
|
||||
millisecond: 'HH:mm:ss',
|
||||
second: 'HH:mm:ss',
|
||||
minute: 'HH:mm',
|
||||
hour: 'MM/dd HH:mm',
|
||||
day: 'MM/dd',
|
||||
week: 'MM/dd',
|
||||
month: 'yy-MM',
|
||||
year: 'yy',
|
||||
},
|
||||
},
|
||||
type: 'time',
|
||||
ticks: { color: getAxisLabelColor(currentTheme) },
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
grid: {
|
||||
display: true,
|
||||
color: getGridColor(),
|
||||
},
|
||||
ticks: {
|
||||
color: getAxisLabelColor(currentTheme),
|
||||
// Include a dollar sign in the ticks
|
||||
callback(value) {
|
||||
return getYAxisFormattedValue(value.toString(), yAxisUnit);
|
||||
},
|
||||
},
|
||||
},
|
||||
stacked: {
|
||||
display: isStacked === undefined ? false : 'auto',
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
},
|
||||
point: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
hoverBackgroundColor: (ctx: any) => {
|
||||
if (ctx?.element?.options?.borderColor) {
|
||||
return ctx.element.options.borderColor;
|
||||
}
|
||||
return 'rgba(0,0,0,0.1)';
|
||||
},
|
||||
hoverRadius: 5,
|
||||
},
|
||||
},
|
||||
onClick: (event, element, chart) => {
|
||||
if (onClickHandler) {
|
||||
onClickHandler(event, element, chart, data);
|
||||
}
|
||||
},
|
||||
onHover: (event, _, chart) => {
|
||||
if (event.native) {
|
||||
const interactions = chart.getElementsAtEventForMode(
|
||||
event.native,
|
||||
'nearest',
|
||||
{
|
||||
intersect: false,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
if (interactions[0]) {
|
||||
nearestDatasetIndex.current = interactions[0].datasetIndex;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const chartHasData = hasData(data);
|
||||
const chartPlugins = [];
|
||||
|
||||
if (chartHasData) {
|
||||
chartPlugins.push(createIntersectionCursorPlugin());
|
||||
chartPlugins.push(createDragSelectPlugin());
|
||||
chartPlugins.push(legend(name, data.datasets.length > 3));
|
||||
} else {
|
||||
chartPlugins.push(emptyGraph);
|
||||
const getGridColor = useCallback(() => {
|
||||
if (currentTheme === undefined) {
|
||||
return 'rgba(231,233,237,0.1)';
|
||||
}
|
||||
|
||||
lineChartRef.current = new Chart(chartRef.current, {
|
||||
type,
|
||||
data,
|
||||
options,
|
||||
plugins: chartPlugins,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
animate,
|
||||
title,
|
||||
getGridColor,
|
||||
xAxisTimeUnit?.unitName,
|
||||
xAxisTimeUnit?.stepSize,
|
||||
isStacked,
|
||||
type,
|
||||
data,
|
||||
name,
|
||||
yAxisUnit,
|
||||
onClickHandler,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
currentTheme,
|
||||
]);
|
||||
if (currentTheme === 'dark') {
|
||||
return 'rgba(231,233,237,0.1)';
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
buildChart();
|
||||
}, [buildChart, forceReRender]);
|
||||
return 'rgba(231,233,237,0.8)';
|
||||
}, [currentTheme]);
|
||||
|
||||
return (
|
||||
<div style={{ height: containerHeight }}>
|
||||
<canvas ref={chartRef} />
|
||||
<LegendsContainer id={name} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const buildChart = useCallback(() => {
|
||||
if (lineChartRef.current !== undefined) {
|
||||
lineChartRef.current.destroy();
|
||||
}
|
||||
|
||||
if (chartRef.current !== null) {
|
||||
const options: CustomChartOptions = getGraphOptions(
|
||||
animate,
|
||||
staticLine,
|
||||
title,
|
||||
nearestDatasetIndex,
|
||||
yAxisUnit,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
currentTheme,
|
||||
getGridColor,
|
||||
xAxisTimeUnit,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
data,
|
||||
);
|
||||
|
||||
const chartHasData = hasData(data);
|
||||
const chartPlugins = [];
|
||||
|
||||
if (chartHasData) {
|
||||
chartPlugins.push(createIntersectionCursorPlugin());
|
||||
chartPlugins.push(createDragSelectPlugin());
|
||||
} else {
|
||||
chartPlugins.push(emptyGraph);
|
||||
}
|
||||
|
||||
chartPlugins.push(legend(name, data.datasets.length > 3));
|
||||
|
||||
lineChartRef.current = new Chart(chartRef.current, {
|
||||
type,
|
||||
data,
|
||||
options,
|
||||
plugins: chartPlugins,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
animate,
|
||||
staticLine,
|
||||
title,
|
||||
yAxisUnit,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
currentTheme,
|
||||
getGridColor,
|
||||
xAxisTimeUnit,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
data,
|
||||
name,
|
||||
type,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
buildChart();
|
||||
}, [buildChart, forceReRender]);
|
||||
|
||||
return (
|
||||
<div style={{ height: containerHeight }}>
|
||||
<canvas ref={chartRef} />
|
||||
<LegendsContainer id={name} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
declare module 'chart.js' {
|
||||
interface TooltipPositionerMap {
|
||||
@ -336,45 +187,6 @@ declare module 'chart.js' {
|
||||
}
|
||||
}
|
||||
|
||||
type CustomChartOptions = ChartOptions & {
|
||||
plugins: {
|
||||
[dragSelectPluginId]: DragSelectPluginOptions | false;
|
||||
[intersectionCursorPluginId]: IntersectionCursorPluginOptions | false;
|
||||
};
|
||||
};
|
||||
|
||||
export interface GraphProps {
|
||||
animate?: boolean;
|
||||
type: ChartType;
|
||||
data: Chart['data'];
|
||||
title?: string;
|
||||
isStacked?: boolean;
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
forceReRender?: boolean | null | number;
|
||||
staticLine?: StaticLineProps | undefined;
|
||||
containerHeight?: string | number;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
dragSelectColor?: string;
|
||||
}
|
||||
|
||||
export interface StaticLineProps {
|
||||
yMin: number | undefined;
|
||||
yMax: number | undefined;
|
||||
borderColor: string;
|
||||
borderWidth: number;
|
||||
lineText: string;
|
||||
textColor: string;
|
||||
}
|
||||
|
||||
export type GraphOnClickHandler = (
|
||||
event: ChartEvent,
|
||||
elements: ActiveElement[],
|
||||
chart: Chart,
|
||||
data: ChartData,
|
||||
) => void;
|
||||
|
||||
Graph.defaultProps = {
|
||||
animate: undefined,
|
||||
title: undefined,
|
||||
@ -388,6 +200,8 @@ Graph.defaultProps = {
|
||||
dragSelectColor: undefined,
|
||||
};
|
||||
|
||||
Graph.displayName = 'Graph';
|
||||
|
||||
export default memo(Graph, (prevProps, nextProps) =>
|
||||
isEqual(prevProps.data, nextProps.data),
|
||||
);
|
||||
|
78
frontend/src/components/Graph/types.ts
Normal file
78
frontend/src/components/Graph/types.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import {
|
||||
ActiveElement,
|
||||
Chart,
|
||||
ChartData,
|
||||
ChartEvent,
|
||||
ChartOptions,
|
||||
ChartType,
|
||||
TimeUnit,
|
||||
} from 'chart.js';
|
||||
import { ForwardedRef } from 'react';
|
||||
|
||||
import {
|
||||
dragSelectPluginId,
|
||||
DragSelectPluginOptions,
|
||||
} from './Plugin/DragSelect';
|
||||
import {
|
||||
intersectionCursorPluginId,
|
||||
IntersectionCursorPluginOptions,
|
||||
} from './Plugin/IntersectionCursor';
|
||||
|
||||
export interface StaticLineProps {
|
||||
yMin: number | undefined;
|
||||
yMax: number | undefined;
|
||||
borderColor: string;
|
||||
borderWidth: number;
|
||||
lineText: string;
|
||||
textColor: string;
|
||||
}
|
||||
|
||||
export type GraphOnClickHandler = (
|
||||
event: ChartEvent,
|
||||
elements: ActiveElement[],
|
||||
chart: Chart,
|
||||
data: ChartData,
|
||||
) => void;
|
||||
|
||||
export type ToggleGraphProps = {
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void;
|
||||
};
|
||||
|
||||
export type CustomChartOptions = ChartOptions & {
|
||||
plugins: {
|
||||
[dragSelectPluginId]: DragSelectPluginOptions | false;
|
||||
[intersectionCursorPluginId]: IntersectionCursorPluginOptions | false;
|
||||
};
|
||||
};
|
||||
|
||||
export interface GraphProps {
|
||||
animate?: boolean;
|
||||
type: ChartType;
|
||||
data: Chart['data'];
|
||||
title?: string;
|
||||
isStacked?: boolean;
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
forceReRender?: boolean | null | number;
|
||||
staticLine?: StaticLineProps | undefined;
|
||||
containerHeight?: string | number;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
dragSelectColor?: string;
|
||||
ref?: ForwardedRef<ToggleGraphProps | undefined>;
|
||||
}
|
||||
|
||||
export interface IAxisTimeUintConfig {
|
||||
unitName: TimeUnit;
|
||||
multiplier: number;
|
||||
}
|
||||
|
||||
export interface IAxisTimeConfig {
|
||||
unitName: TimeUnit;
|
||||
stepSize: number;
|
||||
}
|
||||
|
||||
export interface ITimeRange {
|
||||
minTime: number | null;
|
||||
maxTime: number | null;
|
||||
}
|
223
frontend/src/components/Graph/utils.ts
Normal file
223
frontend/src/components/Graph/utils.ts
Normal file
@ -0,0 +1,223 @@
|
||||
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
|
||||
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
||||
import dayjs from 'dayjs';
|
||||
import { MutableRefObject } from 'react';
|
||||
|
||||
import { getAxisLabelColor } from './helpers';
|
||||
import {
|
||||
createDragSelectPluginOptions,
|
||||
dragSelectPluginId,
|
||||
} from './Plugin/DragSelect';
|
||||
import {
|
||||
createIntersectionCursorPluginOptions,
|
||||
intersectionCursorPluginId,
|
||||
} from './Plugin/IntersectionCursor';
|
||||
import {
|
||||
CustomChartOptions,
|
||||
GraphOnClickHandler,
|
||||
IAxisTimeConfig,
|
||||
StaticLineProps,
|
||||
} from './types';
|
||||
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
|
||||
|
||||
export const toggleGraph = (
|
||||
graphIndex: number,
|
||||
isVisible: boolean,
|
||||
lineChartRef: MutableRefObject<Chart | undefined>,
|
||||
): void => {
|
||||
if (lineChartRef && lineChartRef.current) {
|
||||
const { type } = lineChartRef.current?.config as ChartConfiguration;
|
||||
if (type === 'pie' || type === 'doughnut') {
|
||||
lineChartRef.current?.toggleDataVisibility(graphIndex);
|
||||
} else {
|
||||
lineChartRef.current?.setDatasetVisibility(graphIndex, isVisible);
|
||||
}
|
||||
lineChartRef.current?.update();
|
||||
}
|
||||
};
|
||||
|
||||
export const getGraphOptions = (
|
||||
animate: boolean,
|
||||
staticLine: StaticLineProps | undefined,
|
||||
title: string | undefined,
|
||||
nearestDatasetIndex: MutableRefObject<number | null>,
|
||||
yAxisUnit: string,
|
||||
onDragSelect: ((start: number, end: number) => void) | undefined,
|
||||
dragSelectColor: string | undefined,
|
||||
currentTheme: 'dark' | 'light',
|
||||
getGridColor: () => 'rgba(231,233,237,0.1)' | 'rgba(231,233,237,0.8)',
|
||||
xAxisTimeUnit: IAxisTimeConfig,
|
||||
isStacked: boolean | undefined,
|
||||
onClickHandler: GraphOnClickHandler | undefined,
|
||||
data: ChartData,
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
): CustomChartOptions => ({
|
||||
animation: {
|
||||
duration: animate ? 200 : 0,
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
annotation: staticLine
|
||||
? {
|
||||
annotations: [
|
||||
{
|
||||
type: 'line',
|
||||
yMin: staticLine.yMin,
|
||||
yMax: staticLine.yMax,
|
||||
borderColor: staticLine.borderColor,
|
||||
borderWidth: staticLine.borderWidth,
|
||||
label: {
|
||||
content: staticLine.lineText,
|
||||
enabled: true,
|
||||
font: {
|
||||
size: 10,
|
||||
},
|
||||
borderWidth: 0,
|
||||
position: 'start',
|
||||
backgroundColor: 'transparent',
|
||||
color: staticLine.textColor,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
title: {
|
||||
display: title !== undefined,
|
||||
text: title,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title(context): string | string[] {
|
||||
const date = dayjs(context[0].parsed.x);
|
||||
return date.format('MMM DD, YYYY, HH:mm:ss');
|
||||
},
|
||||
label(context): string | string[] {
|
||||
let label = context.dataset.label || '';
|
||||
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += getToolTipValue(context.parsed.y.toString(), yAxisUnit);
|
||||
}
|
||||
|
||||
return label;
|
||||
},
|
||||
labelTextColor(labelData): Color {
|
||||
if (labelData.datasetIndex === nearestDatasetIndex.current) {
|
||||
return 'rgba(255, 255, 255, 1)';
|
||||
}
|
||||
|
||||
return 'rgba(255, 255, 255, 0.75)';
|
||||
},
|
||||
},
|
||||
position: 'custom',
|
||||
itemSort(item1, item2): number {
|
||||
return item2.parsed.y - item1.parsed.y;
|
||||
},
|
||||
},
|
||||
[dragSelectPluginId]: createDragSelectPluginOptions(
|
||||
!!onDragSelect,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
),
|
||||
[intersectionCursorPluginId]: createIntersectionCursorPluginOptions(
|
||||
!!onDragSelect,
|
||||
currentTheme === 'dark' ? 'white' : 'black',
|
||||
),
|
||||
},
|
||||
layout: {
|
||||
padding: 0,
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: true,
|
||||
color: getGridColor(),
|
||||
drawTicks: true,
|
||||
},
|
||||
adapters: {
|
||||
date: chartjsAdapter,
|
||||
},
|
||||
time: {
|
||||
unit: xAxisTimeUnit?.unitName || 'minute',
|
||||
stepSize: xAxisTimeUnit?.stepSize || 1,
|
||||
displayFormats: {
|
||||
millisecond: 'HH:mm:ss',
|
||||
second: 'HH:mm:ss',
|
||||
minute: 'HH:mm',
|
||||
hour: 'MM/dd HH:mm',
|
||||
day: 'MM/dd',
|
||||
week: 'MM/dd',
|
||||
month: 'yy-MM',
|
||||
year: 'yy',
|
||||
},
|
||||
},
|
||||
type: 'time',
|
||||
ticks: { color: getAxisLabelColor(currentTheme) },
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
grid: {
|
||||
display: true,
|
||||
color: getGridColor(),
|
||||
},
|
||||
ticks: {
|
||||
color: getAxisLabelColor(currentTheme),
|
||||
// Include a dollar sign in the ticks
|
||||
callback(value): string {
|
||||
return getYAxisFormattedValue(value.toString(), yAxisUnit);
|
||||
},
|
||||
},
|
||||
},
|
||||
stacked: {
|
||||
display: isStacked === undefined ? false : 'auto',
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
},
|
||||
point: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
hoverBackgroundColor: (ctx: any): string => {
|
||||
if (ctx?.element?.options?.borderColor) {
|
||||
return ctx.element.options.borderColor;
|
||||
}
|
||||
return 'rgba(0,0,0,0.1)';
|
||||
},
|
||||
hoverRadius: 5,
|
||||
},
|
||||
},
|
||||
onClick: (event, element, chart): void => {
|
||||
if (onClickHandler) {
|
||||
onClickHandler(event, element, chart, data);
|
||||
}
|
||||
},
|
||||
onHover: (event, _, chart): void => {
|
||||
if (event.native) {
|
||||
const interactions = chart.getElementsAtEventForMode(
|
||||
event.native,
|
||||
'nearest',
|
||||
{
|
||||
intersect: false,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
if (interactions[0]) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
nearestDatasetIndex.current = interactions[0].datasetIndex;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
@ -4,20 +4,7 @@ import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
interface IAxisTimeUintConfig {
|
||||
unitName: TimeUnit;
|
||||
multiplier: number;
|
||||
}
|
||||
|
||||
interface IAxisTimeConfig {
|
||||
unitName: TimeUnit;
|
||||
stepSize: number;
|
||||
}
|
||||
|
||||
export interface ITimeRange {
|
||||
minTime: number | null;
|
||||
maxTime: number | null;
|
||||
}
|
||||
import { IAxisTimeConfig, IAxisTimeUintConfig, ITimeRange } from './types';
|
||||
|
||||
export const TIME_UNITS: Record<TimeUnit, TimeUnit> = {
|
||||
millisecond: 'millisecond',
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { DrawerProps } from 'antd';
|
||||
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
||||
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
export type LogDetailProps = {
|
||||
log: ILog | null;
|
||||
onClose: () => void;
|
||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||
Pick<ActionItemProps, 'onClickActionItem'>;
|
||||
Pick<ActionItemProps, 'onClickActionItem'> &
|
||||
Pick<DrawerProps, 'onClose'>;
|
||||
|
@ -11,10 +11,6 @@ function LogDetail({
|
||||
onAddToQuery,
|
||||
onClickActionItem,
|
||||
}: LogDetailProps): JSX.Element {
|
||||
const onDrawerClose = (): void => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const items = useMemo(
|
||||
() => [
|
||||
{
|
||||
@ -43,7 +39,7 @@ function LogDetail({
|
||||
title="Log Details"
|
||||
placement="right"
|
||||
closable
|
||||
onClose={onDrawerClose}
|
||||
onClose={onClose}
|
||||
open={log !== null}
|
||||
style={{ overscrollBehavior: 'contain' }}
|
||||
destroyOnClose
|
||||
|
@ -1,9 +1,18 @@
|
||||
import { blue, grey, orange } from '@ant-design/colors';
|
||||
import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
CopyFilled,
|
||||
ExpandAltOutlined,
|
||||
LinkOutlined,
|
||||
MonitorOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import Convert from 'ansi-to-html';
|
||||
import { Button, Divider, Row, Typography } from 'antd';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
// utils
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
@ -85,24 +94,39 @@ function LogSelectedField({
|
||||
|
||||
type ListLogViewProps = {
|
||||
logData: ILog;
|
||||
onOpenDetailedView: (log: ILog) => void;
|
||||
selectedFields: IField[];
|
||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'>;
|
||||
};
|
||||
|
||||
function ListLogView({
|
||||
logData,
|
||||
selectedFields,
|
||||
onOpenDetailedView,
|
||||
onAddToQuery,
|
||||
}: ListLogViewProps): JSX.Element {
|
||||
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
||||
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
|
||||
logData.id,
|
||||
);
|
||||
const {
|
||||
activeLog: activeContextLog,
|
||||
onSetActiveLog: handleSetActiveContextLog,
|
||||
onClearActiveLog: handleClearActiveContextLog,
|
||||
} = useActiveLog();
|
||||
const {
|
||||
activeLog,
|
||||
onSetActiveLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery,
|
||||
} = useActiveLog();
|
||||
|
||||
const handleDetailedView = useCallback(() => {
|
||||
onOpenDetailedView(logData);
|
||||
}, [logData, onOpenDetailedView]);
|
||||
onSetActiveLog(logData);
|
||||
}, [logData, onSetActiveLog]);
|
||||
|
||||
const handleShowContext = useCallback(() => {
|
||||
handleSetActiveContextLog(logData);
|
||||
}, [logData, handleSetActiveContextLog]);
|
||||
|
||||
const handleCopyJSON = (): void => {
|
||||
setCopy(JSON.stringify(logData, null, 2));
|
||||
@ -125,7 +149,7 @@ function ListLogView({
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Container $isActiveLog={isHighlighted}>
|
||||
<div>
|
||||
<LogContainer>
|
||||
<>
|
||||
@ -169,6 +193,42 @@ function ListLogView({
|
||||
>
|
||||
Copy JSON
|
||||
</Button>
|
||||
|
||||
{isLogsExplorerPage && (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={handleShowContext}
|
||||
style={{ color: grey[1] }}
|
||||
icon={<MonitorOutlined />}
|
||||
>
|
||||
Show in Context
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={onLogCopy}
|
||||
style={{ color: grey[1] }}
|
||||
icon={<LinkOutlined />}
|
||||
>
|
||||
Copy Link
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeContextLog && (
|
||||
<LogsExplorerContext
|
||||
log={activeContextLog}
|
||||
onClose={handleClearActiveContextLog}
|
||||
/>
|
||||
)}
|
||||
<LogDetail
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
|
@ -1,12 +1,17 @@
|
||||
import { Card, Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { getActiveLogBackground } from 'utils/logs';
|
||||
|
||||
export const Container = styled(Card)`
|
||||
export const Container = styled(Card)<{
|
||||
$isActiveLog: boolean;
|
||||
}>`
|
||||
width: 100% !important;
|
||||
margin-bottom: 0.3rem;
|
||||
.ant-card-body {
|
||||
padding: 0.3rem 0.6rem;
|
||||
}
|
||||
|
||||
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
||||
`;
|
||||
|
||||
export const Text = styled(Typography.Text)`
|
||||
|
@ -1,16 +1,32 @@
|
||||
import { ExpandAltOutlined } from '@ant-design/icons';
|
||||
// const Convert = require('ansi-to-html');
|
||||
import {
|
||||
ExpandAltOutlined,
|
||||
LinkOutlined,
|
||||
MonitorOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import Convert from 'ansi-to-html';
|
||||
import { Button, DrawerProps, Tooltip } from 'antd';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
// hooks
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
KeyboardEvent,
|
||||
MouseEvent,
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
// interfaces
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
// styles
|
||||
import {
|
||||
ActionButtonsWrapper,
|
||||
ExpandIconWrapper,
|
||||
RawLogContent,
|
||||
RawLogViewContainer,
|
||||
@ -19,15 +35,34 @@ import {
|
||||
const convert = new Convert();
|
||||
|
||||
interface RawLogViewProps {
|
||||
isActiveLog?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
data: ILog;
|
||||
linesPerRow: number;
|
||||
onClickExpand: (log: ILog) => void;
|
||||
}
|
||||
|
||||
function RawLogView(props: RawLogViewProps): JSX.Element {
|
||||
const { data, linesPerRow, onClickExpand } = props;
|
||||
const { isActiveLog = false, isReadOnly = false, data, linesPerRow } = props;
|
||||
|
||||
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
|
||||
data.id,
|
||||
);
|
||||
const {
|
||||
activeLog: activeContextLog,
|
||||
onSetActiveLog: handleSetActiveContextLog,
|
||||
onClearActiveLog: handleClearActiveContextLog,
|
||||
} = useActiveLog();
|
||||
const {
|
||||
activeLog,
|
||||
onSetActiveLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery,
|
||||
} = useActiveLog();
|
||||
|
||||
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
|
||||
|
||||
const text = useMemo(
|
||||
() =>
|
||||
@ -38,8 +73,43 @@ function RawLogView(props: RawLogViewProps): JSX.Element {
|
||||
);
|
||||
|
||||
const handleClickExpand = useCallback(() => {
|
||||
onClickExpand(data);
|
||||
}, [onClickExpand, data]);
|
||||
if (activeContextLog || isReadOnly) return;
|
||||
|
||||
onSetActiveLog(data);
|
||||
}, [activeContextLog, isReadOnly, data, onSetActiveLog]);
|
||||
|
||||
const handleCloseLogDetail: DrawerProps['onClose'] = useCallback(
|
||||
(
|
||||
event: MouseEvent<Element, globalThis.MouseEvent> | KeyboardEvent<Element>,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onClearActiveLog();
|
||||
},
|
||||
[onClearActiveLog],
|
||||
);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
if (isReadOnlyLog) return;
|
||||
|
||||
setHasActionButtons(true);
|
||||
}, [isReadOnlyLog]);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
if (isReadOnlyLog) return;
|
||||
|
||||
setHasActionButtons(false);
|
||||
}, [isReadOnlyLog]);
|
||||
|
||||
const handleShowContext: MouseEventHandler<HTMLElement> = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleSetActiveContextLog(data);
|
||||
},
|
||||
[data, handleSetActiveContextLog],
|
||||
);
|
||||
|
||||
const html = useMemo(
|
||||
() => ({
|
||||
@ -48,19 +118,69 @@ function RawLogView(props: RawLogViewProps): JSX.Element {
|
||||
[text],
|
||||
);
|
||||
|
||||
const mouseActions = useMemo(
|
||||
() => ({ onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave }),
|
||||
[handleMouseEnter, handleMouseLeave],
|
||||
);
|
||||
|
||||
return (
|
||||
<RawLogViewContainer
|
||||
onClick={handleClickExpand}
|
||||
wrap={false}
|
||||
align="middle"
|
||||
$isDarkMode={isDarkMode}
|
||||
$isReadOnly={isReadOnly}
|
||||
$isActiveLog={isHighlighted}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...mouseActions}
|
||||
>
|
||||
<ExpandIconWrapper flex="30px">
|
||||
<ExpandAltOutlined />
|
||||
</ExpandIconWrapper>
|
||||
<RawLogContent linesPerRow={linesPerRow} dangerouslySetInnerHTML={html} />
|
||||
{!isReadOnly && (
|
||||
<ExpandIconWrapper flex="30px">
|
||||
<ExpandAltOutlined />
|
||||
</ExpandIconWrapper>
|
||||
)}
|
||||
|
||||
<RawLogContent
|
||||
$isReadOnly={isReadOnly}
|
||||
$isActiveLog={isActiveLog}
|
||||
linesPerRow={linesPerRow}
|
||||
dangerouslySetInnerHTML={html}
|
||||
/>
|
||||
|
||||
{hasActionButtons && (
|
||||
<ActionButtonsWrapper>
|
||||
<Tooltip title="Show Context">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<MonitorOutlined />}
|
||||
onClick={handleShowContext}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Copy Link">
|
||||
<Button size="small" icon={<LinkOutlined />} onClick={onLogCopy} />
|
||||
</Tooltip>
|
||||
</ActionButtonsWrapper>
|
||||
)}
|
||||
|
||||
{activeContextLog && (
|
||||
<LogsExplorerContext
|
||||
log={activeContextLog}
|
||||
onClose={handleClearActiveContextLog}
|
||||
/>
|
||||
)}
|
||||
<LogDetail
|
||||
log={activeLog}
|
||||
onClose={handleCloseLogDetail}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
</RawLogViewContainer>
|
||||
);
|
||||
}
|
||||
|
||||
RawLogView.defaultProps = {
|
||||
isActiveLog: false,
|
||||
isReadOnly: false,
|
||||
};
|
||||
|
||||
export default RawLogView;
|
||||
|
@ -1,8 +1,14 @@
|
||||
import { blue } from '@ant-design/colors';
|
||||
import { Col, Row } from 'antd';
|
||||
import { Col, Row, Space } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs';
|
||||
|
||||
export const RawLogViewContainer = styled(Row)<{ $isDarkMode: boolean }>`
|
||||
export const RawLogViewContainer = styled(Row)<{
|
||||
$isDarkMode: boolean;
|
||||
$isReadOnly: boolean;
|
||||
$isActiveLog: boolean;
|
||||
}>`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
font-weight: 700;
|
||||
font-size: 0.625rem;
|
||||
@ -10,10 +16,12 @@ export const RawLogViewContainer = styled(Row)<{ $isDarkMode: boolean }>`
|
||||
|
||||
transition: background-color 0.2s ease-in;
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ $isDarkMode }): string =>
|
||||
$isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0, 0, 0, 0.1)'};
|
||||
}
|
||||
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
||||
|
||||
${({ $isReadOnly, $isDarkMode, $isActiveLog }): string =>
|
||||
$isActiveLog
|
||||
? getActiveLogBackground()
|
||||
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
|
||||
`;
|
||||
|
||||
export const ExpandIconWrapper = styled(Col)`
|
||||
@ -25,6 +33,8 @@ export const ExpandIconWrapper = styled(Col)`
|
||||
|
||||
interface RawLogContentProps {
|
||||
linesPerRow: number;
|
||||
$isReadOnly: boolean;
|
||||
$isActiveLog: boolean;
|
||||
}
|
||||
|
||||
export const RawLogContent = styled.div<RawLogContentProps>`
|
||||
@ -42,5 +52,17 @@ export const RawLogContent = styled.div<RawLogContentProps>`
|
||||
font-size: 1rem;
|
||||
line-height: 2rem;
|
||||
|
||||
cursor: ${(props): string =>
|
||||
props.$isActiveLog || props.$isReadOnly ? 'initial' : 'pointer'};
|
||||
|
||||
${(props): string =>
|
||||
props.$isReadOnly && !props.$isActiveLog ? 'padding: 0 1.5rem;' : ''}
|
||||
`;
|
||||
|
||||
export const ActionButtonsWrapper = styled(Space)`
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 50%;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
@ -10,7 +10,6 @@ export type LogsTableViewProps = {
|
||||
logs: ILog[];
|
||||
fields: IField[];
|
||||
linesPerRow: number;
|
||||
onClickExpand: (log: ILog) => void;
|
||||
};
|
||||
|
||||
export type UseTableViewResult = {
|
||||
@ -20,4 +19,12 @@ export type UseTableViewResult = {
|
||||
|
||||
export type UseTableViewProps = {
|
||||
appendTo?: 'center' | 'end';
|
||||
onOpenLogsContext?: (log: ILog) => void;
|
||||
onClickExpand?: (log: ILog) => void;
|
||||
} & LogsTableViewProps;
|
||||
|
||||
export type ActionsColumnProps = {
|
||||
logId: string;
|
||||
logs: ILog[];
|
||||
onOpenLogsContext?: (log: ILog) => void;
|
||||
};
|
||||
|
@ -1,16 +1,22 @@
|
||||
import { ExpandAltOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
ExpandAltOutlined,
|
||||
LinkOutlined,
|
||||
MonitorOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import Convert from 'ansi-to-html';
|
||||
import { Typography } from 'antd';
|
||||
import { Button, Space, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { ExpandIconWrapper } from '../RawLogView/styles';
|
||||
import { defaultCellStyle, defaultTableStyle } from './config';
|
||||
import { TableBodyContent } from './styles';
|
||||
import {
|
||||
ActionsColumnProps,
|
||||
ColumnTypeRender,
|
||||
UseTableViewProps,
|
||||
UseTableViewResult,
|
||||
@ -18,19 +24,60 @@ import {
|
||||
|
||||
const convert = new Convert();
|
||||
|
||||
function ActionsColumn({
|
||||
logId,
|
||||
logs,
|
||||
onOpenLogsContext,
|
||||
}: ActionsColumnProps): JSX.Element {
|
||||
const currentLog = useMemo(() => logs.find(({ id }) => id === logId), [
|
||||
logs,
|
||||
logId,
|
||||
]);
|
||||
|
||||
const { onLogCopy } = useCopyLogLink(currentLog?.id);
|
||||
|
||||
const handleShowContext = useCallback(() => {
|
||||
if (!onOpenLogsContext || !currentLog) return;
|
||||
|
||||
onOpenLogsContext(currentLog);
|
||||
}, [currentLog, onOpenLogsContext]);
|
||||
|
||||
return (
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleShowContext}
|
||||
icon={<MonitorOutlined />}
|
||||
/>
|
||||
<Button size="small" onClick={onLogCopy} icon={<LinkOutlined />} />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
const {
|
||||
logs,
|
||||
fields,
|
||||
linesPerRow,
|
||||
onClickExpand,
|
||||
appendTo = 'center',
|
||||
onOpenLogsContext,
|
||||
onClickExpand,
|
||||
} = props;
|
||||
const { isLogsExplorerPage } = useCopyLogLink();
|
||||
|
||||
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
|
||||
logs,
|
||||
]);
|
||||
|
||||
const handleClickExpand = useCallback(
|
||||
(index: number): void => {
|
||||
if (!onClickExpand) return;
|
||||
|
||||
onClickExpand(logs[index]);
|
||||
},
|
||||
[logs, onClickExpand],
|
||||
);
|
||||
|
||||
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
||||
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
||||
.filter((e) => e.name !== 'id')
|
||||
@ -63,7 +110,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
children: (
|
||||
<ExpandIconWrapper
|
||||
onClick={(): void => {
|
||||
onClickExpand(logs[index]);
|
||||
handleClickExpand(index);
|
||||
}}
|
||||
>
|
||||
<ExpandAltOutlined />
|
||||
@ -106,8 +153,34 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
}),
|
||||
},
|
||||
...(appendTo === 'end' ? fieldColumns : []),
|
||||
...(isLogsExplorerPage
|
||||
? ([
|
||||
{
|
||||
title: 'actions',
|
||||
dataIndex: 'actions',
|
||||
key: 'actions',
|
||||
render: (_, log): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
children: (
|
||||
<ActionsColumn
|
||||
logId={(log.id as unknown) as string}
|
||||
logs={logs}
|
||||
onOpenLogsContext={onOpenLogsContext}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
},
|
||||
] as ColumnsType<Record<string, unknown>>)
|
||||
: []),
|
||||
];
|
||||
}, [fields, appendTo, linesPerRow, onClickExpand, logs]);
|
||||
}, [
|
||||
logs,
|
||||
fields,
|
||||
appendTo,
|
||||
linesPerRow,
|
||||
isLogsExplorerPage,
|
||||
handleClickExpand,
|
||||
onOpenLogsContext,
|
||||
]);
|
||||
|
||||
return { columns, dataSource: flattenLogData };
|
||||
};
|
||||
|
22
frontend/src/components/TableRenderer/utils.ts
Normal file
22
frontend/src/components/TableRenderer/utils.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
|
||||
export const generatorResizeTableColumns = <T>({
|
||||
baseColumnOptions,
|
||||
dynamicColumnOption,
|
||||
}: GeneratorResizeTableColumnsProp<T>): ColumnsType<T> =>
|
||||
baseColumnOptions.map((config: ColumnType<T>) => {
|
||||
const { key } = config;
|
||||
const extraConfig = dynamicColumnOption.find(
|
||||
(dynamicConfigItem) => dynamicConfigItem.key === key,
|
||||
);
|
||||
return {
|
||||
...config,
|
||||
...extraConfig?.columnOption,
|
||||
};
|
||||
});
|
||||
|
||||
interface GeneratorResizeTableColumnsProp<T> {
|
||||
baseColumnOptions: ColumnsType<T>;
|
||||
dynamicColumnOption: { key: string; columnOption: ColumnType<T> }[];
|
||||
}
|
4
frontend/src/constants/events.ts
Normal file
4
frontend/src/constants/events.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum Events {
|
||||
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
|
||||
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
|
||||
}
|
@ -8,6 +8,7 @@ export enum LOCALSTORAGE {
|
||||
LOGS_LINES_PER_ROW = 'LOGS_LINES_PER_ROW',
|
||||
LOGS_LIST_OPTIONS = 'LOGS_LIST_OPTIONS',
|
||||
TRACES_LIST_OPTIONS = 'TRACES_LIST_OPTIONS',
|
||||
GRAPH_VISIBILITY_STATES = 'GRAPH_VISIBILITY_STATES',
|
||||
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
|
||||
}
|
||||
|
@ -16,4 +16,6 @@ export enum QueryParams {
|
||||
widgetId = 'widgetId',
|
||||
order = 'order',
|
||||
q = 'q',
|
||||
activeLogId = 'activeLogId',
|
||||
timeRange = 'timeRange',
|
||||
}
|
||||
|
@ -32,6 +32,8 @@ const ROUTES = {
|
||||
HOME_PAGE: '/',
|
||||
PASSWORD_RESET: '/password-reset',
|
||||
LIST_LICENSES: '/licenses',
|
||||
TRACE_EXPLORER: '/trace-explorer',
|
||||
PIPELINES: '/pipelines',
|
||||
};
|
||||
|
||||
export default ROUTES;
|
||||
|
@ -38,11 +38,20 @@ const themeColors = {
|
||||
whiteCream: '#ffffffd5',
|
||||
white: '#ffffff',
|
||||
black: '#000000',
|
||||
darkGrey: '#262626',
|
||||
lightBlack: '#141414',
|
||||
lightgrey: '#ddd',
|
||||
lightWhite: '#ffffffd9',
|
||||
borderLightGrey: '#d9d9d9',
|
||||
borderDarkGrey: '#424242',
|
||||
gainsboro: '#DBDBDB',
|
||||
navyBlue: '#1668DC',
|
||||
lightSkyBlue: '#8DCFF8',
|
||||
neroBlack: '#1d1d1d',
|
||||
snowWhite: '#fafafa',
|
||||
gamboge: '#D89614',
|
||||
bckgGrey: '#1d1d1d',
|
||||
lightBlue: '#177ddc',
|
||||
};
|
||||
|
||||
export { themeColors };
|
||||
|
@ -6,7 +6,8 @@ import Header from 'container/Header';
|
||||
import SideNav from 'container/SideNav';
|
||||
import TopNav from 'container/TopNav';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { ReactNode, useEffect, useRef } from 'react';
|
||||
import { ReactNode, useEffect, useMemo, useRef } from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@ -25,13 +26,14 @@ import {
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import { ChildrenContainer, Layout } from './styles';
|
||||
import { getRouteKey } from './utils';
|
||||
|
||||
function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const { isLoggedIn, user } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
const { pathname } = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation(['titles']);
|
||||
|
||||
const [
|
||||
getUserVersionResponse,
|
||||
@ -226,8 +228,15 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
const isToDisplayLayout = isLoggedIn;
|
||||
|
||||
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
|
||||
const pageTitle = t(routeKey);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Helmet>
|
||||
<title>{pageTitle}</title>
|
||||
</Helmet>
|
||||
|
||||
{isToDisplayLayout && <Header />}
|
||||
<Layout>
|
||||
{isToDisplayLayout && <SideNav />}
|
||||
|
9
frontend/src/container/AppLayout/utils.ts
Normal file
9
frontend/src/container/AppLayout/utils.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
export function getRouteKey(pathname: string): string {
|
||||
const [routeKey] = Object.entries(ROUTES).find(
|
||||
([, value]) => value === pathname,
|
||||
) || ['DEFAULT'];
|
||||
|
||||
return routeKey;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { StaticLineProps } from 'components/Graph';
|
||||
import { StaticLineProps } from 'components/Graph/types';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
|
@ -0,0 +1,207 @@
|
||||
import { Button, Input } from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { Events } from 'constants/events';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
|
||||
import { getGraphVisibilityStateOnDataChange } from '../utils';
|
||||
import {
|
||||
FilterTableAndSaveContainer,
|
||||
FilterTableContainer,
|
||||
SaveCancelButtonContainer,
|
||||
SaveContainer,
|
||||
} from './styles';
|
||||
import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns';
|
||||
import { ExtendedChartDataset, GraphManagerProps } from './types';
|
||||
import {
|
||||
getDefaultTableDataSet,
|
||||
saveLegendEntriesToLocalStorage,
|
||||
} from './utils';
|
||||
|
||||
function GraphManager({
|
||||
data,
|
||||
name,
|
||||
yAxisUnit,
|
||||
onToggleModelHandler,
|
||||
}: GraphManagerProps): JSX.Element {
|
||||
const {
|
||||
graphVisibilityStates: localstoredVisibilityStates,
|
||||
legendEntry,
|
||||
} = useMemo(
|
||||
() =>
|
||||
getGraphVisibilityStateOnDataChange({
|
||||
data,
|
||||
isExpandedName: false,
|
||||
name,
|
||||
}),
|
||||
[data, name],
|
||||
);
|
||||
|
||||
const [graphVisibilityState, setGraphVisibilityState] = useState<boolean[]>(
|
||||
localstoredVisibilityStates,
|
||||
);
|
||||
|
||||
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(
|
||||
getDefaultTableDataSet(data),
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
// useEffect for updating graph visibility state on data change
|
||||
useEffect(() => {
|
||||
const newGraphVisibilityStates = Array<boolean>(data.datasets.length).fill(
|
||||
true,
|
||||
);
|
||||
data.datasets.forEach((dataset, i) => {
|
||||
const index = legendEntry.findIndex(
|
||||
(entry) => entry.label === dataset.label,
|
||||
);
|
||||
if (index !== -1) {
|
||||
newGraphVisibilityStates[i] = legendEntry[index].show;
|
||||
}
|
||||
});
|
||||
eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, {
|
||||
name,
|
||||
graphVisibilityStates: newGraphVisibilityStates,
|
||||
});
|
||||
setGraphVisibilityState(newGraphVisibilityStates);
|
||||
}, [data, name, legendEntry]);
|
||||
|
||||
// useEffect for listening to events event graph legend is clicked
|
||||
useEffect(() => {
|
||||
const eventListener = eventEmitter.on(
|
||||
Events.UPDATE_GRAPH_MANAGER_TABLE,
|
||||
(data) => {
|
||||
if (data.name === name) {
|
||||
const newGraphVisibilityStates = graphVisibilityState;
|
||||
newGraphVisibilityStates[data.index] = !newGraphVisibilityStates[
|
||||
data.index
|
||||
];
|
||||
eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, {
|
||||
name,
|
||||
graphVisibilityStates: newGraphVisibilityStates,
|
||||
});
|
||||
setGraphVisibilityState([...newGraphVisibilityStates]);
|
||||
}
|
||||
},
|
||||
);
|
||||
return (): void => {
|
||||
eventListener.off(Events.UPDATE_GRAPH_MANAGER_TABLE);
|
||||
};
|
||||
}, [graphVisibilityState, name]);
|
||||
|
||||
const checkBoxOnChangeHandler = useCallback(
|
||||
(e: CheckboxChangeEvent, index: number): void => {
|
||||
graphVisibilityState[index] = e.target.checked;
|
||||
setGraphVisibilityState([...graphVisibilityState]);
|
||||
eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, {
|
||||
name,
|
||||
graphVisibilityStates: [...graphVisibilityState],
|
||||
});
|
||||
},
|
||||
[graphVisibilityState, name],
|
||||
);
|
||||
|
||||
const labelClickedHandler = useCallback(
|
||||
(labelIndex: number): void => {
|
||||
const newGraphVisibilityStates = Array<boolean>(data.datasets.length).fill(
|
||||
false,
|
||||
);
|
||||
newGraphVisibilityStates[labelIndex] = true;
|
||||
setGraphVisibilityState([...newGraphVisibilityStates]);
|
||||
eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, {
|
||||
name,
|
||||
graphVisibilityStates: newGraphVisibilityStates,
|
||||
});
|
||||
},
|
||||
[data.datasets.length, name],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getGraphManagerTableColumns({
|
||||
data,
|
||||
checkBoxOnChangeHandler,
|
||||
graphVisibilityState,
|
||||
labelClickedHandler,
|
||||
yAxisUnit,
|
||||
}),
|
||||
[
|
||||
checkBoxOnChangeHandler,
|
||||
data,
|
||||
graphVisibilityState,
|
||||
labelClickedHandler,
|
||||
yAxisUnit,
|
||||
],
|
||||
);
|
||||
|
||||
const filterHandler = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const value = event.target.value.toString().toLowerCase();
|
||||
const updatedDataSet = tableDataSet.map((item) => {
|
||||
if (item.label?.toLocaleLowerCase().includes(value)) {
|
||||
return { ...item, show: true };
|
||||
}
|
||||
return { ...item, show: false };
|
||||
});
|
||||
setTableDataSet(updatedDataSet);
|
||||
},
|
||||
[tableDataSet],
|
||||
);
|
||||
|
||||
const saveHandler = useCallback((): void => {
|
||||
saveLegendEntriesToLocalStorage({
|
||||
data,
|
||||
graphVisibilityState,
|
||||
name,
|
||||
});
|
||||
notifications.success({
|
||||
message: 'The updated graphs & legends are saved',
|
||||
});
|
||||
if (onToggleModelHandler) {
|
||||
onToggleModelHandler();
|
||||
}
|
||||
}, [data, graphVisibilityState, name, notifications, onToggleModelHandler]);
|
||||
|
||||
const dataSource = tableDataSet.filter((item) => item.show);
|
||||
|
||||
return (
|
||||
<FilterTableAndSaveContainer>
|
||||
<FilterTableContainer>
|
||||
<Input onChange={filterHandler} placeholder="Filter Series" />
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
rowKey="index"
|
||||
pagination={false}
|
||||
scroll={{ y: 240 }}
|
||||
/>
|
||||
</FilterTableContainer>
|
||||
<SaveContainer>
|
||||
<SaveCancelButtonContainer>
|
||||
<Button type="default" onClick={onToggleModelHandler}>
|
||||
Cancel
|
||||
</Button>
|
||||
</SaveCancelButtonContainer>
|
||||
<SaveCancelButtonContainer>
|
||||
<Button onClick={saveHandler} type="primary">
|
||||
Save
|
||||
</Button>
|
||||
</SaveCancelButtonContainer>
|
||||
</SaveContainer>
|
||||
</FilterTableAndSaveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
GraphManager.defaultProps = {
|
||||
graphVisibilityStateHandler: undefined,
|
||||
};
|
||||
|
||||
export default memo(
|
||||
GraphManager,
|
||||
(prevProps, nextProps) =>
|
||||
isEqual(prevProps.data, nextProps.data) && prevProps.name === nextProps.name,
|
||||
);
|
@ -0,0 +1,33 @@
|
||||
import { Checkbox, ConfigProvider } from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
|
||||
import { CheckBoxProps } from '../types';
|
||||
|
||||
function CustomCheckBox({
|
||||
data,
|
||||
index,
|
||||
graphVisibilityState,
|
||||
checkBoxOnChangeHandler,
|
||||
}: CheckBoxProps): JSX.Element {
|
||||
const { datasets } = data;
|
||||
|
||||
const onChangeHandler = (e: CheckboxChangeEvent): void => {
|
||||
checkBoxOnChangeHandler(e, index);
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: datasets[index].borderColor?.toString(),
|
||||
colorBorder: datasets[index].borderColor?.toString(),
|
||||
colorBgContainer: datasets[index].borderColor?.toString(),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Checkbox onChange={onChangeHandler} checked={graphVisibilityState[index]} />
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomCheckBox;
|
@ -0,0 +1,27 @@
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import { ChartData } from 'chart.js';
|
||||
|
||||
import { DataSetProps } from '../types';
|
||||
import CustomCheckBox from './CustomCheckBox';
|
||||
|
||||
export const getCheckBox = ({
|
||||
data,
|
||||
checkBoxOnChangeHandler,
|
||||
graphVisibilityState,
|
||||
}: GetCheckBoxProps): ColumnType<DataSetProps> => ({
|
||||
render: (index: number): JSX.Element => (
|
||||
<CustomCheckBox
|
||||
data={data}
|
||||
index={index}
|
||||
checkBoxOnChangeHandler={checkBoxOnChangeHandler}
|
||||
graphVisibilityState={graphVisibilityState}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
interface GetCheckBoxProps {
|
||||
data: ChartData;
|
||||
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
||||
graphVisibilityState: boolean[];
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
|
||||
import { DataSetProps } from '../types';
|
||||
import Label from './Label';
|
||||
|
||||
export const getLabel = (
|
||||
labelClickedHandler: (labelIndex: number) => void,
|
||||
): ColumnType<DataSetProps> => ({
|
||||
render: (label: string, _, index): JSX.Element => (
|
||||
<Label
|
||||
label={label}
|
||||
labelIndex={index}
|
||||
labelClickedHandler={labelClickedHandler}
|
||||
/>
|
||||
),
|
||||
});
|
@ -0,0 +1,80 @@
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import { ChartData } from 'chart.js';
|
||||
|
||||
import { ColumnsKeyAndDataIndex, ColumnsTitle } from '../contants';
|
||||
import { DataSetProps } from '../types';
|
||||
import { getGraphManagerTableHeaderTitle } from '../utils';
|
||||
import { getCheckBox } from './GetCheckBox';
|
||||
import { getLabel } from './GetLabel';
|
||||
|
||||
export const getGraphManagerTableColumns = ({
|
||||
data,
|
||||
checkBoxOnChangeHandler,
|
||||
graphVisibilityState,
|
||||
labelClickedHandler,
|
||||
yAxisUnit,
|
||||
}: GetGraphManagerTableColumnsProps): ColumnType<DataSetProps>[] => [
|
||||
{
|
||||
title: '',
|
||||
width: 50,
|
||||
dataIndex: ColumnsKeyAndDataIndex.Index,
|
||||
key: ColumnsKeyAndDataIndex.Index,
|
||||
...getCheckBox({
|
||||
checkBoxOnChangeHandler,
|
||||
graphVisibilityState,
|
||||
data,
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: ColumnsTitle[ColumnsKeyAndDataIndex.Label],
|
||||
width: 300,
|
||||
dataIndex: ColumnsKeyAndDataIndex.Label,
|
||||
key: ColumnsKeyAndDataIndex.Label,
|
||||
...getLabel(labelClickedHandler),
|
||||
},
|
||||
{
|
||||
title: getGraphManagerTableHeaderTitle(
|
||||
ColumnsTitle[ColumnsKeyAndDataIndex.Avg],
|
||||
yAxisUnit,
|
||||
),
|
||||
width: 90,
|
||||
dataIndex: ColumnsKeyAndDataIndex.Avg,
|
||||
key: ColumnsKeyAndDataIndex.Avg,
|
||||
},
|
||||
{
|
||||
title: getGraphManagerTableHeaderTitle(
|
||||
ColumnsTitle[ColumnsKeyAndDataIndex.Sum],
|
||||
yAxisUnit,
|
||||
),
|
||||
width: 90,
|
||||
dataIndex: ColumnsKeyAndDataIndex.Sum,
|
||||
key: ColumnsKeyAndDataIndex.Sum,
|
||||
},
|
||||
{
|
||||
title: getGraphManagerTableHeaderTitle(
|
||||
ColumnsTitle[ColumnsKeyAndDataIndex.Max],
|
||||
yAxisUnit,
|
||||
),
|
||||
width: 90,
|
||||
dataIndex: ColumnsKeyAndDataIndex.Max,
|
||||
key: ColumnsKeyAndDataIndex.Max,
|
||||
},
|
||||
{
|
||||
title: getGraphManagerTableHeaderTitle(
|
||||
ColumnsTitle[ColumnsKeyAndDataIndex.Min],
|
||||
yAxisUnit,
|
||||
),
|
||||
width: 90,
|
||||
dataIndex: ColumnsKeyAndDataIndex.Min,
|
||||
key: ColumnsKeyAndDataIndex.Min,
|
||||
},
|
||||
];
|
||||
|
||||
interface GetGraphManagerTableColumnsProps {
|
||||
data: ChartData;
|
||||
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
||||
labelClickedHandler: (labelIndex: number) => void;
|
||||
graphVisibilityState: boolean[];
|
||||
yAxisUnit?: string;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { LabelContainer } from '../styles';
|
||||
import { LabelProps } from '../types';
|
||||
import { getAbbreviatedLabel } from '../utils';
|
||||
|
||||
function Label({
|
||||
labelClickedHandler,
|
||||
labelIndex,
|
||||
label,
|
||||
}: LabelProps): JSX.Element {
|
||||
const onClickHandler = (): void => {
|
||||
labelClickedHandler(labelIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
<LabelContainer type="button" onClick={onClickHandler}>
|
||||
{getAbbreviatedLabel(label)}
|
||||
</LabelContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Label;
|
@ -0,0 +1,29 @@
|
||||
import { PanelTypeAndGraphManagerVisibilityProps } from './types';
|
||||
|
||||
export enum ColumnsKeyAndDataIndex {
|
||||
Index = 'index',
|
||||
Legend = 'legend',
|
||||
Label = 'label',
|
||||
Avg = 'avg',
|
||||
Sum = 'sum',
|
||||
Max = 'max',
|
||||
Min = 'min',
|
||||
}
|
||||
|
||||
export const ColumnsTitle = {
|
||||
[ColumnsKeyAndDataIndex.Index]: 'Index',
|
||||
[ColumnsKeyAndDataIndex.Label]: 'Label',
|
||||
[ColumnsKeyAndDataIndex.Avg]: 'Avg',
|
||||
[ColumnsKeyAndDataIndex.Sum]: 'Sum',
|
||||
[ColumnsKeyAndDataIndex.Max]: 'Max',
|
||||
[ColumnsKeyAndDataIndex.Min]: 'Min',
|
||||
};
|
||||
|
||||
export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityProps = {
|
||||
TIME_SERIES: true,
|
||||
VALUE: false,
|
||||
TABLE: false,
|
||||
LIST: false,
|
||||
TRACE: false,
|
||||
EMPTY_WIDGET: false,
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import { Button } from 'antd';
|
||||
import { GraphOnClickHandler } from 'components/Graph';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import Spinner from 'components/Spinner';
|
||||
import TimePreference from 'components/TimePreferenceDropDown';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
@ -9,15 +9,20 @@ import {
|
||||
} from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { useChartMutable } from 'hooks/useChartMutable';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { TimeContainer } from './styles';
|
||||
import { toggleGraphsVisibilityInChart } from '../utils';
|
||||
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
||||
import GraphManager from './GraphManager';
|
||||
import { GraphContainer, TimeContainer } from './styles';
|
||||
import { FullViewProps } from './types';
|
||||
import { getIsGraphLegendToggleAvailable } from './utils';
|
||||
|
||||
function FullView({
|
||||
widget,
|
||||
@ -27,6 +32,8 @@ function FullView({
|
||||
yAxisUnit,
|
||||
onDragSelect,
|
||||
isDependedDataLoaded = false,
|
||||
graphsVisibilityStates,
|
||||
onToggleModelHandler,
|
||||
}: FullViewProps): JSX.Element {
|
||||
const { selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
@ -39,6 +46,22 @@ function FullView({
|
||||
[widget],
|
||||
);
|
||||
|
||||
const canModifyChart = useChartMutable({
|
||||
panelType: widget.panelTypes,
|
||||
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
||||
});
|
||||
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
|
||||
useEffect(() => {
|
||||
if (graphsVisibilityStates && canModifyChart && lineChartRef.current) {
|
||||
toggleGraphsVisibilityInChart({
|
||||
graphsVisibilityStates,
|
||||
lineChartRef,
|
||||
});
|
||||
}
|
||||
}, [graphsVisibilityStates, canModifyChart]);
|
||||
|
||||
const [selectedTime, setSelectedTime] = useState<timePreferance>({
|
||||
name: getSelectedTime()?.name || '',
|
||||
enum: widget?.timePreferance || 'GLOBAL_TIME',
|
||||
@ -78,7 +101,11 @@ function FullView({
|
||||
[response],
|
||||
);
|
||||
|
||||
if (response.status === 'idle' || response.status === 'loading') {
|
||||
const isGraphLegendToggleAvailable = getIsGraphLegendToggleAvailable(
|
||||
widget.panelTypes,
|
||||
);
|
||||
|
||||
if (response.isFetching) {
|
||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||
}
|
||||
|
||||
@ -101,33 +128,35 @@ function FullView({
|
||||
</TimeContainer>
|
||||
)}
|
||||
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={chartDataSet}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={widget.title}
|
||||
onClickHandler={onClickHandler}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
panelData={response.data?.payload.data.newResult.data.result || []}
|
||||
query={widget.query}
|
||||
/>
|
||||
<GraphContainer isGraphLegendToggleAvailable={isGraphLegendToggleAvailable}>
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={chartDataSet}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={widget.title}
|
||||
onClickHandler={onClickHandler}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
panelData={response.data?.payload.data.newResult.data.result || []}
|
||||
query={widget.query}
|
||||
ref={lineChartRef}
|
||||
/>
|
||||
</GraphContainer>
|
||||
|
||||
{canModifyChart && (
|
||||
<GraphManager
|
||||
data={chartDataSet}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface FullViewProps {
|
||||
widget: Widgets;
|
||||
fullViewOptions?: boolean;
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
isDependedDataLoaded?: boolean;
|
||||
}
|
||||
|
||||
FullView.defaultProps = {
|
||||
fullViewOptions: undefined,
|
||||
onClickHandler: undefined,
|
||||
@ -136,4 +165,6 @@ FullView.defaultProps = {
|
||||
isDependedDataLoaded: undefined,
|
||||
};
|
||||
|
||||
FullView.displayName = 'FullView';
|
||||
|
||||
export default FullView;
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
|
||||
|
||||
import { GraphContainerProps } from './types';
|
||||
|
||||
interface Props {
|
||||
$panelType: PANEL_TYPES;
|
||||
}
|
||||
@ -22,3 +25,36 @@ export const TimeContainer = styled.div<Props>`
|
||||
`
|
||||
: css``}
|
||||
`;
|
||||
|
||||
export const GraphContainer = styled.div<GraphContainerProps>`
|
||||
height: ${({ isGraphLegendToggleAvailable }): string =>
|
||||
isGraphLegendToggleAvailable ? '50%' : '100%'};
|
||||
`;
|
||||
|
||||
export const FilterTableAndSaveContainer = styled.div`
|
||||
margin-top: 1.875rem;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
`;
|
||||
|
||||
export const FilterTableContainer = styled.div`
|
||||
flex-basis: 80%;
|
||||
`;
|
||||
|
||||
export const SaveContainer = styled.div`
|
||||
flex-basis: 20%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
export const SaveCancelButtonContainer = styled.span`
|
||||
margin: 0 0.313rem;
|
||||
`;
|
||||
|
||||
export const LabelContainer = styled.button`
|
||||
max-width: 18.75rem;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: ${themeColors.white};
|
||||
`;
|
||||
|
@ -0,0 +1,77 @@
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { ChartData, ChartDataset } from 'chart.js';
|
||||
import { GraphOnClickHandler } from 'components/Graph/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface DataSetProps {
|
||||
index: number;
|
||||
data: number | null;
|
||||
label: string;
|
||||
borderWidth: number;
|
||||
spanGaps: boolean;
|
||||
animations: boolean;
|
||||
borderColor: string;
|
||||
showLine: boolean;
|
||||
pointRadius: number;
|
||||
}
|
||||
|
||||
export interface LegendEntryProps {
|
||||
label: string;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export type ExtendedChartDataset = ChartDataset & {
|
||||
show: boolean;
|
||||
sum: number;
|
||||
avg: number;
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
|
||||
export type PanelTypeAndGraphManagerVisibilityProps = Record<
|
||||
keyof typeof PANEL_TYPES,
|
||||
boolean
|
||||
>;
|
||||
|
||||
export interface LabelProps {
|
||||
labelClickedHandler: (labelIndex: number) => void;
|
||||
labelIndex: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface GraphManagerProps {
|
||||
data: ChartData;
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
onToggleModelHandler?: () => void;
|
||||
}
|
||||
|
||||
export interface CheckBoxProps {
|
||||
data: ChartData;
|
||||
index: number;
|
||||
graphVisibilityState: boolean[];
|
||||
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
||||
}
|
||||
|
||||
export interface FullViewProps {
|
||||
widget: Widgets;
|
||||
fullViewOptions?: boolean;
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
isDependedDataLoaded?: boolean;
|
||||
graphsVisibilityStates?: boolean[];
|
||||
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
||||
}
|
||||
|
||||
export interface SaveLegendEntriesToLocalStoreProps {
|
||||
data: ChartData;
|
||||
graphVisibilityState: boolean[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface GraphContainerProps {
|
||||
isGraphLegendToggleAvailable: boolean;
|
||||
}
|
123
frontend/src/container/GridGraphLayout/Graph/FullView/utils.ts
Normal file
123
frontend/src/container/GridGraphLayout/Graph/FullView/utils.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { ChartData, ChartDataset } from 'chart.js';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
import {
|
||||
ExtendedChartDataset,
|
||||
LegendEntryProps,
|
||||
SaveLegendEntriesToLocalStoreProps,
|
||||
} from './types';
|
||||
|
||||
function convertToTwoDecimalsOrZero(value: number): number {
|
||||
if (
|
||||
typeof value === 'number' &&
|
||||
!Number.isNaN(value) &&
|
||||
value !== Infinity &&
|
||||
value !== -Infinity
|
||||
) {
|
||||
const result = value ? value.toFixed(20).match(/^-?\d*\.?0*\d{0,2}/) : null;
|
||||
return result ? parseFloat(result[0]) : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export const getDefaultTableDataSet = (
|
||||
data: ChartData,
|
||||
): ExtendedChartDataset[] =>
|
||||
data.datasets.map(
|
||||
(item: ChartDataset): ExtendedChartDataset => {
|
||||
if (item.data.length === 0) {
|
||||
return {
|
||||
...item,
|
||||
show: true,
|
||||
sum: 0,
|
||||
avg: 0,
|
||||
max: 0,
|
||||
min: 0,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
show: true,
|
||||
sum: convertToTwoDecimalsOrZero(
|
||||
(item.data as number[]).reduce((a, b) => a + b, 0),
|
||||
),
|
||||
avg: convertToTwoDecimalsOrZero(
|
||||
(item.data as number[]).reduce((a, b) => a + b, 0) / item.data.length,
|
||||
),
|
||||
max: convertToTwoDecimalsOrZero(Math.max(...(item.data as number[]))),
|
||||
min: convertToTwoDecimalsOrZero(Math.min(...(item.data as number[]))),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const getAbbreviatedLabel = (label: string): string => {
|
||||
let newLabel = label;
|
||||
if (label.length > 30) {
|
||||
newLabel = `${label.substring(0, 30)}...`;
|
||||
}
|
||||
return newLabel;
|
||||
};
|
||||
|
||||
export const showAllDataSet = (data: ChartData): LegendEntryProps[] =>
|
||||
data.datasets.map(
|
||||
(item): LegendEntryProps => ({
|
||||
label: item.label || '',
|
||||
show: true,
|
||||
}),
|
||||
);
|
||||
|
||||
export const saveLegendEntriesToLocalStorage = ({
|
||||
data,
|
||||
graphVisibilityState,
|
||||
name,
|
||||
}: SaveLegendEntriesToLocalStoreProps): void => {
|
||||
const newLegendEntry = {
|
||||
name,
|
||||
dataIndex: data.datasets.map(
|
||||
(item, index): LegendEntryProps => ({
|
||||
label: item.label || '',
|
||||
show: graphVisibilityState[index],
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
let existingEntries: { name: string; dataIndex: LegendEntryProps[] }[] = [];
|
||||
|
||||
try {
|
||||
existingEntries = JSON.parse(
|
||||
localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) || '[]',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error parsing LEGEND_GRAPH from local storage', error);
|
||||
}
|
||||
|
||||
const entryIndex = existingEntries.findIndex((entry) => entry.name === name);
|
||||
|
||||
if (entryIndex >= 0) {
|
||||
existingEntries[entryIndex] = newLegendEntry;
|
||||
} else {
|
||||
existingEntries = [...existingEntries, newLegendEntry];
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(
|
||||
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
|
||||
JSON.stringify(existingEntries),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error setting LEGEND_GRAPH to local storage', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getIsGraphLegendToggleAvailable = (
|
||||
panelType: PANEL_TYPES,
|
||||
): boolean => panelType === PANEL_TYPES.TIME_SERIES;
|
||||
|
||||
export const getGraphManagerTableHeaderTitle = (
|
||||
title: string,
|
||||
yAxisUnit?: string,
|
||||
): string => {
|
||||
const yAxisUnitText = yAxisUnit ? `(in ${yAxisUnit})` : '';
|
||||
return `${title} ${yAxisUnitText}`;
|
||||
};
|
62
frontend/src/container/GridGraphLayout/Graph/Graph.test.tsx
Normal file
62
frontend/src/container/GridGraphLayout/Graph/Graph.test.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { mockTestData } from './__mock__/mockChartData';
|
||||
import { mocklegendEntryResult } from './__mock__/mockLegendEntryData';
|
||||
import { showAllDataSet } from './FullView/utils';
|
||||
import { getGraphVisibilityStateOnDataChange } from './utils';
|
||||
|
||||
describe('getGraphVisibilityStateOnDataChange', () => {
|
||||
beforeEach(() => {
|
||||
const localStorageMock = {
|
||||
getItem: jest.fn(),
|
||||
};
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||
});
|
||||
|
||||
it('should return the correct visibility state and legend entry', () => {
|
||||
// Mock the localStorage behavior
|
||||
const mockLocalStorageData = [
|
||||
{
|
||||
name: 'exampleexpanded',
|
||||
dataIndex: [
|
||||
{ label: 'customer', show: true },
|
||||
{ label: 'demo-app', show: false },
|
||||
],
|
||||
},
|
||||
];
|
||||
jest
|
||||
.spyOn(window.localStorage, 'getItem')
|
||||
.mockReturnValue(JSON.stringify(mockLocalStorageData));
|
||||
|
||||
const result1 = getGraphVisibilityStateOnDataChange({
|
||||
data: mockTestData,
|
||||
isExpandedName: true,
|
||||
name: 'example',
|
||||
});
|
||||
expect(result1.graphVisibilityStates).toEqual([true, false]);
|
||||
expect(result1.legendEntry).toEqual(mocklegendEntryResult);
|
||||
|
||||
const result2 = getGraphVisibilityStateOnDataChange({
|
||||
data: mockTestData,
|
||||
isExpandedName: false,
|
||||
name: 'example',
|
||||
});
|
||||
expect(result2.graphVisibilityStates).toEqual(
|
||||
Array(mockTestData.datasets.length).fill(true),
|
||||
);
|
||||
expect(result2.legendEntry).toEqual(showAllDataSet(mockTestData));
|
||||
});
|
||||
|
||||
it('should return default values if localStorage data is not available', () => {
|
||||
// Mock the localStorage behavior to return null
|
||||
jest.spyOn(window.localStorage, 'getItem').mockReturnValue(null);
|
||||
|
||||
const result = getGraphVisibilityStateOnDataChange({
|
||||
data: mockTestData,
|
||||
isExpandedName: true,
|
||||
name: 'example',
|
||||
});
|
||||
expect(result.graphVisibilityStates).toEqual(
|
||||
Array(mockTestData.datasets.length).fill(true),
|
||||
);
|
||||
expect(result.legendEntry).toEqual(showAllDataSet(mockTestData));
|
||||
});
|
||||
});
|
@ -0,0 +1,336 @@
|
||||
import { Typography } from 'antd';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { Events } from 'constants/events';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { useChartMutable } from 'hooks/useChartMutable';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { DeleteWidget } from 'store/actions/dashboard/deleteWidget';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { UpdateDashboard } from '../utils';
|
||||
import WidgetHeader from '../WidgetHeader';
|
||||
import FullView from './FullView';
|
||||
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './FullView/contants';
|
||||
import { FullViewContainer, Modal } from './styles';
|
||||
import { DispatchProps, WidgetGraphComponentProps } from './types';
|
||||
import {
|
||||
getGraphVisibilityStateOnDataChange,
|
||||
toggleGraphsVisibilityInChart,
|
||||
} from './utils';
|
||||
|
||||
function WidgetGraphComponent({
|
||||
enableModel,
|
||||
enableWidgetHeader,
|
||||
data,
|
||||
widget,
|
||||
queryResponse,
|
||||
errorMessage,
|
||||
name,
|
||||
yAxisUnit,
|
||||
layout = [],
|
||||
deleteWidget,
|
||||
setLayout,
|
||||
onDragSelect,
|
||||
onClickHandler,
|
||||
allowClone = true,
|
||||
allowDelete = true,
|
||||
allowEdit = true,
|
||||
}: WidgetGraphComponentProps): JSX.Element {
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const [modal, setModal] = useState<boolean>(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const { notifications } = useNotifications();
|
||||
const { t } = useTranslation(['common']);
|
||||
|
||||
const { graphVisibilityStates: localstoredVisibilityStates } = useMemo(
|
||||
() =>
|
||||
getGraphVisibilityStateOnDataChange({
|
||||
data,
|
||||
isExpandedName: true,
|
||||
name,
|
||||
}),
|
||||
[data, name],
|
||||
);
|
||||
|
||||
const [graphsVisibilityStates, setGraphsVisilityStates] = useState<boolean[]>(
|
||||
localstoredVisibilityStates,
|
||||
);
|
||||
|
||||
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
const [selectedDashboard] = dashboards;
|
||||
|
||||
const canModifyChart = useChartMutable({
|
||||
panelType: widget.panelTypes,
|
||||
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
||||
});
|
||||
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
|
||||
// Updating the visibility state of the graph on data change according to global time range
|
||||
useEffect(() => {
|
||||
if (canModifyChart) {
|
||||
const newGraphVisibilityState = getGraphVisibilityStateOnDataChange({
|
||||
data,
|
||||
isExpandedName: true,
|
||||
name,
|
||||
});
|
||||
setGraphsVisilityStates(newGraphVisibilityState.graphVisibilityStates);
|
||||
}
|
||||
}, [canModifyChart, data, name]);
|
||||
|
||||
useEffect(() => {
|
||||
const eventListener = eventEmitter.on(
|
||||
Events.UPDATE_GRAPH_VISIBILITY_STATE,
|
||||
(data) => {
|
||||
if (data.name === `${name}expanded` && canModifyChart) {
|
||||
setGraphsVisilityStates([...data.graphVisibilityStates]);
|
||||
}
|
||||
},
|
||||
);
|
||||
return (): void => {
|
||||
eventListener.off(Events.UPDATE_GRAPH_VISIBILITY_STATE);
|
||||
};
|
||||
}, [canModifyChart, name]);
|
||||
|
||||
useEffect(() => {
|
||||
if (canModifyChart && lineChartRef.current) {
|
||||
toggleGraphsVisibilityInChart({
|
||||
graphsVisibilityStates,
|
||||
lineChartRef,
|
||||
});
|
||||
}
|
||||
}, [graphsVisibilityStates, canModifyChart]);
|
||||
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
const onToggleModal = useCallback(
|
||||
(func: Dispatch<SetStateAction<boolean>>) => {
|
||||
func((value) => !value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onDeleteHandler = useCallback(() => {
|
||||
const isEmptyWidget = widget?.id === 'empty' || isEmpty(widget);
|
||||
const widgetId = isEmptyWidget ? layout[0].i : widget?.id;
|
||||
|
||||
featureResponse
|
||||
.refetch()
|
||||
.then(() => {
|
||||
deleteWidget({ widgetId, setLayout });
|
||||
onToggleModal(setDeleteModal);
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.error({
|
||||
message: t('common:something_went_wrong'),
|
||||
});
|
||||
});
|
||||
}, [
|
||||
widget,
|
||||
layout,
|
||||
featureResponse,
|
||||
deleteWidget,
|
||||
setLayout,
|
||||
onToggleModal,
|
||||
notifications,
|
||||
t,
|
||||
]);
|
||||
|
||||
const onCloneHandler = async (): Promise<void> => {
|
||||
const uuid = v4();
|
||||
|
||||
const layout = [
|
||||
{
|
||||
i: uuid,
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 2,
|
||||
y: 0,
|
||||
},
|
||||
...(selectedDashboard.data.layout || []),
|
||||
];
|
||||
|
||||
if (widget) {
|
||||
await UpdateDashboard(
|
||||
{
|
||||
data: selectedDashboard.data,
|
||||
generateWidgetId: uuid,
|
||||
graphType: widget?.panelTypes,
|
||||
selectedDashboard,
|
||||
layout,
|
||||
widgetData: widget,
|
||||
isRedirected: false,
|
||||
},
|
||||
notifications,
|
||||
).then(() => {
|
||||
notifications.success({
|
||||
message: 'Panel cloned successfully, redirecting to new copy.',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
history.push(
|
||||
`${history.location.pathname}/new?graphType=${widget?.panelTypes}&widgetId=${uuid}`,
|
||||
);
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnView = (): void => {
|
||||
onToggleModal(setModal);
|
||||
};
|
||||
|
||||
const handleOnDelete = (): void => {
|
||||
onToggleModal(setDeleteModal);
|
||||
};
|
||||
|
||||
const onDeleteModelHandler = (): void => {
|
||||
onToggleModal(setDeleteModal);
|
||||
};
|
||||
|
||||
const onToggleModelHandler = (): void => {
|
||||
onToggleModal(setModal);
|
||||
};
|
||||
|
||||
const getModals = (): JSX.Element => (
|
||||
<>
|
||||
<Modal
|
||||
destroyOnClose
|
||||
onCancel={onDeleteModelHandler}
|
||||
open={deleteModal}
|
||||
title="Delete"
|
||||
height="10vh"
|
||||
onOk={onDeleteHandler}
|
||||
centered
|
||||
>
|
||||
<Typography>Are you sure you want to delete this widget</Typography>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="View"
|
||||
footer={[]}
|
||||
centered
|
||||
open={modal}
|
||||
onCancel={onToggleModelHandler}
|
||||
width="85%"
|
||||
destroyOnClose
|
||||
>
|
||||
<FullViewContainer>
|
||||
<FullView
|
||||
name={`${name}expanded`}
|
||||
widget={widget}
|
||||
yAxisUnit={yAxisUnit}
|
||||
graphsVisibilityStates={graphsVisibilityStates}
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
/>
|
||||
</FullViewContainer>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
onMouseOver={(): void => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onFocus={(): void => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseOut={(): void => {
|
||||
setHovered(false);
|
||||
}}
|
||||
onBlur={(): void => {
|
||||
setHovered(false);
|
||||
}}
|
||||
>
|
||||
{enableModel && getModals()}
|
||||
{!isEmpty(widget) && data && (
|
||||
<>
|
||||
{enableWidgetHeader && (
|
||||
<div className="drag-handle">
|
||||
<WidgetHeader
|
||||
parentHover={hovered}
|
||||
title={widget?.title}
|
||||
widget={widget}
|
||||
onView={handleOnView}
|
||||
onDelete={handleOnDelete}
|
||||
onClone={onCloneHandler}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
allowClone={allowClone}
|
||||
allowDelete={allowDelete}
|
||||
allowEdit={allowEdit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={data}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={' '}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onClickHandler={onClickHandler}
|
||||
onDragSelect={onDragSelect}
|
||||
panelData={[]}
|
||||
query={widget.query}
|
||||
ref={lineChartRef}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
WidgetGraphComponent.defaultProps = {
|
||||
yAxisUnit: undefined,
|
||||
layout: undefined,
|
||||
setLayout: undefined,
|
||||
onDragSelect: undefined,
|
||||
onClickHandler: undefined,
|
||||
allowDelete: true,
|
||||
allowClone: true,
|
||||
allowEdit: true,
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
deleteWidget: bindActionCreators(DeleteWidget, dispatch),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps,
|
||||
)(
|
||||
memo(
|
||||
WidgetGraphComponent,
|
||||
(prevProps, nextProps) =>
|
||||
isEqual(prevProps.data, nextProps.data) && prevProps.name === nextProps.name,
|
||||
),
|
||||
);
|
@ -0,0 +1,15 @@
|
||||
import { ChartData } from 'chart.js';
|
||||
|
||||
export const mockTestData: ChartData = {
|
||||
labels: ['test1', 'test2'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'customer',
|
||||
data: [481.60377358490564, 730.0000000000002],
|
||||
},
|
||||
{
|
||||
label: 'demo-app',
|
||||
data: [4471.4285714285725],
|
||||
},
|
||||
],
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
import { LegendEntryProps } from '../FullView/types';
|
||||
|
||||
export const mocklegendEntryResult: LegendEntryProps[] = [
|
||||
{
|
||||
label: 'customer',
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
label: 'demo-app',
|
||||
show: false,
|
||||
},
|
||||
];
|
@ -1,56 +1,25 @@
|
||||
import { Typography } from 'antd';
|
||||
import { ChartData } from 'chart.js';
|
||||
import { GraphOnClickHandler } from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { UpdateDashboard } from 'container/GridGraphLayout/utils';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import usePreviousValue from 'hooks/usePreviousValue';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import history from 'lib/history';
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import {
|
||||
DeleteWidget,
|
||||
DeleteWidgetProps,
|
||||
} from 'store/actions/dashboard/deleteWidget';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import {
|
||||
getSelectedDashboard,
|
||||
getSelectedDashboardVariable,
|
||||
} from 'utils/dashboard/selectedDashboard';
|
||||
import { v4 } from 'uuid';
|
||||
import { getSelectedDashboardVariable } from 'utils/dashboard/selectedDashboard';
|
||||
|
||||
import { LayoutProps } from '..';
|
||||
import EmptyWidget from '../EmptyWidget';
|
||||
import WidgetHeader from '../WidgetHeader';
|
||||
import FullView from './FullView';
|
||||
import { FullViewContainer, Modal } from './styles';
|
||||
import { GridCardGraphProps } from './types';
|
||||
import WidgetGraphComponent from './WidgetGraphComponent';
|
||||
|
||||
function GridCardGraph({
|
||||
widget,
|
||||
deleteWidget,
|
||||
name,
|
||||
yAxisUnit,
|
||||
layout = [],
|
||||
@ -62,33 +31,26 @@ function GridCardGraph({
|
||||
allowEdit,
|
||||
isQueryEnabled,
|
||||
}: GridCardGraphProps): JSX.Element {
|
||||
const { isAddWidget } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
|
||||
const { ref: graphRef, inView: isGraphVisible } = useInView({
|
||||
threshold: 0,
|
||||
triggerOnce: true,
|
||||
initialInView: false,
|
||||
});
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { t } = useTranslation(['common']);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [modal, setModal] = useState(false);
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
|
||||
const selectedDashboard = getSelectedDashboard(dashboards);
|
||||
const variables = getSelectedDashboardVariable(dashboards);
|
||||
|
||||
const updatedQuery = useStepInterval(widget?.query);
|
||||
@ -116,7 +78,7 @@ function GridCardGraph({
|
||||
variables,
|
||||
],
|
||||
keepPreviousData: true,
|
||||
enabled: isGraphVisible && !isEmptyWidget && isQueryEnabled,
|
||||
enabled: isGraphVisible && !isEmptyWidget && isQueryEnabled && !isAddWidget,
|
||||
refetchOnMount: false,
|
||||
onError: (error) => {
|
||||
setErrorMessage(error.message);
|
||||
@ -138,153 +100,31 @@ function GridCardGraph({
|
||||
|
||||
const prevChartDataSetRef = usePreviousValue<ChartData>(chartData);
|
||||
|
||||
const onToggleModal = useCallback(
|
||||
(func: Dispatch<SetStateAction<boolean>>) => {
|
||||
func((value) => !value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onDeleteHandler = useCallback(() => {
|
||||
const widgetId = isEmptyWidget ? layout[0].i : widget?.id;
|
||||
|
||||
featureResponse
|
||||
.refetch()
|
||||
.then(() => {
|
||||
deleteWidget({ widgetId, setLayout });
|
||||
onToggleModal(setDeleteModal);
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.error({
|
||||
message: t('common:something_went_wrong'),
|
||||
});
|
||||
});
|
||||
}, [
|
||||
isEmptyWidget,
|
||||
widget?.id,
|
||||
layout,
|
||||
featureResponse,
|
||||
deleteWidget,
|
||||
setLayout,
|
||||
onToggleModal,
|
||||
notifications,
|
||||
t,
|
||||
]);
|
||||
|
||||
const onCloneHandler = async (): Promise<void> => {
|
||||
const uuid = v4();
|
||||
|
||||
const layout = [
|
||||
{
|
||||
i: uuid,
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 2,
|
||||
y: 0,
|
||||
},
|
||||
...(selectedDashboard?.data.layout || []),
|
||||
];
|
||||
|
||||
if (widget && selectedDashboard) {
|
||||
await UpdateDashboard(
|
||||
{
|
||||
data: selectedDashboard.data,
|
||||
generateWidgetId: uuid,
|
||||
graphType: widget?.panelTypes,
|
||||
selectedDashboard,
|
||||
layout,
|
||||
widgetData: widget,
|
||||
isRedirected: false,
|
||||
},
|
||||
notifications,
|
||||
).then(() => {
|
||||
notifications.success({
|
||||
message: 'Panel cloned successfully, redirecting to new copy.',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
history.push(
|
||||
`${history.location.pathname}/new?graphType=${widget?.panelTypes}&widgetId=${uuid}`,
|
||||
);
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getModals = (): JSX.Element => (
|
||||
<>
|
||||
<Modal
|
||||
destroyOnClose
|
||||
onCancel={(): void => onToggleModal(setDeleteModal)}
|
||||
open={deleteModal}
|
||||
title="Delete"
|
||||
height="10vh"
|
||||
onOk={onDeleteHandler}
|
||||
centered
|
||||
>
|
||||
<Typography>Are you sure you want to delete this widget</Typography>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="View"
|
||||
footer={[]}
|
||||
centered
|
||||
open={modal}
|
||||
onCancel={(): void => onToggleModal(setModal)}
|
||||
width="85%"
|
||||
destroyOnClose
|
||||
>
|
||||
<FullViewContainer>
|
||||
<FullView name={`${name}expanded`} widget={widget} yAxisUnit={yAxisUnit} />
|
||||
</FullViewContainer>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
const handleOnView = (): void => {
|
||||
onToggleModal(setModal);
|
||||
};
|
||||
|
||||
const handleOnDelete = (): void => {
|
||||
onToggleModal(setDeleteModal);
|
||||
};
|
||||
|
||||
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
|
||||
|
||||
if (queryResponse.isRefetching) {
|
||||
return <Spinner height="20vh" tip="Loading..." />;
|
||||
}
|
||||
|
||||
if (queryResponse.isError && !isEmptyLayout) {
|
||||
return (
|
||||
<span ref={graphRef}>
|
||||
{getModals()}
|
||||
{!isEmpty(widget) && prevChartDataSetRef && (
|
||||
<>
|
||||
<div className="drag-handle">
|
||||
<WidgetHeader
|
||||
parentHover={hovered}
|
||||
title={widget?.title}
|
||||
widget={widget}
|
||||
onView={handleOnView}
|
||||
onDelete={handleOnDelete}
|
||||
onClone={onCloneHandler}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
allowClone={allowClone}
|
||||
allowDelete={allowDelete}
|
||||
allowEdit={allowEdit}
|
||||
/>
|
||||
</div>
|
||||
<GridPanelSwitch
|
||||
panelType={widget?.panelTypes}
|
||||
data={prevChartDataSetRef}
|
||||
isStacked={widget?.isStacked}
|
||||
opacity={widget?.opacity}
|
||||
title={' '}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onClickHandler={onClickHandler}
|
||||
panelData={[]}
|
||||
query={widget.query}
|
||||
/>
|
||||
</>
|
||||
<WidgetGraphComponent
|
||||
enableModel
|
||||
enableWidgetHeader
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
data={prevChartDataSetRef}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
layout={layout}
|
||||
setLayout={setLayout}
|
||||
allowClone={allowClone}
|
||||
allowDelete={allowDelete}
|
||||
allowEdit={allowEdit}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
@ -294,35 +134,22 @@ function GridCardGraph({
|
||||
return (
|
||||
<span ref={graphRef}>
|
||||
{!isEmpty(widget) && prevChartDataSetRef?.labels ? (
|
||||
<>
|
||||
<div className="drag-handle">
|
||||
<WidgetHeader
|
||||
parentHover={hovered}
|
||||
title={widget?.title}
|
||||
widget={widget}
|
||||
onView={handleOnView}
|
||||
onDelete={handleOnDelete}
|
||||
onClone={onCloneHandler}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
allowClone={allowClone}
|
||||
allowDelete={allowDelete}
|
||||
allowEdit={allowEdit}
|
||||
/>
|
||||
</div>
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={prevChartDataSetRef}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={' '}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onClickHandler={onClickHandler}
|
||||
panelData={[]}
|
||||
query={widget.query}
|
||||
/>
|
||||
</>
|
||||
<WidgetGraphComponent
|
||||
enableModel={false}
|
||||
enableWidgetHeader
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
data={prevChartDataSetRef}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
layout={layout}
|
||||
setLayout={setLayout}
|
||||
allowClone={allowClone}
|
||||
allowDelete={allowDelete}
|
||||
allowEdit={allowEdit}
|
||||
onClickHandler={onClickHandler}
|
||||
/>
|
||||
) : (
|
||||
<Spinner height="20vh" tip="Loading..." />
|
||||
)}
|
||||
@ -331,54 +158,21 @@ function GridCardGraph({
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={graphRef}
|
||||
onMouseOver={(): void => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onFocus={(): void => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseOut={(): void => {
|
||||
setHovered(false);
|
||||
}}
|
||||
onBlur={(): void => {
|
||||
setHovered(false);
|
||||
}}
|
||||
>
|
||||
{!isEmptyLayout && (
|
||||
<div className="drag-handle">
|
||||
<WidgetHeader
|
||||
parentHover={hovered}
|
||||
title={widget?.title}
|
||||
widget={widget}
|
||||
onView={handleOnView}
|
||||
onDelete={handleOnDelete}
|
||||
onClone={onCloneHandler}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
allowClone={allowClone}
|
||||
allowDelete={allowDelete}
|
||||
allowEdit={allowEdit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isEmptyLayout && getModals()}
|
||||
|
||||
<span ref={graphRef}>
|
||||
{!isEmpty(widget) && !!queryResponse.data?.payload && (
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
<WidgetGraphComponent
|
||||
enableModel={!isEmptyLayout}
|
||||
enableWidgetHeader={!isEmptyLayout}
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
data={chartData}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={' '} // `empty title to accommodate absolutely positioned widget header
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
onClickHandler={onClickHandler}
|
||||
panelData={queryResponse.data?.payload.data.newResult.data.result || []}
|
||||
query={widget.query}
|
||||
allowClone={allowClone}
|
||||
allowDelete={allowDelete}
|
||||
allowEdit={allowEdit}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -387,28 +181,6 @@ function GridCardGraph({
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
deleteWidget: ({
|
||||
widgetId,
|
||||
}: DeleteWidgetProps) => (dispatch: Dispatch<AppActions>) => void;
|
||||
}
|
||||
|
||||
interface GridCardGraphProps extends DispatchProps {
|
||||
widget: Widgets;
|
||||
name: string;
|
||||
yAxisUnit: string | undefined;
|
||||
// eslint-disable-next-line react/require-default-props
|
||||
layout?: Layout[];
|
||||
// eslint-disable-next-line react/require-default-props
|
||||
setLayout?: Dispatch<SetStateAction<LayoutProps[]>>;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
allowDelete?: boolean;
|
||||
allowClone?: boolean;
|
||||
allowEdit?: boolean;
|
||||
isQueryEnabled?: boolean;
|
||||
}
|
||||
|
||||
GridCardGraph.defaultProps = {
|
||||
onDragSelect: undefined,
|
||||
onClickHandler: undefined,
|
||||
@ -418,10 +190,4 @@ GridCardGraph.defaultProps = {
|
||||
isQueryEnabled: true,
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
deleteWidget: bindActionCreators(DeleteWidget, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(memo(GridCardGraph));
|
||||
export default memo(GridCardGraph);
|
||||
|
71
frontend/src/container/GridGraphLayout/Graph/types.ts
Normal file
71
frontend/src/container/GridGraphLayout/Graph/types.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { ChartData } from 'chart.js';
|
||||
import { GraphOnClickHandler, ToggleGraphProps } from 'components/Graph/types';
|
||||
import { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { DeleteWidgetProps } from 'store/actions/dashboard/deleteWidget';
|
||||
import AppActions from 'types/actions';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { LayoutProps } from '..';
|
||||
import { LegendEntryProps } from './FullView/types';
|
||||
|
||||
export interface GraphVisibilityLegendEntryProps {
|
||||
graphVisibilityStates: boolean[];
|
||||
legendEntry: LegendEntryProps[];
|
||||
}
|
||||
|
||||
export interface DispatchProps {
|
||||
deleteWidget: ({
|
||||
widgetId,
|
||||
}: DeleteWidgetProps) => (dispatch: Dispatch<AppActions>) => void;
|
||||
}
|
||||
|
||||
export interface WidgetGraphComponentProps extends DispatchProps {
|
||||
enableModel: boolean;
|
||||
enableWidgetHeader: boolean;
|
||||
widget: Widgets;
|
||||
queryResponse: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
|
||||
>;
|
||||
errorMessage: string | undefined;
|
||||
data: ChartData;
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
layout?: Layout[];
|
||||
setLayout?: Dispatch<SetStateAction<LayoutProps[]>>;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
allowDelete?: boolean;
|
||||
allowClone?: boolean;
|
||||
allowEdit?: boolean;
|
||||
}
|
||||
|
||||
export interface GridCardGraphProps {
|
||||
widget: Widgets;
|
||||
name: string;
|
||||
yAxisUnit: string | undefined;
|
||||
// eslint-disable-next-line react/require-default-props
|
||||
layout?: Layout[];
|
||||
// eslint-disable-next-line react/require-default-props
|
||||
setLayout?: Dispatch<SetStateAction<LayoutProps[]>>;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
allowDelete?: boolean;
|
||||
allowClone?: boolean;
|
||||
allowEdit?: boolean;
|
||||
isQueryEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||
data: ChartData;
|
||||
isExpandedName: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ToggleGraphsVisibilityInChartProps {
|
||||
graphsVisibilityStates: GraphVisibilityLegendEntryProps['graphVisibilityStates'];
|
||||
lineChartRef: MutableRefObject<ToggleGraphProps | undefined>;
|
||||
}
|
66
frontend/src/container/GridGraphLayout/Graph/utils.ts
Normal file
66
frontend/src/container/GridGraphLayout/Graph/utils.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
import { LegendEntryProps } from './FullView/types';
|
||||
import { showAllDataSet } from './FullView/utils';
|
||||
import {
|
||||
GetGraphVisibilityStateOnLegendClickProps,
|
||||
GraphVisibilityLegendEntryProps,
|
||||
ToggleGraphsVisibilityInChartProps,
|
||||
} from './types';
|
||||
|
||||
export const getGraphVisibilityStateOnDataChange = ({
|
||||
data,
|
||||
isExpandedName,
|
||||
name,
|
||||
}: GetGraphVisibilityStateOnLegendClickProps): GraphVisibilityLegendEntryProps => {
|
||||
const visibilityStateAndLegendEntry: GraphVisibilityLegendEntryProps = {
|
||||
graphVisibilityStates: Array(data.datasets.length).fill(true),
|
||||
legendEntry: showAllDataSet(data),
|
||||
};
|
||||
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
|
||||
const legendGraphFromLocalStore = localStorage.getItem(
|
||||
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
|
||||
);
|
||||
let legendFromLocalStore: {
|
||||
name: string;
|
||||
dataIndex: LegendEntryProps[];
|
||||
}[] = [];
|
||||
|
||||
try {
|
||||
legendFromLocalStore = JSON.parse(legendGraphFromLocalStore || '[]');
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error parsing GRAPH_VISIBILITY_STATES from local storage',
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
const newGraphVisibilityStates = Array(data.datasets.length).fill(true);
|
||||
legendFromLocalStore.forEach((item) => {
|
||||
const newName = isExpandedName ? `${name}expanded` : name;
|
||||
if (item.name === newName) {
|
||||
visibilityStateAndLegendEntry.legendEntry = item.dataIndex;
|
||||
data.datasets.forEach((datasets, i) => {
|
||||
const index = item.dataIndex.findIndex(
|
||||
(dataKey) => dataKey.label === datasets.label,
|
||||
);
|
||||
if (index !== -1) {
|
||||
newGraphVisibilityStates[i] = item.dataIndex[index].show;
|
||||
}
|
||||
});
|
||||
visibilityStateAndLegendEntry.graphVisibilityStates = newGraphVisibilityStates;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return visibilityStateAndLegendEntry;
|
||||
};
|
||||
|
||||
export const toggleGraphsVisibilityInChart = ({
|
||||
graphsVisibilityStates,
|
||||
lineChartRef,
|
||||
}: ToggleGraphsVisibilityInChartProps): void => {
|
||||
graphsVisibilityStates?.forEach((showLegendData, index) => {
|
||||
lineChartRef?.current?.toggleGraph(index, showLegendData);
|
||||
});
|
||||
};
|
@ -8,6 +8,7 @@ import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
|
||||
import { LayoutProps, State } from '.';
|
||||
import {
|
||||
@ -28,6 +29,9 @@ function GraphLayout({
|
||||
widgets,
|
||||
setLayout,
|
||||
}: GraphLayoutProps): JSX.Element {
|
||||
const { isAddWidget } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@ -53,7 +57,7 @@ function GraphLayout({
|
||||
{addPanelPermission && (
|
||||
<Button
|
||||
loading={addPanelLoading}
|
||||
disabled={addPanelLoading}
|
||||
disabled={addPanelLoading || isAddWidget}
|
||||
onClick={onAddPanelHandler}
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
const positionCss: CSSProperties['position'] = 'fixed';
|
||||
const positionCss: CSSProperties['position'] = 'absolute';
|
||||
|
||||
export const spinnerStyles = { position: positionCss, right: '0.5rem' };
|
||||
export const tooltipStyles = {
|
||||
|
@ -1,73 +1,85 @@
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { PANEL_TYPES_COMPONENT_MAP } from 'constants/panelTypes';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
|
||||
import { FC, memo, useMemo } from 'react';
|
||||
import { FC, forwardRef, memo, useMemo } from 'react';
|
||||
|
||||
import { GridPanelSwitchProps, PropsTypePropsMap } from './types';
|
||||
|
||||
function GridPanelSwitch({
|
||||
panelType,
|
||||
data,
|
||||
title,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
panelData,
|
||||
query,
|
||||
}: GridPanelSwitchProps): JSX.Element | null {
|
||||
const currentProps: PropsTypePropsMap = useMemo(() => {
|
||||
const result: PropsTypePropsMap = {
|
||||
[PANEL_TYPES.TIME_SERIES]: {
|
||||
type: 'line',
|
||||
data,
|
||||
title,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
},
|
||||
[PANEL_TYPES.VALUE]: {
|
||||
title,
|
||||
data,
|
||||
yAxisUnit,
|
||||
},
|
||||
[PANEL_TYPES.TABLE]: { ...GRID_TABLE_CONFIG, data: panelData, query },
|
||||
[PANEL_TYPES.LIST]: null,
|
||||
[PANEL_TYPES.TRACE]: null,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||
};
|
||||
const GridPanelSwitch = forwardRef<
|
||||
ToggleGraphProps | undefined,
|
||||
GridPanelSwitchProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
panelType,
|
||||
data,
|
||||
title,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
panelData,
|
||||
query,
|
||||
},
|
||||
ref,
|
||||
): JSX.Element | null => {
|
||||
const currentProps: PropsTypePropsMap = useMemo(() => {
|
||||
const result: PropsTypePropsMap = {
|
||||
[PANEL_TYPES.TIME_SERIES]: {
|
||||
type: 'line',
|
||||
data,
|
||||
title,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
ref,
|
||||
},
|
||||
[PANEL_TYPES.VALUE]: {
|
||||
title,
|
||||
data,
|
||||
yAxisUnit,
|
||||
},
|
||||
[PANEL_TYPES.TABLE]: { ...GRID_TABLE_CONFIG, data: panelData, query },
|
||||
[PANEL_TYPES.LIST]: null,
|
||||
[PANEL_TYPES.TRACE]: null,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||
};
|
||||
|
||||
return result;
|
||||
}, [
|
||||
data,
|
||||
isStacked,
|
||||
name,
|
||||
onClickHandler,
|
||||
onDragSelect,
|
||||
staticLine,
|
||||
title,
|
||||
yAxisUnit,
|
||||
panelData,
|
||||
query,
|
||||
]);
|
||||
return result;
|
||||
}, [
|
||||
data,
|
||||
isStacked,
|
||||
name,
|
||||
onClickHandler,
|
||||
onDragSelect,
|
||||
staticLine,
|
||||
title,
|
||||
yAxisUnit,
|
||||
panelData,
|
||||
query,
|
||||
ref,
|
||||
]);
|
||||
|
||||
const Component = PANEL_TYPES_COMPONENT_MAP[panelType] as FC<
|
||||
PropsTypePropsMap[typeof panelType]
|
||||
>;
|
||||
const componentProps = useMemo(() => currentProps[panelType], [
|
||||
panelType,
|
||||
currentProps,
|
||||
]);
|
||||
const Component = PANEL_TYPES_COMPONENT_MAP[panelType] as FC<
|
||||
PropsTypePropsMap[typeof panelType]
|
||||
>;
|
||||
const componentProps = useMemo(() => currentProps[panelType], [
|
||||
panelType,
|
||||
currentProps,
|
||||
]);
|
||||
|
||||
if (!Component || !componentProps) return null;
|
||||
if (!Component || !componentProps) return null;
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <Component {...componentProps} />;
|
||||
},
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <Component {...componentProps} />;
|
||||
}
|
||||
GridPanelSwitch.displayName = 'GridPanelSwitch';
|
||||
|
||||
export default memo(GridPanelSwitch);
|
||||
|
@ -3,7 +3,7 @@ import {
|
||||
GraphOnClickHandler,
|
||||
GraphProps,
|
||||
StaticLineProps,
|
||||
} from 'components/Graph';
|
||||
} from 'components/Graph/types';
|
||||
import { GridTableComponentProps } from 'container/GridTableComponent/types';
|
||||
import { GridValueComponentProps } from 'container/GridValueComponent/types';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
36
frontend/src/container/LogsContextList/ShowButton.tsx
Normal file
36
frontend/src/container/LogsContextList/ShowButton.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Button, Typography } from 'antd';
|
||||
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||
|
||||
import { ShowButtonWrapper } from './styles';
|
||||
|
||||
interface ShowButtonProps {
|
||||
isLoading: boolean;
|
||||
isDisabled: boolean;
|
||||
order: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function ShowButton({
|
||||
isLoading,
|
||||
isDisabled,
|
||||
order,
|
||||
onClick,
|
||||
}: ShowButtonProps): JSX.Element {
|
||||
return (
|
||||
<ShowButtonWrapper>
|
||||
<Typography>
|
||||
Showing 10 lines {order === FILTERS.ASC ? 'after' : 'before'} match
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
disabled={isLoading || isDisabled}
|
||||
loading={isLoading}
|
||||
onClick={onClick}
|
||||
>
|
||||
Show 10 more lines
|
||||
</Button>
|
||||
</ShowButtonWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShowButton;
|
9
frontend/src/container/LogsContextList/configs.ts
Normal file
9
frontend/src/container/LogsContextList/configs.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const INITIAL_PAGE_SIZE = 5;
|
||||
export const LOGS_MORE_PAGE_SIZE = 10;
|
||||
|
||||
export const getOrderByTimestamp = (order: string): OrderByPayload => ({
|
||||
columnName: 'timestamp',
|
||||
order,
|
||||
});
|
198
frontend/src/container/LogsContextList/index.tsx
Normal file
198
frontend/src/container/LogsContextList/index.tsx
Normal file
@ -0,0 +1,198 @@
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
getOrderByTimestamp,
|
||||
INITIAL_PAGE_SIZE,
|
||||
LOGS_MORE_PAGE_SIZE,
|
||||
} from './configs';
|
||||
import ShowButton from './ShowButton';
|
||||
import { EmptyText, ListContainer } from './styles';
|
||||
import { getRequestData } from './utils';
|
||||
|
||||
interface LogsContextListProps {
|
||||
isEdit: boolean;
|
||||
query: Query;
|
||||
log: ILog;
|
||||
order: string;
|
||||
filters: TagFilter | null;
|
||||
}
|
||||
|
||||
function LogsContextList({
|
||||
isEdit,
|
||||
query,
|
||||
log,
|
||||
order,
|
||||
filters,
|
||||
}: LogsContextListProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [logs, setLogs] = useState<ILog[]>([]);
|
||||
const [page, setPage] = useState<number>(1);
|
||||
|
||||
const firstLog = useMemo(() => logs[0], [logs]);
|
||||
const lastLog = useMemo(() => logs[logs.length - 1], [logs]);
|
||||
const orderByTimestamp = useMemo(() => getOrderByTimestamp(order), [order]);
|
||||
|
||||
const logsMorePageSize = useMemo(() => (page - 1) * LOGS_MORE_PAGE_SIZE, [
|
||||
page,
|
||||
]);
|
||||
const pageSize = useMemo(
|
||||
() => (page <= 1 ? INITIAL_PAGE_SIZE : logsMorePageSize + INITIAL_PAGE_SIZE),
|
||||
[page, logsMorePageSize],
|
||||
);
|
||||
const isDisabledFetch = useMemo(() => logs.length < pageSize, [
|
||||
logs.length,
|
||||
pageSize,
|
||||
]);
|
||||
|
||||
const currentStagedQueryData = useMemo(() => {
|
||||
if (!query || query.builder.queryData.length !== 1) return null;
|
||||
|
||||
return query.builder.queryData[0];
|
||||
}, [query]);
|
||||
|
||||
const initialLogsRequest = useMemo(
|
||||
() =>
|
||||
getRequestData({
|
||||
stagedQueryData: currentStagedQueryData,
|
||||
query,
|
||||
log,
|
||||
orderByTimestamp,
|
||||
page,
|
||||
}),
|
||||
[currentStagedQueryData, page, log, query, orderByTimestamp],
|
||||
);
|
||||
|
||||
const [requestData, setRequestData] = useState<Query | null>(
|
||||
initialLogsRequest,
|
||||
);
|
||||
|
||||
const handleSuccess = useCallback(
|
||||
(data: SuccessResponse<MetricRangePayloadProps, unknown>) => {
|
||||
const currentData = data?.payload.data.newResult.data.result || [];
|
||||
|
||||
if (currentData.length > 0 && currentData[0].list) {
|
||||
const currentLogs: ILog[] = currentData[0].list.map((item) => ({
|
||||
...item.data,
|
||||
timestamp: item.timestamp,
|
||||
}));
|
||||
|
||||
if (order === FILTERS.ASC) {
|
||||
const reversedCurrentLogs = currentLogs.reverse();
|
||||
setLogs((prevLogs) => [...reversedCurrentLogs, ...prevLogs]);
|
||||
} else {
|
||||
setLogs((prevLogs) => [...prevLogs, ...currentLogs]);
|
||||
}
|
||||
}
|
||||
},
|
||||
[order],
|
||||
);
|
||||
|
||||
const { isError, isFetching } = useGetExplorerQueryRange(
|
||||
requestData,
|
||||
PANEL_TYPES.LIST,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
onSuccess: handleSuccess,
|
||||
},
|
||||
);
|
||||
|
||||
const handleShowNextLines = useCallback(() => {
|
||||
if (isDisabledFetch) return;
|
||||
|
||||
const log = order === FILTERS.ASC ? firstLog : lastLog;
|
||||
|
||||
const newRequestData = getRequestData({
|
||||
stagedQueryData: currentStagedQueryData,
|
||||
query,
|
||||
log,
|
||||
orderByTimestamp,
|
||||
page: page + 1,
|
||||
pageSize: LOGS_MORE_PAGE_SIZE,
|
||||
});
|
||||
|
||||
setPage((prevPage) => prevPage + 1);
|
||||
setRequestData(newRequestData);
|
||||
}, [
|
||||
query,
|
||||
firstLog,
|
||||
lastLog,
|
||||
page,
|
||||
order,
|
||||
currentStagedQueryData,
|
||||
isDisabledFetch,
|
||||
orderByTimestamp,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEdit) return;
|
||||
|
||||
const newRequestData = getRequestData({
|
||||
stagedQueryData: currentStagedQueryData,
|
||||
query,
|
||||
log,
|
||||
orderByTimestamp,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
setPage(1);
|
||||
setLogs([]);
|
||||
setRequestData(newRequestData);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filters]);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(_: number, log: ILog): JSX.Element => (
|
||||
<RawLogView isReadOnly key={log.id} data={log} linesPerRow={1} />
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{order === FILTERS.ASC && (
|
||||
<ShowButton
|
||||
isLoading={isFetching}
|
||||
isDisabled={isDisabledFetch}
|
||||
order={order}
|
||||
onClick={handleShowNextLines}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ListContainer $isDarkMode={isDarkMode}>
|
||||
{((!logs.length && !isFetching) || isError) && (
|
||||
<EmptyText>No Data</EmptyText>
|
||||
)}
|
||||
{isFetching && <Spinner size="large" height="10rem" />}
|
||||
|
||||
<Virtuoso
|
||||
initialTopMostItemIndex={0}
|
||||
data={logs}
|
||||
itemContent={getItemContent}
|
||||
followOutput={order === FILTERS.DESC}
|
||||
/>
|
||||
</ListContainer>
|
||||
|
||||
{order === FILTERS.DESC && (
|
||||
<ShowButton
|
||||
isLoading={isFetching}
|
||||
isDisabled={isDisabledFetch}
|
||||
order={order}
|
||||
onClick={handleShowNextLines}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LogsContextList);
|
25
frontend/src/container/LogsContextList/styles.ts
Normal file
25
frontend/src/container/LogsContextList/styles.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Space, Typography } from 'antd';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ListContainer = styled.div<{ $isDarkMode: boolean }>`
|
||||
position: relative;
|
||||
margin: 0 -1.5rem;
|
||||
height: 10rem;
|
||||
overflow-y: scroll;
|
||||
|
||||
background-color: ${({ $isDarkMode }): string =>
|
||||
$isDarkMode ? themeColors.darkGrey : themeColors.lightgrey};
|
||||
`;
|
||||
|
||||
export const ShowButtonWrapper = styled(Space)`
|
||||
margin: 0.625rem 0;
|
||||
`;
|
||||
|
||||
export const EmptyText = styled(Typography)`
|
||||
padding: 0 1.5rem;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
`;
|
52
frontend/src/container/LogsContextList/utils.ts
Normal file
52
frontend/src/container/LogsContextList/utils.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
OrderByPayload,
|
||||
Query,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { INITIAL_PAGE_SIZE } from './configs';
|
||||
|
||||
type GetRequestDataProps = {
|
||||
query: Query | null;
|
||||
stagedQueryData: IBuilderQuery | null;
|
||||
log: ILog;
|
||||
orderByTimestamp: OrderByPayload;
|
||||
page: number;
|
||||
pageSize?: number;
|
||||
};
|
||||
|
||||
export const getRequestData = ({
|
||||
query,
|
||||
stagedQueryData,
|
||||
log,
|
||||
orderByTimestamp,
|
||||
page,
|
||||
pageSize = INITIAL_PAGE_SIZE,
|
||||
}: GetRequestDataProps): Query | null => {
|
||||
if (!query) return null;
|
||||
|
||||
const paginateData = getPaginationQueryData({
|
||||
currentStagedQueryData: stagedQueryData,
|
||||
listItemId: log ? log.id : null,
|
||||
orderByTimestamp,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
const data: Query = {
|
||||
...query,
|
||||
builder: {
|
||||
...query.builder,
|
||||
queryData: query.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
...paginateData,
|
||||
pageSize,
|
||||
orderBy: [orderByTimestamp],
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
109
frontend/src/container/LogsExplorerContext/index.tsx
Normal file
109
frontend/src/container/LogsExplorerContext/index.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import { EditFilled } from '@ant-design/icons';
|
||||
import { Typography } from 'antd';
|
||||
import Modal from 'antd/es/modal/Modal';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import LogsContextList from 'container/LogsContextList';
|
||||
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { EditButton, TitleWrapper } from './styles';
|
||||
import { LogsExplorerContextProps } from './types';
|
||||
import useInitialQuery from './useInitialQuery';
|
||||
|
||||
function LogsExplorerContext({
|
||||
log,
|
||||
onClose,
|
||||
}: LogsExplorerContextProps): JSX.Element | null {
|
||||
const initialContextQuery = useInitialQuery(log);
|
||||
|
||||
const [contextQuery, setContextQuery] = useState<Query>(initialContextQuery);
|
||||
const [filters, setFilters] = useState<TagFilter | null>(null);
|
||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const handleClickEditButton = useCallback(
|
||||
() => setIsEdit((prevValue) => !prevValue),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(tagFilters: TagFilter): void => {
|
||||
const tagFiltersLength = tagFilters.items.length;
|
||||
|
||||
if (
|
||||
(!tagFiltersLength && (!filters || !filters.items.length)) ||
|
||||
tagFiltersLength === filters?.items.length
|
||||
)
|
||||
return;
|
||||
|
||||
const nextQuery: Query = {
|
||||
...contextQuery,
|
||||
builder: {
|
||||
...contextQuery.builder,
|
||||
queryData: contextQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
filters: tagFilters,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
setFilters(tagFilters);
|
||||
setContextQuery(nextQuery);
|
||||
},
|
||||
[contextQuery, filters],
|
||||
);
|
||||
|
||||
const contextListParams = useMemo(
|
||||
() => ({ log, isEdit, filters, query: contextQuery }),
|
||||
[isEdit, log, filters, contextQuery],
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
centered
|
||||
destroyOnClose
|
||||
open
|
||||
width={816}
|
||||
onCancel={onClose}
|
||||
onOk={onClose}
|
||||
footer={null}
|
||||
title={
|
||||
<TitleWrapper block>
|
||||
<Typography>Logs Context</Typography>
|
||||
|
||||
<EditButton
|
||||
$isDarkMode={isDarkMode}
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<EditFilled />}
|
||||
onClick={handleClickEditButton}
|
||||
/>
|
||||
</TitleWrapper>
|
||||
}
|
||||
>
|
||||
{isEdit && (
|
||||
<QueryBuilderSearch
|
||||
query={contextQuery?.builder.queryData[0]}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
)}
|
||||
<LogsContextList
|
||||
order={FILTERS.ASC}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...contextListParams}
|
||||
/>
|
||||
<RawLogView isActiveLog isReadOnly data={log} linesPerRow={1} />
|
||||
<LogsContextList
|
||||
order={FILTERS.DESC}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...contextListParams}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LogsExplorerContext);
|
30
frontend/src/container/LogsExplorerContext/styles.ts
Normal file
30
frontend/src/container/LogsExplorerContext/styles.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Button, Space } from 'antd';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import styled from 'styled-components';
|
||||
import getAlphaColor from 'utils/getAlphaColor';
|
||||
|
||||
export const TitleWrapper = styled(Space.Compact)`
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const EditButton = styled(Button)<{ $isDarkMode: boolean }>`
|
||||
margin-right: 0.938rem;
|
||||
width: 1.375rem !important;
|
||||
height: 1.375rem;
|
||||
position: absolute;
|
||||
|
||||
top: 1rem;
|
||||
right: 1.563rem;
|
||||
padding: 0;
|
||||
|
||||
border-radius: 0.125rem;
|
||||
|
||||
border-start-start-radius: 0.125rem !important;
|
||||
border-end-start-radius: 0.125rem !important;
|
||||
|
||||
color: ${({ $isDarkMode }): string =>
|
||||
$isDarkMode
|
||||
? getAlphaColor(themeColors.white)[45]
|
||||
: getAlphaColor(themeColors.black)[45]};
|
||||
`;
|
6
frontend/src/container/LogsExplorerContext/types.ts
Normal file
6
frontend/src/container/LogsExplorerContext/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
export interface LogsExplorerContextProps {
|
||||
log: ILog;
|
||||
onClose: VoidFunction;
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { getFiltersFromResources } from './utils';
|
||||
|
||||
const useInitialQuery = (log: ILog): Query => {
|
||||
const { updateAllQueriesOperators } = useQueryBuilder();
|
||||
const resourcesFilters = getFiltersFromResources(log.resources_string);
|
||||
|
||||
const updatedAllQueriesOperator = updateAllQueriesOperators(
|
||||
initialQueriesMap.logs,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.LOGS,
|
||||
);
|
||||
|
||||
const data: Query = {
|
||||
...updatedAllQueriesOperator,
|
||||
builder: {
|
||||
...updatedAllQueriesOperator.builder,
|
||||
queryData: updatedAllQueriesOperator.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: [...item.filters.items, ...resourcesFilters],
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export default useInitialQuery;
|
22
frontend/src/container/LogsExplorerContext/utils.ts
Normal file
22
frontend/src/container/LogsExplorerContext/utils.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export const getFiltersFromResources = (
|
||||
resources: ILog['resources_string'],
|
||||
): TagFilterItem[] =>
|
||||
Object.keys(resources).map((key: string) => {
|
||||
const resourceValue = resources[key] as string;
|
||||
return {
|
||||
id: uuid(),
|
||||
key: {
|
||||
key,
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: resourceValue,
|
||||
};
|
||||
});
|
@ -1,3 +1,4 @@
|
||||
import Spinner from 'components/Spinner';
|
||||
import { dragColumnParams } from 'hooks/useDragColumns/configs';
|
||||
import ReactDragListView from 'react-drag-listview';
|
||||
import { TableComponents } from 'react-virtuoso';
|
||||
@ -5,13 +6,18 @@ import { TableComponents } from 'react-virtuoso';
|
||||
import { TableStyled } from './styles';
|
||||
|
||||
interface LogsCustomTableProps {
|
||||
isLoading?: boolean;
|
||||
handleDragEnd: (fromIndex: number, toIndex: number) => void;
|
||||
}
|
||||
|
||||
export const LogsCustomTable = ({
|
||||
isLoading,
|
||||
handleDragEnd,
|
||||
}: LogsCustomTableProps): TableComponents['Table'] =>
|
||||
function CustomTable({ style, children }): JSX.Element {
|
||||
if (isLoading) {
|
||||
return <Spinner height="35px" tip="Getting Logs" />;
|
||||
}
|
||||
return (
|
||||
<ReactDragListView.DragColumn
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
export const infinityDefaultStyles: CSSProperties = {
|
||||
height: 'auto',
|
||||
width: '100%',
|
||||
overflowX: 'scroll',
|
||||
};
|
||||
|
@ -1,16 +1,26 @@
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { ColumnTypeRender } from 'components/Logs/TableView/types';
|
||||
import { useTableView } from 'components/Logs/TableView/useTableView';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import useDragColumns from 'hooks/useDragColumns';
|
||||
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||
import {
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { TableComponents, TableVirtuoso } from 'react-virtuoso';
|
||||
import {
|
||||
TableComponents,
|
||||
TableVirtuoso,
|
||||
TableVirtuosoHandle,
|
||||
} from 'react-virtuoso';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { infinityDefaultStyles } from './config';
|
||||
import { LogsCustomTable } from './LogsCustomTable';
|
||||
@ -22,105 +32,151 @@ import {
|
||||
import { InfinityTableProps } from './types';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const CustomTableRow: TableComponents['TableRow'] = ({
|
||||
const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
|
||||
children,
|
||||
context,
|
||||
...props
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
}) => <TableRowStyled {...props}>{children}</TableRowStyled>;
|
||||
|
||||
function InfinityTable({
|
||||
tableViewProps,
|
||||
infitiyTableProps,
|
||||
}: InfinityTableProps): JSX.Element | null {
|
||||
const { onEndReached } = infitiyTableProps;
|
||||
const { dataSource, columns } = useTableView(tableViewProps);
|
||||
const { draggedColumns, onDragColumns } = useDragColumns<
|
||||
Record<string, unknown>
|
||||
>(LOCALSTORAGE.LOGS_LIST_COLUMNS);
|
||||
|
||||
const tableColumns = useMemo(
|
||||
() => getDraggedColumns<Record<string, unknown>>(columns, draggedColumns),
|
||||
[columns, draggedColumns],
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(fromIndex: number, toIndex: number) =>
|
||||
onDragColumns(tableColumns, fromIndex, toIndex),
|
||||
[tableColumns, onDragColumns],
|
||||
);
|
||||
|
||||
const itemContent = useCallback(
|
||||
(index: number, log: Record<string, unknown>): JSX.Element => (
|
||||
<>
|
||||
{tableColumns.map((column) => {
|
||||
if (!column.render) return <td>Empty</td>;
|
||||
|
||||
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
|
||||
log[column.key as keyof Record<string, unknown>],
|
||||
log,
|
||||
index,
|
||||
);
|
||||
|
||||
const elementWithChildren = element as Exclude<
|
||||
ColumnTypeRender<Record<string, unknown>>,
|
||||
ReactNode
|
||||
>;
|
||||
|
||||
const children = elementWithChildren.children as ReactElement;
|
||||
const props = elementWithChildren.props as Record<string, unknown>;
|
||||
|
||||
return (
|
||||
<TableCellStyled key={column.key}>
|
||||
{cloneElement(children, props)}
|
||||
</TableCellStyled>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
),
|
||||
[tableColumns],
|
||||
);
|
||||
|
||||
const tableHeader = useCallback(
|
||||
() => (
|
||||
<tr>
|
||||
{tableColumns.map((column) => {
|
||||
const isDragColumn = column.key !== 'expand';
|
||||
|
||||
return (
|
||||
<TableHeaderCellStyled
|
||||
isDragColumn={isDragColumn}
|
||||
key={column.key}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...(isDragColumn && { className: 'dragHandler' })}
|
||||
>
|
||||
{column.title as string}
|
||||
</TableHeaderCellStyled>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
),
|
||||
[tableColumns],
|
||||
);
|
||||
}) => {
|
||||
const { isHighlighted } = useCopyLogLink(props.item.id);
|
||||
|
||||
return (
|
||||
<TableVirtuoso
|
||||
style={infinityDefaultStyles}
|
||||
data={dataSource}
|
||||
components={{
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
Table: LogsCustomTable({ handleDragEnd }),
|
||||
// TODO: fix it in the future
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
TableRow: CustomTableRow,
|
||||
}}
|
||||
itemContent={itemContent}
|
||||
fixedHeaderContent={tableHeader}
|
||||
endReached={onEndReached}
|
||||
totalCount={dataSource.length}
|
||||
/>
|
||||
<TableRowStyled
|
||||
$isActiveLog={isHighlighted}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TableRowStyled>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
function InfinityTableView(
|
||||
{ isLoading, tableViewProps, infitiyTableProps },
|
||||
ref,
|
||||
): JSX.Element | null {
|
||||
const {
|
||||
activeLog: activeContextLog,
|
||||
onSetActiveLog: handleSetActiveContextLog,
|
||||
onClearActiveLog: handleClearActiveContextLog,
|
||||
} = useActiveLog();
|
||||
const {
|
||||
activeLog,
|
||||
onSetActiveLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery,
|
||||
} = useActiveLog();
|
||||
|
||||
const { onEndReached } = infitiyTableProps;
|
||||
const { dataSource, columns } = useTableView({
|
||||
...tableViewProps,
|
||||
onClickExpand: onSetActiveLog,
|
||||
onOpenLogsContext: handleSetActiveContextLog,
|
||||
});
|
||||
const { draggedColumns, onDragColumns } = useDragColumns<
|
||||
Record<string, unknown>
|
||||
>(LOCALSTORAGE.LOGS_LIST_COLUMNS);
|
||||
|
||||
const tableColumns = useMemo(
|
||||
() => getDraggedColumns<Record<string, unknown>>(columns, draggedColumns),
|
||||
[columns, draggedColumns],
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(fromIndex: number, toIndex: number) =>
|
||||
onDragColumns(tableColumns, fromIndex, toIndex),
|
||||
[tableColumns, onDragColumns],
|
||||
);
|
||||
|
||||
const itemContent = useCallback(
|
||||
(index: number, log: Record<string, unknown>): JSX.Element => (
|
||||
<>
|
||||
{tableColumns.map((column) => {
|
||||
if (!column.render) return <td>Empty</td>;
|
||||
|
||||
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
|
||||
log[column.key as keyof Record<string, unknown>],
|
||||
log,
|
||||
index,
|
||||
);
|
||||
|
||||
const elementWithChildren = element as Exclude<
|
||||
ColumnTypeRender<Record<string, unknown>>,
|
||||
ReactNode
|
||||
>;
|
||||
|
||||
const children = elementWithChildren.children as ReactElement;
|
||||
const props = elementWithChildren.props as Record<string, unknown>;
|
||||
|
||||
return (
|
||||
<TableCellStyled key={column.key}>
|
||||
{cloneElement(children, props)}
|
||||
</TableCellStyled>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
),
|
||||
[tableColumns],
|
||||
);
|
||||
|
||||
const tableHeader = useCallback(
|
||||
() => (
|
||||
<tr>
|
||||
{tableColumns.map((column) => {
|
||||
const isDragColumn = column.key !== 'expand';
|
||||
|
||||
return (
|
||||
<TableHeaderCellStyled
|
||||
isDragColumn={isDragColumn}
|
||||
key={column.key}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...(isDragColumn && { className: 'dragHandler' })}
|
||||
>
|
||||
{column.title as string}
|
||||
</TableHeaderCellStyled>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
),
|
||||
[tableColumns],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableVirtuoso
|
||||
useWindowScroll
|
||||
ref={ref}
|
||||
style={infinityDefaultStyles}
|
||||
data={dataSource}
|
||||
components={{
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
Table: LogsCustomTable({ isLoading, handleDragEnd }),
|
||||
// TODO: fix it in the future
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
TableRow: CustomTableRow,
|
||||
}}
|
||||
itemContent={itemContent}
|
||||
fixedHeaderContent={tableHeader}
|
||||
endReached={onEndReached}
|
||||
totalCount={dataSource.length}
|
||||
/>
|
||||
|
||||
{activeContextLog && (
|
||||
<LogsExplorerContext
|
||||
log={activeContextLog}
|
||||
onClose={handleClearActiveContextLog}
|
||||
/>
|
||||
)}
|
||||
<LogDetail
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default InfinityTable;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { themeColors } from 'constants/theme';
|
||||
import styled from 'styled-components';
|
||||
import { getActiveLogBackground } from 'utils/logs';
|
||||
|
||||
interface TableHeaderCellStyledProps {
|
||||
isDragColumn: boolean;
|
||||
@ -22,10 +23,19 @@ export const TableCellStyled = styled.td`
|
||||
background-color: ${themeColors.lightBlack};
|
||||
`;
|
||||
|
||||
export const TableRowStyled = styled.tr`
|
||||
export const TableRowStyled = styled.tr<{
|
||||
$isActiveLog: boolean;
|
||||
}>`
|
||||
td {
|
||||
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
${TableCellStyled} {
|
||||
background-color: #1d1d1d;
|
||||
${({ $isActiveLog }): string =>
|
||||
$isActiveLog
|
||||
? getActiveLogBackground()
|
||||
: `background-color: ${themeColors.bckgGrey};`}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -33,7 +43,7 @@ export const TableRowStyled = styled.tr`
|
||||
export const TableHeaderCellStyled = styled.th<TableHeaderCellStyledProps>`
|
||||
padding: 0.5rem;
|
||||
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
|
||||
background-color: #1d1d1d;
|
||||
background-color: ${themeColors.bckgGrey};
|
||||
${({ isDragColumn }): string => (isDragColumn ? 'cursor: col-resize;' : '')}
|
||||
|
||||
&:first-child {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { UseTableViewProps } from 'components/Logs/TableView/types';
|
||||
|
||||
export type InfinityTableProps = {
|
||||
tableViewProps: UseTableViewProps;
|
||||
isLoading?: boolean;
|
||||
tableViewProps: Omit<UseTableViewProps, 'onOpenLogsContext' | 'onClickExpand'>;
|
||||
infitiyTableProps: {
|
||||
onEndReached: (index: number) => void;
|
||||
};
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@ -7,6 +6,4 @@ export type LogsExplorerListProps = {
|
||||
currentStagedQueryData: IBuilderQuery | null;
|
||||
logs: ILog[];
|
||||
onEndReached: (index: number) => void;
|
||||
onExpand: (log: ILog) => void;
|
||||
onOpenDetailedView: (log: ILog) => void;
|
||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'>;
|
||||
};
|
||||
|
@ -8,10 +8,11 @@ import ExplorerControlPanel from 'container/ExplorerControlPanel';
|
||||
import { Heading } from 'container/LogsTable/styles';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { contentStyle } from 'container/Trace/Search/config';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useFontFaceObserver from 'hooks/useFontObserver';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
// interfaces
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
@ -29,13 +30,13 @@ function LogsExplorerList({
|
||||
isLoading,
|
||||
currentStagedQueryData,
|
||||
logs,
|
||||
onOpenDetailedView,
|
||||
onEndReached,
|
||||
onExpand,
|
||||
onAddToQuery,
|
||||
}: LogsExplorerListProps): JSX.Element {
|
||||
const ref = useRef<VirtuosoHandle>(null);
|
||||
const { initialDataSource } = useQueryBuilder();
|
||||
|
||||
const { activeLogId } = useCopyLogLink();
|
||||
|
||||
const { options, config } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: initialDataSource || DataSource.METRICS,
|
||||
@ -43,6 +44,11 @@ function LogsExplorerList({
|
||||
currentStagedQueryData?.aggregateOperator || StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const activeLogIndex = useMemo(
|
||||
() => logs.findIndex(({ id }) => id === activeLogId),
|
||||
[logs, activeLogId],
|
||||
);
|
||||
|
||||
useFontFaceObserver(
|
||||
[
|
||||
{
|
||||
@ -65,35 +71,27 @@ function LogsExplorerList({
|
||||
(_: number, log: ILog): JSX.Element => {
|
||||
if (options.format === 'raw') {
|
||||
return (
|
||||
<RawLogView
|
||||
key={log.id}
|
||||
data={log}
|
||||
linesPerRow={options.maxLines}
|
||||
onClickExpand={onExpand}
|
||||
/>
|
||||
<RawLogView key={log.id} data={log} linesPerRow={options.maxLines} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListLogView
|
||||
key={log.id}
|
||||
logData={log}
|
||||
selectedFields={selectedFields}
|
||||
onOpenDetailedView={onOpenDetailedView}
|
||||
onAddToQuery={onAddToQuery}
|
||||
/>
|
||||
<ListLogView key={log.id} logData={log} selectedFields={selectedFields} />
|
||||
);
|
||||
},
|
||||
[
|
||||
options.format,
|
||||
options.maxLines,
|
||||
selectedFields,
|
||||
onOpenDetailedView,
|
||||
onAddToQuery,
|
||||
onExpand,
|
||||
],
|
||||
[options.format, options.maxLines, selectedFields],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeLogId || activeLogIndex < 0) return;
|
||||
|
||||
ref?.current?.scrollToIndex({
|
||||
index: activeLogIndex,
|
||||
align: 'start',
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, [activeLogId, activeLogIndex]);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
const components = isLoading
|
||||
? {
|
||||
@ -104,11 +102,12 @@ function LogsExplorerList({
|
||||
if (options.format === 'table') {
|
||||
return (
|
||||
<InfinityTableView
|
||||
ref={ref}
|
||||
isLoading={isLoading}
|
||||
tableViewProps={{
|
||||
logs,
|
||||
fields: selectedFields,
|
||||
linesPerRow: options.maxLines,
|
||||
onClickExpand: onExpand,
|
||||
appendTo: 'end',
|
||||
}}
|
||||
infitiyTableProps={{ onEndReached }}
|
||||
@ -119,6 +118,7 @@ function LogsExplorerList({
|
||||
return (
|
||||
<Card style={{ width: '100%' }} bodyStyle={{ ...contentStyle }}>
|
||||
<Virtuoso
|
||||
ref={ref}
|
||||
useWindowScroll
|
||||
data={logs}
|
||||
endReached={onEndReached}
|
||||
@ -136,7 +136,6 @@ function LogsExplorerList({
|
||||
onEndReached,
|
||||
getItemContent,
|
||||
selectedFields,
|
||||
onExpand,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
@ -1,13 +1,10 @@
|
||||
import { TabsProps } from 'antd';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import TabLabel from 'components/TabLabel';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialAutocompleteData,
|
||||
initialQueriesMap,
|
||||
OPERATORS,
|
||||
PANEL_TYPES,
|
||||
QueryBuilderKeys,
|
||||
} from 'constants/queryBuilder';
|
||||
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
|
||||
import ROUTES from 'constants/routes';
|
||||
@ -21,25 +18,20 @@ import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/const
|
||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
||||
import { LogTimeRange } from 'hooks/logs/types';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { generatePath, useHistory } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
IQueryAutocompleteResponse,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
OrderByPayload,
|
||||
@ -47,7 +39,6 @@ import {
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { ActionsWrapper, TabsStyled } from './LogsExplorerViews.styled';
|
||||
|
||||
@ -55,8 +46,7 @@ function LogsExplorerViews(): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
const history = useHistory();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink();
|
||||
const { queryData: pageSize } = useUrlQueryData(
|
||||
queryParamNamesMap.pageSize,
|
||||
DEFAULT_PER_PAGE_VALUE,
|
||||
@ -79,7 +69,6 @@ function LogsExplorerViews(): JSX.Element {
|
||||
} = useQueryBuilder();
|
||||
|
||||
// State
|
||||
const [activeLog, setActiveLog] = useState<ILog | null>(null);
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [logs, setLogs] = useState<ILog[]>([]);
|
||||
const [requestData, setRequestData] = useState<Query | null>(null);
|
||||
@ -167,16 +156,16 @@ function LogsExplorerViews(): JSX.Element {
|
||||
keepPreviousData: true,
|
||||
enabled: !isLimit,
|
||||
},
|
||||
{
|
||||
...(timeRange &&
|
||||
activeLogId &&
|
||||
!logs.length && {
|
||||
start: timeRange.start,
|
||||
end: timeRange.end,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const handleSetActiveLog = useCallback((nextActiveLog: ILog) => {
|
||||
setActiveLog(nextActiveLog);
|
||||
}, []);
|
||||
|
||||
const handleClearActiveLog = useCallback(() => {
|
||||
setActiveLog(null);
|
||||
}, []);
|
||||
|
||||
const getUpdateQuery = useCallback(
|
||||
(newPanelType: PANEL_TYPES): Query => {
|
||||
let query = updateAllQueriesOperators(
|
||||
@ -245,51 +234,6 @@ function LogsExplorerViews(): JSX.Element {
|
||||
[currentStagedQueryData, orderByTimestamp],
|
||||
);
|
||||
|
||||
const handleAddToQuery = useCallback(
|
||||
(fieldKey: string, fieldValue: string, operator: string): void => {
|
||||
const keysAutocomplete: BaseAutocompleteData[] =
|
||||
queryClient.getQueryData<SuccessResponse<IQueryAutocompleteResponse>>(
|
||||
[QueryBuilderKeys.GET_AGGREGATE_KEYS],
|
||||
{ exact: false },
|
||||
)?.payload.attributeKeys || [];
|
||||
|
||||
const existAutocompleteKey = chooseAutocompleteFromCustomValue(
|
||||
keysAutocomplete,
|
||||
fieldKey,
|
||||
);
|
||||
|
||||
const currentOperator =
|
||||
Object.keys(OPERATORS).find((op) => op === operator) || '';
|
||||
|
||||
const nextQuery: Query = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: [
|
||||
...item.filters.items.filter(
|
||||
(item) => item.key?.id !== existAutocompleteKey.id,
|
||||
),
|
||||
{
|
||||
id: uuid(),
|
||||
key: existAutocompleteKey,
|
||||
op: currentOperator,
|
||||
value: fieldValue,
|
||||
},
|
||||
],
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
redirectWithQueryBuilderData(nextQuery);
|
||||
},
|
||||
[currentQuery, queryClient, redirectWithQueryBuilderData],
|
||||
);
|
||||
|
||||
const handleEndReached = useCallback(
|
||||
(index: number) => {
|
||||
if (isLimit) return;
|
||||
@ -397,14 +341,24 @@ function LogsExplorerViews(): JSX.Element {
|
||||
}, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentParams = data?.params as Omit<LogTimeRange, 'pageSize'>;
|
||||
const currentData = data?.payload.data.newResult.data.result || [];
|
||||
if (currentData.length > 0 && currentData[0].list) {
|
||||
const currentLogs: ILog[] = currentData[0].list.map((item) => ({
|
||||
...item.data,
|
||||
timestamp: item.timestamp,
|
||||
}));
|
||||
setLogs((prevLogs) => [...prevLogs, ...currentLogs]);
|
||||
const newLogs = [...logs, ...currentLogs];
|
||||
|
||||
setLogs(newLogs);
|
||||
onTimeRangeChange({
|
||||
start: currentParams?.start,
|
||||
end: timeRange?.end || currentParams?.end,
|
||||
pageSize: newLogs.length,
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -415,14 +369,28 @@ function LogsExplorerViews(): JSX.Element {
|
||||
const newRequestData = getRequestData(stagedQuery, {
|
||||
page: 1,
|
||||
log: null,
|
||||
pageSize,
|
||||
pageSize:
|
||||
timeRange?.pageSize && activeLogId ? timeRange?.pageSize : pageSize,
|
||||
});
|
||||
setLogs([]);
|
||||
setPage(1);
|
||||
setRequestData(newRequestData);
|
||||
currentMinTimeRef.current = minTime;
|
||||
|
||||
if (!activeLogId) {
|
||||
onTimeRangeChange(null);
|
||||
}
|
||||
}
|
||||
}, [stagedQuery, requestData, getRequestData, pageSize, minTime]);
|
||||
}, [
|
||||
stagedQuery,
|
||||
requestData,
|
||||
getRequestData,
|
||||
pageSize,
|
||||
minTime,
|
||||
timeRange,
|
||||
activeLogId,
|
||||
onTimeRangeChange,
|
||||
]);
|
||||
|
||||
const tabsItems: TabsProps['items'] = useMemo(
|
||||
() => [
|
||||
@ -441,10 +409,7 @@ function LogsExplorerViews(): JSX.Element {
|
||||
isLoading={isFetching}
|
||||
currentStagedQueryData={currentStagedQueryData}
|
||||
logs={logs}
|
||||
onOpenDetailedView={handleSetActiveLog}
|
||||
onEndReached={handleEndReached}
|
||||
onExpand={handleSetActiveLog}
|
||||
onAddToQuery={handleAddToQuery}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@ -472,9 +437,7 @@ function LogsExplorerViews(): JSX.Element {
|
||||
isFetching,
|
||||
currentStagedQueryData,
|
||||
logs,
|
||||
handleSetActiveLog,
|
||||
handleEndReached,
|
||||
handleAddToQuery,
|
||||
data,
|
||||
isError,
|
||||
],
|
||||
@ -524,12 +487,6 @@ function LogsExplorerViews(): JSX.Element {
|
||||
onChange={handleChangeView}
|
||||
destroyInactiveTabPane
|
||||
/>
|
||||
<LogDetail
|
||||
log={activeLog}
|
||||
onClose={handleClearActiveLog}
|
||||
onAddToQuery={handleAddToQuery}
|
||||
onClickActionItem={handleAddToQuery}
|
||||
/>
|
||||
|
||||
<GoToTop />
|
||||
</>
|
||||
|
@ -4,18 +4,13 @@ import ListLogView from 'components/Logs/ListLogView';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import LogsTableView from 'components/Logs/TableView';
|
||||
import Spinner from 'components/Spinner';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { contentStyle } from 'container/Trace/Search/config';
|
||||
import useFontFaceObserver from 'hooks/useFontObserver';
|
||||
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { AppState } from 'store/reducers';
|
||||
// interfaces
|
||||
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
// styles
|
||||
@ -26,15 +21,10 @@ export type LogViewMode = 'raw' | 'table' | 'list';
|
||||
type LogsTableProps = {
|
||||
viewMode: LogViewMode;
|
||||
linesPerRow: number;
|
||||
onClickExpand: (logData: ILog) => void;
|
||||
};
|
||||
|
||||
function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
const { viewMode, onClickExpand, linesPerRow } = props;
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { viewMode, linesPerRow } = props;
|
||||
|
||||
useFontFaceObserver(
|
||||
[
|
||||
@ -52,7 +42,6 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
const {
|
||||
logs,
|
||||
fields: { selected },
|
||||
searchFilter: { queryString },
|
||||
isLoading,
|
||||
liveTail,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
@ -67,75 +56,23 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
liveTail,
|
||||
]);
|
||||
|
||||
const handleOpenDetailedView = useCallback(
|
||||
(logData: ILog) => {
|
||||
dispatch({
|
||||
type: SET_DETAILED_LOG_DATA,
|
||||
payload: logData,
|
||||
});
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleAddToQuery = useCallback(
|
||||
(fieldKey: string, fieldValue: string, operator: string) => {
|
||||
const updatedQueryString = getGeneratedFilterQueryString(
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
operator,
|
||||
queryString,
|
||||
);
|
||||
|
||||
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
|
||||
},
|
||||
[history, queryString],
|
||||
);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(index: number): JSX.Element => {
|
||||
const log = logs[index];
|
||||
|
||||
if (viewMode === 'raw') {
|
||||
return (
|
||||
<RawLogView
|
||||
key={log.id}
|
||||
data={log}
|
||||
linesPerRow={linesPerRow}
|
||||
onClickExpand={onClickExpand}
|
||||
/>
|
||||
);
|
||||
return <RawLogView key={log.id} data={log} linesPerRow={linesPerRow} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListLogView
|
||||
key={log.id}
|
||||
logData={log}
|
||||
selectedFields={selected}
|
||||
onOpenDetailedView={handleOpenDetailedView}
|
||||
onAddToQuery={handleAddToQuery}
|
||||
/>
|
||||
);
|
||||
return <ListLogView key={log.id} logData={log} selectedFields={selected} />;
|
||||
},
|
||||
[
|
||||
logs,
|
||||
viewMode,
|
||||
selected,
|
||||
linesPerRow,
|
||||
onClickExpand,
|
||||
handleOpenDetailedView,
|
||||
handleAddToQuery,
|
||||
],
|
||||
[logs, viewMode, selected, linesPerRow],
|
||||
);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
if (viewMode === 'table') {
|
||||
return (
|
||||
<LogsTableView
|
||||
logs={logs}
|
||||
fields={selected}
|
||||
linesPerRow={linesPerRow}
|
||||
onClickExpand={onClickExpand}
|
||||
/>
|
||||
<LogsTableView logs={logs} fields={selected} linesPerRow={linesPerRow} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -148,7 +85,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}, [getItemContent, linesPerRow, logs, onClickExpand, selected, viewMode]);
|
||||
}, [getItemContent, linesPerRow, logs, selected, viewMode]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner height={20} tip="Getting Logs" />;
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
} from 'types/common/queryBuilder';
|
||||
|
||||
import { DataType, FORMULA, MetricsType, WidgetKeys } from '../constant';
|
||||
import { IServiceName } from '../Tabs/types';
|
||||
import { DatabaseCallProps, DatabaseCallsRPSProps } from '../types';
|
||||
import {
|
||||
getQueryBuilderQueries,
|
||||
getQueryBuilderQuerieswithFormula,
|
||||
@ -103,8 +103,8 @@ export const databaseCallsAvgDuration = ({
|
||||
|
||||
const legends = ['', ''];
|
||||
const disabled = [true, true];
|
||||
const legendFormula = 'Average Duration';
|
||||
const expression = FORMULA.DATABASE_CALLS_AVG_DURATION;
|
||||
const legendFormulas = ['Average Duration'];
|
||||
const expressions = [FORMULA.DATABASE_CALLS_AVG_DURATION];
|
||||
const aggregateOperators = [
|
||||
MetricAggregateOperator.SUM,
|
||||
MetricAggregateOperator.SUM,
|
||||
@ -116,18 +116,9 @@ export const databaseCallsAvgDuration = ({
|
||||
additionalItems,
|
||||
legends,
|
||||
disabled,
|
||||
expression,
|
||||
legendFormula,
|
||||
expressions,
|
||||
legendFormulas,
|
||||
aggregateOperators,
|
||||
dataSource,
|
||||
});
|
||||
};
|
||||
|
||||
interface DatabaseCallsRPSProps extends DatabaseCallProps {
|
||||
legend: '{{db_system}}';
|
||||
}
|
||||
|
||||
interface DatabaseCallProps {
|
||||
servicename: IServiceName['servicename'];
|
||||
tagFilterItems: TagFilterItem[];
|
||||
}
|
||||
|
@ -83,22 +83,18 @@ export const externalCallErrorPercent = ({
|
||||
},
|
||||
...tagFilterItems,
|
||||
];
|
||||
const legendFormulas = [legend];
|
||||
const expressions = [FORMULA.ERROR_PERCENTAGE];
|
||||
const disabled = [true, true];
|
||||
const autocompleteData = [autocompleteDataA, autocompleteDataB];
|
||||
|
||||
const legendFormula = legend;
|
||||
const expression = FORMULA.ERROR_PERCENTAGE;
|
||||
const autocompleteData: BaseAutocompleteData[] = [
|
||||
autocompleteDataA,
|
||||
autocompleteDataB,
|
||||
const additionalItems = [additionalItemsA, additionalItemsB];
|
||||
|
||||
const aggregateOperators = [
|
||||
MetricAggregateOperator.SUM,
|
||||
MetricAggregateOperator.SUM,
|
||||
];
|
||||
|
||||
const additionalItems: TagFilterItem[][] = [
|
||||
additionalItemsA,
|
||||
additionalItemsB,
|
||||
];
|
||||
|
||||
const legends = Array(2).fill(legend);
|
||||
const aggregateOperators = Array(2).fill(MetricAggregateOperator.SUM);
|
||||
const disabled = Array(2).fill(true);
|
||||
const legends = [legend, legend];
|
||||
const dataSource = DataSource.METRICS;
|
||||
|
||||
return getQueryBuilderQuerieswithFormula({
|
||||
@ -107,8 +103,8 @@ export const externalCallErrorPercent = ({
|
||||
legends,
|
||||
groupBy,
|
||||
disabled,
|
||||
expression,
|
||||
legendFormula,
|
||||
expressions,
|
||||
legendFormulas,
|
||||
aggregateOperators,
|
||||
dataSource,
|
||||
});
|
||||
@ -130,11 +126,10 @@ export const externalCallDuration = ({
|
||||
key: WidgetKeys.SignozExternalCallLatencyCount,
|
||||
type: null,
|
||||
};
|
||||
|
||||
const expression = FORMULA.DATABASE_CALLS_AVG_DURATION;
|
||||
const legendFormula = 'Average Duration';
|
||||
const expressions = [FORMULA.DATABASE_CALLS_AVG_DURATION];
|
||||
const legendFormulas = ['Average Duration'];
|
||||
const legend = '';
|
||||
const disabled = Array(2).fill(true);
|
||||
const disabled = [true, true];
|
||||
const additionalItemsA: TagFilterItem[] = [
|
||||
{
|
||||
id: '',
|
||||
@ -150,28 +145,25 @@ export const externalCallDuration = ({
|
||||
...tagFilterItems,
|
||||
];
|
||||
|
||||
const autocompleteData: BaseAutocompleteData[] = [
|
||||
autocompleteDataA,
|
||||
autocompleteDataB,
|
||||
];
|
||||
const autocompleteData = [autocompleteDataA, autocompleteDataB];
|
||||
|
||||
const additionalItems: TagFilterItem[][] = [
|
||||
additionalItemsA,
|
||||
additionalItemsA,
|
||||
const additionalItems = [additionalItemsA, additionalItemsA];
|
||||
const legends = [legend, legend];
|
||||
const aggregateOperators = [
|
||||
MetricAggregateOperator.SUM,
|
||||
MetricAggregateOperator.SUM,
|
||||
];
|
||||
|
||||
const legends = Array(2).fill(legend);
|
||||
const aggregateOperators = Array(2).fill(MetricAggregateOperator.SUM);
|
||||
const dataSource = DataSource.METRICS;
|
||||
|
||||
return getQueryBuilderQuerieswithFormula({
|
||||
autocompleteData,
|
||||
additionalItems,
|
||||
legends,
|
||||
disabled,
|
||||
expression,
|
||||
legendFormula,
|
||||
expressions,
|
||||
legendFormulas,
|
||||
aggregateOperators,
|
||||
dataSource: DataSource.METRICS,
|
||||
dataSource,
|
||||
});
|
||||
};
|
||||
|
||||
@ -234,8 +226,8 @@ export const externalCallDurationByAddress = ({
|
||||
key: WidgetKeys.SignozExternalCallLatencyCount,
|
||||
type: null,
|
||||
};
|
||||
const expression = FORMULA.DATABASE_CALLS_AVG_DURATION;
|
||||
const legendFormula = legend;
|
||||
const expressions = [FORMULA.DATABASE_CALLS_AVG_DURATION];
|
||||
const legendFormulas = [legend];
|
||||
const disabled = [true, true];
|
||||
const additionalItemsA: TagFilterItem[] = [
|
||||
{
|
||||
@ -252,18 +244,13 @@ export const externalCallDurationByAddress = ({
|
||||
...tagFilterItems,
|
||||
];
|
||||
|
||||
const autocompleteData: BaseAutocompleteData[] = [
|
||||
autocompleteDataA,
|
||||
autocompleteDataB,
|
||||
const autocompleteData = [autocompleteDataA, autocompleteDataB];
|
||||
const additionalItems = [additionalItemsA, additionalItemsA];
|
||||
const legends = [legend, legend];
|
||||
const aggregateOperators = [
|
||||
MetricAggregateOperator.SUM,
|
||||
MetricAggregateOperator.SUM,
|
||||
];
|
||||
|
||||
const additionalItems: TagFilterItem[][] = [
|
||||
additionalItemsA,
|
||||
additionalItemsA,
|
||||
];
|
||||
|
||||
const legends = Array(2).fill(legend);
|
||||
const aggregateOperators = Array(2).fill(MetricAggregateOperator.SUM_RATE);
|
||||
const dataSource = DataSource.METRICS;
|
||||
|
||||
return getQueryBuilderQuerieswithFormula({
|
||||
@ -272,8 +259,8 @@ export const externalCallDurationByAddress = ({
|
||||
legends,
|
||||
groupBy,
|
||||
disabled,
|
||||
expression,
|
||||
legendFormula,
|
||||
expressions,
|
||||
legendFormulas,
|
||||
aggregateOperators,
|
||||
dataSource,
|
||||
});
|
||||
|
@ -67,18 +67,16 @@ export const getQueryBuilderQuerieswithFormula = ({
|
||||
legends,
|
||||
groupBy = [],
|
||||
disabled,
|
||||
expression,
|
||||
legendFormula,
|
||||
expressions,
|
||||
legendFormulas,
|
||||
aggregateOperators,
|
||||
dataSource,
|
||||
}: BuilderQuerieswithFormulaProps): QueryBuilderData => ({
|
||||
queryFormulas: [
|
||||
{
|
||||
...initialFormulaBuilderFormValues,
|
||||
expression,
|
||||
legend: legendFormula,
|
||||
},
|
||||
],
|
||||
queryFormulas: expressions.map((expression, index) => ({
|
||||
...initialFormulaBuilderFormValues,
|
||||
expression,
|
||||
legend: legendFormulas[index],
|
||||
})),
|
||||
queryData: autocompleteData.map((_, index) => ({
|
||||
...initialQueryBuilderFormValuesMap.metrics,
|
||||
aggregateOperator: aggregateOperators[index],
|
||||
|
@ -224,8 +224,8 @@ export const errorPercentage = ({
|
||||
const additionalItems = [additionalItemsA, additionalItemsB];
|
||||
const legends = [GraphTitle.ERROR_PERCENTAGE];
|
||||
const disabled = [true, true];
|
||||
const expression = FORMULA.ERROR_PERCENTAGE;
|
||||
const legendFormula = GraphTitle.ERROR_PERCENTAGE;
|
||||
const expressions = [FORMULA.ERROR_PERCENTAGE];
|
||||
const legendFormulas = [GraphTitle.ERROR_PERCENTAGE];
|
||||
const aggregateOperators = [
|
||||
MetricAggregateOperator.SUM_RATE,
|
||||
MetricAggregateOperator.SUM_RATE,
|
||||
@ -237,8 +237,8 @@ export const errorPercentage = ({
|
||||
additionalItems,
|
||||
legends,
|
||||
disabled,
|
||||
expression,
|
||||
legendFormula,
|
||||
expressions,
|
||||
legendFormulas,
|
||||
aggregateOperators,
|
||||
dataSource,
|
||||
});
|
||||
|
@ -124,8 +124,8 @@ export const topOperationQueries = ({
|
||||
MetricAggregateOperator.SUM_RATE,
|
||||
MetricAggregateOperator.SUM_RATE,
|
||||
];
|
||||
const expression = 'D*100/E';
|
||||
const legendFormula = GraphTitle.ERROR_PERCENTAGE;
|
||||
const expressions = ['D*100/E'];
|
||||
const legendFormulas = [GraphTitle.ERROR_PERCENTAGE];
|
||||
const dataSource = DataSource.METRICS;
|
||||
|
||||
return getQueryBuilderQuerieswithFormula({
|
||||
@ -134,8 +134,8 @@ export const topOperationQueries = ({
|
||||
disabled,
|
||||
legends,
|
||||
aggregateOperators,
|
||||
expression,
|
||||
legendFormula,
|
||||
expressions,
|
||||
legendFormulas,
|
||||
dataSource,
|
||||
groupBy,
|
||||
});
|
||||
|
@ -4,12 +4,13 @@ import { topOperationQueries } from 'container/MetricsApplication/MetricsPageQue
|
||||
import { QueryTable } from 'container/QueryTable';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { ReactNode, useMemo, useState } from 'react';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -18,18 +19,19 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { IServiceName } from '../types';
|
||||
import { title } from './config';
|
||||
import ColumnWithLink from './TableRenderer/ColumnWithLink';
|
||||
import { getTableColumnRenderer } from './TableRenderer/TableColumnRenderer';
|
||||
|
||||
function TopOperationMetrics(): JSX.Element {
|
||||
const { servicename } = useParams<IServiceName>();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const { queries } = useResourceAttribute();
|
||||
|
||||
const selectedTraceTags = JSON.stringify(
|
||||
@ -80,7 +82,7 @@ function TopOperationMetrics(): JSX.Element {
|
||||
enabled: !isEmptyWidget,
|
||||
refetchOnMount: false,
|
||||
onError: (error) => {
|
||||
setErrorMessage(error.message);
|
||||
notifications.error({ message: error.message });
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -104,13 +106,8 @@ function TopOperationMetrics(): JSX.Element {
|
||||
[servicename, minTime, maxTime, selectedTraceTags],
|
||||
);
|
||||
|
||||
if (errorMessage) {
|
||||
return <div>{errorMessage}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryTable
|
||||
title={title}
|
||||
query={updatedQuery}
|
||||
queryTableData={queryTableData}
|
||||
loading={isLoading}
|
||||
|
@ -36,8 +36,8 @@ export interface BuilderQuerieswithFormulaProps {
|
||||
legends: string[];
|
||||
disabled: boolean[];
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
expression: string;
|
||||
legendFormula: string;
|
||||
expressions: string[];
|
||||
legendFormulas: string[];
|
||||
additionalItems: TagFilterItem[][];
|
||||
aggregateOperators: MetricAggregateOperator[];
|
||||
dataSource: DataSource;
|
||||
|
@ -34,6 +34,7 @@ export enum KeyOperationTableHeader {
|
||||
P99 = 'P99',
|
||||
NUM_OF_CALLS = 'Number of Calls',
|
||||
ERROR_RATE = 'Error Rate',
|
||||
OPERATION_PR_SECOND = 'Op/s',
|
||||
}
|
||||
|
||||
export enum DataType {
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { IServiceName } from './Tabs/types';
|
||||
|
||||
export interface GetWidgetQueryBuilderProps {
|
||||
query: Widgets['query'];
|
||||
@ -13,3 +16,12 @@ export interface NavigateToTraceProps {
|
||||
maxTime: number;
|
||||
selectedTraceTags: string;
|
||||
}
|
||||
|
||||
export interface DatabaseCallsRPSProps extends DatabaseCallProps {
|
||||
legend: '{{db_system}}';
|
||||
}
|
||||
|
||||
export interface DatabaseCallProps {
|
||||
servicename: IServiceName['servicename'];
|
||||
tagFilterItems: TagFilterItem[];
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import query from 'api/dashboard/variables/query';
|
||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||
import map from 'lodash-es/map';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { variablePropsToPayloadVariables } from '../utils';
|
||||
@ -24,6 +24,16 @@ interface VariableItemProps {
|
||||
onAllSelectedUpdate: (name: string, arg1: boolean) => void;
|
||||
lastUpdatedVar: string;
|
||||
}
|
||||
|
||||
const getSelectValue = (
|
||||
selectedValue: IDashboardVariable['selectedValue'],
|
||||
): string | string[] => {
|
||||
if (Array.isArray(selectedValue)) {
|
||||
return selectedValue.map((item) => item.toString());
|
||||
}
|
||||
return selectedValue?.toString() || '';
|
||||
};
|
||||
|
||||
function VariableItem({
|
||||
variableData,
|
||||
existingVariables,
|
||||
@ -141,9 +151,15 @@ function VariableItem({
|
||||
}
|
||||
};
|
||||
|
||||
const { selectedValue } = variableData;
|
||||
const selectedValueStringified = useMemo(() => getSelectValue(selectedValue), [
|
||||
selectedValue,
|
||||
]);
|
||||
|
||||
const selectValue = variableData.allSelected
|
||||
? 'ALL'
|
||||
: variableData.selectedValue?.toString() || '';
|
||||
: selectedValueStringified;
|
||||
|
||||
const mode =
|
||||
variableData.multiSelect && !variableData.allSelected
|
||||
? 'multiple'
|
||||
|
@ -0,0 +1,14 @@
|
||||
import { IconDataSpan } from 'container/PipelinePage/styles';
|
||||
|
||||
import { getDeploymentStage, getDeploymentStageIcon } from './utils';
|
||||
|
||||
function DeploymentStage(deployStatus: string): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{getDeploymentStageIcon(deployStatus)}
|
||||
<IconDataSpan>{getDeploymentStage(deployStatus)}</IconDataSpan>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeploymentStage;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user