Compare commits

..

35 Commits

Author SHA1 Message Date
Naman Verma
65835394c0 Merge branch 'main' into nv/dashboard-migration 2026-06-30 18:39:53 +05:30
Abhi kumar
0711786701 fix(dashboards-v2): panel editor fixes + span-gaps Disconnect Values control (#11864)
* fix(dashboards-v2): disable panel types unsupported by the datasource

A new panel's builder is seeded with the kind's default signal, but
`spec.queries` stays empty until the query is modified — so the type
switcher saw an undefined datasource and never disabled incompatible
types (e.g. List on a metrics panel, which then breaks rendering).
Resolve the signal with a fallback to the kind's default signal so
compatibility is enforced from the first render.

* fix(dashboards-v2): place toolbar-created panels in the root section

The top-right "New Panel" button creates a panel with no section context,
which createPanelOps resolved to the LAST section instead of the root.
Fall back to the first (root) section when no valid index is given; still
create an untitled section when the dashboard has none.

* feat(dashboards-v2): add "Move out of section" panel action

The "Move to section" submenu only listed titled sections, so a panel
in a titled section couldn't be moved back to the untitled root. Add a
direct "Move out of section" action, shown when the panel sits in a
titled section and an untitled root section exists to receive it.

* fix(dashboards-v2): allow clearing the threshold value input

The threshold "Value" field was a controlled numeric input, so an
emptied field snapped back to 0 (Number("") is 0, not NaN) and the
seeded 0 could never be removed. Hold a local string so the field can
be cleared and edited; shared by all threshold row variants.

* feat(dashboards-v2): redesign span-gaps as a "Disconnect values" control

Replace the raw seconds input with a Never/Threshold toggle plus a
duration "Threshold value" field. The threshold is stored verbatim as a
duration string ("10m", "5s") — the wire format the backend expects — and
parsed back to seconds only for rendering and validation. Threads the
query step interval through the config pane to seed/floor the threshold,
and rejects invalid or below-step-interval entries inline (V1 parity).

* refactor(dashboards-v2): rename Formatting section to "Formatting & Units"

Serialize section header test ids (lowercase, spaces → dashes) so a
multi-word title doesn't break the data-testid, and update the test.

* chore(dashboards-v2): tidy panel-editor query helpers

- useLegendSeries: drop redundant optional chaining on panel.spec.
- Remove the unused getPanelKindLabel util.

* chore: pr review changes

* chore: fmt fix

* feat(dashboards-v2): deterministic panel capability guard (type × query-type × signal) (#11865)

* feat(dashboards-v2): add a panel capability guard

Centralize "what works with what" for V2 dashboard panels into one
deterministic guard. Each panel kind declares its supported query types and
optional query-builder field rules alongside its existing supported signals; a
pure `capabilities` module reads the panel registry to answer panel x
query-type x signal validity, coerce an unsupported query type, and resolve the
query-builder fields a kind hides.

`supportedQueryTypes` is required, so the registry's mapped type forces every
present and future kind to declare it. This re-homes the panel->query-type
compatibility that V2 previously imported from V1's NewWidget/utils into V2 land.

No behavior change: no consumer is wired to the guard yet.

* feat(dashboards-v2): gate the panel editor through the capability guard

Route the panel editor's query builder and visualization type switcher through
the capability guard instead of V1's PANEL_TYPE_TO_QUERY_TYPES (now no longer
imported by any V2 file):

- PanelEditorQueryBuilder is keyed on PanelKind; its query-type tabs and
  query-builder field visibility come from the guard.
- Switching the panel kind coerces the active query type via the guard.
- The visualization type switcher disables a kind when the active query type or
  datasource is incompatible with it (e.g. List under ClickHouse/PromQL, or List
  with a metrics query). The live query type is read from the query-builder
  provider so a not-yet-staged new panel still gates correctly, and a tooltip
  explains why a type is disabled. ConfigSelect gains opt-in per-option tooltips.

* chore: pr review changes

* chore: lint fix
2026-06-30 09:40:15 +00:00
Vinicius Lourenço
aeda0a5144 refactor(authz): fixes on error feedback (#11901)
* refactor(authz): ensure all error messages matches with api

* refactor(roles): little fixes on UI
2026-06-30 09:33:56 +00:00
Yuvraj Singh Jadon
b71c49d01f docs: refresh README with current SigNoz offerings and features (#11595)
* docs: refresh README

* docs: update README Noz section

* docs: add Foundry to README self-host path

* docs: add README feature docs links

* docs: refine README links and badges

* docs: fix README LinkedIn badge link

* docs: update README instrumentation links

* docs: emphasize README contribution help

* docs: link Noz docs in README
2026-06-30 09:06:35 +00:00
Abhi kumar
570bc0aef1 feat(dashboards-v2): switch panel visualization type in the editor (#11820)
* refactor(dashboards-v2): extend ConfigSelect for the type switcher

Allow ConfigSelect items to carry an arbitrary icon node and a disabled
flag, and align the ConfigSegmented styling — the building blocks the
panel-type switcher needs.

* feat(dashboards-v2): add panel-type switch logic

Add getSwitchedPluginSpec (a reversible per-kind plugin-spec transform)
and the usePanelTypeSwitch hook that rebuilds the builder query and spec
when the panel kind changes, guarding null queries.

* feat(dashboards-v2): add the PanelTypeSwitcher control

A ConfigSelect-based control listing the panel kinds, disabling types
whose supported signals exclude the current datasource.

* refactor(dashboards-v2): component-based icons in the panel-type modal

Store icon components (not pre-rendered elements) in PANEL_TYPES so each
consumer controls sizing; rename constants.tsx to constants.ts now that
it holds no JSX.

* feat(dashboards-v2): wire the type switcher into the config pane

Render the switcher in the Visualization section, forward the panel
kind + datasource signal + switch handler through SectionSlot/registry,
and declare a Visualization section on every panel kind.

* feat(dashboards-v2): reuse findFreeSlot for panel clone placement

Clone now places the copy beside the last row when it fits, else wraps
to a fresh row at the section bottom — matching the new-panel save path
instead of always starting a new row.

* style(dashboards-v2): format getSwitchedPluginSpec test with oxfmt

Wrap the long getSwitchedPluginSpec(...) calls so the file passes
`oxfmt --check` (the fmt / js CI gate). Formatting only, no behavior change.

* chore: pr review changes

* refactor(dashboards-v2): make SectionKind a real enum

Replace the SectionKind string-literal union and the SECTION_KIND const
object with a TypeScript enum. SectionSpecMap, SectionControls,
SECTION_METADATA and the section registry are keyed by enum members
(computed keys), and every section-kind site (kind configs, section
editors, buildDefaultPluginSpec, getSwitchedPluginSpec) references
SectionKind.* instead of bare string literals.

* refactor(dashboards-v2): drop dead queries null-handling

panel.spec.queries is a required DashboardtypesQueryDTO[], so the
defensive `|| []` / `?? null` / optional-chaining and the dead
getBuilderQueries/toQueryEnvelopes guards were no-ops. Remove them across
the V2 dashboard: renderers, config pane, panel-type switch, query sync,
perses adapters, panel-query hook, and the query-range builder. Also
applies the same cleanup in usePanelEditorQuerySync and persesQueryAdapters
(missed by the first pass), and seeds the now-required `queries` field in
the ListPanel renderer test fixture.
2026-06-30 08:08:05 +00:00
Aditya Singh
a22e7b7b16 test(trace-details): add E2E coverage for Trace Details (#11846)
* feat(trace-details): move no-data component from v2 code to v3 before v2 cleanup

* feat(trace-details): move span logs out from v2 to v3 before cleanup

* feat(trace-details): move events  out from v2 to v3 before cleanup

* feat(trace-details): remove usage of getTraceV2 from V3 code

* fix(trace-details): fix serviceName path in trace funnel

* feat(trace-details): remove Trace Details V2 page and its module import

* feat(trace-details): remove unused trace details v2 code

* feat(trace-details): fix failing test

* test(trace-details): add e2e helper and large-trace fixture

* test(trace-details): add flamegraph e2e + canvas test hook

* test(trace-details): add waterfall e2e + row instrumentation

* test(trace-details): add highlight-errors filter e2e

* test(trace-details): add analytics panel e2e

* test(trace-details): add span details drawer e2e

* test(trace-details): add preview-fields hover card e2e

* test(trace-details): minor refactor

* test(trace-details): add common helpers

* test(trace-details): use playwright context instead of browser

* test(trace-details): add pinned side nav logic

* test(trace-details): remove tests which could go as unit or integration

* test(trace-details): remove tests and renumber
2026-06-30 06:46:56 +00:00
Naman Verma
f132b7e53a fix: handle list of widget IDs in panel maps 2026-06-29 23:51:55 +05:30
Naman Verma
d4ae156dc4 fix: make threshold value non-nullable 2026-06-29 23:18:17 +05:30
Naman Verma
d6bdf9c2b2 fix: catch error from decodeTelemetryFields 2026-06-29 23:02:40 +05:30
Naman Verma
7ea654f1aa test: fix unit tests 2026-06-29 22:57:40 +05:30
Naman Verma
3fd7d013a1 fix: adjust to schema changes in variables 2026-06-29 22:56:36 +05:30
Naman Verma
fb921dd381 fix: read threshold value properly during migration 2026-06-29 22:56:12 +05:30
Naman Verma
58020d9e00 fix: note error in query parsing 2026-06-29 22:53:40 +05:30
Naman Verma
7a5933e822 fix: ignore react placeholders in layout 2026-06-29 22:52:48 +05:30
Naman Verma
2533683de6 fix: allow zero value for threshold values 2026-06-29 22:51:56 +05:30
Naman Verma
2670d53170 Merge branch 'main' into nv/dashboard-migration 2026-06-29 20:28:39 +05:30
Naman Verma
8943a9454b Merge branch 'main' into nv/dashboard-migration 2026-06-26 02:15:22 +05:30
Naman Verma
9a7ed5b711 feat: note down all errors in migration 2026-06-26 02:07:06 +05:30
Naman Verma
2d75e3d32d chore: separate error type for migration 2026-06-26 01:07:59 +05:30
Naman Verma
1d6eabf927 chore: remove spec md file 2026-06-25 22:37:52 +05:30
Naman Verma
082d7b1b77 test: fix ut to check for v2 internal name 2026-06-25 22:34:39 +05:30
Naman Verma
5019dee2d7 Merge branch 'main' into nv/dashboard-migration 2026-06-25 22:30:55 +05:30
Naman Verma
216de973fb fix: remove nil nil return 2026-06-25 03:07:58 +05:30
Naman Verma
18c0eec5e2 chore: catch typecast errors 2026-06-19 15:49:01 +05:30
Naman Verma
2ccdeb3631 chore: add a catch all panic check to log migration error 2026-06-19 15:15:17 +05:30
Naman Verma
ad12e50bbc fix: extract row and widget positions to build expanded sections 2026-06-19 15:00:32 +05:30
Naman Verma
e247bf3864 fix: sanitize tags instead of throwing error 2026-06-19 14:17:36 +05:30
Naman Verma
f4651ea134 fix: match with lower case signal for variables 2026-06-19 12:17:42 +05:30
Naman Verma
d449a2dbf2 fix: generate internal name from title 2026-06-19 11:24:40 +05:30
Naman Verma
d4b9f91062 Merge branch 'main' into nv/dashboard-migration 2026-06-18 12:28:22 +05:30
Naman Verma
530710b7bc Merge branch 'nv/dashboard-migration' of https://github.com/SigNoz/signoz into nv/dashboard-migration 2026-06-17 12:42:24 +05:30
Naman Verma
4fb5eec08d fix: move WrapInV5Envelope to types package 2026-06-17 12:42:20 +05:30
Naman Verma
f889d36f0f Merge branch 'main' into nv/dashboard-migration 2026-06-17 12:32:08 +05:30
Naman Verma
db12d44523 Merge branch 'main' into nv/dashboard-migration 2026-06-17 07:30:34 +05:30
Naman Verma
86fc0e81ba chore: add migration script from current to perses dashboard 2026-06-14 22:58:08 +05:30
156 changed files with 10120 additions and 1333 deletions

View File

@@ -1,197 +1,190 @@
<p align="center">
<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>
<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>
</p>
<p align="center">
<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>
<a href="README.md">English</a> ·
<a href="README.zh-cn.md">中文</a> ·
<a href="README.pt-br.md">Português</a>
</p>
<h3 align="center">
<a href="https://signoz.io/docs"><b>Dokumentation</b></a> &bull;
<a href="https://github.com/SigNoz/signoz/blob/main/README.md"><b>Readme auf Englisch </b></a> &bull;
<a href="https://github.com/SigNoz/signoz/blob/main/README.zh-cn.md"><b>ReadMe auf Chinesisch</b></a> &bull;
<a href="https://github.com/SigNoz/signoz/blob/main/README.pt-br.md"><b>ReadMe auf Portugiesisch</b></a> &bull;
<a href="https://signoz.io/slack"><b>Slack Community</b></a> &bull;
<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 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.
SigNoz hilft Entwicklern, Anwendungen zu überwachen und Probleme in ihren bereitgestellten Anwendungen zu beheben. Mit SigNoz können Sie Folgendes tun:
### Wähle, wie du SigNoz betreibst
👉 Visualisieren Sie Metriken, Traces und Logs in einer einzigen Oberfläche.
#### SigNoz Cloud (empfohlen)
👉 Sie können Metriken wie die p99-Latenz, Fehlerquoten für Ihre Dienste, externe API-Aufrufe und individuelle Endpunkte anzeigen.
Vollständig verwaltetes SigNoz mit 30 Tagen kostenloser Testphase, ohne Kreditkarte, nutzungsbasierter Preisgestaltung ab 49 USD und regionalem Datenhosting.
👉 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.
[**Kostenlos starten →**](https://signoz.io/teams/)
👉 Führen Sie Aggregationen auf Trace-Daten durch, um geschäftsrelevante Metriken zu erhalten.
#### Enterprise
👉 Filtern und Abfragen von Logs, Erstellen von Dashboards und Benachrichtigungen basierend auf Attributen in den Logs.
Enterprise Cloud, BYOC oder Enterprise Self-Hosted mit Compliance, Support, benutzerdefinierter Aufbewahrung, RBAC, Ingestion Controls, Datenresidenz und Regionsauswahl.
👉 Automatische Aufzeichnung von Ausnahmen in Python, Java, Ruby und Javascript.
[**Enterprise entdecken →**](https://signoz.io/enterprise/)
👉 Einfache Einrichtung von Benachrichtigungen mit dem selbst erstellbaren Abfrage-Builder.
#### Community
##
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.
### Anwendung Metriken
[**SigNoz installieren →**](https://signoz.io/docs/install/self-host/)
![application_metrics](https://user-images.githubusercontent.com/83692067/226637410-900dbc5e-6705-4b11-a10c-bd0faeb2a92f.png)
### Was kannst du überwachen?
### Verteiltes Tracing
SigNoz hilft Teams, Produktionsprobleme schneller zu debuggen, indem Logs, Metriken, Traces, Alerts, Dashboards, Exceptions und agent-native Workflows an einem Ort verbunden werden.
<img width="2068" alt="distributed_tracing_2 2" src="https://user-images.githubusercontent.com/83692067/226536447-bae58321-6a22-4ed3-af80-e3e964cb3489.png">
#### APM-Überblick
<img width="2068" alt="distributed_tracing_1" src="https://user-images.githubusercontent.com/83692067/226536462-939745b6-4f9d-45a6-8016-814837e7f7b4.png">
Überwache Service-Latenz, Fehlerrate, Durchsatz, Apdex, wichtige Endpunkte, Datenbankaufrufe und externe Aufrufe.
### Log Verwaltung
<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>
<img width="2068" alt="logs_management" src="https://user-images.githubusercontent.com/83692067/226536482-b8a5c4af-b69c-43d5-969c-338bd5eaf1a5.png">
Mehr erfahren: [APM-Dokumentation](https://signoz.io/docs/instrumentation/overview/)
### Infrastruktur Überwachung
#### Log-Management
<img width="2068" alt="infrastructure_monitoring" src="https://user-images.githubusercontent.com/83692067/226536496-f38c4dbf-e03c-4158-8be0-32d4a61158c7.png">
Erfasse, suche, aggregiere und korreliere Logs mit Traces und Metriken über einen visuellen Query Builder.
### Exceptions Monitoring
<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_light](https://user-images.githubusercontent.com/83692067/226637967-4188d024-3ac9-4799-be95-f5ea9c45436f.png)
Mehr erfahren: [Log-Management-Dokumentation](https://signoz.io/docs/logs-management/overview/)
### Alarme
#### Metriken und Dashboards
<img width="2068" alt="alerts_management" src="https://user-images.githubusercontent.com/83692067/226536548-2c81e2e8-c12d-47e8-bad7-c6be79055def.png">
Erstelle Dashboards für Anwendungs-, Infrastruktur- und benutzerdefinierte Metriken mit Query Builder, PromQL oder ClickHouse SQL.
<br /><br />
<p align="center">
<img alt="SigNoz Host-Metrics-Dashboard mit Systemlast- und Netzwerkdiagrammen" src="docs/readme-assets/monitor/metrics.png" width="900">
</p>
## Werde Teil unserer Slack Community
Mehr erfahren: [Metriken-Dokumentation](https://signoz.io/docs/metrics-management/overview/)
Sag Hi zu uns auf [Slack](https://signoz.io/slack) 👋
#### Infrastruktur-Monitoring
<br /><br />
Überwache Kubernetes-Cluster, Pods, Nodes, Workloads sowie Host-CPU, Arbeitsspeicher, Festplatten, Netzwerk, Logs und Traces.
## Funktionen:
<p align="center">
<img alt="SigNoz Kubernetes-Infrastruktur-Dashboard mit Pod- und Node-Metriken" src="docs/readme-assets/monitor/infrastructure.png" width="900">
</p>
- 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.
Mehr erfahren: [Infrastruktur-Monitoring-Dokumentation](https://signoz.io/docs/infrastructure-monitoring/overview/)
<br /><br />
#### LLM- und AI-Observability
## Wieso SigNoz?
Verfolge LLM-Apps, RAG-Pipelines, Prompts, Tool Calls, Tokens, Latenz und Kosten zusammen mit Anwendungs- und Infrastruktur-Telemetrie.
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.
<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>
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.
Mehr erfahren: [LLM-Observability-Dokumentation](https://signoz.io/docs/llm-observability/)
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.
#### Agent-Native Observability und MCP
### Languages supported:
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.
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:
<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>
- Java
- Python
- NodeJS
- Go
- PHP
- .NET
- Ruby
- Elixir
- Rust
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)
Hier findest du die vollständige Liste von unterstützten Programmiersprachen - https://opentelemetry.io/docs/
#### Distributed Tracing
<br /><br />
Verfolge Requests über Services hinweg mit Flamegraphs, Waterfalls, Span Events, Filtern und Trace Analytics.
## Erste Schritte mit SigNoz
<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>
### Bereitstellung mit Docker
Mehr erfahren: [Distributed-Tracing-Dokumentation](https://signoz.io/docs/instrumentation/)
Bitte folge den [hier](https://signoz.io/docs/install/docker/) aufgelisteten Schritten um deine Anwendung mit Docker bereitzustellen.
#### Trace Funnels
Die [Anleitungen zur Fehlerbehebung](https://signoz.io/docs/install/troubleshooting/) könnten hilfreich sein, falls du auf irgendwelche Schwierigkeiten stößt.
Erstelle Funnels aus Traces, um Drop-offs im Request-Flow, fehlgeschlagene Übergänge und systemische Workflow-Probleme zu verstehen.
<p>&nbsp </p>
<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>
### Deploy in Kubernetes using Helm
Mehr erfahren: [Trace-Funnels-Dokumentation](https://signoz.io/docs/trace-funnels/overview/)
Bitte folge den [hier](https://signoz.io/docs/deployment/helm_chart) aufgelisteten Schritten, um deine Anwendung mit Helm Charts bereitzustellen.
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.
<br /><br />
### Warum Teams SigNoz nutzen
## Vergleiche mit bekannten Tools
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.
### SigNoz vs Prometheus
### Erste Schritte
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.
#### Mit Cloud starten
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.
Erstelle einen verwalteten SigNoz-Workspace und erhalte dein erstes Dashboard, ohne Observability-Infrastruktur betreiben zu müssen.
<p>&nbsp </p>
[**Kostenlos mit SigNoz Cloud starten**](https://signoz.io/teams/)
### SigNoz vs Jaeger
#### SigNoz selbst hosten
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.
Betreibe SigNoz in deiner eigenen Infrastruktur mit Foundry, Docker, Kubernetes oder Linux.
Außerdem hat SigNoz noch mehr spezielle Funktionen im Vergleich zu Jaeger:
[**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/)
- 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.
#### Daten senden
<p>&nbsp </p>
Instrumentiere Anwendungen und Infrastruktur mit OpenTelemetry, Prometheus, Sprach-SDKs und Integrationen.
### SigNoz vs Elastic
[**Instrumentation**](https://signoz.io/docs/instrumentation/) · [**Integrationen**](https://signoz.io/docs/integrations/integrations-list/)
- 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.
### Vergleich mit bekannten Tools
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)
SigNoz wird häufig von Teams eingeführt, die von einzelnen Spezialtools oder kommerziellen Plattformen mit unvorhersehbarer Preisgestaltung wechseln.
<p>&nbsp </p>
**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.
### SigNoz vs Loki
**Jaeger**<br>
Jaeger macht ausschließlich Distributed Tracing. SigNoz ergänzt Metriken, Logs, Trace Analytics, Dashboards, Alerts, Exceptions und Trace-to-Log-Workflows.
- 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.
**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).
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)
**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).
<br /><br />
## Mitwirken
## Zum Projekt beitragen
Wir freuen uns über große und kleine Beiträge. Lies bitte [CONTRIBUTING.md](CONTRIBUTING.md), um mit Beiträgen zu SigNoz loszulegen.
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)
Nicht sicher, wie du anfangen sollst? **Schreib uns einfach im Channel `#contributing` in unserer [Slack Community](https://signoz.io/slack).**
<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!
Wie immer: Danke an unsere großartigen Contributors!
<a href="https://github.com/signoz/signoz/graphs/contributors">
<img src="https://contrib.rocks/image?repo=signoz/signoz" />
<img alt="SigNoz Contributors" src="https://contrib.rocks/image?repo=signoz/signoz" />
</a>

278
README.md
View File

@@ -1,244 +1,190 @@
<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">
<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>
<p align="center">
<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>
<a href="README.zh-cn.md">中文</a> ·
<a href="README.de-de.md">Deutsch</a> ·
<a href="README.pt-br.md">Português</a>
</p>
<h3 align="center">
<a href="https://signoz.io/docs"><b>Documentation</b></a> &bull;
<a href="https://github.com/SigNoz/signoz/blob/main/README.zh-cn.md"><b>ReadMe in Chinese</b></a> &bull;
<a href="https://github.com/SigNoz/signoz/blob/main/README.de-de.md"><b>ReadMe in German</b></a> &bull;
<a href="https://github.com/SigNoz/signoz/blob/main/README.pt-br.md"><b>ReadMe in Portuguese</b></a> &bull;
<a href="https://signoz.io/slack"><b>Slack Community</b></a> &bull;
<a href="https://twitter.com/SigNozHq"><b>Twitter</b></a>
</h3>
## Features
<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 is an open-source observability platform built on OpenTelemetry. Were building an enterprise-grade alternative to fragmented monitoring stacks, with logs, metrics, traces, alerts, and dashboards in one place.
### Application Performance Monitoring
### Choose how to run SigNoz
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/).
#### SigNoz Cloud (Recommended)
You can [instrument](https://signoz.io/docs/instrumentation/) your application with OpenTelemetry to get started.
Fully managed SigNoz with a 30-day free trial, no credit card required, usage-based pricing that starts at $49, and regional data hosting.
![apm-cover](https://github.com/user-attachments/assets/fa5c0396-0854-4c8b-b972-9b62fd2a70d2)
[**Start free →**](https://signoz.io/teams/)
#### Enterprise
### Logs Management
Enterprise Cloud, BYOC, or Enterprise Self-Hosted with compliance, support, custom retention, RBAC, ingestion controls, data residency, and region selection.
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.
[**Explore Enterprise →**](https://signoz.io/enterprise/)
You can also create charts on your logs and monitor them with customized dashboards. Read [more](https://signoz.io/log-management/).
#### Community
![logs-management-cover](https://github.com/user-attachments/assets/343588ee-98fb-4310-b3d2-c5bacf9c7384)
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/)
### Distributed Tracing
### What can you monitor?
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.
SigNoz helps teams debug production issues faster by connecting logs, metrics, traces, alerts, dashboards, exceptions, and agent-native workflows in one place.
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.
#### APM Overview
Read [more](https://signoz.io/distributed-tracing/).
Monitor service latency, error rate, throughput, Apdex, top endpoints, database calls, and external calls.
![distributed-tracing-cover](https://github.com/user-attachments/assets/9bfe060a-0c40-4922-9b55-8a97e1a4076c)
<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
### Metrics and Dashboards
Ingest, search, aggregate, and correlate logs with traces and metrics using a visual query builder.
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.
<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>
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.
Learn more: [Log management documentation](https://signoz.io/docs/logs-management/overview/)
Read [more](https://signoz.io/metrics-and-dashboards/).
#### Metrics and Dashboards
![metrics-n-dashboards-cover](https://github.com/user-attachments/assets/a536fd71-1d2c-4681-aa7e-516d754c47a5)
Build dashboards for application, infrastructure, and custom metrics using Query Builder, PromQL, or ClickHouse SQL.
### LLM Observability
<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>
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.
Learn more: [Metrics documentation](https://signoz.io/docs/metrics-management/overview/)
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.
#### Infrastructure Monitoring
[Get started with LLM Observability →](https://signoz.io/docs/llm-observability/)
Monitor Kubernetes clusters, pods, nodes, workloads, and host-level CPU, memory, disk, network, logs, and traces.
![llm-observability-cover](https://github.com/user-attachments/assets/a6cc0ca3-59df-48f9-9c16-7c843fccff96)
<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/)
### Alerts
#### LLM and AI Observability
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.
Trace LLM apps, RAG pipelines, prompts, tool calls, tokens, latency, and costs alongside application and infrastructure telemetry.
Alerts in SigNoz help you identify issues proactively so that you can address them before they reach your customers.
<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>
Read [more](https://signoz.io/alerts-management/).
Learn more: [LLM observability documentation](https://signoz.io/docs/llm-observability/)
![alerts-cover](https://github.com/user-attachments/assets/03873bb8-1b62-4adf-8f56-28bb7b1750ea)
#### Agent-Native Observability and MCP
### Exceptions Monitoring
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.
Monitor exceptions automatically in Python, Java, Ruby, and Javascript. For other languages, just drop in a few lines of code and start monitoring exceptions.
<p align="center">
<img alt="SigNoz Noz interface alongside MCP-powered agent workflow" src="docs/readme-assets/monitor/agent-native.png" width="900">
</p>
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.
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)
Read [more](https://signoz.io/exceptions-monitoring/).
#### Distributed Tracing
Follow requests across services with flamegraphs, waterfalls, span events, filters, and trace analytics.
![exceptions-cover](https://github.com/user-attachments/assets/4be37864-59f2-4e8a-8d6e-e29ad04298c5)
<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/)
<br /><br />
#### Trace Funnels
## Why SigNoz?
Create funnels from traces to understand request-flow drop-offs, failed transitions, and systemic workflow issues.
SigNoz is a single tool for all your monitoring and observability needs. Here are a few reasons why you should choose 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>
- Single tool for observability(logs, metrics, and traces)
Learn more: [Trace funnels documentation](https://signoz.io/docs/trace-funnels/overview/)
- Built on top of [OpenTelemetry](https://opentelemetry.io/), the open-source standard which frees you from any type of vendor lock-in
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.
- Correlated logs, metrics and traces for much richer context while debugging
### Why teams use SigNoz
- Uses ClickHouse (used by likes of Uber & Cloudflare) as datastore - an extremely fast and highly optimized storage for observability data
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.
- DIY Query builder, PromQL, and ClickHouse queries to fulfill all your use-cases around querying observability data
### Getting started
- Open-Source - you can use open-source, our [cloud service](https://signoz.io/teams/) or a mix of both based on your use case
#### Start on Cloud
Create a managed SigNoz workspace and get your first dashboard without running observability infrastructure.
## Getting Started
[**Start free on SigNoz Cloud**](https://signoz.io/teams/)
### Create a SigNoz Cloud Account
#### Self-host SigNoz
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.
Run SigNoz in your own infrastructure with Foundry, Docker, Kubernetes, or Linux.
[Get started for free](https://signoz.io/teams/)
[**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/)
### Deploy using Docker(self-hosted)
#### Send data
Please follow the steps listed [here](https://signoz.io/docs/install/docker/) to install using docker
Instrument applications and infrastructure with OpenTelemetry, Prometheus, language SDKs, and integrations.
The [troubleshooting instructions](https://signoz.io/docs/install/troubleshooting/) may be helpful if you face any issues.
[**Instrumentation**](https://signoz.io/docs/instrumentation/) · [**Integrations**](https://signoz.io/docs/integrations/integrations-list/)
<p>&nbsp </p>
### Deploy in Kubernetes using Helm(self-hosted)
### Comparisons to familiar tools
Please follow the steps listed [here](https://signoz.io/docs/deployment/helm_chart) to install using helm charts
SigNoz is often adopted by teams moving from a stack of single-purpose tools or commercial platforms with unpredictable pricing.
<br /><br />
**Prometheus**<br>
Good if you just need metrics. SigNoz keeps metrics, logs, traces, dashboards, and alerts together so teams can debug with correlated context.
We also offer managed services in your infra. Check our [pricing plans](https://signoz.io/pricing/) for all details.
**Jaeger**<br>
Jaeger only does distributed tracing. SigNoz adds metrics, logs, trace analytics, dashboards, alerts, exceptions, and trace-to-log workflows.
**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).
## 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>&nbsp </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 doesnt show any metrics on traces or on filtered traces
- Jaeger cant 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>&nbsp </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>&nbsp </p>
### SigNoz vs Loki
- SigNoz supports aggregations on high-cardinality data over a huge volume while loki doesnt.
- 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 />
**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).
## 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)
<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)
Not sure how to get started? **Just ping us on `#contributing` in our [slack community](https://signoz.io/slack).**
As always, thanks to our amazing contributors!
<a href="https://github.com/signoz/signoz/graphs/contributors">
<img src="https://contrib.rocks/image?repo=signoz/signoz" />
<img alt="SigNoz contributors" src="https://contrib.rocks/image?repo=signoz/signoz" />
</a>

View File

@@ -1,158 +1,190 @@
<p align="center">
<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>
<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>
</p>
<p align="center">
<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>
<a href="README.md">English</a> ·
<a href="README.zh-cn.md">中文</a> ·
<a href="README.de-de.md">Deutsch</a>
</p>
<h3 align="center">
<a href="https://signoz.io/docs"><b>Documentação</b></a> &bull;
<a href="https://signoz.io/slack"><b>Comunidade no Slack</b></a> &bull;
<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 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.
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.
👉 Você pode verificar métricas como latência p99, taxas de erro em seus serviços, requisições às APIs externas e endpoints individuais.
### Escolha como executar o SigNoz
👉 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 Cloud (recomendado)
👉 Execute agregações em dados de rastreamento para obter métricas de negócios relevantes.
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.
[**Comece gratuitamente →**](https://signoz.io/teams/)
![SigNoz Feature](https://signoz-public.s3.us-east-2.amazonaws.com/signoz_hero_github.png)
#### Enterprise
<br /><br />
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.
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributing.svg" width="50px" />
[**Conheça o Enterprise →**](https://signoz.io/enterprise/)
## Junte-se à nossa comunidade no Slack
#### Community
Venha dizer oi para nós no [Slack](https://signoz.io/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.
<br /><br />
[**Instale o SigNoz →**](https://signoz.io/docs/install/self-host/)
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Features.svg" width="50px" />
### O que você pode monitorar?
## Funções:
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.
- 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.
#### Visão geral de APM
<br /><br />
Monitore latência de serviço, taxa de erro, throughput, Apdex, principais endpoints, chamadas ao banco de dados e chamadas externas.
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/WhatsCool.svg" width="50px" />
<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>
## Por que escolher SigNoz?
Saiba mais: [documentação de APM](https://signoz.io/docs/instrumentation/overview/)
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 .
#### Gerenciamento de logs
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.
Ingira, pesquise, agregue e correlacione logs com traces e métricas usando um construtor visual de consultas.
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.
<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>
### Linguagens Suportadas:
Saiba mais: [documentação de gerenciamento de logs](https://signoz.io/docs/logs-management/overview/)
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:
#### Métricas e dashboards
- Java
- Python
- NodeJS
- Go
Crie dashboards para métricas de aplicação, infraestrutura e métricas personalizadas usando Query Builder, PromQL ou ClickHouse SQL.
Você pode encontrar a lista completa de linguagens aqui - https://opentelemetry.io/docs/
<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>
<br /><br />
Saiba mais: [documentação de métricas](https://signoz.io/docs/metrics-management/overview/)
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Philosophy.svg" width="50px" />
#### Monitoramento de infraestrutura
## Iniciando
### Implantar usando Docker
Monitore clusters Kubernetes, pods, nodes, workloads e CPU, memória, disco, rede, logs e traces em nível de host.
Siga as etapas listadas [aqui](https://signoz.io/docs/install/docker/) para instalar usando o 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>
Esse [guia para solução de problemas](https://signoz.io/docs/install/troubleshooting/) pode ser útil se você enfrentar quaisquer problemas.
Saiba mais: [documentação de monitoramento de infraestrutura](https://signoz.io/docs/infrastructure-monitoring/overview/)
<p>&nbsp </p>
### Implentar no Kubernetes usando Helm
#### Observabilidade de LLM e AI
Siga as etapas listadas [aqui](https://signoz.io/docs/deployment/helm_chart) para instalar usando helm charts.
Rastreie apps LLM, pipelines RAG, prompts, chamadas de ferramentas, tokens, latência e custos junto com telemetria de aplicação e infraestrutura.
<br /><br />
<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>
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/UseSigNoz.svg" width="50px" />
Saiba mais: [documentação de observabilidade de LLM](https://signoz.io/docs/llm-observability/)
## Comparações com ferramentas similares
#### Observabilidade agent-native e MCP
### SigNoz ou Prometheus
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.
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.
<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>
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.
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)
<p>&nbsp </p>
#### Tracing distribuído
### SigNoz ou Jaeger
Acompanhe requisições entre serviços com flamegraphs, waterfalls, eventos de span, filtros e análise de traces.
Jaeger só faz rastreamento distribuído. SigNoz faz métricas e rastreia, e também temos gerenciamento de log em nossos planos.
<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>
Além disso, SigNoz tem alguns recursos mais avançados do que Jaeger:
Saiba mais: [documentação de tracing distribuído](https://signoz.io/docs/instrumentation/)
- 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.
#### Trace Funnels
<br /><br />
Crie funis a partir de traces para entender quedas no fluxo de requisições, transições com falha e problemas sistêmicos de workflow.
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributors.svg" width="50px" />
<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).
## Contribuindo
Adoramos contribuições grandes ou pequenas. Leia [CONTRIBUTING.md](CONTRIBUTING.md) para começar a contribuir com o SigNoz.
Nós ❤️ contribuições grandes ou pequenas. Leia [CONTRIBUTING.md](CONTRIBUTING.md) para começar a fazer contribuições para o SigNoz.
Não sabe como começar? **Fale conosco no `#contributing` na nossa [comunidade Slack](https://signoz.io/slack).**
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!
Como sempre, obrigado aos nossos incríveis contribuidores!
<a href="https://github.com/signoz/signoz/graphs/contributors">
<img src="https://contrib.rocks/image?repo=signoz/signoz" />
<img alt="Contribuidores do SigNoz" src="https://contrib.rocks/image?repo=signoz/signoz" />
</a>

View File

@@ -1,208 +1,190 @@
<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 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>
</p>
<p align="center">
<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>
<a href="README.md">English</a> ·
<a href="README.de-de.md">Deutsch</a> ·
<a href="README.pt-br.md">Português</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>
<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 是一个基于 OpenTelemetry 构建的开源可观测性平台。我们正在构建一个企业级替代方案,用来替代分散的监控工具栈,把日志、指标、链路追踪、告警和仪表盘放在同一个地方。
SigNoz 帮助开发人员监控应用并排查已部署应用的问题。你可以使用 SigNoz 实现如下能力:
### 选择 SigNoz 的运行方式
👉 在同一块面板上,可视化 Metrics, Traces 和 Logs 内容。
#### SigNoz Cloud推荐
👉 你可以关注服务的 p99 延迟和错误率, 包括外部 API 调用和个别的端点
完全托管的 SigNoz提供 30 天免费试用,无需信用卡,按用量计费,起价为 49 美元,并支持区域化数据托管
👉 你可以找到问题的根因,通过提取相关问题的 traces 日志、单独查看请求 traces 的火焰图详情。
[**免费开始 →**](https://signoz.io/teams/)
👉 执行 trace 数据聚合,以获取业务相关的 metrics
#### 企业版
👉 对日志过滤和查询,通过日志的属性建立看板和告警
Enterprise Cloud、BYOC 或 Enterprise Self-Hosted提供合规、支持、自定义保留期、RBAC、摄取控制、数据驻留和区域选择。
👉 通过 PythonjavaRuby 和 Javascript 自动记录异常
[**了解企业版 →**](https://signoz.io/enterprise/)
👉 轻松的自定义查询和设置告警
#### 社区版
### 应用 Metrics 展示
免费的开源 SigNoz可运行在你自己的基础设施中。使用 Docker、Kubernetes 或 Linux 部署,并完全掌控你的数据平面。
![application_metrics](https://user-images.githubusercontent.com/83692067/226637410-900dbc5e-6705-4b11-a10c-bd0faeb2a92f.png)
[**安装 SigNoz →**](https://signoz.io/docs/install/self-host/)
### 分布式追踪
### 你可以监控什么?
<img width="2068" alt="distributed_tracing_2 2" src="https://user-images.githubusercontent.com/83692067/226536447-bae58321-6a22-4ed3-af80-e3e964cb3489.png">
SigNoz 将日志、指标、链路追踪、告警、仪表盘、异常和面向 Agent 的工作流连接在一起,帮助团队更快地调试生产问题。
<img width="2068" alt="distributed_tracing_1" src="https://user-images.githubusercontent.com/83692067/226536462-939745b6-4f9d-45a6-8016-814837e7f7b4.png">
#### APM 概览
### 日志管理
监控服务延迟、错误率、吞吐量、Apdex、核心端点、数据库调用和外部调用。
<img width="2068" alt="logs_management" src="https://user-images.githubusercontent.com/83692067/226536482-b8a5c4af-b69c-43d5-969c-338bd5eaf1a5.png">
<p align="center">
<img alt="SigNoz APM 仪表盘展示延迟、吞吐量、Apdex 和关键操作" src="docs/readme-assets/monitor/apm.png" width="900">
</p>
### 基础设施监控
了解更多:[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">
#### 日志管理
### 异常监控
摄取、搜索、聚合日志,并通过可视化查询构建器将日志与链路追踪和指标关联起来。
![exceptions_light](https://user-images.githubusercontent.com/83692067/226637967-4188d024-3ac9-4799-be95-f5ea9c45436f.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">
#### 指标和仪表盘
<br /><br />
使用 Query Builder、PromQL 或 ClickHouse SQL 为应用、基础设施和自定义指标构建仪表盘。
## 加入我们 Slack 社区
<p align="center">
<img alt="SigNoz 主机指标仪表盘,展示系统负载和网络图表" src="docs/readme-assets/monitor/metrics.png" width="900">
</p>
来 [Slack](https://signoz.io/slack) 和我们打招呼吧 👋
了解更多:[指标文档](https://signoz.io/docs/metrics-management/overview/)
<br /><br />
#### 基础设施监控
## 特性:
监控 Kubernetes 集群、Pod、节点、工作负载以及主机级 CPU、内存、磁盘、网络、日志和链路追踪。
- 为 metrics, traces and logs 制定统一的 UI。 无需切换 Prometheus 到 Jaeger 去查找问题,也无需使用想 Elastic 这样的日志工具分开你的 metrics 和 traces
<p align="center">
<img alt="SigNoz Kubernetes 基础设施仪表盘,展示 Pod 和节点指标" src="docs/readme-assets/monitor/infrastructure.png" width="900">
</p>
- 默认统计应用的 metrics 数据,像 RPS (每秒请求数) 50th/90th/99th 的分位数延迟数据,还有相关的错误率
了解更多:[基础设施监控文档](https://signoz.io/docs/infrastructure-monitoring/overview/)
- 找到应用中最慢的端点
#### LLM 和 AI 可观测性
- 查看准确的请求跟踪数据,找到下游服务的问题了,比如 DB 慢查询,或者调用第三方的支付网关等
追踪 LLM 应用、RAG 流水线、Prompt、工具调用、Token、延迟和成本并与应用和基础设施遥测数据放在一起分析。
- 通过 服务名、操作方式、延迟、错误、标签/注释 过滤 traces 数据
<p align="center">
<img alt="SigNoz LLM 可观测性仪表盘展示链路追踪、Token 使用、延迟和成本" src="docs/readme-assets/monitor/llm.png" width="900">
</p>
- 通过聚合 trace 数据而获得业务相关的 metrics。 比如你可以通过 `customer_type: gold` 或者 `deployment_version: v2` 或者 `external_call: paypal` 获取错误率和 P99 延迟数据
了解更多:[LLM 可观测性文档](https://signoz.io/docs/llm-observability/)
- 原生支持 OpenTelemetry 日志,高级日志查询,自动收集 k8s 相关日志
#### Agent 原生可观测性和 MCP
- 快如闪电的日志分析 ([Logs Perf. Benchmark](https://signoz.io/blog/logs-performance-benchmark/))
使用 SigNoz MCP server 将遥测数据带入编程 Agent或在 SigNoz 中使用 Noz基于生产上下文调查事故、优化告警并构建仪表盘。[Noz](https://signoz.io/docs/ai/noz/) 仅适用于 SigNoz Cloud。
- 可视化点到点的基础设施性能,提取有所有类型机器的 metrics 数据
<p align="center">
<img alt="SigNoz Noz 界面与基于 MCP 的 Agent 工作流" src="docs/readme-assets/monitor/agent-native.png" width="900">
</p>
- 轻易自定义告警查询
了解更多:[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 />
#### 分布式链路追踪
## 为什么使用 SigNoz?
通过火焰图、瀑布图、Span 事件、过滤器和 Trace 分析,跟踪请求在各个服务之间的流转。
作为开发者, 我们发现 SaaS 厂商对一些大家想要的小功能都是闭源的,这种行为真的让人有点恼火。 闭源厂商还会在月底给你一张没有明细的巨额账单。
<p align="center">
<img alt="SigNoz 分布式链路追踪视图,包含火焰图和瀑布图 Span" src="docs/readme-assets/monitor/distributed-tracing.png" width="900">
</p>
我们想做一个自托管并且可开源的工具,像 DataDog 和 NewRelic 那样, 为那些担心数据隐私和安全的公司提供第三方服务。
了解更多:[分布式链路追踪文档](https://signoz.io/docs/instrumentation/)
作为开源的项目,你完全可以自己掌控你的配置、样本和更新。你同样可以基于 SigNoz 拓展特定的业务模块。
#### Trace Funnels
### 支持的编程语言:
基于链路追踪创建漏斗,用于理解请求流中的掉点、失败转换和系统性工作流问题。
我们支持 [OpenTelemetry](https://opentelemetry.io)。作为一个观测你应用的库文件。所以任何 OpenTelemetry 支持的框架和语言,对于 SigNoz 也同样支持。 一些主要支持的语言如下:
<p align="center">
<img alt="SigNoz Trace Funnels展示请求流掉点和失败转换" src="docs/readme-assets/monitor/trace-funnels.png" width="900">
</p>
- Java
- Python
- NodeJS
- Go
- PHP
- .NET
- Ruby
- Elixir
- Rust
了解更多:[Trace Funnels 文档](https://signoz.io/docs/trace-funnels/overview/)
你可以在这里找到全部支持的语言列表 - https://opentelemetry.io/docs/
可以监控:[**异常**](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/)。
<br /><br />
### 为什么团队选择 SigNoz
## 让我们开始吧
1. **OpenTelemetry 原生**<br>
用开放标准完成一次接入,并保持对遥测数据的所有权。
2. **信号关联**<br>
在服务图表、链路追踪、日志、基础设施指标和异常之间切换时,不需要更换工具。
3. **单一列式数据库**<br>
为高基数、高吞吐量的可观测性工作负载而构建。
4. **可预测的定价**<br>
不按主机收费,不按用户席位收费,也不对自定义指标设置特殊价格。
5. **企业就绪**<br>
SOC 2 Type II 和 HIPAA 合规、RBAC、摄取控制、自定义保留期、支持、BYOC 和自托管。
### 使用 Docker 部署
### 快速开始
请一步步跟随 [这里](https://signoz.io/docs/install/docker/) 通过 docker 来安装。
#### 从 Cloud 开始
这个 [排障说明书](https://signoz.io/docs/install/troubleshooting/) 可以帮助你解决碰到的问题
创建一个托管的 SigNoz 工作区,无需运行可观测性基础设施,即可获得第一个仪表盘
<p>&nbsp </p>
[**免费开始使用 SigNoz Cloud**](https://signoz.io/teams/)
### 使用 Helm 在 Kubernetes 部署
#### 自托管 SigNoz
请一步步跟随 [这里](https://signoz.io/docs/deployment/helm_chart) 通过 helm 来安装
在你自己的基础设施中通过 Foundry、Docker、Kubernetes 或 Linux 运行 SigNoz。
<br /><br />
[**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/)
## 比较相似的工具
#### 发送数据
### SigNoz vs Prometheus
使用 OpenTelemetry、Prometheus、语言 SDK 和集成来接入应用与基础设施。
Prometheus 是一个针对 metrics 监控的强大工具。但是如果你想无缝的切换 metrics 和 traces 查询,你当前大概率需要在 Prometheus 和 Jaeger 之间切换。
[**接入文档**](https://signoz.io/docs/instrumentation/) · [**集成列表**](https://signoz.io/docs/integrations/integrations-list/)
我们的目标是提供一个客户观测 metrics 和 traces 整合的 UI。就像 SaaS 供应商 DataDog它提供很多 jaeger 缺失的功能,比如针对 traces 过滤功能和聚合功能。
### 与常见工具的对比
<p>&nbsp </p>
许多团队在从单一用途工具或价格不可预测的商业平台迁移时,会选择 SigNoz。
### SigNoz vs Jaeger
**Prometheus**<br>
如果你只需要指标Prometheus 很合适。SigNoz 将指标、日志、链路追踪、仪表盘和告警放在一起,让团队可以通过关联上下文进行调试。
Jaeger 仅仅是一个分布式追踪系统。 但是 SigNoz 可以提供 metrics, traces 和 logs 所有的观测。
**Jaeger**<br>
Jaeger 只做分布式链路追踪。SigNoz 增加了指标、日志、Trace 分析、仪表盘、告警、异常和 Trace 到日志的工作流。
而且, SigNoz 相较于 Jaeger 拥有更对的高级功能:
**Elastic**<br>
SigNoz 使用列式数据库来高效处理可观测性分析和高基数日志工作负载。在摄取阶段,相比 Elastic 可降低 50% 的资源需求。查看[详细评测](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>&nbsp </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>&nbsp </p>
### SigNoz vs Loki
- SigNoz 支持大容量高基数的聚合,但是 loki 是不支持的。
- SigNoz 支持索引的高基数查询,并且对索引没有数量限制,而 Loki 会在添加部分索引后到达最大上限。
- 相较于 SigNozLoki 在搜索大量数据下既困难又缓慢。
我们已经发布了基准测试对比 Loki 和 SigNoz 性能。请点击 [这里](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
<br /><br />
**Loki**<br>
在链接的评测中SigNoz 在测试设置中索引了所有键,而 Loki 在增加更多标签时遇到了 max stream 错误。查看[详细评测](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)。
## 贡献
我们 ❤️ 你的贡献,无论大小。 请先阅读 [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 src="https://contrib.rocks/image?repo=signoz/signoz" />
<img alt="SigNoz 贡献者" src="https://contrib.rocks/image?repo=signoz/signoz" />
</a>

View File

@@ -2626,7 +2626,6 @@ components:
unit:
type: string
value:
format: double
type: number
required:
- value
@@ -3568,7 +3567,6 @@ components:
unit:
type: string
value:
format: double
type: number
required:
- value
@@ -3605,7 +3603,6 @@ components:
unit:
type: string
value:
format: double
type: number
required:
- value

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 783 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -3338,7 +3338,6 @@ export interface DashboardtypesThresholdWithLabelDTO {
unit?: string;
/**
* @type number
* @format double
*/
value: number;
}
@@ -3866,7 +3865,6 @@ export interface DashboardtypesComparisonThresholdDTO {
unit?: string;
/**
* @type number
* @format double
*/
value: number;
}
@@ -4147,7 +4145,6 @@ export interface DashboardtypesTableThresholdDTO {
unit?: string;
/**
* @type number
* @format double
*/
value: number;
}

View File

@@ -1,5 +1,5 @@
import { ReactElement } from 'react';
import { render, screen } from 'tests/test-utils';
import { render, screen, userEvent, waitFor } 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,6 +66,29 @@ 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 });
@@ -142,4 +165,31 @@ 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);
});
});
});

View File

@@ -7,7 +7,8 @@ import {
} from '@signozhq/ui/tooltip';
import type { BrandedPermission } from 'hooks/useAuthZ/types';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { parsePermission } from 'hooks/useAuthZ/utils';
import { formatPermission } from 'hooks/useAuthZ/utils';
import { useAppContext } from 'providers/App/App';
import styles from './AuthZTooltip.module.scss';
interface AuthZTooltipProps {
@@ -19,19 +20,14 @@ interface AuthZTooltipProps {
function formatDeniedMessage(
denied: BrandedPermission[],
userId: string,
override?: string,
): string {
if (override) {
return override;
}
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`;
const permissions = denied.map(formatPermission).join(', ');
return `user/${userId} is not authorized to perform ${permissions}`;
}
function AuthZTooltip({
@@ -40,6 +36,7 @@ function AuthZTooltip({
enabled = true,
tooltipMessage,
}: AuthZTooltipProps): JSX.Element {
const { user } = useAppContext();
const shouldCheck = enabled && checks.length > 0;
const { permissions, isLoading } = useAuthZ(checks, { enabled: shouldCheck });
@@ -75,7 +72,7 @@ function AuthZTooltip({
</span>
</TooltipTrigger>
<TooltipContent className={styles.errorContent}>
{formatDeniedMessage(deniedPermissions, tooltipMessage)}
{formatDeniedMessage(deniedPermissions, user.id, tooltipMessage)}
</TooltipContent>
</TooltipRoot>
</TooltipProvider>

View File

@@ -2,3 +2,11 @@
box-sizing: border-box;
width: 100%;
}
.permission {
color: var(--l2-foreground);
}
.permissionCode {
font-family: monospace;
}

View File

@@ -5,9 +5,8 @@ describe('PermissionDeniedCallout', () => {
it('renders the permission name in the callout message', () => {
render(<PermissionDeniedCallout permissionName="serviceaccount:attach" />);
expect(screen.getByText(/You don't have/)).toBeInTheDocument();
expect(screen.getByText(/is not authorized/)).toBeInTheDocument();
expect(screen.getByText(/serviceaccount:attach/)).toBeInTheDocument();
expect(screen.getByText(/permission/)).toBeInTheDocument();
});
it('accepts an optional className', () => {

View File

@@ -1,6 +1,8 @@
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;
@@ -11,6 +13,8 @@ function PermissionDeniedCallout({
permissionName,
className,
}: PermissionDeniedCalloutProps): JSX.Element {
const { user } = useAppContext();
return (
<Callout
type="error"
@@ -18,7 +22,11 @@ function PermissionDeniedCallout({
size="small"
className={cx(styles.callout, className)}
>
{`You don't have ${permissionName} permission`}
<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>
</Callout>
);
}

View File

@@ -267,11 +267,10 @@ describe('createGuardedRoute', () => {
await waitFor(() => {
const heading = document.querySelector('h3');
expect(heading).toBeInTheDocument();
expect(heading?.textContent).toMatch(/permission to view/i);
expect(heading?.textContent).toMatch(/not authorized/i);
});
expect(screen.getByText('update')).toBeInTheDocument();
expect(screen.getByText('role:123')).toBeInTheDocument();
expect(screen.getByText(/update:role:123/)).toBeInTheDocument();
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();

View File

@@ -5,7 +5,8 @@ import {
AuthZRelation,
BrandedPermission,
} from 'hooks/useAuthZ/types';
import { parsePermission } from 'hooks/useAuthZ/utils';
import { formatPermission } from 'hooks/useAuthZ/utils';
import { useAppContext } from 'providers/App/App';
import noDataUrl from '@/assets/Icons/no-data.svg';
@@ -17,21 +18,16 @@ import './createGuardedRoute.styles.scss';
function OnNoPermissionsFallback(response: {
requiredPermissionName: BrandedPermission;
}): ReactElement {
const { relation, object } = parsePermission(response.requiredPermissionName);
const { user } = useAppContext();
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 dont have permission to view this page.</h3>
<h3>Uh-oh! You are not authorized</h3>
<p>
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.
<code>user/{user.id}</code> is not authorized to perform{' '}
<code>{formatPermission(response.requiredPermissionName)}</code>
</p>
</div>
</div>

View File

@@ -105,3 +105,8 @@
height: 1px;
background: var(--l1-border);
}
.errorInPlaceContainer {
border-color: var(--callout-error-border) !important;
background: var(--callout-error-background) !important;
}

View File

@@ -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,8 +212,10 @@ function CreateEditRolePage(): JSX.Element {
<ErrorInPlace
error={saveError}
height="auto"
bordered
data-testid="save-error-banner"
padding={0}
bordered={true}
className={styles.errorInPlaceContainer}
/>
)}

View File

@@ -216,6 +216,47 @@ 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) =>

View File

@@ -520,4 +520,115 @@ 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();
});
});
});
});

View File

@@ -8,6 +8,10 @@
transition: border-color 0.15s ease;
}
.resourceCardError {
border-color: var(--destructive);
}
.resourceCardHeader {
display: flex;
align-items: center;

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { ChevronDown, ChevronRight } from '@signozhq/icons';
import type { AuthZResource, AuthZVerb } from 'hooks/useAuthZ/types';
@@ -10,6 +10,7 @@ import ActionToggle from './ActionToggle';
import styles from './ResourceCard.module.scss';
import { PermissionScope, ResourcePermissions } from '../../types';
import cx from 'classnames';
interface ResourceCardProps {
resource: ResourcePermissions;
@@ -74,10 +75,22 @@ 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={styles.resourceCard}
className={cx(
styles.resourceCard,
hasErrorOnResource && !isExpanded && styles.resourceCardError,
)}
data-testid={`resource-card-${resource.resourceId}`}
data-state={hasErrorOnResource && !isExpanded ? 'error' : undefined}
>
<button
type="button"

View File

@@ -125,8 +125,10 @@ export function useCreateEditRolePageActions(
...prev,
[field]: value,
}));
clearValidationErrors();
setSaveError(null);
},
[],
[clearValidationErrors],
);
const handleModeChange = useCallback(
@@ -139,8 +141,10 @@ export function useCreateEditRolePageActions(
const handleResourcesChange = useCallback(
(resources: ResourcePermissions[]): void => {
setLocalResources(resources);
clearValidationErrors();
setSaveError(null);
},
[],
[clearValidationErrors],
);
const hasUnsavedChanges = useRoleUnsavedChanges(
@@ -153,7 +157,17 @@ export function useCreateEditRolePageActions(
const handleSave = useCallback(async (): Promise<boolean> => {
if (!formData.name.trim()) {
toast.error('Role name is required', { position: 'bottom-center' });
setSaveError(
new APIError({
httpStatusCode: 400,
error: {
code: 'VALIDATION_ERROR',
message: 'Role name is required',
url: '',
errors: [],
},
}),
);
return false;
}

View File

@@ -41,6 +41,11 @@ 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]) {

View File

@@ -1,11 +1,9 @@
import { Input } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type {
DashboardtypesPanelSpecDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
import { getBuilderQueries } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/getBuilderQueries';
import { resolveSignal } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/getBuilderQueries';
import type { EQueryType } from 'types/common/dashboard';
import type { LegendSeries } from '../hooks/useLegendSeries';
import type { TableColumnOption } from '../hooks/useTableColumns';
@@ -20,10 +18,20 @@ 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;
}
/**
@@ -36,15 +44,16 @@ function ConfigPane({
panelKind,
spec,
onChangeSpec,
onChangePanelKind,
queryType,
legendSeries,
tableColumns,
stepInterval,
}: ConfigPaneProps): JSX.Element {
const definition = getPanelDefinition(panelKind);
const sections = definition.sections;
const signal = getBuilderQueries(spec.queries || [])[0]?.signal as
| TelemetrytypesSignalDTO
| undefined;
const signal = resolveSignal(spec.queries, definition.supportedSignals[0]);
// 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.
@@ -95,6 +104,10 @@ function ConfigPane({
legendSeries={legendSeries}
tableColumns={tableColumns}
signal={signal}
panelKind={panelKind}
onChangePanelKind={onChangePanelKind}
queryType={queryType}
stepInterval={stepInterval}
/>
))}
</div>

View File

@@ -0,0 +1,6 @@
// Matches ConfigPane's `.field` so the switcher lines up with the title/description fields.
.field {
display: flex;
flex-direction: column;
gap: 8px;
}

View File

@@ -0,0 +1,64 @@
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;

View File

@@ -0,0 +1,122 @@
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');
});
});

View File

@@ -0,0 +1,73 @@
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");
});
});

View File

@@ -0,0 +1,46 @@
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;
}

View File

@@ -1,29 +1,21 @@
import type {
DashboardtypesPanelSpecDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import {
type PanelFormattingSlice,
SECTION_METADATA,
type SectionConfig,
SectionKind,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type { LegendSeries } from '../../hooks/useLegendSeries';
import type { TableColumnOption } from '../../hooks/useTableColumns';
import type { SectionEditorContext } from '../sectionContext';
import { resolveSectionEditor } from '../sectionRegistry';
import SettingsSection from '../SettingsSection/SettingsSection';
interface SectionSlotProps {
// `yAxisUnit` is derived from the spec below, not forwarded, so it's omitted.
type SectionSlotProps = {
config: SectionConfig;
spec: DashboardtypesPanelSpecDTO;
onChangeSpec: (next: DashboardtypesPanelSpecDTO) => void;
/** 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;
}
} & Omit<SectionEditorContext, 'yAxisUnit'>;
/**
* Renders one configuration section: its collapsible wrapper plus the registered editor
@@ -38,6 +30,10 @@ function SectionSlot({
legendSeries,
tableColumns,
signal,
panelKind,
onChangePanelKind,
queryType,
stepInterval,
}: 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.
@@ -60,7 +56,12 @@ function SectionSlot({
.formatting?.unit;
return (
<SettingsSection title={title} icon={<Icon size={15} />}>
<SettingsSection
title={title}
icon={<Icon size={15} />}
// Open Visualization by default so the type switcher is visible.
defaultOpen={config.kind === SectionKind.Visualization}
>
<Component
value={get(spec)}
controls={controls}
@@ -69,6 +70,10 @@ function SectionSlot({
yAxisUnit={yAxisUnit}
tableColumns={tableColumns}
signal={signal}
panelKind={panelKind}
onChangePanelKind={onChangePanelKind}
queryType={queryType}
stepInterval={stepInterval}
/>
</SettingsSection>
);

View File

@@ -26,13 +26,15 @@ 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-${title}`}
data-testid={`config-section-${serializedTitle}`}
onClick={(): void => setIsOpen((prev) => !prev)}
>
{icon && (

View File

@@ -1,5 +1,6 @@
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';
@@ -21,6 +22,8 @@ function renderConfigPane(
panelKind: 'signoz/TimeSeriesPanel',
spec: spec(),
onChangeSpec: jest.fn(),
onChangePanelKind: jest.fn(),
queryType: EQueryType.QUERY_BUILDER,
legendSeries: [],
tableColumns: [],
...overrides,
@@ -56,6 +59,8 @@ 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')).toBeInTheDocument();
expect(
screen.getByTestId('config-section-formatting-&-units'),
).toBeInTheDocument();
});
});

View File

@@ -1,5 +1,5 @@
.group {
width: min(350px, 100%);
width: 100%;
}
.segment {

View File

@@ -10,11 +10,11 @@ export interface ConfigSegmentedItem {
icon?: SegmentIconName;
}
interface ConfigSegmentedProps {
interface ConfigSegmentedProps<T extends string = string> {
testId: string;
value: string | undefined;
value: T | undefined;
items: ConfigSegmentedItem[];
onChange: (value: string) => void;
onChange: (value: T) => void;
}
/**
@@ -23,12 +23,12 @@ interface ConfigSegmentedProps {
* brightens with the selected state (it inherits the toggle's `currentColor`). Built on
* the Periscope ToggleGroup so it stays theme-faithful.
*/
function ConfigSegmented({
function ConfigSegmented<T extends string = string>({
testId,
value,
items,
onChange,
}: ConfigSegmentedProps): JSX.Element {
}: ConfigSegmentedProps<T>): JSX.Element {
return (
<ToggleGroupSimple
type="single"
@@ -47,7 +47,7 @@ function ConfigSegmented({
}))}
// 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: string): void => {
onChange={(next: T): void => {
if (next) {
onChange(next);
}

View File

@@ -8,3 +8,11 @@
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;
}

View File

@@ -1,21 +1,24 @@
import { Select } from 'antd';
import { SegmentIcon, type SegmentIconName } from '../segmentIcons';
import type { ReactNode } from 'react';
import { Select, Tooltip } from 'antd';
import styles from './ConfigSelect.module.scss';
export interface ConfigSelectItem {
value: string;
export interface ConfigSelectItem<T extends string = string> {
value: T;
label: string;
icon?: SegmentIconName;
/** 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;
}
interface ConfigSelectProps {
interface ConfigSelectProps<T extends string = string> {
testId: string;
value: string | undefined;
value: T | undefined;
placeholder?: string;
items: ConfigSelectItem[];
onChange: (value: string) => void;
items: ConfigSelectItem<T>[];
onChange: (value: T) => void;
}
/**
@@ -23,32 +26,42 @@ interface ConfigSelectProps {
* `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({
function ConfigSelect<T extends string = string>({
testId,
value,
placeholder,
items,
onChange,
}: ConfigSelectProps): JSX.Element {
}: ConfigSelectProps<T>): JSX.Element {
return (
<Select<string>
<Select<T>
className={styles.select}
data-testid={testId}
value={value}
placeholder={placeholder}
onChange={onChange}
virtual={false}
options={items.map((item) => ({
value: item.value,
label: item.icon ? (
options={items.map((item) => {
const content = item.icon ? (
<span className={styles.item}>
<SegmentIcon name={item.icon} />
{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
),
};
})}
/>
);
}

View File

@@ -0,0 +1,22 @@
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;
}

View File

@@ -8,14 +8,15 @@ import type {
DashboardtypesPanelSpecDTO,
DashboardtypesTimeSeriesChartAppearanceDTO,
} from 'api/generated/services/sigNoz.schemas';
import type {
AnyThreshold,
PanelFormattingSlice,
SectionEditorProps,
import {
SectionKind,
SectionSpecMap,
type AnyThreshold,
type PanelFormattingSlice,
type SectionEditorProps,
type 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';
@@ -69,27 +70,27 @@ function updatePluginSlice(
export const SECTION_REGISTRY: {
[K in SectionKind]?: SectionDescriptor<K>;
} = {
formatting: {
[SectionKind.Formatting]: {
Component: FormattingSection,
get: (spec): PanelFormattingSlice | undefined =>
getPluginSlice<PanelFormattingSlice>(spec, 'formatting'),
update: (spec, formatting): PanelSpec =>
updatePluginSlice(spec, 'formatting', formatting),
},
axes: {
[SectionKind.Axes]: {
Component: AxesSection,
get: (spec): DashboardtypesAxesDTO | undefined =>
getPluginSlice<DashboardtypesAxesDTO>(spec, 'axes'),
update: (spec, axes): PanelSpec => updatePluginSlice(spec, 'axes', axes),
},
legend: {
[SectionKind.Legend]: {
Component: LegendSection,
get: (spec): DashboardtypesLegendDTO | undefined =>
getPluginSlice<DashboardtypesLegendDTO>(spec, 'legend'),
update: (spec, legend): PanelSpec =>
updatePluginSlice(spec, 'legend', legend),
},
chartAppearance: {
[SectionKind.ChartAppearance]: {
Component: ChartAppearanceSection,
get: (spec): DashboardtypesTimeSeriesChartAppearanceDTO | undefined =>
getPluginSlice<DashboardtypesTimeSeriesChartAppearanceDTO>(
@@ -99,7 +100,7 @@ export const SECTION_REGISTRY: {
update: (spec, chartAppearance): PanelSpec =>
updatePluginSlice(spec, 'chartAppearance', chartAppearance),
},
visualization: {
[SectionKind.Visualization]: {
Component: VisualizationSection,
get: (spec): DashboardtypesBarChartVisualizationDTO | undefined =>
getPluginSlice<DashboardtypesBarChartVisualizationDTO>(
@@ -109,14 +110,14 @@ export const SECTION_REGISTRY: {
update: (spec, visualization): PanelSpec =>
updatePluginSlice(spec, 'visualization', visualization),
},
buckets: {
[SectionKind.Buckets]: {
Component: BucketsSection,
get: (spec): DashboardtypesHistogramBucketsDTO | undefined =>
getPluginSlice<DashboardtypesHistogramBucketsDTO>(spec, 'histogramBuckets'),
update: (spec, buckets): PanelSpec =>
updatePluginSlice(spec, 'histogramBuckets', buckets),
},
contextLinks: {
[SectionKind.ContextLinks]: {
Component: ContextLinksSection,
// Panel-level slice (spec.links), not under the plugin spec — no cast needed.
get: (spec): DashboardLinkDTO[] | undefined => spec.links,
@@ -125,7 +126,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.
thresholds: {
[SectionKind.Thresholds]: {
Component: ThresholdsSection,
get: (spec): AnyThreshold[] | undefined =>
getPluginSlice<AnyThreshold[]>(spec, 'thresholds'),
@@ -142,22 +143,13 @@ export const SECTION_REGISTRY: {
* `get` → `Component` → `update` without any further casts.
*/
export interface ErasedSectionDescriptor {
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;
}>;
Component: ComponentType<
{
value: unknown;
controls?: unknown;
onChange: (next: unknown) => void;
} & SectionEditorContext
>;
get: (spec: PanelSpec) => unknown;
update: (spec: PanelSpec, value: unknown) => PanelSpec;
}

View File

@@ -1,7 +1,10 @@
import type { ChangeEvent } from 'react';
import { Typography } from '@signozhq/ui/typography';
import { Input } from 'antd';
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type {
SectionEditorProps,
SectionKind,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import ConfigSegmented from '../../controls/ConfigSegmented/ConfigSegmented';
@@ -22,7 +25,7 @@ function AxesSection({
value,
controls,
onChange,
}: SectionEditorProps<'axes'>): JSX.Element {
}: SectionEditorProps<SectionKind.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 =

View File

@@ -2,7 +2,10 @@ 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 } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type {
SectionEditorProps,
SectionKind,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import ConfigSwitch from '../../controls/ConfigSwitch/ConfigSwitch';
@@ -23,7 +26,7 @@ function BucketsSection({
value,
controls,
onChange,
}: SectionEditorProps<'buckets'>): JSX.Element {
}: SectionEditorProps<SectionKind.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 =

View File

@@ -3,3 +3,14 @@
flex-direction: column;
gap: 8px;
}
.thresholdField {
display: flex;
flex-direction: column;
gap: 8px;
}
.thresholdPrefix {
padding-right: 4px;
opacity: 0.6;
}

View File

@@ -1,16 +1,20 @@
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 } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type {
SectionEditorProps,
SectionKind,
} 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';
@@ -31,22 +35,22 @@ const LINE_INTERPOLATION_OPTIONS = [
{
value: DashboardtypesLineInterpolationDTO.linear,
label: 'Linear',
icon: 'interp-linear' as const,
icon: <SegmentIcon name="interp-linear" />,
},
{
value: DashboardtypesLineInterpolationDTO.spline,
label: 'Spline',
icon: 'interp-spline' as const,
icon: <SegmentIcon name="interp-spline" />,
},
{
value: DashboardtypesLineInterpolationDTO.step_before,
label: 'Step before',
icon: 'interp-step-before' as const,
icon: <SegmentIcon name="interp-step-before" />,
},
{
value: DashboardtypesLineInterpolationDTO.step_after,
label: 'Step after',
icon: 'interp-step-after' as const,
icon: <SegmentIcon name="interp-step-after" />,
},
];
@@ -77,16 +81,9 @@ function ChartAppearanceSection({
value,
controls,
onChange,
}: 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 },
});
};
stepInterval,
}: SectionEditorProps<SectionKind.ChartAppearance> &
Pick<SectionEditorContext, 'stepInterval'>): JSX.Element {
return (
<>
{controls.lineStyle && (
@@ -114,7 +111,7 @@ function ChartAppearanceSection({
onChange={(next): void =>
onChange({
...value,
lineInterpolation: next as DashboardtypesLineInterpolationDTO,
lineInterpolation: next,
})
}
/>
@@ -146,16 +143,12 @@ function ChartAppearanceSection({
)}
{controls.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>
<DisconnectValuesField
testId="panel-editor-v2-span-gaps"
value={value?.spanGaps}
stepInterval={stepInterval}
onChange={(spanGaps): void => onChange({ ...value, spanGaps })}
/>
)}
</>
);

View File

@@ -0,0 +1,97 @@
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;
const isThreshold = !!duration;
// Remember the last threshold so toggling Never → Threshold restores it.
const [lastDuration, setLastDuration] = useState(
duration ?? defaultDuration(stepInterval),
);
useEffect(() => {
if (duration) {
setLastDuration(duration);
}
}, [duration]);
const handleMode = (mode: DisconnectValuesMode): void => {
onChange(
mode === DisconnectValuesMode.THRESHOLD
? { ...value, fillLessThan: lastDuration }
: 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 && (
<div className={styles.field}>
<Typography.Text>Threshold value</Typography.Text>
<DisconnectValuesThresholdInput
testId={`${testId}-value`}
value={lastDuration}
minValue={stepInterval}
onChange={(next): void => onChange({ ...value, fillLessThan: next })}
/>
</div>
)}
</>
);
}
export default DisconnectValuesField;

View File

@@ -0,0 +1,94 @@
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;
}
/**
* 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]);
const commit = (raw: string): void => {
if (!raw) {
return;
}
let seconds: number;
try {
seconds = rangeUtil.isValidTimeSpan(raw)
? rangeUtil.intervalToSeconds(raw)
: NaN;
} catch {
seconds = NaN;
}
if (!Number.isFinite(seconds) || seconds <= 0) {
setError('Enter a valid duration (e.g. 30s, 1m, 1h)');
return;
}
if (minValue !== undefined && seconds < minValue) {
setError(`Threshold should be > ${rangeUtil.secondsToHms(minValue)}`);
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}>&gt;</span>}
value={text}
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
setText(e.target.value);
if (error) {
setError(null);
}
}}
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;

View File

@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DashboardtypesLineStyleDTO } from 'api/generated/services/sigNoz.schemas';
@@ -60,7 +60,8 @@ describe('ChartAppearanceSection', () => {
).not.toBeInTheDocument();
});
it('writes the chosen fill mode through the segmented control', () => {
it('writes the chosen fill mode through the segmented control', async () => {
const user = userEvent.setup();
const onChange = jest.fn();
render(
<ChartAppearanceSection
@@ -70,7 +71,7 @@ describe('ChartAppearanceSection', () => {
/>,
);
fireEvent.click(screen.getByText('Gradient'));
await user.click(screen.getByText('Gradient'));
expect(onChange).toHaveBeenCalledWith({
lineStyle: 'solid',
@@ -93,7 +94,8 @@ describe('ChartAppearanceSection', () => {
expect(onChange).toHaveBeenCalledWith({ lineInterpolation: 'spline' });
});
it('toggles show points through onChange', () => {
it('toggles show points through onChange', async () => {
const user = userEvent.setup();
const onChange = jest.fn();
render(
<ChartAppearanceSection
@@ -103,14 +105,30 @@ describe('ChartAppearanceSection', () => {
/>,
);
fireEvent.click(screen.getByTestId('panel-editor-v2-show-points'));
await user.click(screen.getByTestId('panel-editor-v2-show-points'));
expect(onChange).toHaveBeenCalledWith({ showPoints: true });
});
it('writes a span-gaps threshold and clears it when emptied', () => {
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();
const onChange = jest.fn();
const { rerender } = render(
render(
<ChartAppearanceSection
value={undefined}
controls={{ spanGaps: true }}
@@ -118,23 +136,112 @@ describe('ChartAppearanceSection', () => {
/>,
);
fireEvent.change(screen.getByTestId('panel-editor-v2-span-gaps'), {
target: { value: '60' },
});
expect(onChange).toHaveBeenLastCalledWith({
spanGaps: { fillLessThan: '60' },
});
await user.click(screen.getByText('Threshold'));
rerender(
expect(onChange).toHaveBeenLastCalledWith({
spanGaps: { 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: '60' } }}
value={{ spanGaps: { fillLessThan: '1m' } }}
controls={{ spanGaps: true }}
onChange={onChange}
/>,
);
fireEvent.change(screen.getByTestId('panel-editor-v2-span-gaps'), {
target: { value: '' },
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: { 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: { 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: undefined });
});
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();
});
});

View File

@@ -4,7 +4,10 @@ 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 } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type {
SectionEditorProps,
SectionKind,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import styles from './ContextLinksSection.module.scss';
@@ -17,7 +20,7 @@ import styles from './ContextLinksSection.module.scss';
function ContextLinksSection({
value,
onChange,
}: SectionEditorProps<'contextLinks'>): JSX.Element {
}: SectionEditorProps<SectionKind.ContextLinks>): JSX.Element {
const links = value ?? [];
const updateAt = (index: number, patch: Partial<DashboardLinkDTO>): void =>

View File

@@ -2,18 +2,19 @@ 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 } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type {
SectionEditorProps,
SectionKind,
} 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<'formatting'> & {
/** Table panel's resolved value columns; required for the column-units editor. */
tableColumns?: TableColumnOption[];
};
type FormattingSectionProps = SectionEditorProps<SectionKind.Formatting> &
Pick<SectionEditorContext, 'tableColumns'>;
// `full` means "show the raw value, no rounding"; the digits round to that many places.
const DECIMAL_OPTIONS: {
@@ -65,7 +66,7 @@ function FormattingSection({
onChange={(next): void =>
onChange({
...value,
decimalPrecision: next as DashboardtypesPrecisionOptionDTO,
decimalPrecision: next,
})
}
/>

View File

@@ -1,17 +1,18 @@
import { Typography } from '@signozhq/ui/typography';
import { DashboardtypesLegendPositionDTO } from 'api/generated/services/sigNoz.schemas';
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type {
SectionEditorProps,
SectionKind,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import ConfigSegmented from '../../controls/ConfigSegmented/ConfigSegmented';
import LegendColors from '../../controls/LegendColors/LegendColors';
import type { LegendSeries } from '../../../hooks/useLegendSeries';
import type { SectionEditorContext } from '../../sectionContext';
import styles from './LegendSection.module.scss';
type LegendSectionProps = SectionEditorProps<'legend'> & {
/** Panel's resolved series, forwarded by SectionSlot for the colors control. */
legendSeries?: LegendSeries[];
};
type LegendSectionProps = SectionEditorProps<SectionKind.Legend> &
Pick<SectionEditorContext, 'legendSeries'>;
const POSITION_OPTIONS = [
{

View File

@@ -14,6 +14,7 @@ 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';
@@ -61,11 +62,7 @@ type ThresholdsSectionProps = {
/** `variant` picks the row editor + element shape; defaults to `label`. */
controls?: { variant?: ThresholdVariant };
onChange: (next: AnyThreshold[]) => void;
/** 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[];
};
} & Pick<SectionEditorContext, 'yAxisUnit' | 'tableColumns'>;
/**
* Edits the `thresholds` slice for every panel kind. All variants share the same

View File

@@ -123,6 +123,25 @@ 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('does not commit edits when Discard is clicked', async () => {
const user = userEvent.setup();
const onChange = jest.fn();

View File

@@ -1,3 +1,4 @@
import { useEffect, useState } from 'react';
import { Typography } from '@signozhq/ui/typography';
import { Input } from 'antd';
@@ -16,6 +17,12 @@ 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>
@@ -23,8 +30,11 @@ function ThresholdValueField({
data-testid={testId}
type="number"
placeholder="Value"
value={value}
onChange={(e): void => onChange(e.target.value)}
value={raw}
onChange={(e): void => {
setRaw(e.target.value);
onChange(e.target.value);
}}
/>
</div>
);

View File

@@ -1,26 +1,49 @@
import { Typography } from '@signozhq/ui/typography';
import { DashboardtypesTimePreferenceDTO } from 'api/generated/services/sigNoz.schemas';
import type { SectionEditorProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
import type {
SectionEditorProps,
SectionKind,
} 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 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.
* 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.
*/
function VisualizationSection({
value,
controls,
onChange,
}: SectionEditorProps<'visualization'>): JSX.Element {
panelKind,
onChangePanelKind,
queryType,
signal,
}: VisualizationSectionProps): 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>
@@ -32,7 +55,7 @@ function VisualizationSection({
onChange={(next): void =>
onChange({
...value,
timePreference: next as DashboardtypesTimePreferenceDTO,
timePreference: next,
})
}
/>

View File

@@ -4,6 +4,15 @@ 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();
@@ -17,7 +26,12 @@ describe('VisualizationSection', () => {
render(
<VisualizationSection
value={undefined}
controls={{ timePreference: true, stacking: true, fillSpans: true }}
controls={{
switchPanelKind: true,
timePreference: true,
stacking: true,
fillSpans: true,
}}
onChange={jest.fn()}
/>,
);
@@ -35,7 +49,10 @@ describe('VisualizationSection', () => {
render(
<VisualizationSection
value={undefined}
controls={{ timePreference: true }}
controls={{
switchPanelKind: true,
timePreference: true,
}}
onChange={jest.fn()}
/>,
);
@@ -56,7 +73,10 @@ describe('VisualizationSection', () => {
render(
<VisualizationSection
value={undefined}
controls={{ timePreference: true }}
controls={{
switchPanelKind: true,
timePreference: true,
}}
onChange={onChange}
/>,
);
@@ -74,7 +94,10 @@ describe('VisualizationSection', () => {
timePreference: DashboardtypesTimePreferenceDTO.global_time,
stackedBarChart: false,
}}
controls={{ stacking: true }}
controls={{
switchPanelKind: true,
stacking: true,
}}
onChange={onChange}
/>,
);
@@ -92,7 +115,10 @@ describe('VisualizationSection', () => {
render(
<VisualizationSection
value={{ fillSpans: false }}
controls={{ fillSpans: true }}
controls={{
switchPanelKind: true,
fillSpans: true,
}}
onChange={onChange}
/>,
);
@@ -101,4 +127,43 @@ 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();
});
});

View File

@@ -4,15 +4,19 @@ 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[] = [
{ 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<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',
},
];

View File

@@ -8,23 +8,35 @@ 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 {
panelType: PANEL_TYPES;
/** 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;
/** 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. */
@@ -41,12 +53,15 @@ interface PanelEditorQueryBuilderProps {
* `QueryBuilderProvider`. `usePanelEditorQuerySync` owns the panel↔provider sync.
*/
function PanelEditorQueryBuilder({
panelType,
panelKind,
signal,
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();
@@ -74,13 +89,15 @@ 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(
() => ({ stepInterval: { isHidden: false, isDisabled: false } }),
[],
() => getHiddenQueryBuilderFields(panelKind, signal),
[panelKind, signal],
);
const items = useMemo(() => {
const supportedQueryTypes = PANEL_TYPE_TO_QUERY_TYPES[panelType] || [];
const supportedQueryTypes = getSupportedQueryTypes(panelKind);
const queryTypeComponents = {
[EQueryType.QUERY_BUILDER]: {
@@ -127,7 +144,7 @@ function PanelEditorQueryBuilder({
),
children: queryTypeComponents[queryType].component,
}));
}, [panelType, filterConfigs, isDarkMode]);
}, [panelKind, panelType, filterConfigs, isDarkMode]);
return (
<div

View File

@@ -0,0 +1,145 @@
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 },
});
});
});

View File

@@ -0,0 +1,98 @@
import {
TelemetrytypesSignalDTO,
type DashboardtypesPanelSpecDTO,
} 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');
});
});

View File

@@ -0,0 +1,70 @@
import type {
DashboardtypesPanelSpecDTO,
TelemetrytypesSignalDTO,
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 {
SectionKind,
type PanelFormattingSlice,
} 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[];
}
/**
* 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 only cross-kind config worth keeping — unit + decimal precision. 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;
}
}
return result;
}

View File

@@ -0,0 +1,194 @@
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);
});
});

View File

@@ -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) => {

View File

@@ -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.
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only snapshot
const savedQueries = useMemo(() => draft.spec?.queries ?? [], []);
const savedQueries = 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;
}

View File

@@ -0,0 +1,134 @@
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 };
}

View File

@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import {
ResizableHandle,
ResizablePanel,
@@ -11,6 +11,7 @@ import {
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,
@@ -18,6 +19,7 @@ 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';
@@ -29,6 +31,7 @@ import { usePanelQuery } from '../hooks/usePanelQuery';
import { usePanelEditorDraft } from './hooks/usePanelEditorDraft';
import { usePanelEditorQuerySync } from './hooks/usePanelEditorQuerySync';
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';
@@ -65,6 +68,10 @@ 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,
@@ -113,6 +120,9 @@ function PanelEditorContainer({
signal: defaultSignal,
});
// Switch the panel's visualization kind in place (reversible per session).
const { onChangePanelKind } = usePanelTypeSwitch({ spec, panelType, setSpec });
// 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;
@@ -121,8 +131,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`).
@@ -146,6 +156,14 @@ 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.
@@ -197,7 +215,8 @@ function PanelEditorContainer({
<ResizableHandle withHandle className={styles.handle} />
<ResizablePanel minSize="35%" maxSize="45%" defaultSize="40%">
<PanelEditorQueryBuilder
panelType={panelType}
panelKind={fullKind}
signal={listSignal}
isLoadingQueries={isFetching}
onStageRunQuery={runQuery}
onCancelQuery={cancelQuery}
@@ -226,8 +245,11 @@ function PanelEditorContainer({
panelKind={draft.spec.plugin.kind}
spec={spec}
onChangeSpec={setSpec}
onChangePanelKind={onChangePanelKind}
queryType={currentQuery.queryType}
legendSeries={legendSeries}
tableColumns={tableColumns}
stepInterval={stepInterval}
/>
</ResizablePanel>
</ResizablePanelGroup>

View File

@@ -0,0 +1,167 @@
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 },
});
});
});
});

View File

@@ -0,0 +1,91 @@
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 };
}

View File

@@ -49,7 +49,7 @@ function BarPanelRenderer({
);
const builderQueries = useMemo(
() => getBuilderQueries(panel.spec.queries || []),
() => getBuilderQueries(panel.spec.queries),
[panel.spec.queries],
);

View File

@@ -2,6 +2,7 @@ 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',
@@ -13,6 +14,12 @@ export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
supportedQueryTypes: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
queryBuilderFields: {},
actions: {
view: true,
edit: true,

View File

@@ -1,12 +1,15 @@
import type { SectionConfig } from '../../types/sections';
import { SectionKind, 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: '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' },
{
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 },
];

View File

@@ -41,7 +41,7 @@ function HistogramPanelRenderer({
);
const builderQueries = useMemo(
() => getBuilderQueries(panel.spec.queries || []),
() => getBuilderQueries(panel.spec.queries),
[panel.spec.queries],
);

View File

@@ -2,6 +2,7 @@ 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',
@@ -13,6 +14,12 @@ export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
supportedQueryTypes: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
queryBuilderFields: {},
actions: {
view: true,
edit: true,

View File

@@ -1,10 +1,14 @@
import type { DashboardtypesHistogramPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import type { SectionConfig } from '../../types/sections';
import { SectionKind, type SectionConfig } from '../../types/sections';
export const sections: SectionConfig[] = [
{
kind: 'legend',
kind: SectionKind.Visualization,
controls: { switchPanelKind: true },
},
{
kind: SectionKind.Legend,
controls: { position: true },
// Merging all queries collapses to one distribution with no legend.
isHidden: (spec): boolean =>
@@ -14,8 +18,8 @@ export const sections: SectionConfig[] = [
),
},
{
kind: 'buckets',
kind: SectionKind.Buckets,
controls: { count: true, width: true, mergeQueries: true },
},
{ kind: 'contextLinks' },
{ kind: SectionKind.ContextLinks },
];

View File

@@ -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],
);

View File

@@ -20,7 +20,7 @@ function panelWith(
): PanelOfKind<'signoz/ListPanel'> {
return {
kind: 'Panel',
spec: { plugin: { kind: 'signoz/ListPanel', spec } },
spec: { plugin: { kind: 'signoz/ListPanel', spec }, queries: [] },
} as unknown as PanelOfKind<'signoz/ListPanel'>;
}

View File

@@ -2,6 +2,8 @@ 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',
@@ -12,6 +14,21 @@ 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,

View File

@@ -1,3 +1,8 @@
import type { SectionConfig } from '../../types/sections';
import { SectionKind, type SectionConfig } from '../../types/sections';
export const sections: SectionConfig[] = [];
export const sections: SectionConfig[] = [
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true },
},
];

View File

@@ -2,6 +2,7 @@ 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/NumberPanel'> = {
kind: 'signoz/NumberPanel',
@@ -13,6 +14,12 @@ export const definition: PanelDefinition<'signoz/NumberPanel'> = {
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
supportedQueryTypes: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
queryBuilderFields: {},
actions: {
view: true,
edit: true,

View File

@@ -1,8 +1,11 @@
import type { SectionConfig } from '../../types/sections';
import { SectionKind, type SectionConfig } from '../../types/sections';
export const sections: SectionConfig[] = [
{ kind: 'visualization', controls: { timePreference: true } },
{ kind: 'formatting', controls: { unit: true, decimals: true } },
{ kind: 'thresholds', controls: { variant: 'comparison' } },
{ kind: 'contextLinks' },
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true, timePreference: true },
},
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
{ kind: SectionKind.Thresholds, controls: { variant: 'comparison' } },
{ kind: SectionKind.ContextLinks },
];

View File

@@ -2,6 +2,7 @@ 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/PieChartPanel'> = {
kind: 'signoz/PieChartPanel',
@@ -13,6 +14,8 @@ export const definition: PanelDefinition<'signoz/PieChartPanel'> = {
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
supportedQueryTypes: [EQueryType.QUERY_BUILDER, EQueryType.CLICKHOUSE],
queryBuilderFields: {},
actions: {
view: true,
edit: true,

View File

@@ -1,10 +1,13 @@
import type { SectionConfig } from '../../types/sections';
import { SectionKind, type SectionConfig } from '../../types/sections';
// Pie has no axes, thresholds, or stacking — just value formatting and a legend.
// Legend `colors` is omitted: the pie legend is always interactive swatches.
export const sections: SectionConfig[] = [
{ kind: 'visualization', controls: { timePreference: true } },
{ kind: 'formatting', controls: { unit: true, decimals: true } },
{ kind: 'legend', controls: { position: true } },
{ kind: 'contextLinks' },
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true, timePreference: true },
},
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
{ kind: SectionKind.Legend, controls: { position: true } },
{ kind: SectionKind.ContextLinks },
];

View File

@@ -2,6 +2,7 @@ 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/TablePanel'> = {
kind: 'signoz/TablePanel',
@@ -13,6 +14,8 @@ export const definition: PanelDefinition<'signoz/TablePanel'> = {
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
supportedQueryTypes: [EQueryType.QUERY_BUILDER, EQueryType.CLICKHOUSE],
queryBuilderFields: {},
// Tables carry tabular data worth exporting (V1 parity: download is table-only).
actions: {
view: true,

View File

@@ -1,11 +1,17 @@
import type { SectionConfig } from '../../types/sections';
import { SectionKind, type SectionConfig } from '../../types/sections';
// A table panel renders one scalar result (the V5 backend joins every query into a
// single column set). It exposes the per-panel time scope, formatting (decimals +
// per-column units), per-column thresholds, and context links.
export const sections: SectionConfig[] = [
{ kind: 'visualization', controls: { timePreference: true } },
{ kind: 'formatting', controls: { decimals: true, columnUnits: true } },
{ kind: 'thresholds', controls: { variant: 'table' } },
{ kind: 'contextLinks' },
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true, timePreference: true },
},
{
kind: SectionKind.Formatting,
controls: { decimals: true, columnUnits: true },
},
{ kind: SectionKind.Thresholds, controls: { variant: 'table' } },
{ kind: SectionKind.ContextLinks },
];

View File

@@ -49,7 +49,7 @@ function TimeSeriesPanelRenderer({
);
const builderQueries = useMemo(
() => getBuilderQueries(panel.spec.queries || []),
() => getBuilderQueries(panel.spec.queries),
[panel.spec.queries],
);

View File

@@ -2,6 +2,7 @@ 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/TimeSeriesPanel'> = {
kind: 'signoz/TimeSeriesPanel',
@@ -13,6 +14,12 @@ export const definition: PanelDefinition<'signoz/TimeSeriesPanel'> = {
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
supportedQueryTypes: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
queryBuilderFields: {},
actions: {
view: true,
edit: true,

View File

@@ -1,12 +1,15 @@
import type { SectionConfig } from '../../types/sections';
import { SectionKind, type SectionConfig } from '../../types/sections';
export const sections: SectionConfig[] = [
{ kind: 'visualization', controls: { timePreference: true, fillSpans: true } },
{ kind: 'formatting', controls: { unit: true, decimals: true } },
{ kind: 'axes', controls: { minMax: true, logScale: true } },
{ kind: 'legend', controls: { position: true, colors: true } },
{
kind: 'chartAppearance',
kind: SectionKind.Visualization,
controls: { switchPanelKind: true, timePreference: true, fillSpans: true },
},
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
{ kind: SectionKind.Axes, controls: { minMax: true, logScale: true } },
{ kind: SectionKind.Legend, controls: { position: true, colors: true } },
{
kind: SectionKind.ChartAppearance,
controls: {
lineStyle: true,
lineInterpolation: true,
@@ -15,6 +18,6 @@ export const sections: SectionConfig[] = [
spanGaps: true,
},
},
{ kind: 'thresholds', controls: { variant: 'label' } },
{ kind: 'contextLinks' },
{ kind: SectionKind.Thresholds, controls: { variant: 'label' } },
{ kind: SectionKind.ContextLinks },
];

View File

@@ -0,0 +1,20 @@
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import type { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
/**
* Query-builder field-visibility config a panel kind can declare, mirroring the
* shape `QueryBuilderV2` consumes via its `filterConfigs` prop. Derived from that
* prop type (the underlying `FilterConfigs` isn't exported) so the two never drift.
*/
export type FilterConfigsPartial = NonNullable<
QueryBuilderProps['filterConfigs']
>;
/**
* Per-signal query-builder field rules for a panel kind. `default` applies to every
* signal; a per-signal entry is merged over it (signal wins). The capabilities guard
* resolves this into a single `FilterConfigsPartial` via `getHiddenQueryBuilderFields`.
*/
export type QueryBuilderFieldRule = {
default?: FilterConfigsPartial;
} & Partial<Record<TelemetrytypesSignalDTO, FilterConfigsPartial>>;

View File

@@ -1,9 +1,11 @@
import type { ComponentType } from 'react';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import type { EQueryType } from 'types/common/dashboard';
import type { SectionConfig } from './sections';
import type { AnyPanelInteractionProps } from './interactions';
import type { PanelKind } from './panelKind';
import type { QueryBuilderFieldRule } from './panelCapabilities';
import type { BaseRendererProps, PanelRendererProps } from './rendererProps';
/**
@@ -35,7 +37,12 @@ export interface PanelDefinition<K extends PanelKind = PanelKind> {
displayName: string;
Renderer: ComponentType<PanelRendererProps<K>>;
sections: SectionConfig[];
/** Signals this kind can visualize. */
supportedSignals: TelemetrytypesSignalDTO[];
/** Query languages this kind supports (Query Builder / ClickHouse / PromQL). */
supportedQueryTypes: EQueryType[];
/** Query-builder fields this kind hides/disables, optionally per signal (`{}` hides none). */
queryBuilderFields: QueryBuilderFieldRule;
actions: PanelActionCapabilities;
}

View File

@@ -35,6 +35,22 @@ export interface SectionMetadata {
description?: string;
}
/**
* Discriminant for each config section (the `kind` field of a `SectionConfig`). The
* string values match the keys each slice persists under in the plugin spec.
*/
export enum SectionKind {
Formatting = 'formatting',
Axes = 'axes',
Legend = 'legend',
ChartAppearance = 'chartAppearance',
Buckets = 'buckets',
Visualization = 'visualization',
Thresholds = 'thresholds',
ContextLinks = 'contextLinks',
Columns = 'columns',
}
/**
* Which threshold editor a kind uses. All three variants persist to the same
* `plugin.spec.thresholds` key with different element shapes:
@@ -60,17 +76,17 @@ export type PanelFormattingSlice = DashboardtypesPanelFormattingDTO &
Pick<DashboardtypesTableFormattingDTO, 'columnUnits'>;
export interface SectionSpecMap {
formatting: PanelFormattingSlice; // spec.plugin.spec.formatting
axes: DashboardtypesAxesDTO; // spec.plugin.spec.axes
legend: DashboardtypesLegendDTO; // spec.plugin.spec.legend
chartAppearance: DashboardtypesTimeSeriesChartAppearanceDTO; // spec.plugin.spec.chartAppearance
buckets: DashboardtypesHistogramBucketsDTO; // spec.plugin.spec.histogramBuckets
[SectionKind.Formatting]: PanelFormattingSlice; // spec.plugin.spec.formatting
[SectionKind.Axes]: DashboardtypesAxesDTO; // spec.plugin.spec.axes
[SectionKind.Legend]: DashboardtypesLegendDTO; // spec.plugin.spec.legend
[SectionKind.ChartAppearance]: DashboardtypesTimeSeriesChartAppearanceDTO; // spec.plugin.spec.chartAppearance
[SectionKind.Buckets]: DashboardtypesHistogramBucketsDTO; // spec.plugin.spec.histogramBuckets
// spec.plugin.spec.visualization — typed as the Bar shape (widest superset);
// the `controls` bag gates which fields each kind writes.
visualization: DashboardtypesBarChartVisualizationDTO;
thresholds: AnyThreshold[]; // spec.plugin.spec.thresholds (variant picks the editor)
contextLinks: DashboardLinkDTO[]; // spec.links (PANEL-level)
columns: TelemetrytypesTelemetryFieldKeyDTO[]; // spec.plugin.spec.selectFields (List)
[SectionKind.Visualization]: DashboardtypesBarChartVisualizationDTO;
[SectionKind.Thresholds]: AnyThreshold[]; // spec.plugin.spec.thresholds (variant picks the editor)
[SectionKind.ContextLinks]: DashboardLinkDTO[]; // spec.links (PANEL-level)
[SectionKind.Columns]: TelemetrytypesTelemetryFieldKeyDTO[]; // spec.plugin.spec.selectFields (List)
}
/**
@@ -78,33 +94,42 @@ export interface SectionSpecMap {
* analogue of V1's `allowSoftMinMax` / `allowLegendColors` flags).
*/
export interface SectionControls {
formatting: { unit?: boolean; decimals?: boolean; columnUnits?: boolean };
axes: { minMax?: boolean; logScale?: boolean }; // minMax → softMin/softMax
legend: { position?: boolean; colors?: boolean }; // colors → customColors
chartAppearance: {
[SectionKind.Formatting]: {
unit?: boolean;
decimals?: boolean;
columnUnits?: boolean;
};
[SectionKind.Axes]: { minMax?: boolean; logScale?: boolean }; // minMax → softMin/softMax
[SectionKind.Legend]: { position?: boolean; colors?: boolean }; // colors → customColors
[SectionKind.ChartAppearance]: {
lineStyle?: boolean;
lineInterpolation?: boolean;
fillMode?: boolean;
showPoints?: boolean;
spanGaps?: boolean;
};
buckets: { count?: boolean; width?: boolean; mergeQueries?: boolean };
// stacking → stackedBarChart (Bar); fillSpans → fill gaps with 0 (TimeSeries).
visualization: {
[SectionKind.Buckets]: {
count?: boolean;
width?: boolean;
mergeQueries?: boolean;
};
// switchPanelKind → the visualization-type switcher (every kind, so you can switch
// away from any panel); stacking → stackedBarChart (Bar); fillSpans → fill gaps with
// 0 (TimeSeries).
[SectionKind.Visualization]: {
switchPanelKind: boolean;
timePreference?: boolean;
stacking?: boolean;
fillSpans?: boolean;
};
// Editor discriminator (not a spec field): which threshold variant a kind edits.
thresholds: { variant?: ThresholdVariant };
[SectionKind.Thresholds]: { variant?: ThresholdVariant };
}
export type ControlledSectionKind = keyof SectionControls;
/** Atomic sections — no sub-controls; a kind either shows them or not. */
export type AtomicSectionKind = 'contextLinks' | 'columns';
export type SectionKind = ControlledSectionKind | AtomicSectionKind;
export type AtomicSectionKind = SectionKind.ContextLinks | SectionKind.Columns;
/** Predicate to hide a section from the current spec; returning true removes it. */
export type SectionVisibilityPredicate = (
@@ -128,15 +153,15 @@ export type SectionConfig =
// Per-section title + sidebar icon. Pure data; the editor component + spec lens
// live in the ConfigPane section registry.
export const SECTION_METADATA = {
formatting: { title: 'Formatting', icon: Hash },
axes: { title: 'Axes', icon: Ruler },
legend: { title: 'Legend', icon: Layers },
chartAppearance: { title: 'Chart appearance', icon: Palette },
visualization: { title: 'Visualization', icon: LayoutDashboard },
buckets: { title: 'Histogram / Buckets', icon: BarChart },
thresholds: { title: 'Thresholds', icon: SlidersHorizontal },
contextLinks: { title: 'Context Links', icon: Link },
columns: { title: 'Columns', icon: Columns3 },
[SectionKind.Formatting]: { title: 'Formatting & Units', icon: Hash },
[SectionKind.Axes]: { title: 'Axes', icon: Ruler },
[SectionKind.Legend]: { title: 'Legend', icon: Layers },
[SectionKind.ChartAppearance]: { title: 'Chart appearance', icon: Palette },
[SectionKind.Visualization]: { title: 'Visualization', icon: LayoutDashboard },
[SectionKind.Buckets]: { title: 'Histogram / Buckets', icon: BarChart },
[SectionKind.Thresholds]: { title: 'Thresholds', icon: SlidersHorizontal },
[SectionKind.ContextLinks]: { title: 'Context Links', icon: Link },
[SectionKind.Columns]: { title: 'Columns', icon: Columns3 },
} as const satisfies Record<SectionKind, SectionMetadata>;
/**

View File

@@ -10,7 +10,7 @@ import { sections as barSections } from '../../kinds/BarChartPanel/sections';
import { sections as histogramSections } from '../../kinds/HistogramPanel/sections';
import { sections as listSections } from '../../kinds/ListPanel/sections';
import { sections as timeSeriesSections } from '../../kinds/TimeSeriesPanel/sections';
import type { SectionConfig } from '../../types/sections';
import { SectionKind, type SectionConfig } from '../../types/sections';
import { buildDefaultPluginSpec } from '../buildDefaultPluginSpec';
describe('buildDefaultPluginSpec', () => {
@@ -50,17 +50,17 @@ describe('buildDefaultPluginSpec', () => {
it('does not seed controls that already show a clear default', () => {
// `axes` and `formatting` stay unset — their empty state is the chart default.
const sections: SectionConfig[] = [
{ kind: 'axes', controls: { minMax: true, logScale: true } },
{ kind: 'formatting', controls: { unit: true, decimals: true } },
{ kind: 'thresholds', controls: { variant: 'label' } },
{ kind: 'contextLinks' },
{ kind: SectionKind.Axes, controls: { minMax: true, logScale: true } },
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
{ kind: SectionKind.Thresholds, controls: { variant: 'label' } },
{ kind: SectionKind.ContextLinks },
];
expect(buildDefaultPluginSpec(sections)).toStrictEqual({});
});
it('only seeds the legend position when the kind exposes that control', () => {
const sections: SectionConfig[] = [
{ kind: 'legend', controls: { colors: true } },
{ kind: SectionKind.Legend, controls: { colors: true } },
];
expect(buildDefaultPluginSpec(sections)).toStrictEqual({});
});

View File

@@ -0,0 +1,30 @@
import type { DashboardtypesQueryDTO } from 'api/generated/services/sigNoz.schemas';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { resolveSignal } from '../getBuilderQueries';
function builderQuery(signal: string): DashboardtypesQueryDTO {
return {
spec: { plugin: { kind: 'signoz/BuilderQuery', spec: { signal } } },
} as unknown as DashboardtypesQueryDTO;
}
const promqlQuery = {
spec: { plugin: { kind: 'signoz/PromQuery', spec: { query: 'up' } } },
} as unknown as DashboardtypesQueryDTO;
describe('resolveSignal', () => {
const DEFAULT = TelemetrytypesSignalDTO.metrics;
it("uses the first builder query's signal when present", () => {
expect(resolveSignal([builderQuery('logs')], DEFAULT)).toBe('logs');
});
it('prefers the builder signal over the default', () => {
expect(resolveSignal([builderQuery('traces')], DEFAULT)).toBe('traces');
});
it('stays undefined when queries exist but none are builder queries (PromQL/ClickHouse)', () => {
expect(resolveSignal([promqlQuery], DEFAULT)).toBeUndefined();
});
});

View File

@@ -6,7 +6,11 @@ import {
DashboardtypesTimePreferenceDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { SectionConfig, SectionSpecMap } from '../types/sections';
import {
SectionKind,
type SectionConfig,
type SectionSpecMap,
} from '../types/sections';
/**
* Seeded plugin-spec slices, typed as canonical section slices so each value is
@@ -14,9 +18,9 @@ import type { SectionConfig, SectionSpecMap } from '../types/sections';
* so the union cast stays localized to `createDefaultPanel`.
*/
export interface DefaultPluginSpec {
visualization?: SectionSpecMap['visualization'];
legend?: SectionSpecMap['legend'];
chartAppearance?: SectionSpecMap['chartAppearance'];
visualization?: SectionSpecMap[SectionKind.Visualization];
legend?: SectionSpecMap[SectionKind.Legend];
chartAppearance?: SectionSpecMap[SectionKind.ChartAppearance];
}
/**
@@ -31,20 +35,20 @@ export function buildDefaultPluginSpec(
sections.forEach((section) => {
switch (section.kind) {
case 'visualization':
case SectionKind.Visualization:
if (section.controls.timePreference) {
spec.visualization = {
timePreference: DashboardtypesTimePreferenceDTO.global_time,
};
}
break;
case 'legend':
case SectionKind.Legend:
if (section.controls.position) {
spec.legend = { position: DashboardtypesLegendPositionDTO.bottom };
}
break;
case 'chartAppearance': {
const chartAppearance: SectionSpecMap['chartAppearance'] = {};
case SectionKind.ChartAppearance: {
const chartAppearance: SectionSpecMap[SectionKind.ChartAppearance] = {};
if (section.controls.lineStyle) {
chartAppearance.lineStyle = DashboardtypesLineStyleDTO.solid;
}

View File

@@ -0,0 +1,22 @@
import { resolveSpanGaps } from '../resolvers';
describe('resolveSpanGaps', () => {
it('spans all gaps (true) when unset', () => {
expect(resolveSpanGaps(undefined)).toBe(true);
expect(resolveSpanGaps('')).toBe(true);
});
it('parses a duration string into seconds', () => {
expect(resolveSpanGaps('5s')).toBe(5);
expect(resolveSpanGaps('10m')).toBe(600);
expect(resolveSpanGaps('1h')).toBe(3600);
});
it('tolerates a bare seconds number (back-compat)', () => {
expect(resolveSpanGaps('600')).toBe(600);
});
it('falls back to true for unparseable input', () => {
expect(resolveSpanGaps('abc')).toBe(true);
});
});

View File

@@ -1,3 +1,4 @@
import { rangeUtil } from '@grafana/data';
import {
DashboardtypesLegendPositionDTO,
DashboardtypesPrecisionOptionDTO,
@@ -38,9 +39,10 @@ export function resolveDecimalPrecision(
}
/**
* `spec.chartAppearance.spanGaps.fillLessThan` is a stringified number on the
* wire. Empty/missing → span all gaps (default); numeric → forward the threshold
* so uPlot only bridges short runs of nulls.
* `spec.chartAppearance.spanGaps.fillLessThan` is a duration string on the wire
* ("10m", "5s"). Empty/missing → span all gaps (default); otherwise forward the
* threshold in seconds so uPlot only bridges short runs of nulls. Tolerates a
* bare seconds number for back-compat.
*/
export function resolveSpanGaps(
fillLessThan: string | undefined,
@@ -48,8 +50,10 @@ export function resolveSpanGaps(
if (!fillLessThan) {
return true;
}
const parsed = Number(fillLessThan);
return Number.isFinite(parsed) ? parsed : true;
const seconds = rangeUtil.isValidTimeSpan(fillLessThan)
? rangeUtil.intervalToSeconds(fillLessThan)
: Number(fillLessThan);
return Number.isFinite(seconds) && seconds > 0 ? seconds : true;
}
/** Legend position; missing/unknown falls back to `BOTTOM` (chart default, V1 parity). */

Some files were not shown because too many files have changed in this diff Show More