Compare commits
1 Commits
feat/panel
...
feat/code-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aed4821c8 |
2
.github/CODEOWNERS
vendored
@@ -138,7 +138,7 @@ go.mod @therealpandey
|
||||
/tests/integration/ @therealpandey
|
||||
|
||||
# e2e tests
|
||||
/tests/e2e/ @AshwinBhatkal
|
||||
/tests/e2e/ @SigNoz/frontend-maintainers
|
||||
|
||||
# Flagger Owners
|
||||
|
||||
|
||||
233
README.de-de.md
@@ -1,190 +1,197 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/readme-assets/signoz-hero-dark.png" width="900">
|
||||
<source media="(prefers-color-scheme: light)" srcset="docs/readme-assets/signoz-hero-light.png" width="900">
|
||||
<img alt="SigNoz - Observability nach deinen Bedingungen, basierend auf offenen Standards." src="docs/readme-assets/signoz-hero-light.png" width="900">
|
||||
</picture>
|
||||
<img src="https://res.cloudinary.com/dcv3epinx/image/upload/v1618904450/signoz-images/LogoGithub_sigfbu.svg" alt="SigNoz-logo" width="240" />
|
||||
|
||||
<p align="center">Überwache deine Anwendungen und behebe Probleme in deinen bereitgestellten Anwendungen. SigNoz ist eine Open Source Alternative zu DataDog, New Relic, etc.</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> ·
|
||||
<a href="README.zh-cn.md">中文</a> ·
|
||||
<a href="README.pt-br.md">Português</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>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/SigNoz/signoz/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/SigNoz/signoz"></a>
|
||||
<a href="https://github.com/SigNoz/signoz/releases"><img alt="GitHub release" src="https://img.shields.io/github/v/release/SigNoz/signoz?label=release"></a>
|
||||
<a href="https://signoz.io/slack"><img alt="Slack community" src="https://img.shields.io/badge/slack-community-4A154B?logo=slack&logoColor=white"></a>
|
||||
<a href="https://www.linkedin.com/company/signozio/"><img alt="LinkedIn" src="https://img.shields.io/badge/linkedin-SigNoz-0A66C2?logo=linkedin&logoColor=white"></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>
|
||||
</p>
|
||||
<h3 align="center">
|
||||
<a href="https://signoz.io/docs"><b>Dokumentation</b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/main/README.md"><b>Readme auf Englisch </b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/main/README.zh-cn.md"><b>ReadMe auf Chinesisch</b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/main/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>
|
||||
</h3>
|
||||
|
||||
SigNoz ist eine Open-Source-Observability-Plattform auf Basis von OpenTelemetry. Wir bauen eine Enterprise-taugliche Alternative zu fragmentierten Monitoring-Stacks, mit Logs, Metriken, Traces, Alerts und Dashboards an einem Ort.
|
||||
##
|
||||
|
||||
### Wähle, wie du SigNoz betreibst
|
||||
SigNoz hilft Entwicklern, Anwendungen zu überwachen und Probleme in ihren bereitgestellten Anwendungen zu beheben. Mit SigNoz können Sie Folgendes tun:
|
||||
|
||||
#### SigNoz Cloud (empfohlen)
|
||||
👉 Visualisieren Sie Metriken, Traces und Logs in einer einzigen Oberfläche.
|
||||
|
||||
Vollständig verwaltetes SigNoz mit 30 Tagen kostenloser Testphase, ohne Kreditkarte, nutzungsbasierter Preisgestaltung ab 49 USD und regionalem Datenhosting.
|
||||
👉 Sie können Metriken wie die p99-Latenz, Fehlerquoten für Ihre Dienste, externe API-Aufrufe und individuelle Endpunkte anzeigen.
|
||||
|
||||
[**Kostenlos starten →**](https://signoz.io/teams/)
|
||||
👉 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.
|
||||
|
||||
#### Enterprise
|
||||
👉 Führen Sie Aggregationen auf Trace-Daten durch, um geschäftsrelevante Metriken zu erhalten.
|
||||
|
||||
Enterprise Cloud, BYOC oder Enterprise Self-Hosted mit Compliance, Support, benutzerdefinierter Aufbewahrung, RBAC, Ingestion Controls, Datenresidenz und Regionsauswahl.
|
||||
👉 Filtern und Abfragen von Logs, Erstellen von Dashboards und Benachrichtigungen basierend auf Attributen in den Logs.
|
||||
|
||||
[**Enterprise entdecken →**](https://signoz.io/enterprise/)
|
||||
👉 Automatische Aufzeichnung von Ausnahmen in Python, Java, Ruby und Javascript.
|
||||
|
||||
#### Community
|
||||
👉 Einfache Einrichtung von Benachrichtigungen mit dem selbst erstellbaren Abfrage-Builder.
|
||||
|
||||
Kostenloses Open-Source-SigNoz, das in deiner eigenen Infrastruktur läuft. Deployment mit Docker, Kubernetes oder Linux, während du die volle Kontrolle über deine Datenebene behältst.
|
||||
##
|
||||
|
||||
[**SigNoz installieren →**](https://signoz.io/docs/install/self-host/)
|
||||
### Anwendung Metriken
|
||||
|
||||
### Was kannst du überwachen?
|
||||

|
||||
|
||||
SigNoz hilft Teams, Produktionsprobleme schneller zu debuggen, indem Logs, Metriken, Traces, Alerts, Dashboards, Exceptions und agent-native Workflows an einem Ort verbunden werden.
|
||||
### Verteiltes Tracing
|
||||
|
||||
#### APM-Überblick
|
||||
<img width="2068" alt="distributed_tracing_2 2" src="https://user-images.githubusercontent.com/83692067/226536447-bae58321-6a22-4ed3-af80-e3e964cb3489.png">
|
||||
|
||||
Überwache Service-Latenz, Fehlerrate, Durchsatz, Apdex, wichtige Endpunkte, Datenbankaufrufe und externe Aufrufe.
|
||||
<img width="2068" alt="distributed_tracing_1" src="https://user-images.githubusercontent.com/83692067/226536462-939745b6-4f9d-45a6-8016-814837e7f7b4.png">
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz APM-Dashboard mit Latenz, Durchsatz, Apdex und wichtigen Operationen" src="docs/readme-assets/monitor/apm.png" width="900">
|
||||
</p>
|
||||
### Log Verwaltung
|
||||
|
||||
Mehr erfahren: [APM-Dokumentation](https://signoz.io/docs/instrumentation/overview/)
|
||||
<img width="2068" alt="logs_management" src="https://user-images.githubusercontent.com/83692067/226536482-b8a5c4af-b69c-43d5-969c-338bd5eaf1a5.png">
|
||||
|
||||
#### Log-Management
|
||||
### Infrastruktur Überwachung
|
||||
|
||||
Erfasse, suche, aggregiere und korreliere Logs mit Traces und Metriken über einen visuellen Query Builder.
|
||||
<img width="2068" alt="infrastructure_monitoring" src="https://user-images.githubusercontent.com/83692067/226536496-f38c4dbf-e03c-4158-8be0-32d4a61158c7.png">
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz Logs Explorer mit Filtern, Frequenzdiagramm und Log-Zeilen" src="docs/readme-assets/monitor/log-management.svg" width="900">
|
||||
</p>
|
||||
### Exceptions Monitoring
|
||||
|
||||
Mehr erfahren: [Log-Management-Dokumentation](https://signoz.io/docs/logs-management/overview/)
|
||||

|
||||
|
||||
#### Metriken und Dashboards
|
||||
### Alarme
|
||||
|
||||
Erstelle Dashboards für Anwendungs-, Infrastruktur- und benutzerdefinierte Metriken mit Query Builder, PromQL oder ClickHouse SQL.
|
||||
<img width="2068" alt="alerts_management" src="https://user-images.githubusercontent.com/83692067/226536548-2c81e2e8-c12d-47e8-bad7-c6be79055def.png">
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz Host-Metrics-Dashboard mit Systemlast- und Netzwerkdiagrammen" src="docs/readme-assets/monitor/metrics.png" width="900">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
Mehr erfahren: [Metriken-Dokumentation](https://signoz.io/docs/metrics-management/overview/)
|
||||
## Werde Teil unserer Slack Community
|
||||
|
||||
#### Infrastruktur-Monitoring
|
||||
Sag Hi zu uns auf [Slack](https://signoz.io/slack) 👋
|
||||
|
||||
Überwache Kubernetes-Cluster, Pods, Nodes, Workloads sowie Host-CPU, Arbeitsspeicher, Festplatten, Netzwerk, Logs und Traces.
|
||||
<br /><br />
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz Kubernetes-Infrastruktur-Dashboard mit Pod- und Node-Metriken" src="docs/readme-assets/monitor/infrastructure.png" width="900">
|
||||
</p>
|
||||
## Funktionen:
|
||||
|
||||
Mehr erfahren: [Infrastruktur-Monitoring-Dokumentation](https://signoz.io/docs/infrastructure-monitoring/overview/)
|
||||
- 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.
|
||||
|
||||
#### LLM- und AI-Observability
|
||||
<br /><br />
|
||||
|
||||
Verfolge LLM-Apps, RAG-Pipelines, Prompts, Tool Calls, Tokens, Latenz und Kosten zusammen mit Anwendungs- und Infrastruktur-Telemetrie.
|
||||
## Wieso SigNoz?
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz LLM-Observability-Dashboard für Traces, Token-Nutzung, Latenz und Kosten" src="docs/readme-assets/monitor/llm.png" width="900">
|
||||
</p>
|
||||
Als Entwickler fanden wir es anstrengend, uns für jede kleine Funktion, die wir haben wollten, auf Closed Source SaaS Anbieter verlassen zu müssen. Closed Source Anbieter überraschen ihre Kunden zum Monatsende oft mit hohen Rechnungen, die keine Transparenz bzgl. der Kostenaufteilung bieten.
|
||||
|
||||
Mehr erfahren: [LLM-Observability-Dokumentation](https://signoz.io/docs/llm-observability/)
|
||||
Wir wollten eine selbst gehostete, Open Source Variante von Lösungen wie DataDog, NewRelic für Firmen anbieten, die Datenschutz und Sicherheitsbedenken haben, bei der Weitergabe von Kundendaten an Drittanbieter.
|
||||
|
||||
#### Agent-Native Observability und MCP
|
||||
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.
|
||||
|
||||
Nutze den SigNoz MCP-Server, um Telemetrie in Coding Agents zu bringen, oder nutze Noz in SigNoz, um Incidents zu untersuchen, Alerts zu verbessern und Dashboards mit Produktionskontext zu erstellen. [Noz](https://signoz.io/docs/ai/noz/) ist nur in SigNoz Cloud verfügbar.
|
||||
### Languages supported:
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz Noz-Oberfläche neben einem MCP-gestützten Agent-Workflow" src="docs/readme-assets/monitor/agent-native.png" width="900">
|
||||
</p>
|
||||
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:
|
||||
|
||||
Mehr erfahren: [SigNoz MCP-Server-Dokumentation](https://signoz.io/docs/ai/signoz-mcp-server/) · [Agent-Skills-Dokumentation](https://signoz.io/docs/ai/agent-skills/#install-the-plugin)
|
||||
- Java
|
||||
- Python
|
||||
- NodeJS
|
||||
- Go
|
||||
- PHP
|
||||
- .NET
|
||||
- Ruby
|
||||
- Elixir
|
||||
- Rust
|
||||
|
||||
#### Distributed Tracing
|
||||
Hier findest du die vollständige Liste von unterstützten Programmiersprachen - https://opentelemetry.io/docs/
|
||||
|
||||
Verfolge Requests über Services hinweg mit Flamegraphs, Waterfalls, Span Events, Filtern und Trace Analytics.
|
||||
<br /><br />
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz Distributed-Tracing-Ansicht mit Flamegraph und Waterfall-Spans" src="docs/readme-assets/monitor/distributed-tracing.png" width="900">
|
||||
</p>
|
||||
## Erste Schritte mit SigNoz
|
||||
|
||||
Mehr erfahren: [Distributed-Tracing-Dokumentation](https://signoz.io/docs/instrumentation/)
|
||||
### Bereitstellung mit Docker
|
||||
|
||||
#### Trace Funnels
|
||||
Bitte folge den [hier](https://signoz.io/docs/install/docker/) aufgelisteten Schritten um deine Anwendung mit Docker bereitzustellen.
|
||||
|
||||
Erstelle Funnels aus Traces, um Drop-offs im Request-Flow, fehlgeschlagene Übergänge und systemische Workflow-Probleme zu verstehen.
|
||||
Die [Anleitungen zur Fehlerbehebung](https://signoz.io/docs/install/troubleshooting/) könnten hilfreich sein, falls du auf irgendwelche Schwierigkeiten stößt.
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz Trace Funnels mit Request-Flow-Drop-offs und fehlgeschlagenen Übergängen" src="docs/readme-assets/monitor/trace-funnels.png" width="900">
|
||||
</p>
|
||||
<p>  </p>
|
||||
|
||||
Mehr erfahren: [Trace-Funnels-Dokumentation](https://signoz.io/docs/trace-funnels/overview/)
|
||||
### Deploy in Kubernetes using Helm
|
||||
|
||||
Du kannst außerdem [**Exceptions**](https://signoz.io/docs/userguide/exceptions/), [**Alerts**](https://signoz.io/docs/alerts/), [**externe APIs**](https://signoz.io/docs/external-api-monitoring/overview/) und [**Integrationen**](https://signoz.io/docs/integrations/integrations-list/) für OpenTelemetry, Prometheus, Kubernetes, Cloud-Anbieter, Sprach-SDKs, Application Frameworks, Datenbanken und LLM-Tools überwachen.
|
||||
Bitte folge den [hier](https://signoz.io/docs/deployment/helm_chart) aufgelisteten Schritten, um deine Anwendung mit Helm Charts bereitzustellen.
|
||||
|
||||
### Warum Teams SigNoz nutzen
|
||||
<br /><br />
|
||||
|
||||
1. **OpenTelemetry-native**<br>
|
||||
Einmal mit offenen Standards instrumentieren und die Kontrolle über deine Telemetrie behalten.
|
||||
2. **Korrelierte Signale**<br>
|
||||
Von Service-Charts zu Traces, Logs, Infrastrukturmetriken und Exceptions wechseln, ohne das Tool zu wechseln.
|
||||
3. **Eine einzelne spaltenorientierte Datenbank**<br>
|
||||
Gebaut für hochkardinale Observability-Workloads mit hohem Volumen.
|
||||
4. **Vorhersehbare Preise**<br>
|
||||
Keine Preise pro Host, keine Preise pro Nutzerplatz und keine Sonderpreise für Custom Metrics.
|
||||
5. **Enterprise-ready**<br>
|
||||
SOC 2 Type II und HIPAA Compliance, RBAC, Ingestion Controls, benutzerdefinierte Aufbewahrung, Support, BYOC und Self-Hosting.
|
||||
## Vergleiche mit bekannten Tools
|
||||
|
||||
### Erste Schritte
|
||||
### SigNoz vs Prometheus
|
||||
|
||||
#### Mit Cloud starten
|
||||
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.
|
||||
|
||||
Erstelle einen verwalteten SigNoz-Workspace und erhalte dein erstes Dashboard, ohne Observability-Infrastruktur betreiben zu müssen.
|
||||
Unser Ziel ist es, eine integrierte Benutzeroberfläche aus Metriken und Einzelschritt-Fehlersuchen anzubieten, ähnlich wie es SaaS Anbieter wie Datadog tun, mit der Möglichkeit von erweitertem filtern und aggregieren von Fehlersuchen. Etwas, was in Jaeger aktuell fehlt.
|
||||
|
||||
[**Kostenlos mit SigNoz Cloud starten**](https://signoz.io/teams/)
|
||||
<p>  </p>
|
||||
|
||||
#### SigNoz selbst hosten
|
||||
### SigNoz vs Jaeger
|
||||
|
||||
Betreibe SigNoz in deiner eigenen Infrastruktur mit Foundry, Docker, Kubernetes oder Linux.
|
||||
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.
|
||||
|
||||
[**Foundry**](https://github.com/SigNoz/foundry) · [**Docker**](https://signoz.io/docs/install/docker/) · [**Kubernetes**](https://signoz.io/docs/install/kubernetes/) · [**Linux**](https://signoz.io/docs/install/linux/)
|
||||
Außerdem hat SigNoz noch mehr spezielle Funktionen im Vergleich zu Jaeger:
|
||||
|
||||
#### Daten senden
|
||||
- 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.
|
||||
|
||||
Instrumentiere Anwendungen und Infrastruktur mit OpenTelemetry, Prometheus, Sprach-SDKs und Integrationen.
|
||||
<p>  </p>
|
||||
|
||||
[**Instrumentation**](https://signoz.io/docs/instrumentation/) · [**Integrationen**](https://signoz.io/docs/integrations/integrations-list/)
|
||||
### SigNoz vs Elastic
|
||||
|
||||
### Vergleich mit bekannten Tools
|
||||
- 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.
|
||||
|
||||
SigNoz wird häufig von Teams eingeführt, die von einzelnen Spezialtools oder kommerziellen Plattformen mit unvorhersehbarer Preisgestaltung wechseln.
|
||||
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)
|
||||
|
||||
**Prometheus**<br>
|
||||
Gut, wenn du nur Metriken brauchst. SigNoz hält Metriken, Logs, Traces, Dashboards und Alerts zusammen, damit Teams mit korreliertem Kontext debuggen können.
|
||||
<p>  </p>
|
||||
|
||||
**Jaeger**<br>
|
||||
Jaeger macht ausschließlich Distributed Tracing. SigNoz ergänzt Metriken, Logs, Trace Analytics, Dashboards, Alerts, Exceptions und Trace-to-Log-Workflows.
|
||||
### SigNoz vs Loki
|
||||
|
||||
**Elastic**<br>
|
||||
SigNoz nutzt eine spaltenorientierte Datenbank für effiziente Observability-Analysen und hochkardinale Log-Workloads, mit 50 % geringerem Ressourcenbedarf gegenüber Elastic während der Ingestion. Lies die [detaillierte Studie](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark).
|
||||
- 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.
|
||||
|
||||
**Loki**<br>
|
||||
Im verlinkten Benchmark indexierte SigNoz alle Keys im Test-Setup, während Loki beim Hinzufügen weiterer Labels Max-Stream-Fehler erreichte. Lies die [detaillierte Studie](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark).
|
||||
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)
|
||||
|
||||
## Mitwirken
|
||||
<br /><br />
|
||||
|
||||
Wir freuen uns über große und kleine Beiträge. Lies bitte [CONTRIBUTING.md](CONTRIBUTING.md), um mit Beiträgen zu SigNoz loszulegen.
|
||||
## Zum Projekt beitragen
|
||||
|
||||
Nicht sicher, wie du anfangen sollst? **Schreib uns einfach im Channel `#contributing` 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.
|
||||
Du bist dir nicht sicher, wie du anfangen sollst? Schreib uns einfach auf dem #contributing Kanal in unserer [slack community](https://signoz.io/slack)
|
||||
|
||||
Wie immer: Danke an unsere großartigen Contributors!
|
||||
<br /><br />
|
||||
|
||||
## 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 />
|
||||
|
||||
## 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.
|
||||
|
||||
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, Dank an unsere großartigen Mitwirkenden!
|
||||
|
||||
<a href="https://github.com/signoz/signoz/graphs/contributors">
|
||||
<img alt="SigNoz Contributors" src="https://contrib.rocks/image?repo=signoz/signoz" />
|
||||
<img src="https://contrib.rocks/image?repo=signoz/signoz" />
|
||||
</a>
|
||||
|
||||
278
README.md
@@ -1,190 +1,244 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/readme-assets/signoz-hero-dark.png" width="900">
|
||||
<source media="(prefers-color-scheme: light)" srcset="docs/readme-assets/signoz-hero-light.png" width="900">
|
||||
<img alt="SigNoz - Observability on Your Terms, Powered by Open Standards." src="docs/readme-assets/signoz-hero-light.png" width="900">
|
||||
</picture>
|
||||
</p>
|
||||
<h1 align="center" style="border-bottom: none">
|
||||
<a href="https://signoz.io" target="_blank">
|
||||
<img alt="SigNoz" src="https://github.com/user-attachments/assets/ef9a33f7-12d7-4c94-8908-0a02b22f0c18" width="100" height="100">
|
||||
</a>
|
||||
<br>SigNoz
|
||||
</h1>
|
||||
|
||||
<p align="center">All your logs, metrics, and traces in one place. Monitor your application, spot issues before they occur and troubleshoot downtime quickly with rich context. SigNoz is a cost-effective open-source alternative to Datadog and New Relic. Visit <a href="https://signoz.io" target="_blank">signoz.io</a> for the full documentation, tutorials, and guide.</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.zh-cn.md">中文</a> ·
|
||||
<a href="README.de-de.md">Deutsch</a> ·
|
||||
<a href="README.pt-br.md">Português</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>
|
||||
</p>
|
||||
|
||||
|
||||
<h3 align="center">
|
||||
<a href="https://signoz.io/docs"><b>Documentation</b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/main/README.zh-cn.md"><b>ReadMe in Chinese</b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/main/README.de-de.md"><b>ReadMe in German</b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/main/README.pt-br.md"><b>ReadMe in Portuguese</b></a> •
|
||||
<a href="https://signoz.io/slack"><b>Slack Community</b></a> •
|
||||
<a href="https://twitter.com/SigNozHq"><b>Twitter</b></a>
|
||||
</h3>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/SigNoz/signoz/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/SigNoz/signoz"></a>
|
||||
<a href="https://github.com/SigNoz/signoz/releases"><img alt="GitHub release" src="https://img.shields.io/github/v/release/SigNoz/signoz?label=release"></a>
|
||||
<a href="https://signoz.io/slack"><img alt="Slack community" src="https://img.shields.io/badge/slack-community-4A154B?logo=slack&logoColor=white"></a>
|
||||
<a href="https://www.linkedin.com/company/signozio/"><img alt="LinkedIn" src="https://img.shields.io/badge/linkedin-SigNoz-0A66C2?logo=linkedin&logoColor=white"></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>
|
||||
</p>
|
||||
## Features
|
||||
|
||||
SigNoz is an open-source observability platform built on OpenTelemetry. We’re building an enterprise-grade alternative to fragmented monitoring stacks, with logs, metrics, traces, alerts, and dashboards in one place.
|
||||
|
||||
### Choose how to run SigNoz
|
||||
### Application Performance Monitoring
|
||||
|
||||
#### SigNoz Cloud (Recommended)
|
||||
Use SigNoz APM to monitor your applications and services. It comes with out-of-box charts for key application metrics like p99 latency, error rate, Apdex and operations per second. You can also monitor the database and external calls made from your application. Read [more](https://signoz.io/application-performance-monitoring/).
|
||||
|
||||
Fully managed SigNoz with a 30-day free trial, no credit card required, usage-based pricing that starts at $49, and regional data hosting.
|
||||
You can [instrument](https://signoz.io/docs/instrumentation/) your application with OpenTelemetry to get started.
|
||||
|
||||
[**Start free →**](https://signoz.io/teams/)
|
||||

|
||||
|
||||
#### Enterprise
|
||||
|
||||
Enterprise Cloud, BYOC, or Enterprise Self-Hosted with compliance, support, custom retention, RBAC, ingestion controls, data residency, and region selection.
|
||||
### Logs Management
|
||||
|
||||
[**Explore Enterprise →**](https://signoz.io/enterprise/)
|
||||
SigNoz can be used as a centralized log management solution. We use ClickHouse (used by likes of Uber & Cloudflare) as a datastore, ⎯ an extremely fast and highly optimized storage for logs data. Instantly search through all your logs using quick filters and a powerful query builder.
|
||||
|
||||
#### Community
|
||||
You can also create charts on your logs and monitor them with customized dashboards. Read [more](https://signoz.io/log-management/).
|
||||
|
||||
Free open-source SigNoz that runs in your own infrastructure. Deploy with Docker, Kubernetes, or Linux and keep full control of your data plane.
|
||||

|
||||
|
||||
[**Install SigNoz →**](https://signoz.io/docs/install/self-host/)
|
||||
|
||||
### What can you monitor?
|
||||
### Distributed Tracing
|
||||
|
||||
SigNoz helps teams debug production issues faster by connecting logs, metrics, traces, alerts, dashboards, exceptions, and agent-native workflows in one place.
|
||||
Distributed Tracing is essential to troubleshoot issues in microservices applications. Powered by OpenTelemetry, distributed tracing in SigNoz can help you track user requests across services to help you identify performance bottlenecks.
|
||||
|
||||
#### APM Overview
|
||||
See user requests in a detailed breakdown with the help of Flamegraphs and Gantt Charts. Click on any span to see the entire trace represented beautifully, which will help you make sense of where issues actually occurred in the flow of requests.
|
||||
|
||||
Monitor service latency, error rate, throughput, Apdex, top endpoints, database calls, and external calls.
|
||||
Read [more](https://signoz.io/distributed-tracing/).
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz APM dashboard showing latency, throughput, Apdex, and key operations" src="docs/readme-assets/monitor/apm.png" width="900">
|
||||
</p>
|
||||

|
||||
|
||||
Learn more: [APM documentation](https://signoz.io/docs/instrumentation/overview/)
|
||||
|
||||
#### Log Management
|
||||
|
||||
Ingest, search, aggregate, and correlate logs with traces and metrics using a visual query builder.
|
||||
### Metrics and Dashboards
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz logs explorer with filters, frequency chart, and log lines" src="docs/readme-assets/monitor/log-management.svg" width="900">
|
||||
</p>
|
||||
Ingest metrics from your infrastructure or applications and create customized dashboards to monitor them. Create visualization that suits your needs with a variety of panel types like pie chart, time-series, bar chart, etc.
|
||||
|
||||
Learn more: [Log management documentation](https://signoz.io/docs/logs-management/overview/)
|
||||
Create queries on your metrics data quickly with an easy-to-use metrics query builder. Add multiple queries and combine those queries with formulae to create really complex queries quickly.
|
||||
|
||||
#### Metrics and Dashboards
|
||||
Read [more](https://signoz.io/metrics-and-dashboards/).
|
||||
|
||||
Build dashboards for application, infrastructure, and custom metrics using Query Builder, PromQL, or ClickHouse SQL.
|
||||

|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz host metrics dashboard with system load and network charts" src="docs/readme-assets/monitor/metrics.png" width="900">
|
||||
</p>
|
||||
### LLM Observability
|
||||
|
||||
Learn more: [Metrics documentation](https://signoz.io/docs/metrics-management/overview/)
|
||||
Monitor and debug your LLM applications with comprehensive observability. Track LLM calls, analyze token usage, monitor performance, and gain insights into your AI application's behavior in production.
|
||||
|
||||
#### Infrastructure Monitoring
|
||||
SigNoz LLM observability helps you understand how your language models are performing, identify issues with prompts and responses, track token usage and costs, and optimize your AI applications for better performance and reliability.
|
||||
|
||||
Monitor Kubernetes clusters, pods, nodes, workloads, and host-level CPU, memory, disk, network, logs, and traces.
|
||||
[Get started with LLM Observability →](https://signoz.io/docs/llm-observability/)
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz Kubernetes infrastructure dashboard with pod and node metrics" src="docs/readme-assets/monitor/infrastructure.png" width="900">
|
||||
</p>
|
||||

|
||||
|
||||
Learn more: [Infrastructure monitoring documentation](https://signoz.io/docs/infrastructure-monitoring/overview/)
|
||||
|
||||
#### LLM and AI Observability
|
||||
### Alerts
|
||||
|
||||
Trace LLM apps, RAG pipelines, prompts, tool calls, tokens, latency, and costs alongside application and infrastructure telemetry.
|
||||
Use alerts in SigNoz to get notified when anything unusual happens in your application. You can set alerts on any type of telemetry signal (logs, metrics, traces), create thresholds and set up a notification channel to get notified. Advanced features like alert history and anomaly detection can help you create smarter alerts.
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz LLM observability dashboard for traces, token usage, latency, and costs" src="docs/readme-assets/monitor/llm.png" width="900">
|
||||
</p>
|
||||
Alerts in SigNoz help you identify issues proactively so that you can address them before they reach your customers.
|
||||
|
||||
Learn more: [LLM observability documentation](https://signoz.io/docs/llm-observability/)
|
||||
Read [more](https://signoz.io/alerts-management/).
|
||||
|
||||
#### Agent-Native Observability and MCP
|
||||

|
||||
|
||||
Use the SigNoz MCP server to bring telemetry into coding agents, or use Noz inside SigNoz to investigate incidents, tune alerts, and build dashboards with production context. [Noz](https://signoz.io/docs/ai/noz/) is available only on SigNoz Cloud.
|
||||
### Exceptions Monitoring
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz Noz interface alongside MCP-powered agent workflow" src="docs/readme-assets/monitor/agent-native.png" width="900">
|
||||
</p>
|
||||
Monitor exceptions automatically in Python, Java, Ruby, and Javascript. For other languages, just drop in a few lines of code and start monitoring exceptions.
|
||||
|
||||
Learn more: [SigNoz MCP server docs](https://signoz.io/docs/ai/signoz-mcp-server/) · [Agent skills docs](https://signoz.io/docs/ai/agent-skills/#install-the-plugin)
|
||||
See the detailed stack trace for all exceptions caught in your application. You can also log in custom attributes to add more context to your exceptions. For example, you can add attributes to identify users for which exceptions occurred.
|
||||
|
||||
#### Distributed Tracing
|
||||
Read [more](https://signoz.io/exceptions-monitoring/).
|
||||
|
||||
Follow requests across services with flamegraphs, waterfalls, span events, filters, and trace analytics.
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz distributed trace view with flamegraph and waterfall spans" src="docs/readme-assets/monitor/distributed-tracing.png" width="900">
|
||||
</p>
|
||||

|
||||
|
||||
Learn more: [Distributed tracing documentation](https://signoz.io/docs/instrumentation/)
|
||||
|
||||
#### Trace Funnels
|
||||
<br /><br />
|
||||
|
||||
Create funnels from traces to understand request-flow drop-offs, failed transitions, and systemic workflow issues.
|
||||
## Why SigNoz?
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz trace funnels showing request-flow drop-offs and failed transitions" src="docs/readme-assets/monitor/trace-funnels.png" width="900">
|
||||
</p>
|
||||
SigNoz is a single tool for all your monitoring and observability needs. Here are a few reasons why you should choose SigNoz:
|
||||
|
||||
Learn more: [Trace funnels documentation](https://signoz.io/docs/trace-funnels/overview/)
|
||||
- Single tool for observability(logs, metrics, and traces)
|
||||
|
||||
Also monitor: [**exceptions**](https://signoz.io/docs/userguide/exceptions/), [**alerts**](https://signoz.io/docs/alerts/), [**external APIs**](https://signoz.io/docs/external-api-monitoring/overview/), and [**integrations**](https://signoz.io/docs/integrations/integrations-list/) for OpenTelemetry, Prometheus, Kubernetes, cloud providers, language SDKs, application frameworks, databases, and LLM tools.
|
||||
- Built on top of [OpenTelemetry](https://opentelemetry.io/), the open-source standard which frees you from any type of vendor lock-in
|
||||
|
||||
### Why teams use SigNoz
|
||||
- Correlated logs, metrics and traces for much richer context while debugging
|
||||
|
||||
1. **OpenTelemetry-native**<br>
|
||||
Instrument once with open standards and keep ownership of your telemetry.
|
||||
2. **Correlated signals**<br>
|
||||
Move from service charts to traces, logs, infra metrics, and exceptions without switching tools.
|
||||
3. **Single columnar database**<br>
|
||||
Built for high-cardinality, high-volume observability workloads.
|
||||
4. **Predictable pricing**<br>
|
||||
No per-host pricing, no user-seat pricing, and no special pricing for custom metrics.
|
||||
5. **Enterprise ready**<br>
|
||||
SOC 2 Type II and HIPAA compliance, RBAC, ingestion controls, custom retention, support, BYOC, and self-hosting.
|
||||
- Uses ClickHouse (used by likes of Uber & Cloudflare) as datastore - an extremely fast and highly optimized storage for observability data
|
||||
|
||||
### Getting started
|
||||
- DIY Query builder, PromQL, and ClickHouse queries to fulfill all your use-cases around querying observability data
|
||||
|
||||
#### Start on Cloud
|
||||
- Open-Source - you can use open-source, our [cloud service](https://signoz.io/teams/) or a mix of both based on your use case
|
||||
|
||||
Create a managed SigNoz workspace and get your first dashboard without running observability infrastructure.
|
||||
|
||||
[**Start free on SigNoz Cloud**](https://signoz.io/teams/)
|
||||
## Getting Started
|
||||
|
||||
#### Self-host SigNoz
|
||||
### Create a SigNoz Cloud Account
|
||||
|
||||
Run SigNoz in your own infrastructure with Foundry, Docker, Kubernetes, or Linux.
|
||||
SigNoz cloud is the easiest way to get started with SigNoz. Our cloud service is for those users who want to spend more time in getting insights for their application performance without worrying about maintenance.
|
||||
|
||||
[**Foundry**](https://github.com/SigNoz/foundry) · [**Docker**](https://signoz.io/docs/install/docker/) · [**Kubernetes**](https://signoz.io/docs/install/kubernetes/) · [**Linux**](https://signoz.io/docs/install/linux/)
|
||||
[Get started for free](https://signoz.io/teams/)
|
||||
|
||||
#### Send data
|
||||
### Deploy using Docker(self-hosted)
|
||||
|
||||
Instrument applications and infrastructure with OpenTelemetry, Prometheus, language SDKs, and integrations.
|
||||
Please follow the steps listed [here](https://signoz.io/docs/install/docker/) to install using docker
|
||||
|
||||
[**Instrumentation**](https://signoz.io/docs/instrumentation/) · [**Integrations**](https://signoz.io/docs/integrations/integrations-list/)
|
||||
The [troubleshooting instructions](https://signoz.io/docs/install/troubleshooting/) may be helpful if you face any issues.
|
||||
|
||||
### Comparisons to familiar tools
|
||||
<p>  </p>
|
||||
|
||||
|
||||
### Deploy in Kubernetes using Helm(self-hosted)
|
||||
|
||||
SigNoz is often adopted by teams moving from a stack of single-purpose tools or commercial platforms with unpredictable pricing.
|
||||
Please follow the steps listed [here](https://signoz.io/docs/deployment/helm_chart) to install using helm charts
|
||||
|
||||
**Prometheus**<br>
|
||||
Good if you just need metrics. SigNoz keeps metrics, logs, traces, dashboards, and alerts together so teams can debug with correlated context.
|
||||
<br /><br />
|
||||
|
||||
**Jaeger**<br>
|
||||
Jaeger only does distributed tracing. SigNoz adds metrics, logs, trace analytics, dashboards, alerts, exceptions, and trace-to-log workflows.
|
||||
We also offer managed services in your infra. Check our [pricing plans](https://signoz.io/pricing/) for all details.
|
||||
|
||||
**Elastic**<br>
|
||||
SigNoz uses columnar database for efficient observability analytics and high-cardinality log workloads, with 50% lower resource requirement compared to Elastic during ingestion. Check the [detailed study](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark).
|
||||
|
||||
**Loki**<br>
|
||||
In the linked benchmark, SigNoz indexed all keys in the test setup, while Loki hit max stream errors when more labels were added. Check the [detailed study](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark).
|
||||
## Join our Slack community
|
||||
|
||||
Come say Hi to us on [Slack](https://signoz.io/slack) 👋
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
### Languages supported:
|
||||
|
||||
SigNoz supports all major programming languages for monitoring. Any framework and language supported by OpenTelemetry is supported by SigNoz. Find instructions for instrumenting different languages below:
|
||||
|
||||
- [Java](https://signoz.io/docs/instrumentation/java/)
|
||||
- [Python](https://signoz.io/docs/instrumentation/python/)
|
||||
- [Node.js or Javascript](https://signoz.io/docs/instrumentation/javascript/)
|
||||
- [Go](https://signoz.io/docs/instrumentation/golang/)
|
||||
- [PHP](https://signoz.io/docs/instrumentation/php/)
|
||||
- [.NET](https://signoz.io/docs/instrumentation/dotnet/)
|
||||
- [Ruby](https://signoz.io/docs/instrumentation/ruby-on-rails/)
|
||||
- [Elixir](https://signoz.io/docs/instrumentation/elixir/)
|
||||
- [Rust](https://signoz.io/docs/instrumentation/rust/)
|
||||
- [Swift](https://signoz.io/docs/instrumentation/swift/)
|
||||
|
||||
You can find our entire documentation [here](https://signoz.io/docs/introduction/).
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## Comparisons to Familiar Tools
|
||||
|
||||
### SigNoz vs Prometheus
|
||||
|
||||
Prometheus is good if you want to do just metrics. But if you want to have a seamless experience between metrics, logs and traces, then current experience of stitching together Prometheus & other tools is not great.
|
||||
|
||||
SigNoz is a one-stop solution for metrics and other telemetry signals. And because you will use the same standard(OpenTelemetry) to collect all telemetry signals, you can also correlate these signals to troubleshoot quickly.
|
||||
|
||||
For example, if you see that there are issues with infrastructure metrics of your k8s cluster at a timestamp, you can jump to other signals like logs and traces to understand the issue quickly.
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### SigNoz vs Jaeger
|
||||
|
||||
Jaeger only does distributed tracing. SigNoz supports metrics, traces and logs - all the 3 pillars of observability.
|
||||
|
||||
Moreover, SigNoz has few more advanced features wrt Jaeger:
|
||||
|
||||
- Jaegar UI doesn’t show any metrics on traces or on filtered traces
|
||||
- Jaeger can’t get aggregates on filtered traces. For example, p99 latency of requests which have tag - customer_type='premium'. This can be done easily on SigNoz
|
||||
- You can also go from traces to logs easily in SigNoz
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### SigNoz vs Elastic
|
||||
|
||||
- SigNoz Logs management are based on ClickHouse, a columnar OLAP datastore which makes aggregate log analytics queries much more efficient
|
||||
- 50% lower resource requirement compared to Elastic during ingestion
|
||||
|
||||
We have published benchmarks comparing Elastic with SigNoz. Check it out [here](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### SigNoz vs Loki
|
||||
|
||||
- SigNoz supports aggregations on high-cardinality data over a huge volume while loki doesn’t.
|
||||
- SigNoz supports indexes over high cardinality data and has no limitations on the number of indexes, while Loki reaches max streams with a few indexes added to it.
|
||||
- Searching over a huge volume of data is difficult and slow in Loki compared to SigNoz
|
||||
|
||||
We have published benchmarks comparing Loki with SigNoz. Check it out [here](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
We ❤️ contributions big or small. Please read [CONTRIBUTING.md](CONTRIBUTING.md) to get started with making contributions to SigNoz.
|
||||
|
||||
Not sure how to get started? **Just ping us on `#contributing` in our [slack community](https://signoz.io/slack).**
|
||||
Not sure how to get started? Just ping us on `#contributing` in our [slack community](https://signoz.io/slack)
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
You can find docs at https://signoz.io/docs/. If you need any clarification or find something missing, feel free to raise a GitHub issue with the label `documentation` or reach out to us at the community slack channel.
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## Community
|
||||
|
||||
Join the [slack community](https://signoz.io/slack) to know more about distributed tracing, observability, or SigNoz and to connect with other users and contributors.
|
||||
|
||||
If you have any ideas, questions, or any feedback, please share on our [Github Discussions](https://github.com/SigNoz/signoz/discussions)
|
||||
|
||||
As always, thanks to our amazing contributors!
|
||||
|
||||
<a href="https://github.com/signoz/signoz/graphs/contributors">
|
||||
<img alt="SigNoz contributors" src="https://contrib.rocks/image?repo=signoz/signoz" />
|
||||
<img src="https://contrib.rocks/image?repo=signoz/signoz" />
|
||||
</a>
|
||||
|
||||
232
README.pt-br.md
@@ -1,190 +1,158 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/readme-assets/signoz-hero-dark.png" width="900">
|
||||
<source media="(prefers-color-scheme: light)" srcset="docs/readme-assets/signoz-hero-light.png" width="900">
|
||||
<img alt="SigNoz - Observabilidade nos seus termos, baseada em padrões abertos." src="docs/readme-assets/signoz-hero-light.png" width="900">
|
||||
</picture>
|
||||
<img src="https://res.cloudinary.com/dcv3epinx/image/upload/v1618904450/signoz-images/LogoGithub_sigfbu.svg" alt="SigNoz-logo" width="240" />
|
||||
|
||||
<p align="center">Monitore seus aplicativos e solucione problemas em seus aplicativos implantados, uma alternativa de código aberto para soluções como DataDog, New Relic, entre outras.</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> ·
|
||||
<a href="README.zh-cn.md">中文</a> ·
|
||||
<a href="README.de-de.md">Deutsch</a>
|
||||
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/frontend?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>
|
||||
</p>
|
||||
|
||||
|
||||
<h3 align="center">
|
||||
<a href="https://signoz.io/docs"><b>Documentação</b></a> •
|
||||
<a href="https://signoz.io/slack"><b>Comunidade no Slack</b></a> •
|
||||
<a href="https://twitter.com/SigNozHq"><b>Twitter</b></a>
|
||||
</h3>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/SigNoz/signoz/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/SigNoz/signoz"></a>
|
||||
<a href="https://github.com/SigNoz/signoz/releases"><img alt="GitHub release" src="https://img.shields.io/github/v/release/SigNoz/signoz?label=release"></a>
|
||||
<a href="https://signoz.io/slack"><img alt="Slack community" src="https://img.shields.io/badge/slack-community-4A154B?logo=slack&logoColor=white"></a>
|
||||
<a href="https://www.linkedin.com/company/signozio/"><img alt="LinkedIn" src="https://img.shields.io/badge/linkedin-SigNoz-0A66C2?logo=linkedin&logoColor=white"></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>
|
||||
</p>
|
||||
##
|
||||
|
||||
SigNoz é uma plataforma de observabilidade open-source construída sobre OpenTelemetry. Estamos criando uma alternativa de nível empresarial a stacks de monitoramento fragmentadas, com logs, métricas, traces, alertas e dashboards em um só lugar.
|
||||
SigNoz auxilia os desenvolvedores a monitorarem aplicativos e solucionar problemas em seus aplicativos implantados. SigNoz usa rastreamento distribuído para obter visibilidade em sua pilha de software.
|
||||
|
||||
### Escolha como executar o SigNoz
|
||||
👉 Você pode verificar métricas como latência p99, taxas de erro em seus serviços, requisições às APIs externas e endpoints individuais.
|
||||
|
||||
#### SigNoz Cloud (recomendado)
|
||||
👉 Você pode encontrar a causa raiz do problema acessando os rastreamentos exatos que estão causando o problema e verificar os quadros detalhados de cada requisição individual.
|
||||
|
||||
SigNoz totalmente gerenciado, com teste gratuito de 30 dias, sem cartão de crédito, preço baseado em uso a partir de US$ 49 e hospedagem de dados por região.
|
||||
👉 Execute agregações em dados de rastreamento para obter métricas de negócios relevantes.
|
||||
|
||||
[**Comece gratuitamente →**](https://signoz.io/teams/)
|
||||
|
||||
#### Enterprise
|
||||

|
||||
|
||||
Enterprise Cloud, BYOC ou Enterprise Self-Hosted com compliance, suporte, retenção personalizada, RBAC, controles de ingestão, residência de dados e seleção de região.
|
||||
<br /><br />
|
||||
|
||||
[**Conheça o Enterprise →**](https://signoz.io/enterprise/)
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributing.svg" width="50px" />
|
||||
|
||||
#### Community
|
||||
## Junte-se à nossa comunidade no Slack
|
||||
|
||||
SigNoz open-source gratuito, executado na sua própria infraestrutura. Faça o deploy com Docker, Kubernetes ou Linux e mantenha controle total sobre o seu plano de dados.
|
||||
Venha dizer oi para nós no [Slack](https://signoz.io/slack) 👋
|
||||
|
||||
[**Instale o SigNoz →**](https://signoz.io/docs/install/self-host/)
|
||||
<br /><br />
|
||||
|
||||
### O que você pode monitorar?
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Features.svg" width="50px" />
|
||||
|
||||
O SigNoz ajuda equipes a depurar problemas de produção mais rapidamente ao conectar logs, métricas, traces, alertas, dashboards, exceções e fluxos agent-native em um só lugar.
|
||||
## Funções:
|
||||
|
||||
#### Visão geral de APM
|
||||
- Métricas de visão geral do aplicativo, como RPS, latências de percentual 50/90/99 e taxa de erro
|
||||
- Endpoints mais lentos em seu aplicativo
|
||||
- Visualize o rastreamento preciso de requisições de rede para descobrir problemas em serviços downstream, consultas lentas de banco de dados, chamadas para serviços de terceiros, como gateways de pagamento, etc.
|
||||
- Filtre os rastreamentos por nome de serviço, operação, latência, erro, tags / anotações.
|
||||
- Execute agregações em dados de rastreamento (eventos / extensões) para obter métricas de negócios relevantes, como por exemplo, você pode obter a taxa de erro e a latência do 99º percentil de `customer_type: gold` or `deployment_version: v2` or `external_call: paypal`
|
||||
- Interface de Usuário unificada para métricas e rastreios. Não há necessidade de mudar de Prometheus para Jaeger para depurar problemas.
|
||||
|
||||
Monitore latência de serviço, taxa de erro, throughput, Apdex, principais endpoints, chamadas ao banco de dados e chamadas externas.
|
||||
<br /><br />
|
||||
|
||||
<p align="center">
|
||||
<img alt="Dashboard de APM do SigNoz mostrando latência, throughput, Apdex e operações principais" src="docs/readme-assets/monitor/apm.png" width="900">
|
||||
</p>
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/WhatsCool.svg" width="50px" />
|
||||
|
||||
Saiba mais: [documentação de APM](https://signoz.io/docs/instrumentation/overview/)
|
||||
## Por que escolher SigNoz?
|
||||
|
||||
#### Gerenciamento de logs
|
||||
Sendo desenvolvedores, achamos irritante contar com fornecedores de SaaS de código fechado para cada pequeno recurso que queríamos. Fornecedores de código fechado costumam surpreendê-lo com enormes contas no final do mês de uso sem qualquer transparência .
|
||||
|
||||
Ingira, pesquise, agregue e correlacione logs com traces e métricas usando um construtor visual de consultas.
|
||||
Queríamos fazer uma versão auto-hospedada e de código aberto de ferramentas como DataDog, NewRelic para empresas que têm preocupações com privacidade e segurança em ter dados de clientes indo para serviços de terceiros.
|
||||
|
||||
<p align="center">
|
||||
<img alt="Explorador de logs do SigNoz com filtros, gráfico de frequência e linhas de log" src="docs/readme-assets/monitor/log-management.svg" width="900">
|
||||
</p>
|
||||
Ser open source também oferece controle completo de sua configuração, amostragem e tempos de atividade. Você também pode construir módulos sobre o SigNoz para estender recursos específicos do negócio.
|
||||
|
||||
Saiba mais: [documentação de gerenciamento de logs](https://signoz.io/docs/logs-management/overview/)
|
||||
### Linguagens Suportadas:
|
||||
|
||||
#### Métricas e dashboards
|
||||
Nós apoiamos a biblioteca [OpenTelemetry](https://opentelemetry.io) como a biblioteca que você pode usar para instrumentar seus aplicativos. Em outras palavras, SigNoz oferece suporte a qualquer framework e linguagem que suporte a biblioteca OpenTelemetry. As principais linguagens suportadas incluem:
|
||||
|
||||
Crie dashboards para métricas de aplicação, infraestrutura e métricas personalizadas usando Query Builder, PromQL ou ClickHouse SQL.
|
||||
- Java
|
||||
- Python
|
||||
- NodeJS
|
||||
- Go
|
||||
|
||||
<p align="center">
|
||||
<img alt="Dashboard de métricas de host do SigNoz com gráficos de carga do sistema e rede" src="docs/readme-assets/monitor/metrics.png" width="900">
|
||||
</p>
|
||||
Você pode encontrar a lista completa de linguagens aqui - https://opentelemetry.io/docs/
|
||||
|
||||
Saiba mais: [documentação de métricas](https://signoz.io/docs/metrics-management/overview/)
|
||||
<br /><br />
|
||||
|
||||
#### Monitoramento de infraestrutura
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Philosophy.svg" width="50px" />
|
||||
|
||||
Monitore clusters Kubernetes, pods, nodes, workloads e CPU, memória, disco, rede, logs e traces em nível de host.
|
||||
## Iniciando
|
||||
|
||||
|
||||
### Implantar usando Docker
|
||||
|
||||
<p align="center">
|
||||
<img alt="Dashboard de infraestrutura Kubernetes do SigNoz com métricas de pods e nodes" src="docs/readme-assets/monitor/infrastructure.png" width="900">
|
||||
</p>
|
||||
Siga as etapas listadas [aqui](https://signoz.io/docs/install/docker/) para instalar usando o Docker.
|
||||
|
||||
Saiba mais: [documentação de monitoramento de infraestrutura](https://signoz.io/docs/infrastructure-monitoring/overview/)
|
||||
Esse [guia para solução de problemas](https://signoz.io/docs/install/troubleshooting/) pode ser útil se você enfrentar quaisquer problemas.
|
||||
|
||||
#### Observabilidade de LLM e AI
|
||||
<p>  </p>
|
||||
|
||||
|
||||
### Implentar no Kubernetes usando Helm
|
||||
|
||||
Rastreie apps LLM, pipelines RAG, prompts, chamadas de ferramentas, tokens, latência e custos junto com telemetria de aplicação e infraestrutura.
|
||||
Siga as etapas listadas [aqui](https://signoz.io/docs/deployment/helm_chart) para instalar usando helm charts.
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img alt="Dashboard de observabilidade de LLM do SigNoz para traces, uso de tokens, latência e custos" src="docs/readme-assets/monitor/llm.png" width="900">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
Saiba mais: [documentação de observabilidade de LLM](https://signoz.io/docs/llm-observability/)
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/UseSigNoz.svg" width="50px" />
|
||||
|
||||
#### Observabilidade agent-native e MCP
|
||||
## Comparações com ferramentas similares
|
||||
|
||||
Use o servidor MCP do SigNoz para levar telemetria aos agentes de programação, ou use o Noz dentro do SigNoz para investigar incidentes, ajustar alertas e criar dashboards com contexto de produção. O [Noz](https://signoz.io/docs/ai/noz/) está disponível apenas no SigNoz Cloud.
|
||||
### SigNoz ou Prometheus
|
||||
|
||||
<p align="center">
|
||||
<img alt="Interface Noz do SigNoz ao lado de um fluxo agent via MCP" src="docs/readme-assets/monitor/agent-native.png" width="900">
|
||||
</p>
|
||||
Prometheus é bom se você quiser apenas fazer métricas. Mas se você quiser ter uma experiência perfeita entre métricas e rastreamentos, a experiência atual de unir Prometheus e Jaeger não é ótima.
|
||||
|
||||
Saiba mais: [documentação do servidor MCP do SigNoz](https://signoz.io/docs/ai/signoz-mcp-server/) · [documentação de agent skills](https://signoz.io/docs/ai/agent-skills/#install-the-plugin)
|
||||
Nosso objetivo é fornecer uma interface do usuário integrada entre métricas e rastreamentos - semelhante ao que fornecedores de SaaS como o Datadog fornecem - e fornecer filtragem e agregação avançada sobre rastreamentos, algo que a Jaeger atualmente carece.
|
||||
|
||||
#### Tracing distribuído
|
||||
<p>  </p>
|
||||
|
||||
Acompanhe requisições entre serviços com flamegraphs, waterfalls, eventos de span, filtros e análise de traces.
|
||||
### SigNoz ou Jaeger
|
||||
|
||||
<p align="center">
|
||||
<img alt="Visualização de tracing distribuído do SigNoz com flamegraph e spans em waterfall" src="docs/readme-assets/monitor/distributed-tracing.png" width="900">
|
||||
</p>
|
||||
Jaeger só faz rastreamento distribuído. SigNoz faz métricas e rastreia, e também temos gerenciamento de log em nossos planos.
|
||||
|
||||
Saiba mais: [documentação de tracing distribuído](https://signoz.io/docs/instrumentation/)
|
||||
Além disso, SigNoz tem alguns recursos mais avançados do que Jaeger:
|
||||
|
||||
#### Trace Funnels
|
||||
- A interface de usuário do Jaegar não mostra nenhuma métrica em traces ou em traces filtrados
|
||||
- Jaeger não pode obter agregados em rastros filtrados. Por exemplo, latência p99 de solicitações que possuem tag - customer_type='premium'. Isso pode ser feito facilmente com SigNoz.
|
||||
|
||||
Crie funis a partir de traces para entender quedas no fluxo de requisições, transições com falha e problemas sistêmicos de workflow.
|
||||
<br /><br />
|
||||
|
||||
<p align="center">
|
||||
<img alt="Trace Funnels do SigNoz mostrando quedas no fluxo de requisições e transições com falha" src="docs/readme-assets/monitor/trace-funnels.png" width="900">
|
||||
</p>
|
||||
|
||||
Saiba mais: [documentação de Trace Funnels](https://signoz.io/docs/trace-funnels/overview/)
|
||||
|
||||
Também monitore: [**exceções**](https://signoz.io/docs/userguide/exceptions/), [**alertas**](https://signoz.io/docs/alerts/), [**APIs externas**](https://signoz.io/docs/external-api-monitoring/overview/) e [**integrações**](https://signoz.io/docs/integrations/integrations-list/) para OpenTelemetry, Prometheus, Kubernetes, provedores de nuvem, SDKs de linguagem, frameworks de aplicação, bancos de dados e ferramentas de LLM.
|
||||
|
||||
### Por que equipes usam o SigNoz
|
||||
|
||||
1. **Nativo em OpenTelemetry**<br>
|
||||
Instrumente uma vez com padrões abertos e mantenha a posse da sua telemetria.
|
||||
2. **Sinais correlacionados**<br>
|
||||
Vá de gráficos de serviço para traces, logs, métricas de infraestrutura e exceções sem trocar de ferramenta.
|
||||
3. **Um único banco de dados colunar**<br>
|
||||
Construído para workloads de observabilidade de alto volume e alta cardinalidade.
|
||||
4. **Preço previsível**<br>
|
||||
Sem cobrança por host, sem cobrança por usuário e sem preço especial para métricas personalizadas.
|
||||
5. **Pronto para enterprise**<br>
|
||||
Compliance SOC 2 Type II e HIPAA, RBAC, controles de ingestão, retenção personalizada, suporte, BYOC e self-hosting.
|
||||
|
||||
### Primeiros passos
|
||||
|
||||
#### Comece na Cloud
|
||||
|
||||
Crie um workspace gerenciado do SigNoz e obtenha seu primeiro dashboard sem operar infraestrutura de observabilidade.
|
||||
|
||||
[**Comece gratuitamente no SigNoz Cloud**](https://signoz.io/teams/)
|
||||
|
||||
#### Self-host SigNoz
|
||||
|
||||
Execute o SigNoz na sua própria infraestrutura com Foundry, Docker, Kubernetes ou Linux.
|
||||
|
||||
[**Foundry**](https://github.com/SigNoz/foundry) · [**Docker**](https://signoz.io/docs/install/docker/) · [**Kubernetes**](https://signoz.io/docs/install/kubernetes/) · [**Linux**](https://signoz.io/docs/install/linux/)
|
||||
|
||||
#### Envie dados
|
||||
|
||||
Instrumente aplicações e infraestrutura com OpenTelemetry, Prometheus, SDKs de linguagem e integrações.
|
||||
|
||||
[**Instrumentação**](https://signoz.io/docs/instrumentation/) · [**Integrações**](https://signoz.io/docs/integrations/integrations-list/)
|
||||
|
||||
### Comparações com ferramentas conhecidas
|
||||
|
||||
O SigNoz é frequentemente adotado por equipes que estão migrando de ferramentas de propósito único ou plataformas comerciais com preços imprevisíveis.
|
||||
|
||||
**Prometheus**<br>
|
||||
Bom se você precisa apenas de métricas. O SigNoz mantém métricas, logs, traces, dashboards e alertas juntos para que equipes possam depurar com contexto correlacionado.
|
||||
|
||||
**Jaeger**<br>
|
||||
Jaeger faz apenas tracing distribuído. O SigNoz adiciona métricas, logs, análise de traces, dashboards, alertas, exceções e fluxos de trace para log.
|
||||
|
||||
**Elastic**<br>
|
||||
O SigNoz usa banco de dados colunar para análises de observabilidade eficientes e workloads de logs de alta cardinalidade, com 50% menos necessidade de recursos em comparação ao Elastic durante a ingestão. Confira o [estudo detalhado](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark).
|
||||
|
||||
**Loki**<br>
|
||||
No benchmark vinculado, o SigNoz indexou todas as chaves na configuração de teste, enquanto o Loki atingiu erros de max streams ao adicionar mais labels. Confira o [estudo detalhado](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark).
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributors.svg" width="50px" />
|
||||
|
||||
## Contribuindo
|
||||
|
||||
Adoramos contribuições grandes ou pequenas. Leia [CONTRIBUTING.md](CONTRIBUTING.md) para começar a contribuir com o SigNoz.
|
||||
|
||||
Não sabe como começar? **Fale conosco no `#contributing` na nossa [comunidade Slack](https://signoz.io/slack).**
|
||||
Nós ❤️ contribuições grandes ou pequenas. Leia [CONTRIBUTING.md](CONTRIBUTING.md) para começar a fazer contribuições para o SigNoz.
|
||||
|
||||
Como sempre, obrigado aos nossos incríveis contribuidores!
|
||||
Não sabe como começar? Basta enviar um sinal para nós no canal `#contributing` em nossa [comunidade no Slack.](https://signoz.io/slack)
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/DevelopingLocally.svg" width="50px" />
|
||||
|
||||
## Documentação
|
||||
|
||||
Você pode encontrar a documentação em https://signoz.io/docs/. Se você tiver alguma dúvida ou sentir falta de algo, sinta-se à vontade para criar uma issue com a tag `documentation` no GitHub ou entre em contato conosco no canal da comunidade no Slack.
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributing.svg" width="50px" />
|
||||
|
||||
## Comunidade
|
||||
|
||||
Junte-se a [comunidade no Slack](https://signoz.io/slack) para saber mais sobre rastreamento distribuído, observabilidade ou SigNoz e para se conectar com outros usuários e colaboradores.
|
||||
|
||||
Se você tiver alguma ideia, pergunta ou feedback, compartilhe em nosso [Github Discussões](https://github.com/SigNoz/signoz/discussions)
|
||||
|
||||
Como sempre, obrigado aos nossos incríveis colaboradores!
|
||||
|
||||
<a href="https://github.com/signoz/signoz/graphs/contributors">
|
||||
<img alt="Contribuidores do SigNoz" src="https://contrib.rocks/image?repo=signoz/signoz" />
|
||||
<img src="https://contrib.rocks/image?repo=signoz/signoz" />
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
|
||||
244
README.zh-cn.md
@@ -1,190 +1,208 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/readme-assets/signoz-hero-dark.png" width="900">
|
||||
<source media="(prefers-color-scheme: light)" srcset="docs/readme-assets/signoz-hero-light.png" width="900">
|
||||
<img alt="SigNoz - 按你的方式运行的可观测性,由开放标准驱动。" src="docs/readme-assets/signoz-hero-light.png" width="900">
|
||||
</picture>
|
||||
<img src="https://res.cloudinary.com/dcv3epinx/image/upload/v1618904450/signoz-images/LogoGithub_sigfbu.svg" alt="SigNoz-logo" width="240" />
|
||||
|
||||
<p align="center">监控你的应用,并且可排查已部署应用的问题,这是一个可替代 DataDog、NewRelic 的开源方案</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> ·
|
||||
<a href="README.de-de.md">Deutsch</a> ·
|
||||
<a href="README.pt-br.md">Português</a>
|
||||
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/query-service?label=Docker 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>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/SigNoz/signoz/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/SigNoz/signoz"></a>
|
||||
<a href="https://github.com/SigNoz/signoz/releases"><img alt="GitHub release" src="https://img.shields.io/github/v/release/SigNoz/signoz?label=release"></a>
|
||||
<a href="https://signoz.io/slack"><img alt="Slack community" src="https://img.shields.io/badge/slack-community-4A154B?logo=slack&logoColor=white"></a>
|
||||
<a href="https://www.linkedin.com/company/signozio/"><img alt="LinkedIn" src="https://img.shields.io/badge/linkedin-SigNoz-0A66C2?logo=linkedin&logoColor=white"></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>
|
||||
</p>
|
||||
<h3 align="center">
|
||||
<a href="https://signoz.io/docs"><b>文档</b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/main/README.zh-cn.md"><b>中文ReadMe</b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/main/README.de-de.md"><b>德文ReadMe</b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/main/README.pt-br.md"><b>葡萄牙语ReadMe</b></a> •
|
||||
<a href="https://signoz.io/slack"><b>Slack 社区</b></a> •
|
||||
<a href="https://twitter.com/SigNozHq"><b>Twitter</b></a>
|
||||
</h3>
|
||||
|
||||
SigNoz 是一个基于 OpenTelemetry 构建的开源可观测性平台。我们正在构建一个企业级替代方案,用来替代分散的监控工具栈,把日志、指标、链路追踪、告警和仪表盘放在同一个地方。
|
||||
##
|
||||
|
||||
### 选择 SigNoz 的运行方式
|
||||
SigNoz 帮助开发人员监控应用并排查已部署应用的问题。你可以使用 SigNoz 实现如下能力:
|
||||
|
||||
#### SigNoz Cloud(推荐)
|
||||
👉 在同一块面板上,可视化 Metrics, Traces 和 Logs 内容。
|
||||
|
||||
完全托管的 SigNoz,提供 30 天免费试用,无需信用卡,按用量计费,起价为 49 美元,并支持区域化数据托管。
|
||||
👉 你可以关注服务的 p99 延迟和错误率, 包括外部 API 调用和个别的端点。
|
||||
|
||||
[**免费开始 →**](https://signoz.io/teams/)
|
||||
👉 你可以找到问题的根因,通过提取相关问题的 traces 日志、单独查看请求 traces 的火焰图详情。
|
||||
|
||||
#### 企业版
|
||||
👉 执行 trace 数据聚合,以获取业务相关的 metrics
|
||||
|
||||
Enterprise Cloud、BYOC 或 Enterprise Self-Hosted,提供合规、支持、自定义保留期、RBAC、摄取控制、数据驻留和区域选择。
|
||||
👉 对日志过滤和查询,通过日志的属性建立看板和告警
|
||||
|
||||
[**了解企业版 →**](https://signoz.io/enterprise/)
|
||||
👉 通过 Python,java,Ruby 和 Javascript 自动记录异常
|
||||
|
||||
#### 社区版
|
||||
👉 轻松的自定义查询和设置告警
|
||||
|
||||
免费的开源 SigNoz,可运行在你自己的基础设施中。使用 Docker、Kubernetes 或 Linux 部署,并完全掌控你的数据平面。
|
||||
### 应用 Metrics 展示
|
||||
|
||||
[**安装 SigNoz →**](https://signoz.io/docs/install/self-host/)
|
||||

|
||||
|
||||
### 你可以监控什么?
|
||||
### 分布式追踪
|
||||
|
||||
SigNoz 将日志、指标、链路追踪、告警、仪表盘、异常和面向 Agent 的工作流连接在一起,帮助团队更快地调试生产问题。
|
||||
<img width="2068" alt="distributed_tracing_2 2" src="https://user-images.githubusercontent.com/83692067/226536447-bae58321-6a22-4ed3-af80-e3e964cb3489.png">
|
||||
|
||||
#### APM 概览
|
||||
<img width="2068" alt="distributed_tracing_1" src="https://user-images.githubusercontent.com/83692067/226536462-939745b6-4f9d-45a6-8016-814837e7f7b4.png">
|
||||
|
||||
监控服务延迟、错误率、吞吐量、Apdex、核心端点、数据库调用和外部调用。
|
||||
### 日志管理
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz APM 仪表盘,展示延迟、吞吐量、Apdex 和关键操作" src="docs/readme-assets/monitor/apm.png" width="900">
|
||||
</p>
|
||||
<img width="2068" alt="logs_management" src="https://user-images.githubusercontent.com/83692067/226536482-b8a5c4af-b69c-43d5-969c-338bd5eaf1a5.png">
|
||||
|
||||
了解更多:[APM 文档](https://signoz.io/docs/instrumentation/overview/)
|
||||
### 基础设施监控
|
||||
|
||||
#### 日志管理
|
||||
<img width="2068" alt="infrastructure_monitoring" src="https://user-images.githubusercontent.com/83692067/226536496-f38c4dbf-e03c-4158-8be0-32d4a61158c7.png">
|
||||
|
||||
摄取、搜索、聚合日志,并通过可视化查询构建器将日志与链路追踪和指标关联起来。
|
||||
### 异常监控
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz 日志浏览器,包含过滤器、频率图和日志行" src="docs/readme-assets/monitor/log-management.svg" width="900">
|
||||
</p>
|
||||

|
||||
|
||||
了解更多:[日志管理文档](https://signoz.io/docs/logs-management/overview/)
|
||||
### 告警
|
||||
|
||||
#### 指标和仪表盘
|
||||
<img width="2068" alt="alerts_management" src="https://user-images.githubusercontent.com/83692067/226536548-2c81e2e8-c12d-47e8-bad7-c6be79055def.png">
|
||||
|
||||
使用 Query Builder、PromQL 或 ClickHouse SQL 为应用、基础设施和自定义指标构建仪表盘。
|
||||
<br /><br />
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz 主机指标仪表盘,展示系统负载和网络图表" src="docs/readme-assets/monitor/metrics.png" width="900">
|
||||
</p>
|
||||
## 加入我们 Slack 社区
|
||||
|
||||
了解更多:[指标文档](https://signoz.io/docs/metrics-management/overview/)
|
||||
来 [Slack](https://signoz.io/slack) 和我们打招呼吧 👋
|
||||
|
||||
#### 基础设施监控
|
||||
<br /><br />
|
||||
|
||||
监控 Kubernetes 集群、Pod、节点、工作负载,以及主机级 CPU、内存、磁盘、网络、日志和链路追踪。
|
||||
## 特性:
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz Kubernetes 基础设施仪表盘,展示 Pod 和节点指标" src="docs/readme-assets/monitor/infrastructure.png" width="900">
|
||||
</p>
|
||||
- 为 metrics, traces and logs 制定统一的 UI。 无需切换 Prometheus 到 Jaeger 去查找问题,也无需使用想 Elastic 这样的日志工具分开你的 metrics 和 traces
|
||||
|
||||
了解更多:[基础设施监控文档](https://signoz.io/docs/infrastructure-monitoring/overview/)
|
||||
- 默认统计应用的 metrics 数据,像 RPS (每秒请求数), 50th/90th/99th 的分位数延迟数据,还有相关的错误率
|
||||
|
||||
#### LLM 和 AI 可观测性
|
||||
- 找到应用中最慢的端点
|
||||
|
||||
追踪 LLM 应用、RAG 流水线、Prompt、工具调用、Token、延迟和成本,并与应用和基础设施遥测数据放在一起分析。
|
||||
- 查看准确的请求跟踪数据,找到下游服务的问题了,比如 DB 慢查询,或者调用第三方的支付网关等
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz LLM 可观测性仪表盘,展示链路追踪、Token 使用、延迟和成本" src="docs/readme-assets/monitor/llm.png" width="900">
|
||||
</p>
|
||||
- 通过 服务名、操作方式、延迟、错误、标签/注释 过滤 traces 数据
|
||||
|
||||
了解更多:[LLM 可观测性文档](https://signoz.io/docs/llm-observability/)
|
||||
- 通过聚合 trace 数据而获得业务相关的 metrics。 比如你可以通过 `customer_type: gold` 或者 `deployment_version: v2` 或者 `external_call: paypal` 获取错误率和 P99 延迟数据
|
||||
|
||||
#### Agent 原生可观测性和 MCP
|
||||
- 原生支持 OpenTelemetry 日志,高级日志查询,自动收集 k8s 相关日志
|
||||
|
||||
使用 SigNoz MCP server 将遥测数据带入编程 Agent,或在 SigNoz 中使用 Noz,基于生产上下文调查事故、优化告警并构建仪表盘。[Noz](https://signoz.io/docs/ai/noz/) 仅适用于 SigNoz Cloud。
|
||||
- 快如闪电的日志分析 ([Logs Perf. Benchmark](https://signoz.io/blog/logs-performance-benchmark/))
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz Noz 界面与基于 MCP 的 Agent 工作流" src="docs/readme-assets/monitor/agent-native.png" width="900">
|
||||
</p>
|
||||
- 可视化点到点的基础设施性能,提取有所有类型机器的 metrics 数据
|
||||
|
||||
了解更多:[SigNoz MCP server 文档](https://signoz.io/docs/ai/signoz-mcp-server/) · [Agent skills 文档](https://signoz.io/docs/ai/agent-skills/#install-the-plugin)
|
||||
- 轻易自定义告警查询
|
||||
|
||||
#### 分布式链路追踪
|
||||
<br /><br />
|
||||
|
||||
通过火焰图、瀑布图、Span 事件、过滤器和 Trace 分析,跟踪请求在各个服务之间的流转。
|
||||
## 为什么使用 SigNoz?
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz 分布式链路追踪视图,包含火焰图和瀑布图 Span" src="docs/readme-assets/monitor/distributed-tracing.png" width="900">
|
||||
</p>
|
||||
作为开发者, 我们发现 SaaS 厂商对一些大家想要的小功能都是闭源的,这种行为真的让人有点恼火。 闭源厂商还会在月底给你一张没有明细的巨额账单。
|
||||
|
||||
了解更多:[分布式链路追踪文档](https://signoz.io/docs/instrumentation/)
|
||||
我们想做一个自托管并且可开源的工具,像 DataDog 和 NewRelic 那样, 为那些担心数据隐私和安全的公司提供第三方服务。
|
||||
|
||||
#### Trace Funnels
|
||||
作为开源的项目,你完全可以自己掌控你的配置、样本和更新。你同样可以基于 SigNoz 拓展特定的业务模块。
|
||||
|
||||
基于链路追踪创建漏斗,用于理解请求流中的掉点、失败转换和系统性工作流问题。
|
||||
### 支持的编程语言:
|
||||
|
||||
<p align="center">
|
||||
<img alt="SigNoz Trace Funnels,展示请求流掉点和失败转换" src="docs/readme-assets/monitor/trace-funnels.png" width="900">
|
||||
</p>
|
||||
我们支持 [OpenTelemetry](https://opentelemetry.io)。作为一个观测你应用的库文件。所以任何 OpenTelemetry 支持的框架和语言,对于 SigNoz 也同样支持。 一些主要支持的语言如下:
|
||||
|
||||
了解更多:[Trace Funnels 文档](https://signoz.io/docs/trace-funnels/overview/)
|
||||
- Java
|
||||
- Python
|
||||
- NodeJS
|
||||
- Go
|
||||
- PHP
|
||||
- .NET
|
||||
- Ruby
|
||||
- Elixir
|
||||
- Rust
|
||||
|
||||
你还可以监控:[**异常**](https://signoz.io/docs/userguide/exceptions/)、[**告警**](https://signoz.io/docs/alerts/)、[**外部 API**](https://signoz.io/docs/external-api-monitoring/overview/),以及面向 OpenTelemetry、Prometheus、Kubernetes、云服务商、语言 SDK、应用框架、数据库和 LLM 工具的[**集成**](https://signoz.io/docs/integrations/integrations-list/)。
|
||||
你可以在这里找到全部支持的语言列表 - https://opentelemetry.io/docs/
|
||||
|
||||
### 为什么团队选择 SigNoz
|
||||
<br /><br />
|
||||
|
||||
1. **OpenTelemetry 原生**<br>
|
||||
用开放标准完成一次接入,并保持对遥测数据的所有权。
|
||||
2. **信号关联**<br>
|
||||
在服务图表、链路追踪、日志、基础设施指标和异常之间切换时,不需要更换工具。
|
||||
3. **单一列式数据库**<br>
|
||||
为高基数、高吞吐量的可观测性工作负载而构建。
|
||||
4. **可预测的定价**<br>
|
||||
不按主机收费,不按用户席位收费,也不对自定义指标设置特殊价格。
|
||||
5. **企业就绪**<br>
|
||||
SOC 2 Type II 和 HIPAA 合规、RBAC、摄取控制、自定义保留期、支持、BYOC 和自托管。
|
||||
## 让我们开始吧
|
||||
|
||||
### 快速开始
|
||||
### 使用 Docker 部署
|
||||
|
||||
#### 从 Cloud 开始
|
||||
请一步步跟随 [这里](https://signoz.io/docs/install/docker/) 通过 docker 来安装。
|
||||
|
||||
创建一个托管的 SigNoz 工作区,无需运行可观测性基础设施,即可获得第一个仪表盘。
|
||||
这个 [排障说明书](https://signoz.io/docs/install/troubleshooting/) 可以帮助你解决碰到的问题。
|
||||
|
||||
[**免费开始使用 SigNoz Cloud**](https://signoz.io/teams/)
|
||||
<p>  </p>
|
||||
|
||||
#### 自托管 SigNoz
|
||||
### 使用 Helm 在 Kubernetes 部署
|
||||
|
||||
在你自己的基础设施中通过 Foundry、Docker、Kubernetes 或 Linux 运行 SigNoz。
|
||||
请一步步跟随 [这里](https://signoz.io/docs/deployment/helm_chart) 通过 helm 来安装
|
||||
|
||||
[**Foundry**](https://github.com/SigNoz/foundry) · [**Docker**](https://signoz.io/docs/install/docker/) · [**Kubernetes**](https://signoz.io/docs/install/kubernetes/) · [**Linux**](https://signoz.io/docs/install/linux/)
|
||||
<br /><br />
|
||||
|
||||
#### 发送数据
|
||||
## 比较相似的工具
|
||||
|
||||
使用 OpenTelemetry、Prometheus、语言 SDK 和集成来接入应用与基础设施。
|
||||
### SigNoz vs Prometheus
|
||||
|
||||
[**接入文档**](https://signoz.io/docs/instrumentation/) · [**集成列表**](https://signoz.io/docs/integrations/integrations-list/)
|
||||
Prometheus 是一个针对 metrics 监控的强大工具。但是如果你想无缝的切换 metrics 和 traces 查询,你当前大概率需要在 Prometheus 和 Jaeger 之间切换。
|
||||
|
||||
### 与常见工具的对比
|
||||
我们的目标是提供一个客户观测 metrics 和 traces 整合的 UI。就像 SaaS 供应商 DataDog,它提供很多 jaeger 缺失的功能,比如针对 traces 过滤功能和聚合功能。
|
||||
|
||||
许多团队在从单一用途工具或价格不可预测的商业平台迁移时,会选择 SigNoz。
|
||||
<p>  </p>
|
||||
|
||||
**Prometheus**<br>
|
||||
如果你只需要指标,Prometheus 很合适。SigNoz 将指标、日志、链路追踪、仪表盘和告警放在一起,让团队可以通过关联上下文进行调试。
|
||||
### SigNoz vs Jaeger
|
||||
|
||||
**Jaeger**<br>
|
||||
Jaeger 只做分布式链路追踪。SigNoz 增加了指标、日志、Trace 分析、仪表盘、告警、异常和 Trace 到日志的工作流。
|
||||
Jaeger 仅仅是一个分布式追踪系统。 但是 SigNoz 可以提供 metrics, traces 和 logs 所有的观测。
|
||||
|
||||
**Elastic**<br>
|
||||
SigNoz 使用列式数据库来高效处理可观测性分析和高基数日志工作负载。在摄取阶段,相比 Elastic 可降低 50% 的资源需求。查看[详细评测](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)。
|
||||
而且, SigNoz 相较于 Jaeger 拥有更对的高级功能:
|
||||
|
||||
**Loki**<br>
|
||||
在链接的评测中,SigNoz 在测试设置中索引了所有键,而 Loki 在增加更多标签时遇到了 max stream 错误。查看[详细评测](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)。
|
||||
- Jaegar UI 不能提供任何基于 traces 的 metrics 查询和过滤。
|
||||
|
||||
- Jaeger 不能针对过滤的 traces 做聚合。 比如, p99 延迟的请求有个标签是 customer_type='premium'。 而这些在 SigNoz 可以轻松做到。
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### SigNoz vs Elastic
|
||||
|
||||
- SigNoz 的日志管理是基于 ClickHouse 实现的,可以使日志的聚合更加高效,因为它是基于 OLAP 的数据仓储。
|
||||
|
||||
- 与 Elastic 相比,可以节省 50% 的资源成本
|
||||
|
||||
我们已经公布了 Elastic 和 SigNoz 的性能对比。 请点击 [这里](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### SigNoz vs Loki
|
||||
|
||||
- SigNoz 支持大容量高基数的聚合,但是 loki 是不支持的。
|
||||
|
||||
- SigNoz 支持索引的高基数查询,并且对索引没有数量限制,而 Loki 会在添加部分索引后到达最大上限。
|
||||
|
||||
- 相较于 SigNoz,Loki 在搜索大量数据下既困难又缓慢。
|
||||
|
||||
我们已经发布了基准测试对比 Loki 和 SigNoz 性能。请点击 [这里](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 贡献
|
||||
|
||||
无论贡献大小,我们都非常欢迎。请阅读 [CONTRIBUTING.md](CONTRIBUTING.md),开始为 SigNoz 做贡献。
|
||||
我们 ❤️ 你的贡献,无论大小。 请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 再开始给 SigNoz 做贡献。
|
||||
|
||||
不确定如何开始?**可以在我们的 [Slack 社区](https://signoz.io/slack)中通过 `#contributing` 联系我们。**
|
||||
如果你不知道如何开始? 只需要在 [slack 社区](https://signoz.io/slack) 通过 `#contributing` 频道联系我们。
|
||||
|
||||
一如既往,感谢所有出色的贡献者!
|
||||
<br /><br />
|
||||
|
||||
## 文档
|
||||
|
||||
你可以通过 https://signoz.io/docs/ 找到相关文档。如果你需要阐述问题或者发现一些确实的事件, 通过标签为 `documentation` 提交 Github 问题。或者通过 slack 社区频道。
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 社区
|
||||
|
||||
加入 [slack 社区](https://signoz.io/slack) 去了解更多关于分布式追踪、可观测性系统 。或者与 SigNoz 其他用户和贡献者交流。
|
||||
|
||||
如果你有任何想法、问题、或者任何反馈, 请通过 [Github Discussions](https://github.com/SigNoz/signoz/discussions) 分享。
|
||||
|
||||
不管怎么样,感谢这个项目的所有贡献者!
|
||||
|
||||
<a href="https://github.com/signoz/signoz/graphs/contributors">
|
||||
<img alt="SigNoz 贡献者" src="https://contrib.rocks/image?repo=signoz/signoz" />
|
||||
<img src="https://contrib.rocks/image?repo=signoz/signoz" />
|
||||
</a>
|
||||
|
||||
|
Before Width: | Height: | Size: 629 KiB |
|
Before Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 563 KiB |
|
Before Width: | Height: | Size: 434 KiB |
|
Before Width: | Height: | Size: 783 KiB |
|
Before Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 142 KiB |
@@ -1,5 +1,5 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import { buildPermission } from 'hooks/useAuthZ/utils';
|
||||
import type { AuthZObject, BrandedPermission } from 'hooks/useAuthZ/types';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
@@ -66,29 +66,6 @@ describe('AuthZTooltip — single check', () => {
|
||||
expect(screen.getByRole('button', { name: 'Action' })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows formatted permission message in tooltip when denied', async () => {
|
||||
mockUseAuthZ.mockReturnValue({
|
||||
...noPermissions,
|
||||
permissions: { [createPerm]: { isGranted: false } },
|
||||
});
|
||||
|
||||
render(
|
||||
<AuthZTooltip checks={[createPerm]}>
|
||||
<TestButton />
|
||||
</AuthZTooltip>,
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.hover(screen.getByRole('button', { name: 'Action' }));
|
||||
|
||||
const expectedMessage =
|
||||
'user/some-user-id is not authorized to perform create:serviceaccount:*';
|
||||
await waitFor(() => {
|
||||
const matches = screen.queryAllByText(expectedMessage);
|
||||
expect(matches.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('disables child while loading', () => {
|
||||
mockUseAuthZ.mockReturnValue({ ...noPermissions, isLoading: true });
|
||||
|
||||
@@ -165,31 +142,4 @@ describe('AuthZTooltip — multi-check (checks array)', () => {
|
||||
attachRolePerm,
|
||||
);
|
||||
});
|
||||
|
||||
it('shows multiple formatted permissions in tooltip when both denied', async () => {
|
||||
const sa = attachSAPerm('sa-1');
|
||||
mockUseAuthZ.mockReturnValue({
|
||||
...noPermissions,
|
||||
permissions: {
|
||||
[sa]: { isGranted: false },
|
||||
[attachRolePerm]: { isGranted: false },
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<AuthZTooltip checks={[sa, attachRolePerm]}>
|
||||
<TestButton />
|
||||
</AuthZTooltip>,
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.hover(screen.getByRole('button', { name: 'Action' }));
|
||||
|
||||
const expectedMessage =
|
||||
'user/some-user-id is not authorized to perform attach:serviceaccount:sa-1, attach:role:*';
|
||||
await waitFor(() => {
|
||||
const matches = screen.queryAllByText(expectedMessage);
|
||||
expect(matches.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
} from '@signozhq/ui/tooltip';
|
||||
import type { BrandedPermission } from 'hooks/useAuthZ/types';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { formatPermission } from 'hooks/useAuthZ/utils';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { parsePermission } from 'hooks/useAuthZ/utils';
|
||||
import styles from './AuthZTooltip.module.scss';
|
||||
|
||||
interface AuthZTooltipProps {
|
||||
@@ -20,14 +19,19 @@ interface AuthZTooltipProps {
|
||||
|
||||
function formatDeniedMessage(
|
||||
denied: BrandedPermission[],
|
||||
userId: string,
|
||||
override?: string,
|
||||
): string {
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
const permissions = denied.map(formatPermission).join(', ');
|
||||
return `user/${userId} is not authorized to perform ${permissions}`;
|
||||
const labels = denied.map((p) => {
|
||||
const { relation, object } = parsePermission(p);
|
||||
const resource = object.split(':')[0];
|
||||
return `${relation} ${resource}`;
|
||||
});
|
||||
return labels.length === 1
|
||||
? `You don't have ${labels[0]} permission`
|
||||
: `You don't have ${labels.join(', ')} permissions`;
|
||||
}
|
||||
|
||||
function AuthZTooltip({
|
||||
@@ -36,7 +40,6 @@ function AuthZTooltip({
|
||||
enabled = true,
|
||||
tooltipMessage,
|
||||
}: AuthZTooltipProps): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const shouldCheck = enabled && checks.length > 0;
|
||||
|
||||
const { permissions, isLoading } = useAuthZ(checks, { enabled: shouldCheck });
|
||||
@@ -72,7 +75,7 @@ function AuthZTooltip({
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={styles.errorContent}>
|
||||
{formatDeniedMessage(deniedPermissions, user.id, tooltipMessage)}
|
||||
{formatDeniedMessage(deniedPermissions, tooltipMessage)}
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -2,11 +2,3 @@
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.permission {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.permissionCode {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ describe('PermissionDeniedCallout', () => {
|
||||
it('renders the permission name in the callout message', () => {
|
||||
render(<PermissionDeniedCallout permissionName="serviceaccount:attach" />);
|
||||
|
||||
expect(screen.getByText(/is not authorized/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/You don't have/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/serviceaccount:attach/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/permission/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts an optional className', () => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import cx from 'classnames';
|
||||
import styles from './PermissionDeniedCallout.module.scss';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
interface PermissionDeniedCalloutProps {
|
||||
permissionName: string;
|
||||
@@ -13,8 +11,6 @@ function PermissionDeniedCallout({
|
||||
permissionName,
|
||||
className,
|
||||
}: PermissionDeniedCalloutProps): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
|
||||
return (
|
||||
<Callout
|
||||
type="error"
|
||||
@@ -22,11 +18,7 @@ function PermissionDeniedCallout({
|
||||
size="small"
|
||||
className={cx(styles.callout, className)}
|
||||
>
|
||||
<Typography.Text className={styles.permission}>
|
||||
<code className={styles.permissionCode}>user/{user.id}</code> is not
|
||||
authorized to perform{' '}
|
||||
<code className={styles.permissionCode}>{permissionName}</code>
|
||||
</Typography.Text>
|
||||
{`You don't have ${permissionName} permission`}
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
// Sienna chip — matches the dashboard list-row tag badge.
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 240px;
|
||||
height: 24px;
|
||||
padding: 2px 4px 2px 8px;
|
||||
border-radius: 50px;
|
||||
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
|
||||
color: var(--bg-sienna-400);
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.tagLabel {
|
||||
--button-height: auto;
|
||||
--button-padding: 0;
|
||||
--button-gap: 0;
|
||||
--button-variant-ghost-background-color: transparent;
|
||||
--button-variant-ghost-hover-background-color: transparent;
|
||||
--button-variant-ghost-color: inherit;
|
||||
--button-variant-ghost-hover-color: inherit;
|
||||
overflow: hidden;
|
||||
max-width: 200px;
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.remove {
|
||||
// Size overrides to fit the chip, plus a sienna-tinted hover — the Button's
|
||||
// default ghost hover is a grey that clashes with the chip. Resting color is
|
||||
// left at the Button default.
|
||||
--button-height: 16px;
|
||||
--button-padding: 0;
|
||||
--button-border-radius: 50%;
|
||||
--button-variant-ghost-hover-background-color: color-mix(
|
||||
in srgb,
|
||||
var(--bg-sienna-500) 22%,
|
||||
transparent
|
||||
);
|
||||
--button-variant-ghost-hover-color: var(--bg-sienna-400);
|
||||
width: 16px;
|
||||
min-width: 16px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.editInput {
|
||||
width: 160px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--bg-cherry-500);
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
import { type ChangeEvent, type KeyboardEvent, useState } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { X } from '@signozhq/icons';
|
||||
import cx from 'classnames';
|
||||
|
||||
import { parseKeyValueTag } from './utils';
|
||||
|
||||
import styles from './TagKeyValueInput.module.scss';
|
||||
|
||||
interface TagKeyValueInputProps {
|
||||
// Tags as `key:value` strings.
|
||||
tags: string[];
|
||||
onTagsChange: (tags: string[]) => void;
|
||||
placeholder?: string;
|
||||
// Override the outer container styling per host (e.g. the create modal).
|
||||
className?: string;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
// Strict key:value tag editor. A tag is committed only on Enter and only when
|
||||
// it parses to a valid `key:value` pair — bare values are rejected with an
|
||||
// inline error. Existing chips can be edited inline (double-click), and removed.
|
||||
function TagKeyValueInput({
|
||||
tags,
|
||||
onTagsChange,
|
||||
placeholder = 'key:value',
|
||||
className,
|
||||
testId = 'tag-key-value-input',
|
||||
}: TagKeyValueInputProps): JSX.Element {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [editIndex, setEditIndex] = useState(-1);
|
||||
const [editValue, setEditValue] = useState('');
|
||||
|
||||
const removeTag = (tag: string): void => {
|
||||
onTagsChange(tags.filter((t) => t !== tag));
|
||||
};
|
||||
|
||||
const commit = (): void => {
|
||||
const raw = inputValue.trim();
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
const normalized = parseKeyValueTag(raw);
|
||||
if (!normalized) {
|
||||
setError('Tags must be in key:value format (both sides required).');
|
||||
return;
|
||||
}
|
||||
if (tags.includes(normalized)) {
|
||||
setError('This tag already exists.');
|
||||
return;
|
||||
}
|
||||
onTagsChange([...tags, normalized]);
|
||||
setInputValue('');
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setInputValue(e.target.value);
|
||||
if (error) {
|
||||
setError('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
commit();
|
||||
}
|
||||
};
|
||||
|
||||
const startEdit = (index: number): void => {
|
||||
setEditIndex(index);
|
||||
setEditValue(tags[index]);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const cancelEdit = (): void => {
|
||||
setEditIndex(-1);
|
||||
setEditValue('');
|
||||
};
|
||||
|
||||
const commitEdit = (): void => {
|
||||
const normalized = parseKeyValueTag(editValue);
|
||||
// Drop into a no-op (revert) on invalid or duplicate edits rather than
|
||||
// stranding the user in an un-exitable edit box.
|
||||
if (normalized && !tags.some((t, i) => t === normalized && i !== editIndex)) {
|
||||
onTagsChange(tags.map((t, i) => (i === editIndex ? normalized : t)));
|
||||
}
|
||||
cancelEdit();
|
||||
};
|
||||
|
||||
const handleEditKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
commitEdit();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
cancelEdit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cx(styles.container, className)}>
|
||||
<div className={styles.field}>
|
||||
{tags.map((tag, index) =>
|
||||
index === editIndex ? (
|
||||
<Input
|
||||
key={tag}
|
||||
className={styles.editInput}
|
||||
value={editValue}
|
||||
autoFocus
|
||||
testId={`${testId}-edit`}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||
setEditValue(e.target.value)
|
||||
}
|
||||
onKeyDown={handleEditKeyDown}
|
||||
onBlur={commitEdit}
|
||||
/>
|
||||
) : (
|
||||
<div key={tag} className={styles.tag} data-testid={`${testId}-chip`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className={styles.tagLabel}
|
||||
title="Double-click to edit"
|
||||
onDoubleClick={(): void => startEdit(index)}
|
||||
>
|
||||
{tag}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={styles.remove}
|
||||
aria-label={`Remove ${tag}`}
|
||||
onClick={(): void => removeTag(tag)}
|
||||
>
|
||||
<X size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
<Input
|
||||
className={styles.input}
|
||||
value={inputValue}
|
||||
placeholder={placeholder}
|
||||
testId={testId}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<Typography className={styles.error} data-testid={`${testId}-error`}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TagKeyValueInput;
|
||||
@@ -1,31 +0,0 @@
|
||||
import { parseKeyValueTag } from './utils';
|
||||
|
||||
describe('parseKeyValueTag', () => {
|
||||
it('normalizes a valid key:value pair', () => {
|
||||
expect(parseKeyValueTag('env:prod')).toBe('env:prod');
|
||||
});
|
||||
|
||||
it('trims whitespace around key and value', () => {
|
||||
expect(parseKeyValueTag(' env : prod ')).toBe('env:prod');
|
||||
});
|
||||
|
||||
it('keeps colons inside the value', () => {
|
||||
expect(parseKeyValueTag('url:http://x')).toBe('url:http://x');
|
||||
});
|
||||
|
||||
it('rejects a bare value with no colon', () => {
|
||||
expect(parseKeyValueTag('prod')).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects an empty key', () => {
|
||||
expect(parseKeyValueTag(':prod')).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects an empty value', () => {
|
||||
expect(parseKeyValueTag('env:')).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects blank input', () => {
|
||||
expect(parseKeyValueTag(' ')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
// Tags are strictly key:value. Parse a raw input into a normalized `key:value`
|
||||
// string, or null if it isn't a valid pair (both sides non-empty). The first
|
||||
// colon separates key from value, so values may themselves contain colons
|
||||
// (e.g. `url:http://x`).
|
||||
export function parseKeyValueTag(raw: string): string | null {
|
||||
const trimmed = raw.trim();
|
||||
const idx = trimmed.indexOf(':');
|
||||
if (idx <= 0) {
|
||||
return null;
|
||||
}
|
||||
const key = trimmed.slice(0, idx).trim();
|
||||
const value = trimmed.slice(idx + 1).trim();
|
||||
if (!key || !value) {
|
||||
return null;
|
||||
}
|
||||
return `${key}:${value}`;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||
import { SolidAlertTriangle } from '@signozhq/icons';
|
||||
import { Select, Tooltip } from 'antd';
|
||||
import type { DefaultOptionType } from 'antd/es/select';
|
||||
import cx from 'classnames';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { UniversalYAxisUnitMappings } from './constants';
|
||||
import { UniversalYAxisUnit, YAxisUnitSelectorProps } from './types';
|
||||
@@ -72,7 +72,9 @@ function YAxisUnitSelector({
|
||||
}, [categoriesOverride, source]);
|
||||
|
||||
return (
|
||||
<div className={cx('y-axis-unit-selector-component', containerClassName)}>
|
||||
<div
|
||||
className={classNames('y-axis-unit-selector-component', containerClassName)}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
value={universalUnit}
|
||||
@@ -82,17 +84,12 @@ function YAxisUnitSelector({
|
||||
loading={loading}
|
||||
suffixIcon={
|
||||
incompatibleUnitMessage ? (
|
||||
<Tooltip
|
||||
title={incompatibleUnitMessage}
|
||||
overlayClassName="y-axis-unit-warning-tooltip"
|
||||
>
|
||||
<span className="y-axis-unit-warning" role="img" aria-label="warning">
|
||||
<SolidAlertTriangle size="md" />
|
||||
</span>
|
||||
<Tooltip title={incompatibleUnitMessage}>
|
||||
<SolidAlertTriangle role="img" aria-label="warning" size="md" />
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
className={cx({
|
||||
className={classNames({
|
||||
'warning-state': incompatibleUnitMessage,
|
||||
})}
|
||||
data-testid={dataTestId}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { YAxisCategoryNames } from '../constants';
|
||||
import { UniversalYAxisUnit, YAxisSource } from '../types';
|
||||
@@ -7,13 +6,9 @@ import YAxisUnitSelector from '../YAxisUnitSelector';
|
||||
|
||||
describe('YAxisUnitSelector', () => {
|
||||
const mockOnChange = jest.fn();
|
||||
// antd injects its `pointer-events` styles via cssinjs in jsdom, but the SCSS
|
||||
// overrides aren't loaded — skip the pointer-events check so hovers/clicks register.
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockOnChange.mockClear();
|
||||
user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
});
|
||||
|
||||
it('renders with default placeholder', () => {
|
||||
@@ -39,7 +34,7 @@ describe('YAxisUnitSelector', () => {
|
||||
expect(screen.queryByText('Custom placeholder')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onChange when a value is selected', async () => {
|
||||
it('calls onChange when a value is selected', () => {
|
||||
render(
|
||||
<YAxisUnitSelector
|
||||
value=""
|
||||
@@ -49,8 +44,9 @@ describe('YAxisUnitSelector', () => {
|
||||
);
|
||||
const select = screen.getByRole('combobox');
|
||||
|
||||
await user.click(select);
|
||||
await user.click(screen.getByText('Bytes (B)'));
|
||||
fireEvent.mouseDown(select);
|
||||
const option = screen.getByText('Bytes (B)');
|
||||
fireEvent.click(option);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('By', {
|
||||
children: 'Bytes (B)',
|
||||
@@ -59,7 +55,7 @@ describe('YAxisUnitSelector', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('filters options based on search input', async () => {
|
||||
it('filters options based on search input', () => {
|
||||
render(
|
||||
<YAxisUnitSelector
|
||||
value=""
|
||||
@@ -69,13 +65,14 @@ describe('YAxisUnitSelector', () => {
|
||||
);
|
||||
const select = screen.getByRole('combobox');
|
||||
|
||||
await user.click(select);
|
||||
await user.type(select, 'bytes/sec');
|
||||
fireEvent.mouseDown(select);
|
||||
const input = screen.getByRole('combobox');
|
||||
fireEvent.change(input, { target: { value: 'bytes/sec' } });
|
||||
|
||||
expect(screen.getByText('Bytes/sec')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows all categories and their units', async () => {
|
||||
it('shows all categories and their units', () => {
|
||||
render(
|
||||
<YAxisUnitSelector
|
||||
value=""
|
||||
@@ -83,8 +80,9 @@ describe('YAxisUnitSelector', () => {
|
||||
source={YAxisSource.ALERTS}
|
||||
/>,
|
||||
);
|
||||
const select = screen.getByRole('combobox');
|
||||
|
||||
await user.click(screen.getByRole('combobox'));
|
||||
fireEvent.mouseDown(select);
|
||||
|
||||
// Check for category headers
|
||||
expect(screen.getByText('Data')).toBeInTheDocument();
|
||||
@@ -95,7 +93,7 @@ describe('YAxisUnitSelector', () => {
|
||||
expect(screen.getByText('Seconds (s)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows warning message when incompatible unit is selected', async () => {
|
||||
it('shows warning message when incompatible unit is selected', () => {
|
||||
render(
|
||||
<YAxisUnitSelector
|
||||
source={YAxisSource.ALERTS}
|
||||
@@ -106,12 +104,12 @@ describe('YAxisUnitSelector', () => {
|
||||
);
|
||||
const warningIcon = screen.getByLabelText('warning');
|
||||
expect(warningIcon).toBeInTheDocument();
|
||||
await user.hover(warningIcon);
|
||||
await expect(
|
||||
screen.findByText(
|
||||
fireEvent.mouseOver(warningIcon);
|
||||
return screen
|
||||
.findByText(
|
||||
'Unit mismatch. The metric was sent with unit Seconds (s), but Bytes (B) is selected.',
|
||||
),
|
||||
).resolves.toBeInTheDocument();
|
||||
)
|
||||
.then((el) => expect(el).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('does not show warning message when compatible unit is selected', () => {
|
||||
@@ -127,7 +125,7 @@ describe('YAxisUnitSelector', () => {
|
||||
expect(warningIcon).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses categories override to render custom units', async () => {
|
||||
it('uses categories override to render custom units', () => {
|
||||
const customCategories = [
|
||||
{
|
||||
name: YAxisCategoryNames.Data,
|
||||
@@ -149,7 +147,9 @@ describe('YAxisUnitSelector', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('combobox'));
|
||||
const select = screen.getByRole('combobox');
|
||||
|
||||
fireEvent.mouseDown(select);
|
||||
|
||||
expect(screen.getByText('Custom Bytes (B)')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Bytes (B)')).not.toBeInTheDocument();
|
||||
|
||||
@@ -4,13 +4,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable hover on the warning icon: its `.ant-select-arrow` parent sets
|
||||
// `pointer-events: none`, which would otherwise suppress the tooltip.
|
||||
.y-axis-unit-warning {
|
||||
display: inline-flex;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.warning-state {
|
||||
.ant-select-selector {
|
||||
border-color: var(--bg-amber-400) !important;
|
||||
@@ -24,7 +17,3 @@
|
||||
right: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.y-axis-unit-warning-tooltip {
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
@@ -267,10 +267,11 @@ describe('createGuardedRoute', () => {
|
||||
await waitFor(() => {
|
||||
const heading = document.querySelector('h3');
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading?.textContent).toMatch(/not authorized/i);
|
||||
expect(heading?.textContent).toMatch(/permission to view/i);
|
||||
});
|
||||
|
||||
expect(screen.getByText(/update:role:123/)).toBeInTheDocument();
|
||||
expect(screen.getByText('update')).toBeInTheDocument();
|
||||
expect(screen.getByText('role:123')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('Test Component: test-value'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
AuthZRelation,
|
||||
BrandedPermission,
|
||||
} from 'hooks/useAuthZ/types';
|
||||
import { formatPermission } from 'hooks/useAuthZ/utils';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { parsePermission } from 'hooks/useAuthZ/utils';
|
||||
|
||||
import noDataUrl from '@/assets/Icons/no-data.svg';
|
||||
|
||||
@@ -18,16 +17,21 @@ import './createGuardedRoute.styles.scss';
|
||||
function OnNoPermissionsFallback(response: {
|
||||
requiredPermissionName: BrandedPermission;
|
||||
}): ReactElement {
|
||||
const { user } = useAppContext();
|
||||
const { relation, object } = parsePermission(response.requiredPermissionName);
|
||||
|
||||
return (
|
||||
<div className="guard-authz-error-no-authz">
|
||||
<div className="guard-authz-error-no-authz-content">
|
||||
<img src={noDataUrl} alt="No permission" />
|
||||
<h3>Uh-oh! You are not authorized</h3>
|
||||
<h3>Uh-oh! You don’t have permission to view this page.</h3>
|
||||
<p>
|
||||
<code>user/{user.id}</code> is not authorized to perform{' '}
|
||||
<code>{formatPermission(response.requiredPermissionName)}</code>
|
||||
You need the following permission to view this page:
|
||||
<br />
|
||||
Relation: <span>{relation}</span>
|
||||
<br />
|
||||
Object: <span>{object}</span>
|
||||
<br />
|
||||
Please ask your SigNoz administrator to grant access.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,8 +105,3 @@
|
||||
height: 1px;
|
||||
background: var(--l1-border);
|
||||
}
|
||||
|
||||
.errorInPlaceContainer {
|
||||
border-color: var(--callout-error-border) !important;
|
||||
background: var(--callout-error-background) !important;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import APIError from 'types/api/error';
|
||||
|
||||
import PermissionEditor from './components/PermissionEditor';
|
||||
import { useCreateEditRolePageActions } from './useCreateEditRolePageActions';
|
||||
import { useNavigationBlocker } from 'hooks/useNavigationBlocker';
|
||||
import { useNavigationBlocker } from '../../../hooks/useNavigationBlocker';
|
||||
|
||||
import styles from './CreateEditRolePage.module.scss';
|
||||
|
||||
@@ -212,10 +212,8 @@ function CreateEditRolePage(): JSX.Element {
|
||||
<ErrorInPlace
|
||||
error={saveError}
|
||||
height="auto"
|
||||
bordered
|
||||
data-testid="save-error-banner"
|
||||
padding={0}
|
||||
bordered={true}
|
||||
className={styles.errorInPlaceContainer}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -216,47 +216,6 @@ describe('CreateRolePage', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('shows error banner with "Role name is required" when saving with empty name', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderCreatePage();
|
||||
|
||||
const descInput = screen.getByTestId('role-description-input');
|
||||
await user.type(descInput, 'Description only');
|
||||
|
||||
const saveBtn = screen.getByTestId('save-button');
|
||||
await user.click(saveBtn);
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('save-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
await expect(
|
||||
screen.findByText('Role name is required'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clears error banner when user starts typing in name field', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderCreatePage();
|
||||
|
||||
const descInput = screen.getByTestId('role-description-input');
|
||||
await user.type(descInput, 'Description only');
|
||||
|
||||
const saveBtn = screen.getByTestId('save-button');
|
||||
await user.click(saveBtn);
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('save-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
const nameInput = screen.getByTestId('role-name-input');
|
||||
await user.type(nameInput, 'a');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('save-error-banner')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows error banner when API fails', async () => {
|
||||
server.use(
|
||||
rest.post(rolesApiBase, (_req, res, ctx) =>
|
||||
|
||||
@@ -520,115 +520,4 @@ describe('PermissionEditor', () => {
|
||||
expect(header).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resource card error states', () => {
|
||||
it('shows error border on collapsed card with validation error', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
|
||||
const nameInput = screen.getByTestId('role-name-input');
|
||||
await user.type(nameInput, 'valid-role');
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
const header = within(apiKeyCard).getByTestId(
|
||||
'resource-card-header-factor-api-key',
|
||||
);
|
||||
await user.click(header);
|
||||
|
||||
const readToggle = within(apiKeyCard).getByTestId(
|
||||
'action-toggle-factor-api-key-read',
|
||||
);
|
||||
const onlySelectedBtn = await within(readToggle).findByText('Only selected');
|
||||
await user.click(onlySelectedBtn);
|
||||
|
||||
await user.click(header);
|
||||
|
||||
const saveBtn = screen.getByTestId('save-button');
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
const card = screen.getByTestId('resource-card-factor-api-key');
|
||||
expect(card).toHaveAttribute('data-state', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
it('hides error border when card is expanded', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
|
||||
const nameInput = screen.getByTestId('role-name-input');
|
||||
await user.type(nameInput, 'valid-role');
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
const header = within(apiKeyCard).getByTestId(
|
||||
'resource-card-header-factor-api-key',
|
||||
);
|
||||
await user.click(header);
|
||||
|
||||
const readToggle = within(apiKeyCard).getByTestId(
|
||||
'action-toggle-factor-api-key-read',
|
||||
);
|
||||
const onlySelectedBtn = await within(readToggle).findByText('Only selected');
|
||||
await user.click(onlySelectedBtn);
|
||||
|
||||
await user.click(header);
|
||||
|
||||
const saveBtn = screen.getByTestId('save-button');
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
const card = screen.getByTestId('resource-card-factor-api-key');
|
||||
expect(card).toHaveAttribute('data-state', 'error');
|
||||
});
|
||||
|
||||
await user.click(header);
|
||||
|
||||
await waitFor(() => {
|
||||
const card = screen.getByTestId('resource-card-factor-api-key');
|
||||
expect(card).not.toHaveAttribute('data-state');
|
||||
});
|
||||
});
|
||||
|
||||
it('clears validation error when permission is changed', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
|
||||
const nameInput = screen.getByTestId('role-name-input');
|
||||
await user.type(nameInput, 'valid-role');
|
||||
|
||||
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
const header = within(apiKeyCard).getByTestId(
|
||||
'resource-card-header-factor-api-key',
|
||||
);
|
||||
await user.click(header);
|
||||
|
||||
const readToggle = within(apiKeyCard).getByTestId(
|
||||
'action-toggle-factor-api-key-read',
|
||||
);
|
||||
const onlySelectedBtn = await within(readToggle).findByText('Only selected');
|
||||
await user.click(onlySelectedBtn);
|
||||
|
||||
await user.click(header);
|
||||
|
||||
const saveBtn = screen.getByTestId('save-button');
|
||||
await user.click(saveBtn);
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('save-error-banner'),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
await user.click(header);
|
||||
|
||||
const freshCard = screen.getByTestId('resource-card-factor-api-key');
|
||||
const freshToggle = within(freshCard).getByTestId(
|
||||
'action-toggle-factor-api-key-read',
|
||||
);
|
||||
const noneBtn = await within(freshToggle).findByText('None');
|
||||
await user.click(noneBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('save-error-banner')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,10 +8,6 @@
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.resourceCardError {
|
||||
border-color: var(--destructive);
|
||||
}
|
||||
|
||||
.resourceCardHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
import type { AuthZResource, AuthZVerb } from 'hooks/useAuthZ/types';
|
||||
|
||||
@@ -10,7 +10,6 @@ import ActionToggle from './ActionToggle';
|
||||
|
||||
import styles from './ResourceCard.module.scss';
|
||||
import { PermissionScope, ResourcePermissions } from '../../types';
|
||||
import cx from 'classnames';
|
||||
|
||||
interface ResourceCardProps {
|
||||
resource: ResourcePermissions;
|
||||
@@ -75,22 +74,10 @@ function ResourceCard({
|
||||
|
||||
const [grantedCount, totalCount] = useRoleGrantedCount(resource);
|
||||
|
||||
const hasErrorOnResource = useMemo(
|
||||
() =>
|
||||
Array.from(validationErrors ?? []).some((r) =>
|
||||
r.startsWith(resource.resourceId),
|
||||
),
|
||||
[validationErrors, resource.resourceId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
styles.resourceCard,
|
||||
hasErrorOnResource && !isExpanded && styles.resourceCardError,
|
||||
)}
|
||||
className={styles.resourceCard}
|
||||
data-testid={`resource-card-${resource.resourceId}`}
|
||||
data-state={hasErrorOnResource && !isExpanded ? 'error' : undefined}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -125,10 +125,8 @@ export function useCreateEditRolePageActions(
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
clearValidationErrors();
|
||||
setSaveError(null);
|
||||
},
|
||||
[clearValidationErrors],
|
||||
[],
|
||||
);
|
||||
|
||||
const handleModeChange = useCallback(
|
||||
@@ -141,10 +139,8 @@ export function useCreateEditRolePageActions(
|
||||
const handleResourcesChange = useCallback(
|
||||
(resources: ResourcePermissions[]): void => {
|
||||
setLocalResources(resources);
|
||||
clearValidationErrors();
|
||||
setSaveError(null);
|
||||
},
|
||||
[clearValidationErrors],
|
||||
[],
|
||||
);
|
||||
|
||||
const hasUnsavedChanges = useRoleUnsavedChanges(
|
||||
@@ -157,17 +153,7 @@ export function useCreateEditRolePageActions(
|
||||
|
||||
const handleSave = useCallback(async (): Promise<boolean> => {
|
||||
if (!formData.name.trim()) {
|
||||
setSaveError(
|
||||
new APIError({
|
||||
httpStatusCode: 400,
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'Role name is required',
|
||||
url: '',
|
||||
errors: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
toast.error('Role name is required', { position: 'bottom-center' });
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,11 +41,6 @@ export function parsePermission(
|
||||
return { relation: relation as AuthZRelation, object };
|
||||
}
|
||||
|
||||
export function formatPermission(permission: BrandedPermission): string {
|
||||
const { relation, object } = parsePermission(permission);
|
||||
return `${relation}:${object}`;
|
||||
}
|
||||
|
||||
const kindsByType = permissionsType.data.resources.reduce(
|
||||
(acc, r) => {
|
||||
if (!acc[r.type]) {
|
||||
|
||||
@@ -68,3 +68,13 @@
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
// the V1 tags input ships borderless; give the field a visible box to match
|
||||
.tagsField {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 8px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l2-border);
|
||||
// background: var(--l3-background);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
// eslint-disable-next-line signoz/no-antd-components -- multiline TextArea has no @signozhq/ui equivalent yet
|
||||
import { Input as AntdInput } from 'antd';
|
||||
import TagKeyValueInput from 'components/TagKeyValueInput/TagKeyValueInput';
|
||||
import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddBadges';
|
||||
|
||||
import { Base64Icons } from '../utils';
|
||||
import settingsStyles from '../../DashboardSettings.module.scss';
|
||||
@@ -89,7 +89,9 @@ function DashboardInfoForm({
|
||||
|
||||
<div className={styles.infoItemContainer}>
|
||||
<Typography className={styles.infoTitle}>Tags</Typography>
|
||||
<TagKeyValueInput tags={tags} onTagsChange={onTagsChange} />
|
||||
<div className={styles.tagsField}>
|
||||
<AddTags tags={tags} setTags={onTagsChange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { TagtypesPostableTagDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
|
||||
export { parseKeyValueTag } from 'components/TagKeyValueInput/utils';
|
||||
|
||||
// tag UX, a string with no ':' is round-tripped as `{key: x, value: x}` and
|
||||
// collapsed back to just `x` for display.
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Input } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type {
|
||||
DashboardtypesPanelSpecDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
|
||||
import { resolveSignal } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/getBuilderQueries';
|
||||
import type { EQueryType } from 'types/common/dashboard';
|
||||
import { getBuilderQueries } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/getBuilderQueries';
|
||||
|
||||
import type { LegendSeries } from '../hooks/useLegendSeries';
|
||||
import type { TableColumnOption } from '../hooks/useTableColumns';
|
||||
@@ -18,22 +20,10 @@ interface ConfigPaneProps {
|
||||
/** The panel spec — the single editing surface (title/description + section slices). */
|
||||
spec: DashboardtypesPanelSpecDTO;
|
||||
onChangeSpec: (next: DashboardtypesPanelSpecDTO) => void;
|
||||
/** Switch the panel to another visualization kind. */
|
||||
onChangePanelKind: (kind: PanelKind) => void;
|
||||
/**
|
||||
* Active query type from the query-builder provider (the selected tab). Drives which
|
||||
* panel types the visualization switcher disables — read from the provider, not the
|
||||
* spec, because a new panel's spec has no query until staged.
|
||||
*/
|
||||
queryType: EQueryType;
|
||||
/** Panel's resolved series, provided to sections that need them (legend colors). */
|
||||
legendSeries: LegendSeries[];
|
||||
/** Table panel's resolved value columns, for the table-only editors. */
|
||||
tableColumns: TableColumnOption[];
|
||||
/** Query step interval (seconds), for the chart-appearance span-gaps floor. */
|
||||
stepInterval?: number;
|
||||
/** Unit the selected metric was sent with; drives the unit selector's mismatch warning. */
|
||||
metricUnit?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,17 +36,15 @@ function ConfigPane({
|
||||
panelKind,
|
||||
spec,
|
||||
onChangeSpec,
|
||||
onChangePanelKind,
|
||||
queryType,
|
||||
legendSeries,
|
||||
tableColumns,
|
||||
stepInterval,
|
||||
metricUnit,
|
||||
}: ConfigPaneProps): JSX.Element {
|
||||
const definition = getPanelDefinition(panelKind);
|
||||
const sections = definition.sections;
|
||||
|
||||
const signal = resolveSignal(spec.queries, definition.supportedSignals[0]);
|
||||
const signal = getBuilderQueries(spec.queries || [])[0]?.signal as
|
||||
| TelemetrytypesSignalDTO
|
||||
| undefined;
|
||||
|
||||
// Title/description are just a slice of the spec — edit them through the same
|
||||
// onChangeSpec path the sections use, so there's a single editing surface.
|
||||
@@ -107,11 +95,6 @@ function ConfigPane({
|
||||
legendSeries={legendSeries}
|
||||
tableColumns={tableColumns}
|
||||
signal={signal}
|
||||
panelKind={panelKind}
|
||||
onChangePanelKind={onChangePanelKind}
|
||||
queryType={queryType}
|
||||
stepInterval={stepInterval}
|
||||
metricUnit={metricUnit}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// Matches ConfigPane's `.field` so the switcher lines up with the title/description fields.
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import type { PanelKind } from '../../../Panels/types/panelKind';
|
||||
import { PANEL_TYPES } from '../../../PanelsAndSectionsLayout/Panel/PanelTypeSelectionModal/constants';
|
||||
import ConfigSelect from '../controls/ConfigSelect/ConfigSelect';
|
||||
|
||||
import styles from './PanelTypeSwitcher.module.scss';
|
||||
import { getPanelTypeDisabledReason } from './utils';
|
||||
|
||||
interface PanelTypeSwitcherProps {
|
||||
/** The current panel kind (selected value). */
|
||||
panelKind: PanelKind;
|
||||
/** Active query type — a kind that can't be authored in it is disabled (e.g. List is Query-Builder-only, so PromQL/ClickHouse disable it). Defaults to Query Builder. */
|
||||
queryType?: EQueryType;
|
||||
/** Panel's current signal — also gates the disabled rule (List needs logs/traces, not metrics). */
|
||||
signal?: TelemetrytypesSignalDTO;
|
||||
onChange: (kind: PanelKind) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visualization-type selector (rendered inside the Visualization section). A type is
|
||||
* disabled when the active query type or signal is incompatible with it — resolved
|
||||
* through the capabilities guard. The signal is unknown for PromQL/ClickHouse, but
|
||||
* those query types still disable kinds that only support Query Builder (e.g. List).
|
||||
*/
|
||||
function PanelTypeSwitcher({
|
||||
panelKind,
|
||||
queryType,
|
||||
signal,
|
||||
onChange,
|
||||
}: PanelTypeSwitcherProps): JSX.Element {
|
||||
const items = PANEL_TYPES.map(({ panelKind, label, Icon }) => {
|
||||
// One reason drives both the disabled flag and the tooltip, so they can't disagree.
|
||||
const disabledReason = getPanelTypeDisabledReason({
|
||||
kind: panelKind,
|
||||
queryType: queryType ?? EQueryType.QUERY_BUILDER,
|
||||
signal,
|
||||
label,
|
||||
});
|
||||
return {
|
||||
value: panelKind,
|
||||
label,
|
||||
icon: <Icon size={14} />,
|
||||
disabled: !!disabledReason,
|
||||
tooltip: disabledReason,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.field}>
|
||||
<Typography.Text>Panel Type</Typography.Text>
|
||||
<ConfigSelect
|
||||
testId="panel-editor-v2-type-switcher"
|
||||
value={panelKind}
|
||||
items={items}
|
||||
onChange={(value): void => onChange(value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PanelTypeSwitcher;
|
||||
@@ -1,122 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
|
||||
|
||||
import PanelTypeSwitcher from '../PanelTypeSwitcher';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
jest.mock('pages/DashboardPageV2/DashboardContainer/Panels/registry', () => ({
|
||||
getPanelDefinition: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockGetPanelDefinition = getPanelDefinition as unknown as jest.Mock;
|
||||
|
||||
// Query-type support per kind: List is Query-Builder-only; Table/Pie drop PromQL.
|
||||
const SUPPORTED_QUERY_TYPES: Record<string, EQueryType[]> = {
|
||||
'signoz/ListPanel': [EQueryType.QUERY_BUILDER],
|
||||
'signoz/TablePanel': [EQueryType.QUERY_BUILDER, EQueryType.CLICKHOUSE],
|
||||
'signoz/PieChartPanel': [EQueryType.QUERY_BUILDER, EQueryType.CLICKHOUSE],
|
||||
};
|
||||
|
||||
function disabledLabels(): (string | null)[] {
|
||||
return Array.from(
|
||||
document.querySelectorAll('.ant-select-item-option-disabled'),
|
||||
).map((el) => el.textContent);
|
||||
}
|
||||
|
||||
function openDropdown(): void {
|
||||
fireEvent.mouseDown(screen.getByRole('combobox'));
|
||||
}
|
||||
|
||||
describe('PanelTypeSwitcher', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// List supports only logs/traces; every other kind also supports metrics.
|
||||
// Query-type support comes from SUPPORTED_QUERY_TYPES (all three by default).
|
||||
mockGetPanelDefinition.mockImplementation((kind: string) => ({
|
||||
supportedSignals:
|
||||
kind === 'signoz/ListPanel'
|
||||
? ['logs', 'traces']
|
||||
: ['metrics', 'logs', 'traces'],
|
||||
supportedQueryTypes: SUPPORTED_QUERY_TYPES[kind] ?? [
|
||||
EQueryType.QUERY_BUILDER,
|
||||
EQueryType.CLICKHOUSE,
|
||||
EQueryType.PROM,
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
it('fires onChange with the chosen plugin kind', () => {
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<PanelTypeSwitcher panelKind="signoz/TimeSeriesPanel" onChange={onChange} />,
|
||||
);
|
||||
|
||||
openDropdown();
|
||||
fireEvent.click(screen.getByText('List'));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('signoz/ListPanel');
|
||||
});
|
||||
|
||||
it('disables types whose supported signals exclude the current signal', () => {
|
||||
render(
|
||||
<PanelTypeSwitcher
|
||||
panelKind="signoz/TimeSeriesPanel"
|
||||
signal={TelemetrytypesSignalDTO.metrics}
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
openDropdown();
|
||||
// List can't render a metrics query, so it's disabled; Time Series stays enabled.
|
||||
expect(disabledLabels()).toContain('List');
|
||||
expect(disabledLabels()).not.toContain('Time Series');
|
||||
});
|
||||
|
||||
it('does not disable any type when the signal is unknown (builder, no signal)', () => {
|
||||
render(
|
||||
<PanelTypeSwitcher
|
||||
panelKind="signoz/TimeSeriesPanel"
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
openDropdown();
|
||||
expect(
|
||||
document.querySelectorAll('.ant-select-item-option-disabled'),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('disables Query-Builder-only kinds under PromQL even without a signal', () => {
|
||||
render(
|
||||
<PanelTypeSwitcher
|
||||
panelKind="signoz/TimeSeriesPanel"
|
||||
queryType={EQueryType.PROM}
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
openDropdown();
|
||||
// List/Table/Pie can't be authored in PromQL; Time Series can.
|
||||
expect(disabledLabels()).toContain('List');
|
||||
expect(disabledLabels()).toContain('Table');
|
||||
expect(disabledLabels()).toContain('Pie Chart');
|
||||
expect(disabledLabels()).not.toContain('Time Series');
|
||||
});
|
||||
|
||||
it('disables List under ClickHouse while Table/Pie stay enabled', () => {
|
||||
render(
|
||||
<PanelTypeSwitcher
|
||||
panelKind="signoz/TablePanel"
|
||||
queryType={EQueryType.CLICKHOUSE}
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
openDropdown();
|
||||
expect(disabledLabels()).toContain('List');
|
||||
expect(disabledLabels()).not.toContain('Table');
|
||||
expect(disabledLabels()).not.toContain('Pie Chart');
|
||||
expect(disabledLabels()).not.toContain('Time Series');
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { getPanelTypeDisabledReason } from '../utils';
|
||||
|
||||
const { QUERY_BUILDER, CLICKHOUSE, PROM } = EQueryType;
|
||||
const { logs, metrics } = TelemetrytypesSignalDTO;
|
||||
|
||||
describe('getPanelTypeDisabledReason', () => {
|
||||
it('returns undefined for a supported combination', () => {
|
||||
expect(
|
||||
getPanelTypeDisabledReason({
|
||||
kind: 'signoz/TimeSeriesPanel',
|
||||
queryType: PROM,
|
||||
label: 'Time Series',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
getPanelTypeDisabledReason({
|
||||
kind: 'signoz/ListPanel',
|
||||
queryType: QUERY_BUILDER,
|
||||
signal: logs,
|
||||
label: 'List',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('explains an unsupported query type', () => {
|
||||
expect(
|
||||
getPanelTypeDisabledReason({
|
||||
kind: 'signoz/ListPanel',
|
||||
queryType: PROM,
|
||||
label: 'List',
|
||||
}),
|
||||
).toBe("List isn't available for PromQL queries");
|
||||
expect(
|
||||
getPanelTypeDisabledReason({
|
||||
kind: 'signoz/ListPanel',
|
||||
queryType: CLICKHOUSE,
|
||||
label: 'List',
|
||||
}),
|
||||
).toBe("List isn't available for ClickHouse queries");
|
||||
expect(
|
||||
getPanelTypeDisabledReason({
|
||||
kind: 'signoz/TablePanel',
|
||||
queryType: PROM,
|
||||
label: 'Table',
|
||||
}),
|
||||
).toBe("Table isn't available for PromQL queries");
|
||||
});
|
||||
|
||||
it('explains an unsupported signal', () => {
|
||||
expect(
|
||||
getPanelTypeDisabledReason({
|
||||
kind: 'signoz/ListPanel',
|
||||
queryType: QUERY_BUILDER,
|
||||
signal: metrics,
|
||||
label: 'List',
|
||||
}),
|
||||
).toBe("List doesn't support metrics data");
|
||||
});
|
||||
|
||||
it('prefers the query-type reason when both are incompatible', () => {
|
||||
expect(
|
||||
getPanelTypeDisabledReason({
|
||||
kind: 'signoz/ListPanel',
|
||||
queryType: PROM,
|
||||
signal: metrics,
|
||||
label: 'List',
|
||||
}),
|
||||
).toBe("List isn't available for PromQL queries");
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import {
|
||||
isQueryTypeSupported,
|
||||
isSignalSupported,
|
||||
} from '../../../Panels/capabilities';
|
||||
import type { PanelKind } from '../../../Panels/types/panelKind';
|
||||
|
||||
const QUERY_TYPE_LABEL: Record<EQueryType, string> = {
|
||||
[EQueryType.QUERY_BUILDER]: 'Query Builder',
|
||||
[EQueryType.CLICKHOUSE]: 'ClickHouse',
|
||||
[EQueryType.PROM]: 'PromQL',
|
||||
};
|
||||
|
||||
const SIGNAL_LABEL: Record<TelemetrytypesSignalDTO, string> = {
|
||||
[TelemetrytypesSignalDTO.logs]: 'logs',
|
||||
[TelemetrytypesSignalDTO.traces]: 'traces',
|
||||
[TelemetrytypesSignalDTO.metrics]: 'metrics',
|
||||
};
|
||||
|
||||
/**
|
||||
* Why a panel kind can't be selected for the current query type / signal, or
|
||||
* `undefined` when it can. Drives both the type switcher's disabled state and its
|
||||
* tooltip, so the two never disagree. The query-type reason takes precedence (it's the
|
||||
* outer choice): query types carry no signal, so the signal only matters in builder.
|
||||
*/
|
||||
export function getPanelTypeDisabledReason({
|
||||
kind,
|
||||
queryType,
|
||||
signal,
|
||||
label,
|
||||
}: {
|
||||
kind: PanelKind;
|
||||
queryType: EQueryType;
|
||||
signal?: TelemetrytypesSignalDTO;
|
||||
label: string;
|
||||
}): string | undefined {
|
||||
if (!isQueryTypeSupported(kind, queryType)) {
|
||||
return `${label} isn't available for ${QUERY_TYPE_LABEL[queryType]} queries`;
|
||||
}
|
||||
if (signal !== undefined && !isSignalSupported(kind, signal)) {
|
||||
return `${label} doesn't support ${SIGNAL_LABEL[signal]} data`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,21 +1,29 @@
|
||||
import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type {
|
||||
DashboardtypesPanelSpecDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
type PanelFormattingSlice,
|
||||
SECTION_METADATA,
|
||||
type SectionConfig,
|
||||
SectionKind,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
|
||||
import type { SectionEditorContext } from '../sectionContext';
|
||||
import type { LegendSeries } from '../../hooks/useLegendSeries';
|
||||
import type { TableColumnOption } from '../../hooks/useTableColumns';
|
||||
import { resolveSectionEditor } from '../sectionRegistry';
|
||||
import SettingsSection from '../SettingsSection/SettingsSection';
|
||||
|
||||
// `yAxisUnit` is derived from the spec below, not forwarded, so it's omitted.
|
||||
type SectionSlotProps = {
|
||||
interface SectionSlotProps {
|
||||
config: SectionConfig;
|
||||
spec: DashboardtypesPanelSpecDTO;
|
||||
onChangeSpec: (next: DashboardtypesPanelSpecDTO) => void;
|
||||
} & Omit<SectionEditorContext, 'yAxisUnit'>;
|
||||
/** Resolved series, forwarded to editors that need them (legend colors). */
|
||||
legendSeries: LegendSeries[];
|
||||
/** Table panel's resolved value columns, for the table-only editors. */
|
||||
tableColumns: TableColumnOption[];
|
||||
/** Panel's telemetry signal, for editors that fetch field suggestions (List columns). */
|
||||
signal?: TelemetrytypesSignalDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders one configuration section: its collapsible wrapper plus the registered editor
|
||||
@@ -30,11 +38,6 @@ function SectionSlot({
|
||||
legendSeries,
|
||||
tableColumns,
|
||||
signal,
|
||||
panelKind,
|
||||
onChangePanelKind,
|
||||
queryType,
|
||||
stepInterval,
|
||||
metricUnit,
|
||||
}: SectionSlotProps): JSX.Element | null {
|
||||
// A kind can hide a section based on current spec state (e.g. Histogram legend once
|
||||
// queries are merged) — skip it before resolving the editor.
|
||||
@@ -57,12 +60,7 @@ function SectionSlot({
|
||||
.formatting?.unit;
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
title={title}
|
||||
icon={<Icon size={15} />}
|
||||
// Open Visualization by default so the type switcher is visible.
|
||||
defaultOpen={config.kind === SectionKind.Visualization}
|
||||
>
|
||||
<SettingsSection title={title} icon={<Icon size={15} />}>
|
||||
<Component
|
||||
value={get(spec)}
|
||||
controls={controls}
|
||||
@@ -71,11 +69,6 @@ function SectionSlot({
|
||||
yAxisUnit={yAxisUnit}
|
||||
tableColumns={tableColumns}
|
||||
signal={signal}
|
||||
panelKind={panelKind}
|
||||
onChangePanelKind={onChangePanelKind}
|
||||
queryType={queryType}
|
||||
stepInterval={stepInterval}
|
||||
metricUnit={metricUnit}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
|
||||
@@ -26,15 +26,13 @@ function SettingsSection({
|
||||
}: SettingsSectionProps): JSX.Element {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
|
||||
const serializedTitle = title.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.header}
|
||||
aria-expanded={isOpen}
|
||||
data-testid={`config-section-${serializedTitle}`}
|
||||
data-testid={`config-section-${title}`}
|
||||
onClick={(): void => setIsOpen((prev) => !prev)}
|
||||
>
|
||||
{icon && (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import ConfigPane from '../ConfigPane';
|
||||
|
||||
@@ -22,8 +21,6 @@ function renderConfigPane(
|
||||
panelKind: 'signoz/TimeSeriesPanel',
|
||||
spec: spec(),
|
||||
onChangeSpec: jest.fn(),
|
||||
onChangePanelKind: jest.fn(),
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
legendSeries: [],
|
||||
tableColumns: [],
|
||||
...overrides,
|
||||
@@ -59,8 +56,6 @@ describe('ConfigPane', () => {
|
||||
it('renders the Formatting section for a kind that declares it', () => {
|
||||
renderConfigPane();
|
||||
// The TimeSeries kind declares a Formatting section; its collapsible header shows.
|
||||
expect(
|
||||
screen.getByTestId('config-section-formatting-&-units'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('config-section-Formatting')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.group {
|
||||
width: 100%;
|
||||
width: min(350px, 100%);
|
||||
}
|
||||
|
||||
.segment {
|
||||
|
||||
@@ -10,11 +10,11 @@ export interface ConfigSegmentedItem {
|
||||
icon?: SegmentIconName;
|
||||
}
|
||||
|
||||
interface ConfigSegmentedProps<T extends string = string> {
|
||||
interface ConfigSegmentedProps {
|
||||
testId: string;
|
||||
value: T | undefined;
|
||||
value: string | undefined;
|
||||
items: ConfigSegmentedItem[];
|
||||
onChange: (value: T) => void;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,12 +23,12 @@ interface ConfigSegmentedProps<T extends string = string> {
|
||||
* brightens with the selected state (it inherits the toggle's `currentColor`). Built on
|
||||
* the Periscope ToggleGroup so it stays theme-faithful.
|
||||
*/
|
||||
function ConfigSegmented<T extends string = string>({
|
||||
function ConfigSegmented({
|
||||
testId,
|
||||
value,
|
||||
items,
|
||||
onChange,
|
||||
}: ConfigSegmentedProps<T>): JSX.Element {
|
||||
}: ConfigSegmentedProps): JSX.Element {
|
||||
return (
|
||||
<ToggleGroupSimple
|
||||
type="single"
|
||||
@@ -47,7 +47,7 @@ function ConfigSegmented<T extends string = string>({
|
||||
}))}
|
||||
// Single toggle-groups emit '' when the active segment is re-clicked; ignore that
|
||||
// so a required choice (e.g. scale, position) can't be cleared to an empty value.
|
||||
onChange={(next: T): void => {
|
||||
onChange={(next: string): void => {
|
||||
if (next) {
|
||||
onChange(next);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,3 @@
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
// Wraps a tooltip-bearing option so the hover target fills the row and still receives
|
||||
// pointer events when the option is disabled (antd dims it but doesn't block events).
|
||||
.tooltipTrigger {
|
||||
display: block;
|
||||
width: 100%;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Select, Tooltip } from 'antd';
|
||||
import { Select } from 'antd';
|
||||
|
||||
import { SegmentIcon, type SegmentIconName } from '../segmentIcons';
|
||||
|
||||
import styles from './ConfigSelect.module.scss';
|
||||
|
||||
export interface ConfigSelectItem<T extends string = string> {
|
||||
value: T;
|
||||
export interface ConfigSelectItem {
|
||||
value: string;
|
||||
label: string;
|
||||
/** Optional leading icon node rendered before the label. */
|
||||
icon?: ReactNode;
|
||||
disabled?: boolean;
|
||||
/** Hover hint shown on the option — typically the reason a disabled item is disabled. */
|
||||
tooltip?: string;
|
||||
icon?: SegmentIconName;
|
||||
}
|
||||
|
||||
interface ConfigSelectProps<T extends string = string> {
|
||||
interface ConfigSelectProps {
|
||||
testId: string;
|
||||
value: T | undefined;
|
||||
value: string | undefined;
|
||||
placeholder?: string;
|
||||
items: ConfigSelectItem<T>[];
|
||||
onChange: (value: T) => void;
|
||||
items: ConfigSelectItem[];
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,42 +23,32 @@ interface ConfigSelectProps<T extends string = string> {
|
||||
* `Select` so it matches the rest of the editor's antd controls; the menu portals to
|
||||
* `document.body` (antd default) so the surrounding `overflow:auto` pane can't clip it.
|
||||
*/
|
||||
function ConfigSelect<T extends string = string>({
|
||||
function ConfigSelect({
|
||||
testId,
|
||||
value,
|
||||
placeholder,
|
||||
items,
|
||||
onChange,
|
||||
}: ConfigSelectProps<T>): JSX.Element {
|
||||
}: ConfigSelectProps): JSX.Element {
|
||||
return (
|
||||
<Select<T>
|
||||
<Select<string>
|
||||
className={styles.select}
|
||||
data-testid={testId}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
virtual={false}
|
||||
options={items.map((item) => {
|
||||
const content = item.icon ? (
|
||||
options={items.map((item) => ({
|
||||
value: item.value,
|
||||
label: item.icon ? (
|
||||
<span className={styles.item}>
|
||||
{item.icon}
|
||||
<SegmentIcon name={item.icon} />
|
||||
{item.label}
|
||||
</span>
|
||||
) : (
|
||||
item.label
|
||||
);
|
||||
return {
|
||||
value: item.value,
|
||||
disabled: item.disabled,
|
||||
label: item.tooltip ? (
|
||||
<Tooltip title={item.tooltip} placement="top">
|
||||
<span className={styles.tooltipTrigger}>{content}</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
content
|
||||
),
|
||||
};
|
||||
})}
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import type { PanelKind } from '../../Panels/types/panelKind';
|
||||
import type { LegendSeries } from '../hooks/useLegendSeries';
|
||||
import type { TableColumnOption } from '../hooks/useTableColumns';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
/**
|
||||
* Context `SectionSlot` forwards to every section editor (not spec-slice fields — those
|
||||
* come from `SectionEditorProps<K>`); each editor `Pick`s what it consumes. All optional:
|
||||
* editors resolve through the kind-erased descriptor, so receipt isn't type-guaranteed.
|
||||
*/
|
||||
export interface SectionEditorContext {
|
||||
legendSeries?: LegendSeries[];
|
||||
tableColumns?: TableColumnOption[];
|
||||
signal?: TelemetrytypesSignalDTO;
|
||||
panelKind?: PanelKind;
|
||||
onChangePanelKind?: (kind: PanelKind) => void;
|
||||
yAxisUnit?: string;
|
||||
queryType?: EQueryType;
|
||||
stepInterval?: number;
|
||||
/** Unit the selected metric was sent with; drives the unit selector's mismatch warning. */
|
||||
metricUnit?: string;
|
||||
}
|
||||
@@ -8,15 +8,14 @@ import type {
|
||||
DashboardtypesPanelSpecDTO,
|
||||
DashboardtypesTimeSeriesChartAppearanceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
import type {
|
||||
AnyThreshold,
|
||||
PanelFormattingSlice,
|
||||
SectionEditorProps,
|
||||
SectionKind,
|
||||
type AnyThreshold,
|
||||
type PanelFormattingSlice,
|
||||
type SectionEditorProps,
|
||||
type SectionSpecMap,
|
||||
SectionSpecMap,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
|
||||
import type { SectionEditorContext } from './sectionContext';
|
||||
import AxesSection from './sections/AxesSection/AxesSection';
|
||||
import BucketsSection from './sections/BucketsSection/BucketsSection';
|
||||
import ChartAppearanceSection from './sections/ChartAppearanceSection/ChartAppearanceSection';
|
||||
@@ -70,27 +69,27 @@ function updatePluginSlice(
|
||||
export const SECTION_REGISTRY: {
|
||||
[K in SectionKind]?: SectionDescriptor<K>;
|
||||
} = {
|
||||
[SectionKind.Formatting]: {
|
||||
formatting: {
|
||||
Component: FormattingSection,
|
||||
get: (spec): PanelFormattingSlice | undefined =>
|
||||
getPluginSlice<PanelFormattingSlice>(spec, 'formatting'),
|
||||
update: (spec, formatting): PanelSpec =>
|
||||
updatePluginSlice(spec, 'formatting', formatting),
|
||||
},
|
||||
[SectionKind.Axes]: {
|
||||
axes: {
|
||||
Component: AxesSection,
|
||||
get: (spec): DashboardtypesAxesDTO | undefined =>
|
||||
getPluginSlice<DashboardtypesAxesDTO>(spec, 'axes'),
|
||||
update: (spec, axes): PanelSpec => updatePluginSlice(spec, 'axes', axes),
|
||||
},
|
||||
[SectionKind.Legend]: {
|
||||
legend: {
|
||||
Component: LegendSection,
|
||||
get: (spec): DashboardtypesLegendDTO | undefined =>
|
||||
getPluginSlice<DashboardtypesLegendDTO>(spec, 'legend'),
|
||||
update: (spec, legend): PanelSpec =>
|
||||
updatePluginSlice(spec, 'legend', legend),
|
||||
},
|
||||
[SectionKind.ChartAppearance]: {
|
||||
chartAppearance: {
|
||||
Component: ChartAppearanceSection,
|
||||
get: (spec): DashboardtypesTimeSeriesChartAppearanceDTO | undefined =>
|
||||
getPluginSlice<DashboardtypesTimeSeriesChartAppearanceDTO>(
|
||||
@@ -100,7 +99,7 @@ export const SECTION_REGISTRY: {
|
||||
update: (spec, chartAppearance): PanelSpec =>
|
||||
updatePluginSlice(spec, 'chartAppearance', chartAppearance),
|
||||
},
|
||||
[SectionKind.Visualization]: {
|
||||
visualization: {
|
||||
Component: VisualizationSection,
|
||||
get: (spec): DashboardtypesBarChartVisualizationDTO | undefined =>
|
||||
getPluginSlice<DashboardtypesBarChartVisualizationDTO>(
|
||||
@@ -110,14 +109,14 @@ export const SECTION_REGISTRY: {
|
||||
update: (spec, visualization): PanelSpec =>
|
||||
updatePluginSlice(spec, 'visualization', visualization),
|
||||
},
|
||||
[SectionKind.Buckets]: {
|
||||
buckets: {
|
||||
Component: BucketsSection,
|
||||
get: (spec): DashboardtypesHistogramBucketsDTO | undefined =>
|
||||
getPluginSlice<DashboardtypesHistogramBucketsDTO>(spec, 'histogramBuckets'),
|
||||
update: (spec, buckets): PanelSpec =>
|
||||
updatePluginSlice(spec, 'histogramBuckets', buckets),
|
||||
},
|
||||
[SectionKind.ContextLinks]: {
|
||||
contextLinks: {
|
||||
Component: ContextLinksSection,
|
||||
// Panel-level slice (spec.links), not under the plugin spec — no cast needed.
|
||||
get: (spec): DashboardLinkDTO[] | undefined => spec.links,
|
||||
@@ -126,7 +125,7 @@ export const SECTION_REGISTRY: {
|
||||
// One editor for every threshold variant (label / comparison / table); the kind's
|
||||
// `controls.variant` picks the row editor + element shape. All persist to the same
|
||||
// plugin.spec.thresholds key.
|
||||
[SectionKind.Thresholds]: {
|
||||
thresholds: {
|
||||
Component: ThresholdsSection,
|
||||
get: (spec): AnyThreshold[] | undefined =>
|
||||
getPluginSlice<AnyThreshold[]>(spec, 'thresholds'),
|
||||
@@ -143,13 +142,22 @@ export const SECTION_REGISTRY: {
|
||||
* `get` → `Component` → `update` without any further casts.
|
||||
*/
|
||||
export interface ErasedSectionDescriptor {
|
||||
Component: ComponentType<
|
||||
{
|
||||
value: unknown;
|
||||
controls?: unknown;
|
||||
onChange: (next: unknown) => void;
|
||||
} & SectionEditorContext
|
||||
>;
|
||||
Component: ComponentType<{
|
||||
value: unknown;
|
||||
controls?: unknown;
|
||||
onChange: (next: unknown) => void;
|
||||
// Forwarded to every editor; only sections that need the panel's resolved series
|
||||
// (legend colors) read it. Optional so editors can ignore it.
|
||||
legendSeries?: unknown;
|
||||
// The panel's formatting unit; read by editors that scope to it (thresholds).
|
||||
yAxisUnit?: unknown;
|
||||
// The Table panel's resolved value columns; read by the table-only editors
|
||||
// (column units, per-column thresholds) to offer real columns.
|
||||
tableColumns?: unknown;
|
||||
// The panel's telemetry signal; read by editors that fetch field-key
|
||||
// suggestions scoped to it (List column picker).
|
||||
signal?: unknown;
|
||||
}>;
|
||||
get: (spec: PanelSpec) => unknown;
|
||||
update: (spec: PanelSpec, value: unknown) => PanelSpec;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Input } from 'antd';
|
||||
import type {
|
||||
SectionEditorProps,
|
||||
SectionKind,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
|
||||
import ConfigSegmented from '../../controls/ConfigSegmented/ConfigSegmented';
|
||||
|
||||
@@ -25,7 +22,7 @@ function AxesSection({
|
||||
value,
|
||||
controls,
|
||||
onChange,
|
||||
}: SectionEditorProps<SectionKind.Axes>): JSX.Element {
|
||||
}: SectionEditorProps<'axes'>): JSX.Element {
|
||||
// An empty field clears the bound (null); otherwise parse to a number, ignoring
|
||||
// transient non-numeric input (e.g. a lone "-") by leaving the bound unset.
|
||||
const handleBound =
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { ChangeEvent } from 'react';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Input } from 'antd';
|
||||
import type { DashboardtypesHistogramBucketsDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type {
|
||||
SectionEditorProps,
|
||||
SectionKind,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
|
||||
import ConfigSwitch from '../../controls/ConfigSwitch/ConfigSwitch';
|
||||
|
||||
@@ -26,7 +23,7 @@ function BucketsSection({
|
||||
value,
|
||||
controls,
|
||||
onChange,
|
||||
}: SectionEditorProps<SectionKind.Buckets>): JSX.Element {
|
||||
}: SectionEditorProps<'buckets'>): JSX.Element {
|
||||
// Empty clears the bound to null (chart auto-sizes); otherwise parse to a number,
|
||||
// ignoring transient non-numeric input by leaving it unset.
|
||||
const handleNumber =
|
||||
|
||||
@@ -3,14 +3,3 @@
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.thresholdField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.thresholdPrefix {
|
||||
padding-right: 4px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Input } from 'antd';
|
||||
import {
|
||||
DashboardtypesFillModeDTO,
|
||||
DashboardtypesLineInterpolationDTO,
|
||||
DashboardtypesLineStyleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type {
|
||||
SectionEditorProps,
|
||||
SectionKind,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
|
||||
import ConfigSegmented from '../../controls/ConfigSegmented/ConfigSegmented';
|
||||
import ConfigSelect from '../../controls/ConfigSelect/ConfigSelect';
|
||||
import ConfigSwitch from '../../controls/ConfigSwitch/ConfigSwitch';
|
||||
import { SegmentIcon } from '../../controls/segmentIcons';
|
||||
import type { SectionEditorContext } from '../../sectionContext';
|
||||
import DisconnectValuesField from './DisconnectValuesField';
|
||||
|
||||
import styles from './ChartAppearanceSection.module.scss';
|
||||
|
||||
@@ -35,22 +31,22 @@ const LINE_INTERPOLATION_OPTIONS = [
|
||||
{
|
||||
value: DashboardtypesLineInterpolationDTO.linear,
|
||||
label: 'Linear',
|
||||
icon: <SegmentIcon name="interp-linear" />,
|
||||
icon: 'interp-linear' as const,
|
||||
},
|
||||
{
|
||||
value: DashboardtypesLineInterpolationDTO.spline,
|
||||
label: 'Spline',
|
||||
icon: <SegmentIcon name="interp-spline" />,
|
||||
icon: 'interp-spline' as const,
|
||||
},
|
||||
{
|
||||
value: DashboardtypesLineInterpolationDTO.step_before,
|
||||
label: 'Step before',
|
||||
icon: <SegmentIcon name="interp-step-before" />,
|
||||
icon: 'interp-step-before' as const,
|
||||
},
|
||||
{
|
||||
value: DashboardtypesLineInterpolationDTO.step_after,
|
||||
label: 'Step after',
|
||||
icon: <SegmentIcon name="interp-step-after" />,
|
||||
icon: 'interp-step-after' as const,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -81,9 +77,16 @@ function ChartAppearanceSection({
|
||||
value,
|
||||
controls,
|
||||
onChange,
|
||||
stepInterval,
|
||||
}: SectionEditorProps<SectionKind.ChartAppearance> &
|
||||
Pick<SectionEditorContext, 'stepInterval'>): JSX.Element {
|
||||
}: SectionEditorProps<'chartAppearance'>): JSX.Element {
|
||||
// `spanGaps.fillLessThan` is a stringified seconds threshold: empty means "connect
|
||||
// every gap" (the chart default), a number means "only bridge gaps shorter than this".
|
||||
const handleSpanGaps = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
const raw = e.target.value;
|
||||
onChange({
|
||||
...value,
|
||||
spanGaps: raw === '' ? undefined : { ...value?.spanGaps, fillLessThan: raw },
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{controls.lineStyle && (
|
||||
@@ -111,7 +114,7 @@ function ChartAppearanceSection({
|
||||
onChange={(next): void =>
|
||||
onChange({
|
||||
...value,
|
||||
lineInterpolation: next,
|
||||
lineInterpolation: next as DashboardtypesLineInterpolationDTO,
|
||||
})
|
||||
}
|
||||
/>
|
||||
@@ -143,12 +146,16 @@ function ChartAppearanceSection({
|
||||
)}
|
||||
|
||||
{controls.spanGaps && (
|
||||
<DisconnectValuesField
|
||||
testId="panel-editor-v2-span-gaps"
|
||||
value={value?.spanGaps}
|
||||
stepInterval={stepInterval}
|
||||
onChange={(spanGaps): void => onChange({ ...value, spanGaps })}
|
||||
/>
|
||||
<div className={styles.field}>
|
||||
<Typography.Text>Connect gaps shorter than (s)</Typography.Text>
|
||||
<Input
|
||||
data-testid="panel-editor-v2-span-gaps"
|
||||
type="number"
|
||||
placeholder="All gaps"
|
||||
value={value?.spanGaps?.fillLessThan ?? ''}
|
||||
onChange={handleSpanGaps}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { rangeUtil } from '@grafana/data';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { DashboardtypesSpanGapsDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import ConfigSegmented from '../../controls/ConfigSegmented/ConfigSegmented';
|
||||
import DisconnectValuesThresholdInput from './DisconnectValuesThresholdInput';
|
||||
|
||||
import styles from './ChartAppearanceSection.module.scss';
|
||||
|
||||
const DEFAULT_THRESHOLD = '1m';
|
||||
enum DisconnectValuesMode {
|
||||
NEVER = 'never',
|
||||
THRESHOLD = 'threshold',
|
||||
}
|
||||
const MODE_OPTIONS = [
|
||||
{ value: DisconnectValuesMode.NEVER, label: 'Never' },
|
||||
{ value: DisconnectValuesMode.THRESHOLD, label: 'Threshold' },
|
||||
];
|
||||
|
||||
interface DisconnectValuesFieldProps {
|
||||
testId: string;
|
||||
value: DashboardtypesSpanGapsDTO | undefined;
|
||||
/** Query step interval (seconds): seeds the default threshold and floors it. */
|
||||
stepInterval?: number;
|
||||
onChange: (next: DashboardtypesSpanGapsDTO | undefined) => void;
|
||||
}
|
||||
|
||||
/** Default threshold duration: the step interval (smallest meaningful), else 1m. */
|
||||
function defaultDuration(stepInterval?: number): string {
|
||||
return stepInterval && stepInterval > 0
|
||||
? rangeUtil.secondsToHms(stepInterval)
|
||||
: DEFAULT_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Disconnect values": Never (span every gap — the chart default) vs Threshold
|
||||
* (only bridge gaps shorter than a duration). The threshold persists as a
|
||||
* duration string in `spanGaps.fillLessThan` ("10m", "5s") — the wire format the
|
||||
* backend expects.
|
||||
*/
|
||||
function DisconnectValuesField({
|
||||
testId,
|
||||
value,
|
||||
stepInterval,
|
||||
onChange,
|
||||
}: DisconnectValuesFieldProps): JSX.Element {
|
||||
const duration = value?.fillLessThan || undefined;
|
||||
// `fillOnlyBelow` is authoritative; fall back to a stored duration for legacy panels.
|
||||
const isThreshold = value?.fillOnlyBelow ?? !!duration;
|
||||
// Remember the last committed threshold so Never → Threshold restores it.
|
||||
const [lastDuration, setLastDuration] = useState<string | undefined>(duration);
|
||||
|
||||
useEffect(() => {
|
||||
if (duration) {
|
||||
setLastDuration(duration);
|
||||
}
|
||||
}, [duration]);
|
||||
|
||||
const handleMode = (mode: DisconnectValuesMode): void => {
|
||||
if (mode === DisconnectValuesMode.THRESHOLD) {
|
||||
onChange({
|
||||
...value,
|
||||
fillOnlyBelow: true,
|
||||
// Seed from the live stepInterval (async — undefined until results load), not mount.
|
||||
fillLessThan: lastDuration ?? defaultDuration(stepInterval),
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Never spans every gap; drop the duration so the renderer reads a clean "span all".
|
||||
onChange({ ...value, fillOnlyBelow: false, fillLessThan: undefined });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.field}>
|
||||
<Typography.Text>Disconnect values</Typography.Text>
|
||||
<ConfigSegmented
|
||||
testId={testId}
|
||||
value={
|
||||
isThreshold ? DisconnectValuesMode.THRESHOLD : DisconnectValuesMode.NEVER
|
||||
}
|
||||
items={MODE_OPTIONS}
|
||||
onChange={handleMode}
|
||||
/>
|
||||
</div>
|
||||
{isThreshold && duration && (
|
||||
<div className={styles.field}>
|
||||
<Typography.Text>Threshold value</Typography.Text>
|
||||
<DisconnectValuesThresholdInput
|
||||
testId={`${testId}-value`}
|
||||
value={duration}
|
||||
minValue={stepInterval}
|
||||
onChange={(next): void =>
|
||||
onChange({ ...value, fillOnlyBelow: true, fillLessThan: next })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DisconnectValuesField;
|
||||
@@ -1,110 +0,0 @@
|
||||
import { type ChangeEvent, useEffect, useState } from 'react';
|
||||
import { rangeUtil } from '@grafana/data';
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import { Input } from 'antd';
|
||||
|
||||
import styles from './ChartAppearanceSection.module.scss';
|
||||
|
||||
interface DisconnectValuesThresholdInputProps {
|
||||
testId: string;
|
||||
/** Current threshold as a duration string (e.g. "1m") — the stored wire value. */
|
||||
value: string;
|
||||
/** Smallest allowed threshold (the query step interval), in seconds. */
|
||||
minValue?: number;
|
||||
onChange: (duration: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline error for a raw duration, or `null` when valid and in range. The parse is
|
||||
* guarded: `isValidTimeSpan` passes some strings `intervalToSeconds` throws on (e.g. "5x").
|
||||
*/
|
||||
function validationError(raw: string, minValue?: number): string | null {
|
||||
let seconds: number;
|
||||
try {
|
||||
seconds = rangeUtil.isValidTimeSpan(raw)
|
||||
? rangeUtil.intervalToSeconds(raw)
|
||||
: NaN;
|
||||
} catch {
|
||||
seconds = NaN;
|
||||
}
|
||||
if (!Number.isFinite(seconds) || seconds <= 0) {
|
||||
return 'Enter a valid duration (e.g. 30s, 1m, 1h)';
|
||||
}
|
||||
if (minValue !== undefined && seconds < minValue) {
|
||||
return `Threshold should be > ${rangeUtil.secondsToHms(minValue)}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration input for the span-gaps threshold: shows/accepts and reports a human
|
||||
* duration ("30s", "1m", "1h"), which is the value stored verbatim in
|
||||
* `fillLessThan` (a bare number is read as seconds). It is only parsed to seconds
|
||||
* to validate against the query step interval. Invalid entries, or values below
|
||||
* that floor, surface an inline error and are not committed (V1 parity).
|
||||
*/
|
||||
function DisconnectValuesThresholdInput({
|
||||
testId,
|
||||
value,
|
||||
minValue,
|
||||
onChange,
|
||||
}: DisconnectValuesThresholdInputProps): JSX.Element {
|
||||
const [text, setText] = useState(value);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Resync the displayed duration when the committed value changes upstream.
|
||||
useEffect(() => {
|
||||
setText(value);
|
||||
setError(null);
|
||||
}, [value]);
|
||||
|
||||
// Validate live so an invalid entry surfaces immediately, not only on blur.
|
||||
const handleText = (raw: string): void => {
|
||||
setText(raw);
|
||||
setError(raw ? validationError(raw, minValue) : null);
|
||||
};
|
||||
|
||||
const commit = (raw: string): void => {
|
||||
// Skip no-op commits: blur fires when clicking the Never toggle, and re-emitting
|
||||
// the unchanged value there would race the toggle and snap back to Threshold.
|
||||
if (!raw || raw === value) {
|
||||
return;
|
||||
}
|
||||
const message = validationError(raw, minValue);
|
||||
if (message) {
|
||||
setError(message);
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
// Store the user's duration string as-is — the wire format the backend wants.
|
||||
onChange(raw);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.thresholdField}>
|
||||
<Input
|
||||
data-testid={testId}
|
||||
type="text"
|
||||
status={error ? 'error' : undefined}
|
||||
prefix={<span className={styles.thresholdPrefix}>></span>}
|
||||
value={text}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||
handleText(e.target.value)
|
||||
}
|
||||
onBlur={(e): void => commit(e.currentTarget.value)}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
commit(e.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{error && (
|
||||
<Callout type="error" size="small" showIcon>
|
||||
{error}
|
||||
</Callout>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DisconnectValuesThresholdInput;
|
||||
@@ -1,34 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
DashboardtypesLineStyleDTO,
|
||||
type DashboardtypesTimeSeriesChartAppearanceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { DashboardtypesLineStyleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import ChartAppearanceSection from '../ChartAppearanceSection';
|
||||
|
||||
/** Stateful wrapper that feeds onChange back as the spec, mirroring the real editor. */
|
||||
function StatefulSpanGaps({
|
||||
initial,
|
||||
stepInterval,
|
||||
}: {
|
||||
initial?: DashboardtypesTimeSeriesChartAppearanceDTO;
|
||||
stepInterval?: number;
|
||||
}): JSX.Element {
|
||||
const [value, setValue] = useState<
|
||||
DashboardtypesTimeSeriesChartAppearanceDTO | undefined
|
||||
>(initial);
|
||||
return (
|
||||
<ChartAppearanceSection
|
||||
value={value}
|
||||
controls={{ spanGaps: true }}
|
||||
stepInterval={stepInterval}
|
||||
onChange={setValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Open the antd Select by clicking its selector, then pick the option by label. The
|
||||
// line-style and fill-mode controls are ConfigSegmented (buttons), so this helper is
|
||||
// only used for the line-interpolation ConfigSelect.
|
||||
@@ -85,8 +60,7 @@ describe('ChartAppearanceSection', () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('writes the chosen fill mode through the segmented control', async () => {
|
||||
const user = userEvent.setup();
|
||||
it('writes the chosen fill mode through the segmented control', () => {
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
@@ -96,7 +70,7 @@ describe('ChartAppearanceSection', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('Gradient'));
|
||||
fireEvent.click(screen.getByText('Gradient'));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
lineStyle: 'solid',
|
||||
@@ -119,8 +93,7 @@ describe('ChartAppearanceSection', () => {
|
||||
expect(onChange).toHaveBeenCalledWith({ lineInterpolation: 'spline' });
|
||||
});
|
||||
|
||||
it('toggles show points through onChange', async () => {
|
||||
const user = userEvent.setup();
|
||||
it('toggles show points through onChange', () => {
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
@@ -130,187 +103,13 @@ describe('ChartAppearanceSection', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId('panel-editor-v2-show-points'));
|
||||
fireEvent.click(screen.getByTestId('panel-editor-v2-show-points'));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ showPoints: true });
|
||||
});
|
||||
|
||||
it('defaults to "Never" (no threshold) and hides the threshold input', () => {
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={undefined}
|
||||
controls={{ spanGaps: true }}
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Never')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('panel-editor-v2-span-gaps-value'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('switching to "Threshold" seeds the default 1m threshold', async () => {
|
||||
const user = userEvent.setup();
|
||||
it('writes a span-gaps threshold and clears it when emptied', () => {
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={undefined}
|
||||
controls={{ spanGaps: true }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('Threshold'));
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
spanGaps: { fillOnlyBelow: true, fillLessThan: '1m' },
|
||||
});
|
||||
});
|
||||
|
||||
it('stores the threshold as a duration string (not seconds)', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={{ spanGaps: { fillLessThan: '1m' } }}
|
||||
controls={{ spanGaps: true }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByTestId('panel-editor-v2-span-gaps-value');
|
||||
expect(input).toHaveValue('1m');
|
||||
|
||||
await user.clear(input);
|
||||
await user.type(input, '5m');
|
||||
await user.tab();
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
spanGaps: { fillOnlyBelow: true, fillLessThan: '5m' },
|
||||
});
|
||||
});
|
||||
|
||||
it('stores the entry verbatim (bare number kept as typed, not converted)', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={{ spanGaps: { fillLessThan: '1m' } }}
|
||||
controls={{ spanGaps: true }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByTestId('panel-editor-v2-span-gaps-value');
|
||||
await user.clear(input);
|
||||
await user.type(input, '300');
|
||||
await user.tab();
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
spanGaps: { fillOnlyBelow: true, fillLessThan: '300' },
|
||||
});
|
||||
});
|
||||
|
||||
it('switching back to "Never" clears the threshold', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={{ spanGaps: { fillLessThan: '1m' } }}
|
||||
controls={{ spanGaps: true }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('Never'));
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
spanGaps: { fillOnlyBelow: false, fillLessThan: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('selects Never when fillOnlyBelow is false even if a duration lingers', () => {
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={{ spanGaps: { fillOnlyBelow: false, fillLessThan: '1m' } }}
|
||||
controls={{ spanGaps: true }}
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// The flag is authoritative: a stale fillLessThan must not show Threshold.
|
||||
expect(
|
||||
screen.queryByTestId('panel-editor-v2-span-gaps-value'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows an error and does not commit an invalid duration', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={{ spanGaps: { fillLessThan: '1m' } }}
|
||||
controls={{ spanGaps: true }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByTestId('panel-editor-v2-span-gaps-value');
|
||||
await user.clear(input);
|
||||
await user.type(input, 'abc');
|
||||
await user.tab();
|
||||
|
||||
expect(screen.getByText(/valid duration/i)).toBeInTheDocument();
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a threshold below the query step interval', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={{ spanGaps: { fillLessThan: '2m' } }}
|
||||
controls={{ spanGaps: true }}
|
||||
stepInterval={120}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByTestId('panel-editor-v2-span-gaps-value');
|
||||
// 1m (60s) is below the 2m (120s) step interval.
|
||||
await user.clear(input);
|
||||
await user.type(input, '1m');
|
||||
await user.tab();
|
||||
|
||||
expect(screen.getByText(/Threshold should be >/)).toBeInTheDocument();
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('seeds the threshold from the step interval when switching to Threshold', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={undefined}
|
||||
controls={{ spanGaps: true }}
|
||||
stepInterval={300}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('Threshold'));
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
spanGaps: { fillOnlyBelow: true, fillLessThan: '5m' },
|
||||
});
|
||||
});
|
||||
|
||||
it('seeds from the step interval even when it arrives after mount', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
// The step interval is undefined until the query response carries step metadata,
|
||||
// so the panel first renders without it and receives it on a later render.
|
||||
const { rerender } = render(
|
||||
<ChartAppearanceSection
|
||||
value={undefined}
|
||||
@@ -318,85 +117,24 @@ describe('ChartAppearanceSection', () => {
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByTestId('panel-editor-v2-span-gaps'), {
|
||||
target: { value: '60' },
|
||||
});
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
spanGaps: { fillLessThan: '60' },
|
||||
});
|
||||
|
||||
rerender(
|
||||
<ChartAppearanceSection
|
||||
value={undefined}
|
||||
value={{ spanGaps: { fillLessThan: '60' } }}
|
||||
controls={{ spanGaps: true }}
|
||||
stepInterval={300}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('Threshold'));
|
||||
|
||||
// Regression: a value seeded at mount would still be the 1m fallback.
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
spanGaps: { fillOnlyBelow: true, fillLessThan: '5m' },
|
||||
fireEvent.change(screen.getByTestId('panel-editor-v2-span-gaps'), {
|
||||
target: { value: '' },
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a validation error while typing, before blur', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={{ spanGaps: { fillLessThan: '1m' } }}
|
||||
controls={{ spanGaps: true }}
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByTestId('panel-editor-v2-span-gaps-value');
|
||||
await user.clear(input);
|
||||
await user.type(input, 'abc');
|
||||
// No blur / Enter — the error must already be visible.
|
||||
|
||||
expect(screen.getByText(/valid duration/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not re-commit the threshold when blurred without a change', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<ChartAppearanceSection
|
||||
value={{ spanGaps: { fillLessThan: '1m' } }}
|
||||
controls={{ spanGaps: true }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByTestId('panel-editor-v2-span-gaps-value');
|
||||
await user.click(input);
|
||||
await user.tab();
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fully switches from Threshold to Never (the input disappears)', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StatefulSpanGaps initial={{ spanGaps: { fillLessThan: '1m' } }} />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId('panel-editor-v2-span-gaps-value'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Focus the input first so clicking Never also fires its blur (the toggle race).
|
||||
await user.click(screen.getByTestId('panel-editor-v2-span-gaps-value'));
|
||||
await user.click(screen.getByText('Never'));
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('panel-editor-v2-span-gaps-value'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('remembers the last threshold when toggling Never → Threshold', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StatefulSpanGaps initial={{ spanGaps: { fillLessThan: '5m' } }} />);
|
||||
|
||||
await user.click(screen.getByText('Never'));
|
||||
await user.click(screen.getByText('Threshold'));
|
||||
|
||||
expect(screen.getByTestId('panel-editor-v2-span-gaps-value')).toHaveValue(
|
||||
'5m',
|
||||
);
|
||||
expect(onChange).toHaveBeenLastCalledWith({ spanGaps: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,10 +4,7 @@ import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Input } from 'antd';
|
||||
import type { DashboardLinkDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type {
|
||||
SectionEditorProps,
|
||||
SectionKind,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
|
||||
import styles from './ContextLinksSection.module.scss';
|
||||
|
||||
@@ -20,7 +17,7 @@ import styles from './ContextLinksSection.module.scss';
|
||||
function ContextLinksSection({
|
||||
value,
|
||||
onChange,
|
||||
}: SectionEditorProps<SectionKind.ContextLinks>): JSX.Element {
|
||||
}: SectionEditorProps<'contextLinks'>): JSX.Element {
|
||||
const links = value ?? [];
|
||||
|
||||
const updateAt = (index: number, patch: Partial<DashboardLinkDTO>): void =>
|
||||
|
||||
@@ -2,19 +2,18 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import { DashboardtypesPrecisionOptionDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
import type {
|
||||
SectionEditorProps,
|
||||
SectionKind,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
|
||||
import type { TableColumnOption } from '../../../hooks/useTableColumns';
|
||||
import ConfigSelect from '../../controls/ConfigSelect/ConfigSelect';
|
||||
import type { SectionEditorContext } from '../../sectionContext';
|
||||
import ColumnUnits from './ColumnUnits';
|
||||
|
||||
import styles from './FormattingSection.module.scss';
|
||||
|
||||
type FormattingSectionProps = SectionEditorProps<SectionKind.Formatting> &
|
||||
Pick<SectionEditorContext, 'tableColumns' | 'metricUnit'>;
|
||||
type FormattingSectionProps = SectionEditorProps<'formatting'> & {
|
||||
/** Table panel's resolved value columns; required for the column-units editor. */
|
||||
tableColumns?: TableColumnOption[];
|
||||
};
|
||||
|
||||
// `full` means "show the raw value, no rounding"; the digits round to that many places.
|
||||
const DECIMAL_OPTIONS: {
|
||||
@@ -39,7 +38,6 @@ function FormattingSection({
|
||||
controls,
|
||||
onChange,
|
||||
tableColumns = [],
|
||||
metricUnit,
|
||||
}: FormattingSectionProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
@@ -51,7 +49,6 @@ function FormattingSection({
|
||||
data-testid="panel-editor-v2-unit"
|
||||
source={YAxisSource.DASHBOARDS}
|
||||
value={value?.unit}
|
||||
initialValue={metricUnit}
|
||||
onChange={(unit): void => onChange({ ...value, unit })}
|
||||
/>
|
||||
</div>
|
||||
@@ -68,7 +65,7 @@ function FormattingSection({
|
||||
onChange={(next): void =>
|
||||
onChange({
|
||||
...value,
|
||||
decimalPrecision: next,
|
||||
decimalPrecision: next as DashboardtypesPrecisionOptionDTO,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -3,8 +3,6 @@ import userEvent from '@testing-library/user-event';
|
||||
|
||||
import FormattingSection from '../FormattingSection';
|
||||
|
||||
// Auto-seeding is covered by useMetricYAxisUnit's tests; here `metricUnit` is just a prop.
|
||||
|
||||
// Open the Decimals select (clicking its antd selector) and pick the option with the
|
||||
// given visible label.
|
||||
async function pickDecimal(label: string): Promise<void> {
|
||||
@@ -73,31 +71,4 @@ describe('FormattingSection', () => {
|
||||
decimalPrecision: '2',
|
||||
});
|
||||
});
|
||||
|
||||
it('warns when the selected unit mismatches the metric unit', () => {
|
||||
// metric sent in seconds, but bytes is selected.
|
||||
render(
|
||||
<FormattingSection
|
||||
value={{ unit: 'By' }}
|
||||
controls={{ unit: true }}
|
||||
metricUnit="s"
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText('warning')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows no warning when the selected unit matches the metric unit', () => {
|
||||
render(
|
||||
<FormattingSection
|
||||
value={{ unit: 's' }}
|
||||
controls={{ unit: true }}
|
||||
metricUnit="s"
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText('warning')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { DashboardtypesLegendPositionDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type {
|
||||
SectionEditorProps,
|
||||
SectionKind,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
|
||||
import ConfigSegmented from '../../controls/ConfigSegmented/ConfigSegmented';
|
||||
import LegendColors from '../../controls/LegendColors/LegendColors';
|
||||
import type { SectionEditorContext } from '../../sectionContext';
|
||||
import type { LegendSeries } from '../../../hooks/useLegendSeries';
|
||||
|
||||
import styles from './LegendSection.module.scss';
|
||||
|
||||
type LegendSectionProps = SectionEditorProps<SectionKind.Legend> &
|
||||
Pick<SectionEditorContext, 'legendSeries'>;
|
||||
type LegendSectionProps = SectionEditorProps<'legend'> & {
|
||||
/** Panel's resolved series, forwarded by SectionSlot for the colors control. */
|
||||
legendSeries?: LegendSeries[];
|
||||
};
|
||||
|
||||
const POSITION_OPTIONS = [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import {
|
||||
@@ -14,7 +14,6 @@ import type {
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
|
||||
import type { TableColumnOption } from '../../../hooks/useTableColumns';
|
||||
import type { SectionEditorContext } from '../../sectionContext';
|
||||
import ComparisonThresholdRow from './rows/ComparisonThresholdRow';
|
||||
import LabelThresholdRow from './rows/LabelThresholdRow';
|
||||
import TableThresholdRow from './rows/TableThresholdRow';
|
||||
@@ -62,7 +61,11 @@ type ThresholdsSectionProps = {
|
||||
/** `variant` picks the row editor + element shape; defaults to `label`. */
|
||||
controls?: { variant?: ThresholdVariant };
|
||||
onChange: (next: AnyThreshold[]) => void;
|
||||
} & Pick<SectionEditorContext, 'yAxisUnit' | 'tableColumns'>;
|
||||
/** Panel formatting unit; scopes each row's unit picker to its category (V1 parity). */
|
||||
yAxisUnit?: string;
|
||||
/** Table panel's resolved value columns (table variant only). */
|
||||
tableColumns?: TableColumnOption[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits the `thresholds` slice for every panel kind. All variants share the same
|
||||
@@ -82,15 +85,6 @@ function ThresholdsSection({
|
||||
// Which row is being edited, and whether it was just added (so Discard removes it).
|
||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||
const [unsavedIndex, setUnsavedIndex] = useState<number | null>(null);
|
||||
// The saved threshold captured on edit entry, restored if the edit is discarded
|
||||
// (edits stream into the spec live, so Discard can't just drop a local draft).
|
||||
const editSnapshot = useRef<AnyThreshold | null>(null);
|
||||
|
||||
const updateAt =
|
||||
(index: number) =>
|
||||
(next: AnyThreshold): void => {
|
||||
onChange(thresholds.map((t, i) => (i === index ? next : t)));
|
||||
};
|
||||
|
||||
const addThreshold = (): void => {
|
||||
const nextIndex = thresholds.length;
|
||||
@@ -99,11 +93,6 @@ function ThresholdsSection({
|
||||
setUnsavedIndex(nextIndex);
|
||||
};
|
||||
|
||||
const beginEdit = (index: number): void => {
|
||||
editSnapshot.current = thresholds[index] ?? null;
|
||||
setEditingIndex(index);
|
||||
};
|
||||
|
||||
const saveAt =
|
||||
(index: number) =>
|
||||
(next: AnyThreshold): void => {
|
||||
@@ -119,15 +108,11 @@ function ThresholdsSection({
|
||||
};
|
||||
|
||||
const discardAt = (index: number) => (): void => {
|
||||
// A never-saved row is removed; otherwise revert the live edits to the snapshot.
|
||||
// Discarding a row that was never saved removes it; otherwise just exit edit.
|
||||
if (index === unsavedIndex) {
|
||||
removeAt(index);
|
||||
return;
|
||||
}
|
||||
const original = editSnapshot.current;
|
||||
if (original) {
|
||||
onChange(thresholds.map((t, i) => (i === index ? original : t)));
|
||||
}
|
||||
setEditingIndex(null);
|
||||
};
|
||||
|
||||
@@ -138,9 +123,8 @@ function ThresholdsSection({
|
||||
index,
|
||||
yAxisUnit,
|
||||
isEditing: editingIndex === index,
|
||||
onEdit: (): void => beginEdit(index),
|
||||
onEdit: (): void => setEditingIndex(index),
|
||||
onSave: saveAt(index),
|
||||
onLiveChange: updateAt(index),
|
||||
onDiscard: discardAt(index),
|
||||
onRemove: (): void => removeAt(index),
|
||||
};
|
||||
|
||||
@@ -36,16 +36,9 @@ const THRESHOLDS: DashboardtypesComparisonThresholdDTO[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// Stateful harness for flows that depend on the value updating (add/discard/live).
|
||||
function Harness({
|
||||
yAxisUnit,
|
||||
initial = [],
|
||||
}: {
|
||||
yAxisUnit?: string;
|
||||
initial?: DashboardtypesComparisonThresholdDTO[];
|
||||
}): JSX.Element {
|
||||
const [value, setValue] =
|
||||
useState<DashboardtypesComparisonThresholdDTO[]>(initial);
|
||||
// Stateful harness for flows that depend on the value updating (add/discard).
|
||||
function Harness({ yAxisUnit }: { yAxisUnit?: string }): JSX.Element {
|
||||
const [value, setValue] = useState<DashboardtypesComparisonThresholdDTO[]>([]);
|
||||
return (
|
||||
<ComparisonThresholdsSection
|
||||
value={value}
|
||||
@@ -130,63 +123,24 @@ describe('ComparisonThresholdsSection', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('lets the value input be cleared instead of snapping back to 0', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ComparisonThresholdsSection value={THRESHOLDS} onChange={jest.fn()} />,
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId('comparison-threshold-edit-0'));
|
||||
const valueInput = screen.getByTestId('comparison-threshold-value-0');
|
||||
|
||||
// Regression: clearing used to coerce "" → 0 and refill the field, so the
|
||||
// seeded value could never be removed.
|
||||
await user.clear(valueInput);
|
||||
expect(valueInput).toHaveValue(null);
|
||||
|
||||
// And a fresh value can be typed into the now-empty field.
|
||||
await user.type(valueInput, '5');
|
||||
expect(valueInput).toHaveValue(5);
|
||||
});
|
||||
|
||||
it('reflects edits live (before Save) so the preview can react', async () => {
|
||||
it('does not commit edits when Discard is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<ComparisonThresholdsSection value={THRESHOLDS} onChange={onChange} />,
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId('comparison-threshold-edit-0'));
|
||||
await user.clear(screen.getByTestId('comparison-threshold-value-0'));
|
||||
await user.type(screen.getByTestId('comparison-threshold-value-0'), '90');
|
||||
|
||||
// No Save click — the latest edit is already pushed up for the preview.
|
||||
expect(onChange).toHaveBeenLastCalledWith([
|
||||
{
|
||||
value: 90,
|
||||
color: '#F5B225',
|
||||
operator: DashboardtypesComparisonOperatorDTO.above,
|
||||
unit: 'percent',
|
||||
format: DashboardtypesThresholdFormatDTO.background,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('reverts the live edits to the saved value on Discard', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness initial={THRESHOLDS} />);
|
||||
|
||||
await user.click(screen.getByTestId('comparison-threshold-edit-0'));
|
||||
await user.clear(screen.getByTestId('comparison-threshold-value-0'));
|
||||
await user.type(screen.getByTestId('comparison-threshold-value-0'), '90');
|
||||
await user.click(screen.getByTestId('comparison-threshold-discard-0'));
|
||||
|
||||
// Back to view mode, and re-opening shows the rolled-back 80, not 90.
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
// Back to view mode.
|
||||
expect(
|
||||
screen.queryByTestId('comparison-threshold-value-0'),
|
||||
).not.toBeInTheDocument();
|
||||
await user.click(screen.getByTestId('comparison-threshold-edit-0'));
|
||||
expect(screen.getByTestId('comparison-threshold-value-0')).toHaveValue(80);
|
||||
expect(screen.getByTestId('comparison-threshold-edit-0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('removes a threshold from view mode', async () => {
|
||||
|
||||
@@ -10,16 +10,10 @@ const THRESHOLDS: DashboardtypesThresholdWithLabelDTO[] = [
|
||||
{ value: 80, color: '#F5B225', label: 'High', unit: 'percent' },
|
||||
];
|
||||
|
||||
// Stateful harness for flows that depend on the value updating (add/discard/live);
|
||||
// Stateful harness for flows that depend on the value updating (add/discard);
|
||||
// omits `controls` to exercise the default `label` variant.
|
||||
function Harness({
|
||||
yAxisUnit,
|
||||
initial = [],
|
||||
}: {
|
||||
yAxisUnit?: string;
|
||||
initial?: AnyThreshold[];
|
||||
}): JSX.Element {
|
||||
const [value, setValue] = useState<AnyThreshold[]>(initial);
|
||||
function Harness({ yAxisUnit }: { yAxisUnit?: string }): JSX.Element {
|
||||
const [value, setValue] = useState<AnyThreshold[]>([]);
|
||||
return (
|
||||
<ThresholdsSection value={value} onChange={setValue} yAxisUnit={yAxisUnit} />
|
||||
);
|
||||
@@ -76,34 +70,19 @@ describe('ThresholdsSection', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('reflects edits live (before Save) so the preview can react', () => {
|
||||
it('does not commit edits when Discard is clicked', () => {
|
||||
const onChange = jest.fn();
|
||||
render(<ThresholdsSection value={THRESHOLDS} onChange={onChange} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('threshold-edit-0'));
|
||||
fireEvent.change(screen.getByTestId('threshold-value-0'), {
|
||||
target: { value: '90' },
|
||||
});
|
||||
|
||||
// No Save click — the edit is already pushed up for the preview to render.
|
||||
expect(onChange).toHaveBeenLastCalledWith([
|
||||
{ value: 90, color: '#F5B225', label: 'High', unit: 'percent' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('reverts the live edits to the saved value on Discard', () => {
|
||||
render(<Harness initial={THRESHOLDS} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('threshold-edit-0'));
|
||||
fireEvent.change(screen.getByTestId('threshold-value-0'), {
|
||||
target: { value: '90' },
|
||||
});
|
||||
fireEvent.click(screen.getByTestId('threshold-discard-0'));
|
||||
|
||||
// Back to view mode, and re-opening shows the rolled-back 80, not 90.
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
expect(screen.queryByTestId('threshold-value-0')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId('threshold-edit-0'));
|
||||
expect(screen.getByTestId('threshold-value-0')).toHaveValue(80);
|
||||
expect(screen.getByTestId('threshold-edit-0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('removes a threshold from view mode', () => {
|
||||
|
||||
@@ -27,7 +27,6 @@ interface ComparisonThresholdRowProps {
|
||||
isEditing: boolean;
|
||||
onEdit: () => void;
|
||||
onSave: (next: DashboardtypesComparisonThresholdDTO) => void;
|
||||
onLiveChange: (next: DashboardtypesComparisonThresholdDTO) => void;
|
||||
onDiscard: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
@@ -43,15 +42,10 @@ function ComparisonThresholdRow({
|
||||
isEditing,
|
||||
onEdit,
|
||||
onSave,
|
||||
onLiveChange,
|
||||
onDiscard,
|
||||
onRemove,
|
||||
}: ComparisonThresholdRowProps): JSX.Element {
|
||||
const { draft, setDraft, setValue } = useThresholdDraft(
|
||||
threshold,
|
||||
isEditing,
|
||||
onLiveChange,
|
||||
);
|
||||
const { draft, setDraft, setValue } = useThresholdDraft(threshold, isEditing);
|
||||
|
||||
const symbol = threshold.operator ? OPERATOR_SYMBOL[threshold.operator] : '';
|
||||
const summary = (
|
||||
|
||||
@@ -20,7 +20,6 @@ interface LabelThresholdRowProps {
|
||||
isEditing: boolean;
|
||||
onEdit: () => void;
|
||||
onSave: (next: DashboardtypesThresholdWithLabelDTO) => void;
|
||||
onLiveChange: (next: DashboardtypesThresholdWithLabelDTO) => void;
|
||||
onDiscard: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
@@ -33,15 +32,10 @@ function LabelThresholdRow({
|
||||
isEditing,
|
||||
onEdit,
|
||||
onSave,
|
||||
onLiveChange,
|
||||
onDiscard,
|
||||
onRemove,
|
||||
}: LabelThresholdRowProps): JSX.Element {
|
||||
const { draft, setDraft, setValue } = useThresholdDraft(
|
||||
threshold,
|
||||
isEditing,
|
||||
onLiveChange,
|
||||
);
|
||||
const { draft, setDraft, setValue } = useThresholdDraft(threshold, isEditing);
|
||||
|
||||
// Persist an empty-string label when none was entered — the spec requires a string.
|
||||
const handleSave = useCallback((): void => {
|
||||
|
||||
@@ -28,7 +28,6 @@ interface TableThresholdRowProps {
|
||||
isEditing: boolean;
|
||||
onEdit: () => void;
|
||||
onSave: (next: DashboardtypesTableThresholdDTO) => void;
|
||||
onLiveChange: (next: DashboardtypesTableThresholdDTO) => void;
|
||||
onDiscard: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
@@ -46,15 +45,10 @@ function TableThresholdRow({
|
||||
isEditing,
|
||||
onEdit,
|
||||
onSave,
|
||||
onLiveChange,
|
||||
onDiscard,
|
||||
onRemove,
|
||||
}: TableThresholdRowProps): JSX.Element {
|
||||
const { draft, setDraft, setValue } = useThresholdDraft(
|
||||
threshold,
|
||||
isEditing,
|
||||
onLiveChange,
|
||||
);
|
||||
const { draft, setDraft, setValue } = useThresholdDraft(threshold, isEditing);
|
||||
|
||||
// Stored columnName is the query key; resolve its label + configured unit.
|
||||
const columnUnit = tableColumns.find((c) => c.key === draft.columnName)?.unit;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Input } from 'antd';
|
||||
|
||||
@@ -17,12 +16,6 @@ function ThresholdValueField({
|
||||
value,
|
||||
onChange,
|
||||
}: ThresholdValueFieldProps): JSX.Element {
|
||||
const [raw, setRaw] = useState(String(value));
|
||||
|
||||
useEffect(() => {
|
||||
setRaw((prev) => (Number(prev) === value ? prev : String(value)));
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className={styles.field}>
|
||||
<Typography.Text className={styles.fieldLabel}>Value</Typography.Text>
|
||||
@@ -30,11 +23,8 @@ function ThresholdValueField({
|
||||
data-testid={testId}
|
||||
type="number"
|
||||
placeholder="Value"
|
||||
value={raw}
|
||||
onChange={(e): void => {
|
||||
setRaw(e.target.value);
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
value={value}
|
||||
onChange={(e): void => onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,14 +9,12 @@ interface ThresholdDraft<T> {
|
||||
|
||||
/**
|
||||
* Local draft for a threshold row, shared by every variant. Snapshots the saved
|
||||
* threshold on each entry into edit mode and exposes the numeric `value` setter all
|
||||
* variants use. `onLiveChange` mirrors the draft into the spec as the user edits, so the
|
||||
* panel preview updates live (without Save); the section reverts it on Discard.
|
||||
* threshold on each entry into edit mode (so Discard simply drops the draft and the
|
||||
* next edit starts clean) and exposes the numeric `value` setter all variants use.
|
||||
*/
|
||||
export function useThresholdDraft<T extends { value: number }>(
|
||||
threshold: T,
|
||||
isEditing: boolean,
|
||||
onLiveChange?: (draft: T) => void,
|
||||
): ThresholdDraft<T> {
|
||||
const [draft, setDraft] = useState<T>(threshold);
|
||||
|
||||
@@ -27,13 +25,6 @@ export function useThresholdDraft<T extends { value: number }>(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- snapshot only on edit entry
|
||||
}, [isEditing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
onLiveChange?.(draft);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- propagate on draft change only
|
||||
}, [draft]);
|
||||
|
||||
const setValue = (raw: string): void => {
|
||||
const next = Number(raw);
|
||||
setDraft((d) => ({ ...d, value: Number.isNaN(next) ? d.value : next }));
|
||||
|
||||
@@ -1,49 +1,26 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type {
|
||||
SectionEditorProps,
|
||||
SectionKind,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
import { DashboardtypesTimePreferenceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
|
||||
import ConfigSelect from '../../controls/ConfigSelect/ConfigSelect';
|
||||
import ConfigSwitch from '../../controls/ConfigSwitch/ConfigSwitch';
|
||||
import PanelTypeSwitcher from '../../PanelTypeSwitcher/PanelTypeSwitcher';
|
||||
import type { SectionEditorContext } from '../../sectionContext';
|
||||
import { TIME_PREFERENCE_OPTIONS } from './timePreferenceOptions';
|
||||
|
||||
import styles from './VisualizationSection.module.scss';
|
||||
|
||||
type VisualizationSectionProps = SectionEditorProps<SectionKind.Visualization> &
|
||||
Pick<
|
||||
SectionEditorContext,
|
||||
'panelKind' | 'onChangePanelKind' | 'signal' | 'queryType'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Edits the `visualization` slice: the panel-type switcher (`switchPanelKind`, every
|
||||
* kind), the per-panel time preference, bar stacking (`stackedBarChart`, Bar only), and
|
||||
* gap filling (`fillSpans`, TimeSeries only). Each control is gated by its `controls`
|
||||
* flag, so a kind only renders — and only writes — the fields its spec supports.
|
||||
* Edits the `visualization` slice: the per-panel time preference (all kinds), bar
|
||||
* stacking (`stackedBarChart`, Bar only), and gap filling (`fillSpans`, TimeSeries
|
||||
* only). Each control is gated by its `controls` flag, so a kind only renders — and only
|
||||
* writes — the visualization fields its spec actually supports.
|
||||
*/
|
||||
function VisualizationSection({
|
||||
value,
|
||||
controls,
|
||||
onChange,
|
||||
panelKind,
|
||||
onChangePanelKind,
|
||||
queryType,
|
||||
signal,
|
||||
}: VisualizationSectionProps): JSX.Element {
|
||||
}: SectionEditorProps<'visualization'>): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{controls.switchPanelKind && panelKind && onChangePanelKind && (
|
||||
<PanelTypeSwitcher
|
||||
panelKind={panelKind}
|
||||
queryType={queryType}
|
||||
signal={signal}
|
||||
onChange={onChangePanelKind}
|
||||
/>
|
||||
)}
|
||||
|
||||
{controls.timePreference && (
|
||||
<div className={styles.field}>
|
||||
<Typography.Text>Panel time preference</Typography.Text>
|
||||
@@ -55,7 +32,7 @@ function VisualizationSection({
|
||||
onChange={(next): void =>
|
||||
onChange({
|
||||
...value,
|
||||
timePreference: next,
|
||||
timePreference: next as DashboardtypesTimePreferenceDTO,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -4,15 +4,6 @@ import { DashboardtypesTimePreferenceDTO } from 'api/generated/services/sigNoz.s
|
||||
|
||||
import VisualizationSection from '../VisualizationSection';
|
||||
|
||||
// The type switcher resolves each kind's supported signals + query types; stub it so
|
||||
// the test doesn't pull the whole panel registry (renderers, chart libs).
|
||||
jest.mock('pages/DashboardPageV2/DashboardContainer/Panels/registry', () => ({
|
||||
getPanelDefinition: jest.fn(() => ({
|
||||
supportedSignals: ['metrics', 'logs', 'traces'],
|
||||
supportedQueryTypes: ['builder', 'clickhouse_sql', 'promql'],
|
||||
})),
|
||||
}));
|
||||
|
||||
// Open the antd Select by clicking its selector, then pick the option by label.
|
||||
async function pickOption(triggerTestId: string, label: string): Promise<void> {
|
||||
const user = userEvent.setup();
|
||||
@@ -26,12 +17,7 @@ describe('VisualizationSection', () => {
|
||||
render(
|
||||
<VisualizationSection
|
||||
value={undefined}
|
||||
controls={{
|
||||
switchPanelKind: true,
|
||||
timePreference: true,
|
||||
stacking: true,
|
||||
fillSpans: true,
|
||||
}}
|
||||
controls={{ timePreference: true, stacking: true, fillSpans: true }}
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
@@ -49,10 +35,7 @@ describe('VisualizationSection', () => {
|
||||
render(
|
||||
<VisualizationSection
|
||||
value={undefined}
|
||||
controls={{
|
||||
switchPanelKind: true,
|
||||
timePreference: true,
|
||||
}}
|
||||
controls={{ timePreference: true }}
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
@@ -73,10 +56,7 @@ describe('VisualizationSection', () => {
|
||||
render(
|
||||
<VisualizationSection
|
||||
value={undefined}
|
||||
controls={{
|
||||
switchPanelKind: true,
|
||||
timePreference: true,
|
||||
}}
|
||||
controls={{ timePreference: true }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
@@ -94,10 +74,7 @@ describe('VisualizationSection', () => {
|
||||
timePreference: DashboardtypesTimePreferenceDTO.global_time,
|
||||
stackedBarChart: false,
|
||||
}}
|
||||
controls={{
|
||||
switchPanelKind: true,
|
||||
stacking: true,
|
||||
}}
|
||||
controls={{ stacking: true }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
@@ -115,10 +92,7 @@ describe('VisualizationSection', () => {
|
||||
render(
|
||||
<VisualizationSection
|
||||
value={{ fillSpans: false }}
|
||||
controls={{
|
||||
switchPanelKind: true,
|
||||
fillSpans: true,
|
||||
}}
|
||||
controls={{ fillSpans: true }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
@@ -127,43 +101,4 @@ describe('VisualizationSection', () => {
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ fillSpans: true });
|
||||
});
|
||||
|
||||
it('renders the type switcher and switches kind when switchPanelKind is set', async () => {
|
||||
const onChangePanelKind = jest.fn();
|
||||
render(
|
||||
<VisualizationSection
|
||||
value={undefined}
|
||||
controls={{ switchPanelKind: true }}
|
||||
onChange={jest.fn()}
|
||||
panelKind="signoz/TimeSeriesPanel"
|
||||
onChangePanelKind={onChangePanelKind}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByTestId('panel-editor-v2-type-switcher'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await pickOption('panel-editor-v2-type-switcher', 'Table');
|
||||
expect(onChangePanelKind).toHaveBeenCalledWith('signoz/TablePanel');
|
||||
});
|
||||
|
||||
it('hides the type switcher when switchPanelKind is not set', () => {
|
||||
render(
|
||||
<VisualizationSection
|
||||
value={undefined}
|
||||
controls={{
|
||||
switchPanelKind: false,
|
||||
timePreference: true,
|
||||
}}
|
||||
onChange={jest.fn()}
|
||||
panelKind="signoz/TimeSeriesPanel"
|
||||
onChangePanelKind={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('panel-editor-v2-type-switcher'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,19 +4,15 @@ import type { ConfigSelectItem } from '../../controls/ConfigSelect/ConfigSelect'
|
||||
|
||||
// Per-panel time scope. "Global Time" follows the dashboard's time picker; the rest pin
|
||||
// the panel to a fixed relative window regardless of the dashboard range (V1 parity).
|
||||
export const TIME_PREFERENCE_OPTIONS: ConfigSelectItem<DashboardtypesTimePreferenceDTO>[] =
|
||||
[
|
||||
{ value: DashboardtypesTimePreferenceDTO.global_time, label: 'Global Time' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_5_min, label: 'Last 5 min' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_15_min, label: 'Last 15 min' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_30_min, label: 'Last 30 min' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_1_hr, label: 'Last 1 hr' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_6_hr, label: 'Last 6 hr' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_1_day, label: 'Last 1 day' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_3_days, label: 'Last 3 days' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_1_week, label: 'Last 1 week' },
|
||||
{
|
||||
value: DashboardtypesTimePreferenceDTO.last_1_month,
|
||||
label: 'Last 1 month',
|
||||
},
|
||||
];
|
||||
export const TIME_PREFERENCE_OPTIONS: ConfigSelectItem[] = [
|
||||
{ value: DashboardtypesTimePreferenceDTO.global_time, label: 'Global Time' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_5_min, label: 'Last 5 min' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_15_min, label: 'Last 15 min' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_30_min, label: 'Last 30 min' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_1_hr, label: 'Last 1 hr' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_6_hr, label: 'Last 6 hr' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_1_day, label: 'Last 1 day' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_3_days, label: 'Last 3 days' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_1_week, label: 'Last 1 week' },
|
||||
{ value: DashboardtypesTimePreferenceDTO.last_1_month, label: 'Last 1 month' },
|
||||
];
|
||||
|
||||
@@ -8,35 +8,23 @@ import { Color } from '@signozhq/design-tokens';
|
||||
import { Atom, Terminal } from '@signozhq/icons';
|
||||
import { Tabs } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ClickHouseQueryContainer from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse';
|
||||
import PromQLQueryContainer from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL';
|
||||
import { PANEL_TYPE_TO_QUERY_TYPES } from 'container/NewWidget/utils';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import {
|
||||
getHiddenQueryBuilderFields,
|
||||
getSupportedQueryTypes,
|
||||
} from '../../Panels/capabilities';
|
||||
import {
|
||||
PANEL_KIND_TO_PANEL_TYPE,
|
||||
type PanelKind,
|
||||
} from '../../Panels/types/panelKind';
|
||||
|
||||
import styles from './PanelEditorQueryBuilder.module.scss';
|
||||
|
||||
interface PanelEditorQueryBuilderProps {
|
||||
/** The edited panel's visualization kind — drives supported query types + field visibility via the capabilities guard. */
|
||||
panelKind: PanelKind;
|
||||
/** The panel's current signal; selects per-signal query-builder field rules. */
|
||||
signal: TelemetrytypesSignalDTO;
|
||||
panelType: PANEL_TYPES;
|
||||
/** Preview fetch in flight — drives the Stage & Run button's loading/cancel state. */
|
||||
isLoadingQueries: boolean;
|
||||
/** Run the current query (Stage & Run button / ⌘↵). Always re-runs. */
|
||||
@@ -53,15 +41,12 @@ interface PanelEditorQueryBuilderProps {
|
||||
* `QueryBuilderProvider`. `usePanelEditorQuerySync` owns the panel↔provider sync.
|
||||
*/
|
||||
function PanelEditorQueryBuilder({
|
||||
panelKind,
|
||||
signal,
|
||||
panelType,
|
||||
isLoadingQueries,
|
||||
onStageRunQuery,
|
||||
onCancelQuery,
|
||||
footer,
|
||||
}: PanelEditorQueryBuilderProps): JSX.Element {
|
||||
// The shared QueryBuilderV2 / list-view checks still speak the legacy PANEL_TYPES.
|
||||
const panelType = PANEL_KIND_TO_PANEL_TYPE[panelKind];
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@@ -89,15 +74,13 @@ function PanelEditorQueryBuilder({
|
||||
[onStageRunQuery],
|
||||
);
|
||||
|
||||
// Per-kind query-builder field rules from the guard (e.g. List hides step interval
|
||||
// and having), passed to QueryBuilderV2 as its `filterConfigs`.
|
||||
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(
|
||||
() => getHiddenQueryBuilderFields(panelKind, signal),
|
||||
[panelKind, signal],
|
||||
() => ({ stepInterval: { isHidden: false, isDisabled: false } }),
|
||||
[],
|
||||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const supportedQueryTypes = getSupportedQueryTypes(panelKind);
|
||||
const supportedQueryTypes = PANEL_TYPE_TO_QUERY_TYPES[panelType] || [];
|
||||
|
||||
const queryTypeComponents = {
|
||||
[EQueryType.QUERY_BUILDER]: {
|
||||
@@ -144,7 +127,7 @@ function PanelEditorQueryBuilder({
|
||||
),
|
||||
children: queryTypeComponents[queryType].component,
|
||||
}));
|
||||
}, [panelKind, panelType, filterConfigs, isDarkMode]);
|
||||
}, [panelType, filterConfigs, isDarkMode]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import PanelEditorQueryBuilder from '../PanelEditorQueryBuilder';
|
||||
|
||||
// Capture the props the (real-guard-fed) QueryBuilderV2 receives without rendering it.
|
||||
const mockQueryBuilderV2 = jest.fn();
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
}));
|
||||
jest.mock('hooks/useDarkMode', () => ({ useIsDarkMode: (): boolean => false }));
|
||||
jest.mock('components/QueryBuilderV2/QueryBuilderV2', () => ({
|
||||
QueryBuilderV2: (props: unknown): null => {
|
||||
mockQueryBuilderV2(props);
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
jest.mock(
|
||||
'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse',
|
||||
() => ({ __esModule: true, default: (): null => null }),
|
||||
);
|
||||
jest.mock(
|
||||
'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL',
|
||||
() => ({ __esModule: true, default: (): null => null }),
|
||||
);
|
||||
jest.mock('container/QueryBuilder/components/RunQueryBtn/RunQueryBtn', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
jest.mock('components/TextToolTip', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
jest.mock('assets/Dashboard/PromQl', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
const mockUseQueryBuilder = useQueryBuilder as unknown as jest.Mock;
|
||||
|
||||
function renderBuilder(
|
||||
panelKind: string,
|
||||
signal: TelemetrytypesSignalDTO = TelemetrytypesSignalDTO.logs,
|
||||
): void {
|
||||
render(
|
||||
<PanelEditorQueryBuilder
|
||||
panelKind={panelKind as never}
|
||||
signal={signal}
|
||||
isLoadingQueries={false}
|
||||
onStageRunQuery={jest.fn()}
|
||||
onCancelQuery={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
function lastQueryBuilderProps(): {
|
||||
panelType: string;
|
||||
isListViewPanel: boolean;
|
||||
filterConfigs: unknown;
|
||||
} {
|
||||
const calls = mockQueryBuilderV2.mock.calls;
|
||||
return calls[calls.length - 1][0];
|
||||
}
|
||||
|
||||
describe('PanelEditorQueryBuilder query-type tabs (driven by the capabilities guard)', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseQueryBuilder.mockReturnValue({
|
||||
currentQuery: { queryType: EQueryType.QUERY_BUILDER },
|
||||
redirectWithQueryBuilderData: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it('shows only the Query Builder tab for the List kind', () => {
|
||||
renderBuilder('signoz/ListPanel', TelemetrytypesSignalDTO.logs);
|
||||
|
||||
expect(screen.getByText('Query Builder')).toBeInTheDocument();
|
||||
expect(screen.queryByText('ClickHouse Query')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('PromQL')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Query Builder + ClickHouse but not PromQL for the Table kind', () => {
|
||||
renderBuilder('signoz/TablePanel');
|
||||
|
||||
expect(screen.getByText('Query Builder')).toBeInTheDocument();
|
||||
expect(screen.getByText('ClickHouse Query')).toBeInTheDocument();
|
||||
expect(screen.queryByText('PromQL')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows all three tabs for the Time Series kind', () => {
|
||||
renderBuilder('signoz/TimeSeriesPanel');
|
||||
|
||||
expect(screen.getByText('Query Builder')).toBeInTheDocument();
|
||||
expect(screen.getByText('ClickHouse Query')).toBeInTheDocument();
|
||||
expect(screen.getByText('PromQL')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PanelEditorQueryBuilder field visibility (driven by the capabilities guard)', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseQueryBuilder.mockReturnValue({
|
||||
currentQuery: { queryType: EQueryType.QUERY_BUILDER },
|
||||
redirectWithQueryBuilderData: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it('passes empty field config + non-list flag for a non-list kind', () => {
|
||||
renderBuilder('signoz/TimeSeriesPanel', TelemetrytypesSignalDTO.metrics);
|
||||
|
||||
const props = lastQueryBuilderProps();
|
||||
expect(props.panelType).toBe('graph');
|
||||
expect(props.isListViewPanel).toBe(false);
|
||||
expect(props.filterConfigs).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('hides step interval / having and sets body-contains for List + logs', () => {
|
||||
renderBuilder('signoz/ListPanel', TelemetrytypesSignalDTO.logs);
|
||||
|
||||
const props = lastQueryBuilderProps();
|
||||
expect(props.panelType).toBe('list');
|
||||
expect(props.isListViewPanel).toBe(true);
|
||||
expect(props.filterConfigs).toStrictEqual({
|
||||
stepInterval: { isHidden: true, isDisabled: true },
|
||||
having: { isHidden: true, isDisabled: true },
|
||||
filters: { customKey: 'body', customOp: OPERATORS.CONTAINS },
|
||||
});
|
||||
});
|
||||
|
||||
it('additionally hides limit for List + traces', () => {
|
||||
renderBuilder('signoz/ListPanel', TelemetrytypesSignalDTO.traces);
|
||||
|
||||
const props = lastQueryBuilderProps();
|
||||
expect(props.filterConfigs).toStrictEqual({
|
||||
stepInterval: { isHidden: true, isDisabled: true },
|
||||
having: { isHidden: true, isDisabled: true },
|
||||
limit: { isHidden: true, isDisabled: true },
|
||||
filters: { customKey: 'body', customOp: OPERATORS.CONTAINS },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,237 +0,0 @@
|
||||
import {
|
||||
DashboardtypesComparisonOperatorDTO,
|
||||
type DashboardtypesPanelSpecDTO,
|
||||
DashboardtypesThresholdFormatDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
|
||||
|
||||
import { defaultColumnsForSignal } from '../ListColumnsEditor/selectFields';
|
||||
import { getSwitchedPluginSpec } from '../getSwitchedPluginSpec';
|
||||
|
||||
jest.mock('pages/DashboardPageV2/DashboardContainer/Panels/registry', () => ({
|
||||
getPanelDefinition: jest.fn(),
|
||||
}));
|
||||
jest.mock('../ListColumnsEditor/selectFields', () => ({
|
||||
defaultColumnsForSignal: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockGetPanelDefinition = getPanelDefinition as unknown as jest.Mock;
|
||||
const mockDefaultColumnsForSignal =
|
||||
defaultColumnsForSignal as unknown as jest.Mock;
|
||||
|
||||
function specWith(pluginSpec: unknown): DashboardtypesPanelSpecDTO {
|
||||
return {
|
||||
display: { name: 'Panel' },
|
||||
plugin: { kind: 'signoz/TablePanel', spec: pluginSpec },
|
||||
queries: [],
|
||||
} as unknown as DashboardtypesPanelSpecDTO;
|
||||
}
|
||||
|
||||
describe('getSwitchedPluginSpec', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockDefaultColumnsForSignal.mockReturnValue([]);
|
||||
});
|
||||
|
||||
it('carries only unit + decimalPrecision when the new kind has a formatting section', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'formatting', controls: { unit: true, decimals: true } }],
|
||||
});
|
||||
const old = specWith({
|
||||
formatting: { unit: 'ms', decimalPrecision: 2, columnUnits: { A: 'bytes' } },
|
||||
axes: { logScale: true },
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/TimeSeriesPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.formatting).toStrictEqual({ unit: 'ms', decimalPrecision: 2 });
|
||||
// Type-specific config from the old kind is dropped.
|
||||
expect((result as { axes?: unknown }).axes).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not carry formatting when the new kind has no formatting section', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({ sections: [{ kind: 'columns' }] });
|
||||
const old = specWith({ formatting: { unit: 'ms' } });
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/ListPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.formatting).toBeUndefined();
|
||||
});
|
||||
|
||||
it('seeds List columns from the signal when switching into a List', () => {
|
||||
const columns = [{ name: 'body' }];
|
||||
mockDefaultColumnsForSignal.mockReturnValue(columns);
|
||||
mockGetPanelDefinition.mockReturnValue({ sections: [{ kind: 'columns' }] });
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
specWith({}),
|
||||
'signoz/ListPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(mockDefaultColumnsForSignal).toHaveBeenCalledWith(
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
expect(result.selectFields).toBe(columns);
|
||||
});
|
||||
|
||||
it('includes the kind section defaults (e.g. legend position)', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'legend', controls: { position: true } }],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
specWith({}),
|
||||
'signoz/PieChartPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.legend?.position).toBe('bottom');
|
||||
});
|
||||
|
||||
describe('thresholds', () => {
|
||||
it('does not carry thresholds when the new kind has no thresholds section', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({ sections: [{ kind: 'columns' }] });
|
||||
const old = specWith({
|
||||
thresholds: [{ value: 80, color: '#F1575F', label: 'warn' }],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/ListPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toBeUndefined();
|
||||
});
|
||||
|
||||
it('carries thresholds verbatim within the label variant (color/value/unit/label)', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: { variant: 'label' } }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [{ value: 80, color: '#F1575F', unit: 'ms', label: 'warn' }],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/BarChartPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toStrictEqual([
|
||||
{ value: 80, color: '#F1575F', unit: 'ms', label: 'warn' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('remaps label thresholds into the comparison variant, defaulting operator + format', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: { variant: 'comparison' } }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [{ value: 80, color: '#F1575F', label: 'warn' }],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/NumberPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
// The label is dropped; operator/format are seeded so the threshold can match.
|
||||
expect(result.thresholds).toStrictEqual([
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.above,
|
||||
format: DashboardtypesThresholdFormatDTO.text,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('remaps comparison thresholds into the table variant, keeping operator/format and seeding a column', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: { variant: 'table' } }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.below,
|
||||
format: DashboardtypesThresholdFormatDTO.text,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/TablePanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toStrictEqual([
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.below,
|
||||
format: DashboardtypesThresholdFormatDTO.text,
|
||||
columnName: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('drops the table-only columnName when remapping into the label variant', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: { variant: 'label' } }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.above,
|
||||
format: DashboardtypesThresholdFormatDTO.background,
|
||||
columnName: 'p99',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/TimeSeriesPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toStrictEqual([{ value: 80, color: '#F1575F' }]);
|
||||
});
|
||||
|
||||
it('defaults the variant to label when the thresholds section omits controls', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: {} }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [{ value: 80, color: '#F1575F', label: 'warn' }],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/TimeSeriesPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toStrictEqual([
|
||||
{ value: 80, color: '#F1575F', label: 'warn' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,149 +0,0 @@
|
||||
import {
|
||||
DashboardtypesComparisonOperatorDTO,
|
||||
type DashboardtypesPanelSpecDTO,
|
||||
DashboardtypesThresholdFormatDTO,
|
||||
type TelemetrytypesSignalDTO,
|
||||
type TelemetrytypesTelemetryFieldKeyDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
|
||||
import type { PanelKind } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/panelKind';
|
||||
import {
|
||||
type AnyThreshold,
|
||||
type PanelFormattingSlice,
|
||||
type SectionConfig,
|
||||
SectionKind,
|
||||
type ThresholdVariant,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
import {
|
||||
buildDefaultPluginSpec,
|
||||
type DefaultPluginSpec,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/buildDefaultPluginSpec';
|
||||
|
||||
import { defaultColumnsForSignal } from './ListColumnsEditor/selectFields';
|
||||
|
||||
/**
|
||||
* Plugin spec produced on a first-time switch to a new kind. A partial cross-section
|
||||
* of the per-kind spec union; the caller assigns it to `plugin.spec` (typed `unknown`)
|
||||
* at the boundary.
|
||||
*/
|
||||
export interface SwitchedPluginSpec extends DefaultPluginSpec {
|
||||
formatting?: Pick<PanelFormattingSlice, 'unit' | 'decimalPrecision'>;
|
||||
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
thresholds?: AnyThreshold[];
|
||||
}
|
||||
|
||||
/** Every field any threshold variant can hold; switching reads across shapes to remap. */
|
||||
interface AnyThresholdFields {
|
||||
color: string;
|
||||
value: number;
|
||||
unit?: string;
|
||||
operator?: DashboardtypesComparisonOperatorDTO;
|
||||
format?: DashboardtypesThresholdFormatDTO;
|
||||
columnName?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/** The threshold variant a kind edits, or `undefined` when it has no Thresholds section. */
|
||||
function getThresholdVariant(
|
||||
sections: SectionConfig[],
|
||||
): ThresholdVariant | undefined {
|
||||
const section = sections.find(
|
||||
(s): s is Extract<SectionConfig, { kind: SectionKind.Thresholds }> =>
|
||||
s.kind === SectionKind.Thresholds,
|
||||
);
|
||||
return section ? (section.controls.variant ?? 'label') : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remaps a threshold to the target kind's variant: keeps the shared core (color, value,
|
||||
* unit) plus any cross-variant fields, and seeds the rest with the variant's defaults so
|
||||
* the carried threshold stays functional (a comparison/table threshold needs an operator
|
||||
* to match, a table threshold a column).
|
||||
*/
|
||||
function toThresholdVariant(
|
||||
source: AnyThresholdFields,
|
||||
variant: ThresholdVariant,
|
||||
): AnyThreshold {
|
||||
const core = {
|
||||
color: source.color,
|
||||
value: source.value,
|
||||
...(source.unit !== undefined && { unit: source.unit }),
|
||||
};
|
||||
if (variant === 'comparison') {
|
||||
return {
|
||||
...core,
|
||||
operator: source.operator ?? DashboardtypesComparisonOperatorDTO.above,
|
||||
format: source.format ?? DashboardtypesThresholdFormatDTO.text,
|
||||
};
|
||||
}
|
||||
if (variant === 'table') {
|
||||
return {
|
||||
...core,
|
||||
operator: source.operator ?? DashboardtypesComparisonOperatorDTO.above,
|
||||
format: source.format ?? DashboardtypesThresholdFormatDTO.background,
|
||||
columnName: source.columnName ?? '',
|
||||
};
|
||||
}
|
||||
return {
|
||||
...core,
|
||||
...(source.label !== undefined && { label: source.label }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the plugin spec for a first-visit switch to `newKind`: the kind's declared
|
||||
* section defaults (so the config pane opens populated, matching new-panel seeding) plus
|
||||
* the cross-kind config worth keeping — unit + decimal precision, and thresholds when the
|
||||
* new kind supports them (remapped to its variant). Switching into a List seeds the
|
||||
* current signal's default columns so the columns control isn't empty.
|
||||
*
|
||||
* Revisiting a kind restores its stashed spec instead, so this runs only on first visit.
|
||||
*/
|
||||
export function getSwitchedPluginSpec(
|
||||
oldSpec: DashboardtypesPanelSpecDTO,
|
||||
newKind: PanelKind,
|
||||
signal: TelemetrytypesSignalDTO,
|
||||
): SwitchedPluginSpec {
|
||||
const sections = getPanelDefinition(newKind).sections;
|
||||
const result: SwitchedPluginSpec = buildDefaultPluginSpec(sections);
|
||||
|
||||
if (sections.some((section) => section.kind === SectionKind.Formatting)) {
|
||||
const oldFormatting = (
|
||||
oldSpec.plugin.spec as {
|
||||
formatting?: PanelFormattingSlice;
|
||||
}
|
||||
).formatting;
|
||||
const carried: Pick<PanelFormattingSlice, 'unit' | 'decimalPrecision'> = {
|
||||
...(oldFormatting?.unit !== undefined && { unit: oldFormatting.unit }),
|
||||
...(oldFormatting?.decimalPrecision !== undefined && {
|
||||
decimalPrecision: oldFormatting.decimalPrecision,
|
||||
}),
|
||||
};
|
||||
if (Object.keys(carried).length > 0) {
|
||||
result.formatting = carried;
|
||||
}
|
||||
}
|
||||
|
||||
if (sections.some((section) => section.kind === SectionKind.Columns)) {
|
||||
const columns = defaultColumnsForSignal(signal);
|
||||
if (columns.length > 0) {
|
||||
result.selectFields = columns;
|
||||
}
|
||||
}
|
||||
|
||||
const thresholdVariant = getThresholdVariant(sections);
|
||||
if (thresholdVariant) {
|
||||
const oldThresholds = (
|
||||
oldSpec.plugin.spec as {
|
||||
thresholds?: AnyThreshold[] | null;
|
||||
}
|
||||
).thresholds;
|
||||
if (oldThresholds && oldThresholds.length > 0) {
|
||||
result.thresholds = oldThresholds.map((threshold) =>
|
||||
toThresholdVariant(threshold as AnyThresholdFields, thresholdVariant),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
|
||||
|
||||
import { useMetricYAxisUnit } from '../useMetricYAxisUnit';
|
||||
|
||||
jest.mock('hooks/useGetYAxisUnit', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseGetYAxisUnit = useGetYAxisUnit as unknown as jest.Mock;
|
||||
|
||||
function mockMetricUnit(
|
||||
yAxisUnit: string | undefined,
|
||||
isLoading = false,
|
||||
): void {
|
||||
mockUseGetYAxisUnit.mockReturnValue({ yAxisUnit, isLoading, isError: false });
|
||||
}
|
||||
|
||||
describe('useMetricYAxisUnit', () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('seeds the unit from the metric on a new panel', () => {
|
||||
mockMetricUnit('bytes');
|
||||
const onSelectUnit = jest.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useMetricYAxisUnit({ isNewPanel: true, unit: undefined, onSelectUnit }),
|
||||
);
|
||||
|
||||
expect(onSelectUnit).toHaveBeenCalledWith('bytes');
|
||||
});
|
||||
|
||||
it('does not seed when not a new panel', () => {
|
||||
mockMetricUnit('bytes');
|
||||
const onSelectUnit = jest.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useMetricYAxisUnit({ isNewPanel: false, unit: undefined, onSelectUnit }),
|
||||
);
|
||||
|
||||
expect(onSelectUnit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not seed when the metric has no unit', () => {
|
||||
mockMetricUnit(undefined);
|
||||
const onSelectUnit = jest.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useMetricYAxisUnit({ isNewPanel: true, unit: undefined, onSelectUnit }),
|
||||
);
|
||||
|
||||
expect(onSelectUnit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not seed when the unit already matches the metric', () => {
|
||||
mockMetricUnit('bytes');
|
||||
const onSelectUnit = jest.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useMetricYAxisUnit({ isNewPanel: true, unit: 'bytes', onSelectUnit }),
|
||||
);
|
||||
|
||||
expect(onSelectUnit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('re-seeds when the resolved metric unit changes', () => {
|
||||
mockMetricUnit('bytes');
|
||||
const onSelectUnit = jest.fn();
|
||||
|
||||
const { rerender } = renderHook(
|
||||
(props: { unit: string | undefined }) =>
|
||||
useMetricYAxisUnit({
|
||||
isNewPanel: true,
|
||||
unit: props.unit,
|
||||
onSelectUnit,
|
||||
}),
|
||||
{ initialProps: { unit: undefined as string | undefined } },
|
||||
);
|
||||
expect(onSelectUnit).toHaveBeenLastCalledWith('bytes');
|
||||
|
||||
// The metric changes; the panel now holds the previously-seeded unit.
|
||||
mockMetricUnit('ms');
|
||||
rerender({ unit: 'bytes' });
|
||||
|
||||
expect(onSelectUnit).toHaveBeenLastCalledWith('ms');
|
||||
});
|
||||
|
||||
it('returns the resolved metric unit and loading state', () => {
|
||||
mockMetricUnit('bytes', true);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useMetricYAxisUnit({
|
||||
isNewPanel: false,
|
||||
unit: undefined,
|
||||
onSelectUnit: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.metricUnit).toBe('bytes');
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,194 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { handleQueryChange } from 'container/NewWidget/utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { resolveQueryType } from '../../../Panels/capabilities';
|
||||
import { getBuilderQueries } from '../../../Panels/utils/getBuilderQueries';
|
||||
import { toPerses } from '../../../queryV5/persesQueryAdapters';
|
||||
import { getSwitchedPluginSpec } from '../../getSwitchedPluginSpec';
|
||||
import { usePanelTypeSwitch } from '../usePanelTypeSwitch';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
}));
|
||||
jest.mock('container/NewWidget/utils', () => ({
|
||||
handleQueryChange: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../Panels/capabilities', () => ({
|
||||
resolveQueryType: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../queryV5/persesQueryAdapters', () => ({
|
||||
toPerses: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../getSwitchedPluginSpec', () => ({
|
||||
getSwitchedPluginSpec: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../Panels/utils/getBuilderQueries', () => ({
|
||||
getBuilderQueries: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseQueryBuilder = useQueryBuilder as unknown as jest.Mock;
|
||||
const mockHandleQueryChange = handleQueryChange as unknown as jest.Mock;
|
||||
const mockResolveQueryType = resolveQueryType as unknown as jest.Mock;
|
||||
const mockToPerses = toPerses as unknown as jest.Mock;
|
||||
const mockGetSwitchedPluginSpec = getSwitchedPluginSpec as unknown as jest.Mock;
|
||||
const mockGetBuilderQueries = getBuilderQueries as unknown as jest.Mock;
|
||||
|
||||
// Opaque sentinels — the leaf utilities are mocked, so only identity matters.
|
||||
const TABLE_PLUGIN_SPEC = { table: true } as unknown;
|
||||
const TABLE_QUERIES = [{ id: 'table-q' }] as unknown as NonNullable<
|
||||
DashboardtypesPanelSpecDTO['queries']
|
||||
>;
|
||||
const LIST_PLUGIN_SPEC = { list: true } as unknown;
|
||||
const LIST_QUERIES = [{ id: 'list-q' }] as unknown as NonNullable<
|
||||
DashboardtypesPanelSpecDTO['queries']
|
||||
>;
|
||||
const TRANSFORMED = {
|
||||
id: 'transformed',
|
||||
queryType: 'builder',
|
||||
} as unknown as Query;
|
||||
const CONVERTED = [{ id: 'converted' }] as unknown as NonNullable<
|
||||
DashboardtypesPanelSpecDTO['queries']
|
||||
>;
|
||||
const SWITCHED_SPEC = { switched: true } as unknown;
|
||||
|
||||
function makeSpec(
|
||||
kind: string,
|
||||
pluginSpec: unknown,
|
||||
queries: NonNullable<DashboardtypesPanelSpecDTO['queries']>,
|
||||
): DashboardtypesPanelSpecDTO {
|
||||
return {
|
||||
display: { name: 'Panel' },
|
||||
plugin: { kind, spec: pluginSpec },
|
||||
queries,
|
||||
} as unknown as DashboardtypesPanelSpecDTO;
|
||||
}
|
||||
|
||||
const tableSpec = makeSpec(
|
||||
'signoz/TablePanel',
|
||||
TABLE_PLUGIN_SPEC,
|
||||
TABLE_QUERIES,
|
||||
);
|
||||
const listSpec = makeSpec('signoz/ListPanel', LIST_PLUGIN_SPEC, LIST_QUERIES);
|
||||
|
||||
function builderState(currentQuery: Query): {
|
||||
currentQuery: Query;
|
||||
redirectWithQueryBuilderData: jest.Mock;
|
||||
} {
|
||||
return { currentQuery, redirectWithQueryBuilderData: jest.fn() };
|
||||
}
|
||||
|
||||
describe('usePanelTypeSwitch', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockHandleQueryChange.mockReturnValue(TRANSFORMED);
|
||||
mockToPerses.mockReturnValue(CONVERTED);
|
||||
mockGetSwitchedPluginSpec.mockReturnValue(SWITCHED_SPEC);
|
||||
mockGetBuilderQueries.mockReturnValue([{ signal: 'logs' }]);
|
||||
// The guard owns coercion (tested in capabilities.test.ts); here it always
|
||||
// resolves to Query Builder so the coerced type flows into handleQueryChange.
|
||||
mockResolveQueryType.mockReturnValue('builder');
|
||||
});
|
||||
|
||||
it('does nothing when switching to the current kind', () => {
|
||||
const setSpec = jest.fn();
|
||||
const state = builderState({ id: 'q', queryType: 'builder' } as Query);
|
||||
mockUseQueryBuilder.mockReturnValue(state);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelTypeSwitch({
|
||||
spec: tableSpec,
|
||||
panelType: PANEL_TYPES.TABLE,
|
||||
setSpec,
|
||||
}),
|
||||
);
|
||||
act(() => result.current.onChangePanelKind('signoz/TablePanel'));
|
||||
|
||||
expect(setSpec).not.toHaveBeenCalled();
|
||||
expect(state.redirectWithQueryBuilderData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('on first visit: transforms the query and resets the spec to the new kind', () => {
|
||||
const setSpec = jest.fn();
|
||||
const tableQuery = { id: 'table-current', queryType: 'builder' } as Query;
|
||||
const state = builderState(tableQuery);
|
||||
mockUseQueryBuilder.mockReturnValue(state);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelTypeSwitch({
|
||||
spec: tableSpec,
|
||||
panelType: PANEL_TYPES.TABLE,
|
||||
setSpec,
|
||||
}),
|
||||
);
|
||||
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
|
||||
|
||||
expect(setSpec).toHaveBeenCalledTimes(1);
|
||||
const next = setSpec.mock.calls[0][0] as DashboardtypesPanelSpecDTO;
|
||||
expect(next.plugin.kind).toBe('signoz/ListPanel');
|
||||
expect(next.plugin.spec).toBe(SWITCHED_SPEC);
|
||||
expect(next.queries).toBe(CONVERTED);
|
||||
expect(state.redirectWithQueryBuilderData).toHaveBeenCalledWith(TRANSFORMED);
|
||||
});
|
||||
|
||||
it('coerces the query type when the new kind disallows it (promql → List)', () => {
|
||||
const setSpec = jest.fn();
|
||||
const promQuery = { id: 'prom', queryType: 'promql' } as Query;
|
||||
mockUseQueryBuilder.mockReturnValue(builderState(promQuery));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelTypeSwitch({
|
||||
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
setSpec,
|
||||
}),
|
||||
);
|
||||
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
|
||||
|
||||
// The hook asks the guard to resolve the active query type against the new kind…
|
||||
expect(mockResolveQueryType).toHaveBeenCalledWith(
|
||||
'signoz/ListPanel',
|
||||
'promql',
|
||||
);
|
||||
// …and the resolved type ('builder') flows into the query rebuild.
|
||||
const [, queryArg] = mockHandleQueryChange.mock.calls[0];
|
||||
expect((queryArg as Query).queryType).toBe('builder');
|
||||
});
|
||||
|
||||
it('restores the original kind verbatim on switch-back (reversibility)', () => {
|
||||
const setSpec = jest.fn();
|
||||
const tableQuery = { id: 'table-current', queryType: 'builder' } as Query;
|
||||
const listQuery = { id: 'list-current', queryType: 'builder' } as Query;
|
||||
let state = builderState(tableQuery);
|
||||
mockUseQueryBuilder.mockImplementation(() => state);
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
(props: { spec: DashboardtypesPanelSpecDTO; panelType: PANEL_TYPES }) =>
|
||||
usePanelTypeSwitch({ ...props, setSpec }),
|
||||
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
|
||||
);
|
||||
|
||||
// Leave Table for List (stashes Table in its pristine state).
|
||||
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
|
||||
|
||||
// Parent re-renders as a List panel; the builder now holds the List query.
|
||||
state = builderState(listQuery);
|
||||
rerender({ spec: listSpec, panelType: PANEL_TYPES.LIST });
|
||||
|
||||
// Switch back to Table → restored from the stash, not re-transformed.
|
||||
act(() => result.current.onChangePanelKind('signoz/TablePanel'));
|
||||
|
||||
const restored = setSpec.mock.calls[
|
||||
setSpec.mock.calls.length - 1
|
||||
][0] as DashboardtypesPanelSpecDTO;
|
||||
expect(restored.plugin.kind).toBe('signoz/TablePanel');
|
||||
expect(restored.plugin.spec).toBe(TABLE_PLUGIN_SPEC);
|
||||
expect(restored.queries).toBe(TABLE_QUERIES);
|
||||
expect(state.redirectWithQueryBuilderData).toHaveBeenCalledWith(tableQuery);
|
||||
// The restore path must not run the query transform again.
|
||||
expect(mockHandleQueryChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -40,7 +40,7 @@ export function useLegendSeries(
|
||||
getTimeSeriesResults(data?.response),
|
||||
data.legendMap,
|
||||
);
|
||||
const builderQueries = getBuilderQueries(panel.spec.queries);
|
||||
const builderQueries = getBuilderQueries(panel?.spec?.queries || []);
|
||||
|
||||
const byLabel = new Map<string, string>();
|
||||
series.forEach((s) => {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
|
||||
|
||||
interface UseMetricYAxisUnitArgs {
|
||||
/** Only a new panel auto-seeds; editing never overwrites the saved unit. */
|
||||
isNewPanel: boolean;
|
||||
unit: string | undefined;
|
||||
onSelectUnit: (unit: string) => void;
|
||||
}
|
||||
|
||||
interface UseMetricYAxisUnitResult {
|
||||
metricUnit: string | undefined;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the selected metric's unit and, on a new panel only, seeds the formatting unit
|
||||
* from it (V1 parity); returns the unit for the selector's mismatch warning.
|
||||
*/
|
||||
export function useMetricYAxisUnit({
|
||||
isNewPanel,
|
||||
unit,
|
||||
onSelectUnit,
|
||||
}: UseMetricYAxisUnitArgs): UseMetricYAxisUnitResult {
|
||||
const { yAxisUnit: metricUnit, isLoading } = useGetYAxisUnit();
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewPanel && metricUnit && metricUnit !== unit) {
|
||||
onSelectUnit(metricUnit);
|
||||
}
|
||||
// Re-seed only when the resolved metric unit changes, not on every unit edit.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isNewPanel, metricUnit]);
|
||||
|
||||
return { metricUnit, isLoading };
|
||||
}
|
||||
@@ -57,8 +57,8 @@ export function usePanelEditorQuerySync({
|
||||
const { currentQuery, stagedQuery, handleRunQuery } = useQueryBuilder();
|
||||
|
||||
// Saved queries, captured once: seed the builder and serve as the restore target.
|
||||
const savedQueries = draft.spec.queries;
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only snapshot
|
||||
const savedQueries = useMemo(() => draft.spec?.queries ?? [], []);
|
||||
// A new panel has no saved query: seed from the kind's first supported signal
|
||||
// instead of letting `fromPerses` fall back to the metrics default (which List
|
||||
// doesn't support).
|
||||
@@ -93,7 +93,7 @@ export function usePanelEditorQuerySync({
|
||||
// No-op guard at the V5 envelope level: equivalent wrappers (bare
|
||||
// `signoz/BuilderQuery` vs `signoz/CompositeQuery`) unwrap to the same
|
||||
// envelopes, so a structural compare would falsely dirty the draft.
|
||||
const current = draft.spec.queries;
|
||||
const current = draft.spec?.queries ?? [];
|
||||
if (isEqual(toQueryEnvelopes(next), toQueryEnvelopes(current))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import type {
|
||||
DashboardtypesPanelPluginDTO,
|
||||
DashboardtypesPanelSpecDTO,
|
||||
DashboardtypesQueryDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
handleQueryChange,
|
||||
type PartialPanelTypes,
|
||||
} from 'container/NewWidget/utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { resolveQueryType } from '../../Panels/capabilities';
|
||||
import {
|
||||
PANEL_KIND_TO_PANEL_TYPE,
|
||||
type PanelKind,
|
||||
} from '../../Panels/types/panelKind';
|
||||
import { getBuilderQueries } from '../../Panels/utils/getBuilderQueries';
|
||||
import { toPerses } from '../../queryV5/persesQueryAdapters';
|
||||
import {
|
||||
getSwitchedPluginSpec,
|
||||
type SwitchedPluginSpec,
|
||||
} from '../getSwitchedPluginSpec';
|
||||
|
||||
/** What a kind looks like when you leave it; restored verbatim if you return. */
|
||||
interface KindState {
|
||||
pluginSpec: DashboardtypesPanelPluginDTO['spec'];
|
||||
queries: DashboardtypesQueryDTO[];
|
||||
builderQuery: Query;
|
||||
}
|
||||
|
||||
interface UsePanelTypeSwitchArgs {
|
||||
spec: DashboardtypesPanelSpecDTO;
|
||||
panelType: PANEL_TYPES;
|
||||
setSpec: (next: DashboardtypesPanelSpecDTO) => void;
|
||||
}
|
||||
|
||||
interface UsePanelTypeSwitchApi {
|
||||
/** Switch the panel to `newKind`, transforming/restoring its query + spec. */
|
||||
onChangePanelKind: (newKind: PanelKind) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches the edited panel's visualization kind. Mutating `plugin.kind` re-derives the
|
||||
* renderer, config sections, query-builder tabs and request type for free; this hook adds
|
||||
* the two things that don't: a per-kind session cache that makes switching reversible
|
||||
* (`Table → List → Table` restores the original query + spec), and, on first visit to a
|
||||
* kind, a query rebuild (`handleQueryChange`) + spec reset (`getSwitchedPluginSpec`).
|
||||
*/
|
||||
export function usePanelTypeSwitch({
|
||||
spec,
|
||||
panelType,
|
||||
setSpec,
|
||||
}: UsePanelTypeSwitchArgs): UsePanelTypeSwitchApi {
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
|
||||
const cacheRef = useRef<Map<PanelKind, KindState>>(new Map());
|
||||
|
||||
// Latest spec/query/type, read inside the stable callback without re-subscribing.
|
||||
const specRef = useRef(spec);
|
||||
specRef.current = spec;
|
||||
const queryRef = useRef(currentQuery);
|
||||
queryRef.current = currentQuery;
|
||||
const panelTypeRef = useRef(panelType);
|
||||
panelTypeRef.current = panelType;
|
||||
|
||||
const onChangePanelKind = useCallback(
|
||||
(newKind: PanelKind): void => {
|
||||
const currentSpec = specRef.current;
|
||||
const oldKind = currentSpec.plugin.kind as PanelKind;
|
||||
if (newKind === oldKind) {
|
||||
return;
|
||||
}
|
||||
const query = queryRef.current;
|
||||
|
||||
cacheRef.current.set(oldKind, {
|
||||
pluginSpec: currentSpec.plugin.spec,
|
||||
queries: currentSpec.queries,
|
||||
builderQuery: query,
|
||||
});
|
||||
|
||||
const newPanelType = PANEL_KIND_TO_PANEL_TYPE[newKind];
|
||||
|
||||
// Only `plugin` needs a cast: it's a discriminated union over `kind`, and a
|
||||
// dynamically-chosen kind can't be correlated with its spec statically (as in
|
||||
// `createDefaultPanel`). The surrounding spec stays fully typed.
|
||||
const buildSpec = (
|
||||
pluginSpec: DashboardtypesPanelPluginDTO['spec'] | SwitchedPluginSpec,
|
||||
queries: DashboardtypesQueryDTO[],
|
||||
): DashboardtypesPanelSpecDTO => ({
|
||||
...currentSpec,
|
||||
plugin: {
|
||||
...currentSpec.plugin,
|
||||
kind: newKind,
|
||||
spec: pluginSpec,
|
||||
} as DashboardtypesPanelPluginDTO,
|
||||
queries,
|
||||
});
|
||||
|
||||
// Revisit → restore the stash verbatim (the reversibility path).
|
||||
const cached = cacheRef.current.get(newKind);
|
||||
if (cached) {
|
||||
setSpec(buildSpec(cached.pluginSpec, cached.queries));
|
||||
redirectWithQueryBuilderData(cached.builderQuery);
|
||||
return;
|
||||
}
|
||||
|
||||
// First visit → coerce the query type if the new kind disallows it, then
|
||||
// rebuild the builder query for the new type.
|
||||
const queryType = resolveQueryType(newKind, query.queryType);
|
||||
const transformed = handleQueryChange(
|
||||
newPanelType as keyof PartialPanelTypes,
|
||||
{ ...query, queryType },
|
||||
panelTypeRef.current,
|
||||
);
|
||||
const signal = getBuilderQueries(currentSpec.queries)[0]
|
||||
?.signal as TelemetrytypesSignalDTO;
|
||||
|
||||
setSpec(
|
||||
buildSpec(
|
||||
getSwitchedPluginSpec(currentSpec, newKind, signal),
|
||||
toPerses(transformed, newPanelType),
|
||||
),
|
||||
);
|
||||
redirectWithQueryBuilderData(transformed);
|
||||
},
|
||||
[setSpec, redirectWithQueryBuilderData],
|
||||
);
|
||||
|
||||
return { onChangePanelKind };
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
@@ -8,12 +8,9 @@ import {
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import {
|
||||
type DashboardtypesPanelDTO,
|
||||
type DashboardtypesPanelFormattingDTO,
|
||||
type DashboardtypesPanelSpecDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
|
||||
import {
|
||||
PANEL_KIND_TO_PANEL_TYPE,
|
||||
@@ -21,7 +18,6 @@ import {
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/panelKind';
|
||||
import { getBuilderQueries } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/getBuilderQueries';
|
||||
|
||||
import { getExecStats } from '../queryV5/v5ResponseData';
|
||||
import { usePanelInteractions } from '../PanelsAndSectionsLayout/Panel/hooks/usePanelInteractions';
|
||||
import ConfigPane from './ConfigPane/ConfigPane';
|
||||
import Header from './Header/Header';
|
||||
@@ -32,9 +28,7 @@ import { useLegendSeries } from './hooks/useLegendSeries';
|
||||
import { usePanelQuery } from '../hooks/usePanelQuery';
|
||||
import { usePanelEditorDraft } from './hooks/usePanelEditorDraft';
|
||||
import { usePanelEditorQuerySync } from './hooks/usePanelEditorQuerySync';
|
||||
import { useMetricYAxisUnit } from './hooks/useMetricYAxisUnit';
|
||||
import { usePanelEditorSave } from './hooks/usePanelEditorSave';
|
||||
import { usePanelTypeSwitch } from './hooks/usePanelTypeSwitch';
|
||||
import { useSeedNewListColumns } from './hooks/useSeedNewListColumns';
|
||||
import { useSwitchColumnsOnSignalChange } from './hooks/useSwitchColumnsOnSignalChange';
|
||||
import { useTableColumns } from './hooks/useTableColumns';
|
||||
@@ -71,10 +65,6 @@ function PanelEditorContainer({
|
||||
onSaved,
|
||||
}: PanelEditorContainerProps): JSX.Element {
|
||||
const { draft, spec, setSpec, isSpecDirty } = usePanelEditorDraft(panel);
|
||||
// Live query type (the selected tab) — the type switcher disables kinds that can't be
|
||||
// authored in it. Read from the provider, not the spec: a new panel's spec carries no
|
||||
// query until staged, so the spec would lag the tab.
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { save, isSaving } = usePanelEditorSave({
|
||||
dashboardId,
|
||||
panelId,
|
||||
@@ -123,36 +113,6 @@ function PanelEditorContainer({
|
||||
signal: defaultSignal,
|
||||
});
|
||||
|
||||
// Switch the panel's visualization kind in place (reversible per session).
|
||||
const { onChangePanelKind } = usePanelTypeSwitch({ spec, panelType, setSpec });
|
||||
|
||||
// At editor level, not the collapsible FormattingSection, so seeding runs while closed.
|
||||
const formattingUnit = (
|
||||
spec.plugin.spec as {
|
||||
formatting?: DashboardtypesPanelFormattingDTO;
|
||||
}
|
||||
).formatting?.unit;
|
||||
const seedFormattingUnit = useCallback(
|
||||
(unit: string): void => {
|
||||
const pluginSpec = spec.plugin.spec as {
|
||||
formatting?: DashboardtypesPanelFormattingDTO;
|
||||
};
|
||||
setSpec({
|
||||
...spec,
|
||||
plugin: {
|
||||
...spec.plugin,
|
||||
spec: { ...pluginSpec, formatting: { ...pluginSpec.formatting, unit } },
|
||||
},
|
||||
} as DashboardtypesPanelSpecDTO);
|
||||
},
|
||||
[spec, setSpec],
|
||||
);
|
||||
const { metricUnit } = useMetricYAxisUnit({
|
||||
isNewPanel: isNew,
|
||||
unit: formattingUnit,
|
||||
onSelectUnit: seedFormattingUnit,
|
||||
});
|
||||
|
||||
// Spec and query dirtiness are tracked independently so query re-serialization
|
||||
// never false-dirties. A new panel is always savable (you're creating it).
|
||||
const isDirty = isNew || isSpecDirty || isQueryDirty;
|
||||
@@ -161,8 +121,8 @@ function PanelEditorContainer({
|
||||
// values; cast at this boundary (as ConfigPane does) so the columns editor's
|
||||
// field-key lookup is typed.
|
||||
const listSignal =
|
||||
(getBuilderQueries(spec.queries)[0]?.signal as TelemetrytypesSignalDTO) ||
|
||||
TelemetrytypesSignalDTO.logs;
|
||||
(getBuilderQueries(spec.queries || [])[0]
|
||||
?.signal as TelemetrytypesSignalDTO) || TelemetrytypesSignalDTO.logs;
|
||||
|
||||
// Swap the List panel's columns to the new signal's defaults on signal change
|
||||
// (V1 had a per-signal field list; V2 has one `selectFields`).
|
||||
@@ -186,14 +146,6 @@ function PanelEditorContainer({
|
||||
const legendSeries = useLegendSeries(draft, data);
|
||||
const tableColumns = useTableColumns(draft, data);
|
||||
|
||||
// Smallest query step interval (seconds) — the floor for the span-gaps
|
||||
// threshold. Undefined until results carry step metadata.
|
||||
const stepInterval = useMemo((): number | undefined => {
|
||||
const intervals = getExecStats(data.response)?.stepIntervals;
|
||||
const values = intervals ? Object.values(intervals) : [];
|
||||
return values.length ? Math.min(...values) : undefined;
|
||||
}, [data.response]);
|
||||
|
||||
const onSave = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
// Bake the live query into the spec so unstaged edits are saved too.
|
||||
@@ -245,8 +197,7 @@ function PanelEditorContainer({
|
||||
<ResizableHandle withHandle className={styles.handle} />
|
||||
<ResizablePanel minSize="35%" maxSize="45%" defaultSize="40%">
|
||||
<PanelEditorQueryBuilder
|
||||
panelKind={fullKind}
|
||||
signal={listSignal}
|
||||
panelType={panelType}
|
||||
isLoadingQueries={isFetching}
|
||||
onStageRunQuery={runQuery}
|
||||
onCancelQuery={cancelQuery}
|
||||
@@ -275,12 +226,8 @@ function PanelEditorContainer({
|
||||
panelKind={draft.spec.plugin.kind}
|
||||
spec={spec}
|
||||
onChangeSpec={setSpec}
|
||||
onChangePanelKind={onChangePanelKind}
|
||||
queryType={currentQuery.queryType}
|
||||
legendSeries={legendSeries}
|
||||
tableColumns={tableColumns}
|
||||
stepInterval={stepInterval}
|
||||
metricUnit={metricUnit}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import {
|
||||
getHiddenQueryBuilderFields,
|
||||
getSupportedQueryTypes,
|
||||
getSupportedSignals,
|
||||
isPanelCombinationValid,
|
||||
isQueryTypeSupported,
|
||||
isSignalSupported,
|
||||
resolveQueryType,
|
||||
} from '../capabilities';
|
||||
import type { PanelKind } from '../types/panelKind';
|
||||
|
||||
const { QUERY_BUILDER, CLICKHOUSE, PROM } = EQueryType;
|
||||
const { logs, traces, metrics } = TelemetrytypesSignalDTO;
|
||||
|
||||
const EXPECTED_QUERY_TYPES: Record<PanelKind, EQueryType[]> = {
|
||||
'signoz/TimeSeriesPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/BarChartPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/NumberPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/HistogramPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/PieChartPanel': [QUERY_BUILDER, CLICKHOUSE],
|
||||
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE],
|
||||
'signoz/ListPanel': [QUERY_BUILDER],
|
||||
};
|
||||
|
||||
const EXPECTED_SIGNALS: Record<PanelKind, TelemetrytypesSignalDTO[]> = {
|
||||
'signoz/TimeSeriesPanel': [metrics, logs, traces],
|
||||
'signoz/BarChartPanel': [metrics, logs, traces],
|
||||
'signoz/NumberPanel': [metrics, logs, traces],
|
||||
'signoz/HistogramPanel': [metrics, logs, traces],
|
||||
'signoz/PieChartPanel': [metrics, logs, traces],
|
||||
'signoz/TablePanel': [metrics, logs, traces],
|
||||
// List renders raw rows; metrics produce no row data.
|
||||
'signoz/ListPanel': [logs, traces],
|
||||
};
|
||||
|
||||
const ALL_KINDS = Object.keys(EXPECTED_QUERY_TYPES) as PanelKind[];
|
||||
|
||||
describe('panel capabilities guard', () => {
|
||||
describe('query type support', () => {
|
||||
it.each(ALL_KINDS)('declares the expected query types for %s', (kind) => {
|
||||
expect(getSupportedQueryTypes(kind)).toStrictEqual(
|
||||
EXPECTED_QUERY_TYPES[kind],
|
||||
);
|
||||
});
|
||||
|
||||
it('Table and Pie do not support PromQL', () => {
|
||||
expect(isQueryTypeSupported('signoz/TablePanel', PROM)).toBe(false);
|
||||
expect(isQueryTypeSupported('signoz/PieChartPanel', PROM)).toBe(false);
|
||||
});
|
||||
|
||||
it('List only supports Query Builder', () => {
|
||||
expect(isQueryTypeSupported('signoz/ListPanel', QUERY_BUILDER)).toBe(true);
|
||||
expect(isQueryTypeSupported('signoz/ListPanel', CLICKHOUSE)).toBe(false);
|
||||
expect(isQueryTypeSupported('signoz/ListPanel', PROM)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('signal support', () => {
|
||||
it.each(ALL_KINDS)('declares the expected signals for %s', (kind) => {
|
||||
expect(getSupportedSignals(kind)).toStrictEqual(EXPECTED_SIGNALS[kind]);
|
||||
});
|
||||
|
||||
it('List excludes metrics', () => {
|
||||
expect(isSignalSupported('signoz/ListPanel', metrics)).toBe(false);
|
||||
expect(isSignalSupported('signoz/ListPanel', logs)).toBe(true);
|
||||
expect(isSignalSupported('signoz/ListPanel', traces)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPanelCombinationValid', () => {
|
||||
it('accepts a supported triad', () => {
|
||||
expect(
|
||||
isPanelCombinationValid({
|
||||
kind: 'signoz/TimeSeriesPanel',
|
||||
queryType: PROM,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isPanelCombinationValid({
|
||||
kind: 'signoz/ListPanel',
|
||||
queryType: QUERY_BUILDER,
|
||||
signal: logs,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects an unsupported query type', () => {
|
||||
expect(
|
||||
isPanelCombinationValid({ kind: 'signoz/ListPanel', queryType: PROM }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isPanelCombinationValid({ kind: 'signoz/TablePanel', queryType: PROM }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects an unsupported signal when one is given', () => {
|
||||
expect(
|
||||
isPanelCombinationValid({
|
||||
kind: 'signoz/ListPanel',
|
||||
queryType: QUERY_BUILDER,
|
||||
signal: metrics,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores signal when none is given (ClickHouse/PromQL have no signal)', () => {
|
||||
expect(
|
||||
isPanelCombinationValid({
|
||||
kind: 'signoz/ListPanel',
|
||||
queryType: QUERY_BUILDER,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveQueryType', () => {
|
||||
it('keeps a supported query type', () => {
|
||||
expect(resolveQueryType('signoz/TimeSeriesPanel', PROM)).toBe(PROM);
|
||||
expect(resolveQueryType('signoz/ListPanel', QUERY_BUILDER)).toBe(
|
||||
QUERY_BUILDER,
|
||||
);
|
||||
});
|
||||
|
||||
it('coerces an unsupported query type to the first supported one', () => {
|
||||
// PromQL → List has no PromQL, falls back to its first (and only) type.
|
||||
expect(resolveQueryType('signoz/ListPanel', PROM)).toBe(QUERY_BUILDER);
|
||||
expect(resolveQueryType('signoz/TablePanel', PROM)).toBe(QUERY_BUILDER);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHiddenQueryBuilderFields', () => {
|
||||
it('returns {} for kinds that declare no field rules', () => {
|
||||
expect(
|
||||
getHiddenQueryBuilderFields('signoz/TimeSeriesPanel', logs),
|
||||
).toStrictEqual({});
|
||||
expect(getHiddenQueryBuilderFields('signoz/TablePanel', logs)).toStrictEqual(
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
// Mirrors QueryBuilderV2's internal listViewLogFilterConfigs — the guard is the
|
||||
// single source of truth for these values.
|
||||
it('hides step interval / having and sets body-contains for List + logs', () => {
|
||||
expect(getHiddenQueryBuilderFields('signoz/ListPanel', logs)).toStrictEqual({
|
||||
stepInterval: { isHidden: true, isDisabled: true },
|
||||
having: { isHidden: true, isDisabled: true },
|
||||
filters: { customKey: 'body', customOp: OPERATORS.CONTAINS },
|
||||
});
|
||||
});
|
||||
|
||||
// Mirrors listViewTracesFilterConfigs — traces additionally hide `limit`.
|
||||
it('additionally hides limit for List + traces', () => {
|
||||
expect(
|
||||
getHiddenQueryBuilderFields('signoz/ListPanel', traces),
|
||||
).toStrictEqual({
|
||||
stepInterval: { isHidden: true, isDisabled: true },
|
||||
having: { isHidden: true, isDisabled: true },
|
||||
limit: { isHidden: true, isDisabled: true },
|
||||
filters: { customKey: 'body', customOp: OPERATORS.CONTAINS },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,91 +0,0 @@
|
||||
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { getPanelDefinition } from './registry';
|
||||
import type { FilterConfigsPartial } from './types/panelCapabilities';
|
||||
import type { PanelKind } from './types/panelKind';
|
||||
|
||||
/**
|
||||
* The single deterministic guard for V2 dashboards. Every "what works with what"
|
||||
* question — panel kind × query type × signal, and which query-builder fields a kind
|
||||
* hides — is answered here by reading each kind's declared capabilities from the panel
|
||||
* registry. Adding a new kind means declaring its capabilities once in its definition;
|
||||
* these functions then cover it automatically. Pure and side-effect free.
|
||||
*/
|
||||
|
||||
/** Signals a kind can visualize. */
|
||||
export function getSupportedSignals(
|
||||
kind: PanelKind,
|
||||
): TelemetrytypesSignalDTO[] {
|
||||
return getPanelDefinition(kind).supportedSignals;
|
||||
}
|
||||
|
||||
export function isSignalSupported(
|
||||
kind: PanelKind,
|
||||
signal: TelemetrytypesSignalDTO,
|
||||
): boolean {
|
||||
return getSupportedSignals(kind).includes(signal);
|
||||
}
|
||||
|
||||
/** Query languages a kind supports (Query Builder / ClickHouse / PromQL). */
|
||||
export function getSupportedQueryTypes(kind: PanelKind): EQueryType[] {
|
||||
return getPanelDefinition(kind).supportedQueryTypes;
|
||||
}
|
||||
|
||||
export function isQueryTypeSupported(
|
||||
kind: PanelKind,
|
||||
queryType: EQueryType,
|
||||
): boolean {
|
||||
return getSupportedQueryTypes(kind).includes(queryType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Master guard: is this panel kind renderable with this query type (and, in builder
|
||||
* mode, this signal)? ClickHouse/PromQL queries carry no signal, so the signal is
|
||||
* validated only when one is given.
|
||||
*/
|
||||
export function isPanelCombinationValid({
|
||||
kind,
|
||||
queryType,
|
||||
signal,
|
||||
}: {
|
||||
kind: PanelKind;
|
||||
queryType: EQueryType;
|
||||
signal?: TelemetrytypesSignalDTO;
|
||||
}): boolean {
|
||||
if (!isQueryTypeSupported(kind, queryType)) {
|
||||
return false;
|
||||
}
|
||||
if (signal !== undefined && !isSignalSupported(kind, signal)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The query type to use for a kind given a `preferred` one: keep it if the kind
|
||||
* supports it, otherwise fall back to the kind's first supported type. Used when
|
||||
* switching panel kinds to coerce an unsupported active query type (e.g. PromQL → a
|
||||
* List panel coerces to Query Builder).
|
||||
*/
|
||||
export function resolveQueryType(
|
||||
kind: PanelKind,
|
||||
preferred: EQueryType,
|
||||
): EQueryType {
|
||||
const supported = getSupportedQueryTypes(kind);
|
||||
return supported.includes(preferred) ? preferred : supported[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Query-builder field visibility for a kind + signal: the kind's `default` rule with
|
||||
* its per-signal overrides merged over it (signal wins). `{}` when the kind hides
|
||||
* nothing, i.e. the builder shows every field.
|
||||
*/
|
||||
export function getHiddenQueryBuilderFields(
|
||||
kind: PanelKind,
|
||||
signal: TelemetrytypesSignalDTO,
|
||||
): FilterConfigsPartial {
|
||||
const rule = getPanelDefinition(kind).queryBuilderFields;
|
||||
const perSignal = signal ? rule[signal] : undefined;
|
||||
return { ...rule.default, ...perSignal };
|
||||
}
|
||||
@@ -49,7 +49,7 @@ function BarPanelRenderer({
|
||||
);
|
||||
|
||||
const builderQueries = useMemo(
|
||||
() => getBuilderQueries(panel.spec.queries),
|
||||
() => getBuilderQueries(panel.spec.queries || []),
|
||||
[panel.spec.queries],
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
|
||||
kind: 'signoz/BarChartPanel',
|
||||
@@ -14,12 +13,6 @@ export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
TelemetrytypesSignalDTO.traces,
|
||||
],
|
||||
supportedQueryTypes: [
|
||||
EQueryType.QUERY_BUILDER,
|
||||
EQueryType.CLICKHOUSE,
|
||||
EQueryType.PROM,
|
||||
],
|
||||
queryBuilderFields: {},
|
||||
actions: {
|
||||
view: true,
|
||||
edit: true,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { SectionKind, type SectionConfig } from '../../types/sections';
|
||||
import type { SectionConfig } from '../../types/sections';
|
||||
|
||||
// Bar stacking lives in `visualization.stackedBarChart`, so it's a `visualization`
|
||||
// control, not `chartAppearance`. fillSpans is TimeSeries-only, so Bar omits it (V1 parity).
|
||||
export const sections: SectionConfig[] = [
|
||||
{
|
||||
kind: SectionKind.Visualization,
|
||||
controls: { switchPanelKind: true, timePreference: true, stacking: true },
|
||||
},
|
||||
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
|
||||
{ kind: SectionKind.Axes, controls: { minMax: true, logScale: true } },
|
||||
{ kind: SectionKind.Legend, controls: { position: true } },
|
||||
{ kind: SectionKind.Thresholds, controls: { variant: 'label' } },
|
||||
{ kind: SectionKind.ContextLinks },
|
||||
{ kind: 'visualization', controls: { timePreference: true, stacking: true } },
|
||||
{ kind: 'formatting', controls: { unit: true, decimals: true } },
|
||||
{ kind: 'axes', controls: { minMax: true, logScale: true } },
|
||||
{ kind: 'legend', controls: { position: true } },
|
||||
{ kind: 'thresholds', controls: { variant: 'label' } },
|
||||
{ kind: 'contextLinks' },
|
||||
];
|
||||
|
||||
@@ -41,7 +41,7 @@ function HistogramPanelRenderer({
|
||||
);
|
||||
|
||||
const builderQueries = useMemo(
|
||||
() => getBuilderQueries(panel.spec.queries),
|
||||
() => getBuilderQueries(panel.spec.queries || []),
|
||||
[panel.spec.queries],
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
|
||||
kind: 'signoz/HistogramPanel',
|
||||
@@ -14,12 +13,6 @@ export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
TelemetrytypesSignalDTO.traces,
|
||||
],
|
||||
supportedQueryTypes: [
|
||||
EQueryType.QUERY_BUILDER,
|
||||
EQueryType.CLICKHOUSE,
|
||||
EQueryType.PROM,
|
||||
],
|
||||
queryBuilderFields: {},
|
||||
actions: {
|
||||
view: true,
|
||||
edit: true,
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import type { DashboardtypesHistogramPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { SectionKind, type SectionConfig } from '../../types/sections';
|
||||
import type { SectionConfig } from '../../types/sections';
|
||||
|
||||
export const sections: SectionConfig[] = [
|
||||
{
|
||||
kind: SectionKind.Visualization,
|
||||
controls: { switchPanelKind: true },
|
||||
},
|
||||
{
|
||||
kind: SectionKind.Legend,
|
||||
kind: 'legend',
|
||||
controls: { position: true },
|
||||
// Merging all queries collapses to one distribution with no legend.
|
||||
isHidden: (spec): boolean =>
|
||||
@@ -18,8 +14,8 @@ export const sections: SectionConfig[] = [
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: SectionKind.Buckets,
|
||||
kind: 'buckets',
|
||||
controls: { count: true, width: true, mergeQueries: true },
|
||||
},
|
||||
{ kind: SectionKind.ContextLinks },
|
||||
{ kind: 'contextLinks' },
|
||||
];
|
||||
|
||||
@@ -52,7 +52,7 @@ function ListPanelRenderer({
|
||||
// and row-click behavior. Cast is safe — the query carries the same string values.
|
||||
const signal = useMemo(
|
||||
() =>
|
||||
(getBuilderQueries(panel.spec.queries)[0]
|
||||
(getBuilderQueries(panel.spec.queries || [])[0]
|
||||
?.signal as TelemetrytypesSignalDTO) || TelemetrytypesSignalDTO.logs,
|
||||
[panel.spec.queries],
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ function panelWith(
|
||||
): PanelOfKind<'signoz/ListPanel'> {
|
||||
return {
|
||||
kind: 'Panel',
|
||||
spec: { plugin: { kind: 'signoz/ListPanel', spec }, queries: [] },
|
||||
spec: { plugin: { kind: 'signoz/ListPanel', spec } },
|
||||
} as unknown as PanelOfKind<'signoz/ListPanel'>;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export const definition: PanelDefinition<'signoz/ListPanel'> = {
|
||||
kind: 'signoz/ListPanel',
|
||||
@@ -14,21 +12,6 @@ export const definition: PanelDefinition<'signoz/ListPanel'> = {
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
TelemetrytypesSignalDTO.traces,
|
||||
],
|
||||
// Raw rows have no aggregation, so step interval / having never apply, and the
|
||||
// Where clause searches the log/span body via `body CONTAINS`. Traces additionally
|
||||
// hide `limit` (the server paginates raw spans). Mirrors QueryBuilderV2's internal
|
||||
// list configs — the capabilities guard is the single source for both.
|
||||
supportedQueryTypes: [EQueryType.QUERY_BUILDER],
|
||||
queryBuilderFields: {
|
||||
default: {
|
||||
stepInterval: { isHidden: true, isDisabled: true },
|
||||
having: { isHidden: true, isDisabled: true },
|
||||
filters: { customKey: 'body', customOp: OPERATORS.CONTAINS },
|
||||
},
|
||||
[TelemetrytypesSignalDTO.traces]: {
|
||||
limit: { isHidden: true, isDisabled: true },
|
||||
},
|
||||
},
|
||||
sections,
|
||||
actions: {
|
||||
view: true,
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
import { SectionKind, type SectionConfig } from '../../types/sections';
|
||||
import type { SectionConfig } from '../../types/sections';
|
||||
|
||||
export const sections: SectionConfig[] = [
|
||||
{
|
||||
kind: SectionKind.Visualization,
|
||||
controls: { switchPanelKind: true },
|
||||
},
|
||||
];
|
||||
export const sections: SectionConfig[] = [];
|
||||
|
||||