Compare commits

..

117 Commits

Author SHA1 Message Date
Aditya Singh
f8b16e1034 feat: minor refactor 2025-08-05 22:45:15 +05:30
Aditya Singh
749dff2200 feat: minor refactor 2025-08-05 20:03:33 +05:30
Aditya Singh
de05394859 feat: fix header color 2025-08-05 17:56:41 +05:30
Aditya Singh
a6a9bf5bad Merge branch 'feat/drilldowns' of github.com:SigNoz/signoz into feat/drilldowns 2025-08-05 17:37:50 +05:30
Aditya Singh
e767c229aa Merge branch 'main' of github.com:SigNoz/signoz into feat/drilldowns 2025-08-05 17:37:31 +05:30
Aditya Singh
b9cf516201 feat: aggregation header val 2025-08-05 17:30:34 +05:30
Aditya Singh
f87e80a0f5 Merge branch 'main' into feat/drilldowns 2025-08-05 13:41:29 +05:30
Aditya Singh
f114d0249d feat: revert qbv5 2025-08-05 13:32:35 +05:30
Aditya Singh
b4fbd7c673 feat: snapshot update 2025-08-05 12:01:30 +05:30
Aditya Singh
e25d625c4b feat: minor refactor 2025-08-05 11:39:22 +05:30
Aditya Singh
9ca0cc90b0 Merge branch 'main' of github.com:SigNoz/signoz into feat/drilldowns 2025-08-04 19:58:31 +05:30
Aditya Singh
90758dbd32 feat: context menu hook refactor 2025-08-01 15:11:51 +05:30
Aditya Singh
9b559d6251 feat: context menu - increase width and add overlay 2025-07-19 15:16:47 +05:30
Aditya Singh
bdfb712395 feat: add search to breakout and other refactor 2025-07-19 14:39:55 +05:30
Aditya Singh
0d2a4b397a feat: lint fix 2025-07-17 02:03:59 +05:30
Aditya Singh
2c9a51c2ac feat: update click plugin in uplot 2025-07-17 01:43:24 +05:30
Aditya Singh
fb43f12a76 feat: refactor code 2025-07-17 01:05:37 +05:30
Aditya Singh
60e0e84237 feat: drilldown prop drilldowned 2025-07-16 20:29:29 +05:30
Aditya Singh
54d46a1d03 feat: minor refactor 2025-07-16 20:05:26 +05:30
Aditya Singh
73a7246a11 feat: remove unwanted code 2025-07-16 19:47:23 +05:30
Aditya Singh
163d59bf71 feat: add time range to timeseries, bar charts 2025-07-16 17:04:40 +05:30
Aditya Singh
fb672eda11 feat: add drilldown options in uplot 2025-07-16 02:46:42 +05:30
Aditya Singh
43a432b22b feat: add drilldown options in pie chart 2025-07-16 02:20:21 +05:30
Aditya Singh
8107946cb1 feat: added click data utils for uplot and pie charts 2025-07-16 02:16:39 +05:30
Aditya Singh
38ee4aae30 feat: add graph context hook 2025-07-16 02:09:27 +05:30
Aditya Singh
001d9ed9fb feat: fix style 2025-07-16 02:08:44 +05:30
Aditya Singh
e1abae91a3 feat: aggreagate drilldown refactor to use for tables and other panels alike 2025-07-15 20:49:31 +05:30
Aditya Singh
a9ac3b7e15 feat: aggreagate drilldown refactor to use for tables and other panels alike 2025-07-15 19:29:26 +05:30
Aditya Singh
4a98c54e78 feat: minor refactor 2025-07-14 18:06:00 +05:30
Aditya Singh
9ed4a09caf feat: update coordinates fn signature 2025-07-14 16:14:09 +05:30
Aditya Singh
132a31852f fix: remove number data type conversion 2025-07-14 15:08:13 +05:30
Aditya Singh
5686697b6c feat: fix aggreagate context header 2025-07-09 02:43:35 +05:30
Aditya Singh
5f4fc12031 feat: fix datatype 2025-07-09 02:09:24 +05:30
Aditya Singh
fe2c42de90 feat: hide drilldown for non-builder queries 2025-07-09 01:43:25 +05:30
Aditya Singh
d8f2cf1c0e feat: fix metrics view 2025-07-09 01:11:12 +05:30
Aditya Singh
a7e8f31561 feat: style fix 2025-07-08 21:29:58 +05:30
Aditya Singh
d9d6e7b4f1 feat: show reset query 2025-07-08 19:37:43 +05:30
Aditya Singh
f8f1a26a43 feat: style fix 2025-07-08 18:57:19 +05:30
Aditya Singh
79dfd6f17f feat: breakout drilldown option added 2025-07-08 15:31:13 +05:30
Aditya Singh
f386662e00 feat: aggregate col drilldown added 2025-07-03 18:51:36 +05:30
Aditya Singh
b2de302262 feat: context menu config refactor 2025-07-03 14:29:43 +05:30
Aditya Singh
6f63076b8e feat: context menu style fix 2025-07-03 01:24:34 +05:30
Aditya Singh
8007f954e5 feat: use context menu item for filters 2025-07-03 00:54:58 +05:30
Aditya Singh
b39b24c46f feat: context menu style update 2025-07-03 00:53:54 +05:30
Aditya Singh
70472c587d feat: filter drilldown added 2025-07-02 16:02:13 +05:30
Aditya Singh
06e89b7199 feat: added context menu 2025-06-27 11:26:00 +05:30
Aditya Singh
d60ac0d0e1 fix: fix composite query delete on close 2025-06-27 11:25:24 +05:30
Aditya Singh
1e4c213df4 feat: view mode enhancements 2025-06-27 11:24:47 +05:30
SagarRajput-7
9bf112cfcf Merge branch 'main' into feat/query-builder-v2 2025-06-25 16:19:10 +05:30
SagarRajput-7
a611b8f429 feat: new query builder misc fixes (#8359)
* feat: qb fixes

* feat: fixed handlerunquery props

* feat: fixes logs list order by

* feat: fix logs order by issue

* feat: safety check and order by correction

* feat: updated version in new create dashboards

* feat: added new formatOptions for table and fixed the pie chart plotting

* feat: keyboard shortcut overriding issue and pie ch correction in dashboard views

* feat: fixed dashboard data state management across datasource * paneltypes

* feat: fixed explorer pages data management issues

* feat: integrated new backend payload/request diff, to the UI types

* feat: fixed the collapse behaviour of QB - queries

* feat: fix order by and default aggregation to count()
2025-06-25 16:18:15 +05:30
SagarRajput-7
872230169c feat: resolved conflicts 2025-06-25 05:26:52 +05:30
SagarRajput-7
4a28954074 Query builder misc - fixes (#8295)
* feat: trace and logs explorer fixes

* fix: ui fixes

* fix: handle multi arg aggregation

* feat: explorer pages fixes

* feat: added fixes for order by for datasource

* feat: metric order by issue

* feat: support for paneltype selectedview tab switch

* feat: qb v2 compatiblity with url's composite query

* feat: conversion fixes

* feat: where clause and aggregation fix

---------

Co-authored-by: Yunus M <myounis.ar@live.com>
2025-06-25 05:17:57 +05:30
Yunus M
0df2d9e6da feat: fetch more keys is complete list not already fetched 2025-06-25 05:16:45 +05:30
SagarRajput-7
67f412477c feat: query_range migration from v3/v4 -> v5 (#8192)
* feat: query_range migration from v3/v4 -> v5

* feat: cleanup files

* feat: cleanup code

* feat: metric payload improvements

* feat: metric payload improvements

* feat: data retention and qb v2 for dashboard cleanup

* feat: corrected datasource change daata updatation in qb v2

* feat: fix value panel plotting with new query v5

* feat: alert migration

* feat: fixed aggregation css

* feat: explorer pages migration

* feat: trace and logs explorer fixes
2025-06-25 05:16:45 +05:30
Yunus M
43dc060950 fix: responsiveness issues 2025-06-25 05:16:45 +05:30
Yunus M
a21ae43a1f feat: where clause key updates 2025-06-25 05:16:45 +05:30
Yunus M
331a8b386f feat: update styles for light mode 2025-06-25 05:16:45 +05:30
Yunus M
ca6c7afa5c feat: show errors 2025-06-25 05:16:45 +05:30
Yunus M
dc8e5d6df9 feat: update context and show suggestions on select 2025-06-25 05:16:45 +05:30
Yunus M
c68f352aeb feat: add a space after selecting a value from suggestion 2025-06-25 05:16:45 +05:30
Yunus M
7863877a49 feat: improve suggestion ux in query search 2025-06-25 05:16:45 +05:30
Yunus M
76384c2430 feat: ui improvements 2025-06-25 05:16:45 +05:30
Yunus M
4e06d7757b feat: handle close on blur 2025-06-25 05:16:45 +05:30
Yunus M
5c06429ebe feat: query search component clean up 2025-06-25 05:16:45 +05:30
Yunus M
aefc7940a7 feat: handle having option autocomplete ux 2025-06-25 05:16:45 +05:30
Yunus M
0deae0c73b feat: disable clicking on placeholder items in suggestions 2025-06-25 05:16:45 +05:30
Yunus M
a4c16e5847 feat: improve having suggestions 2025-06-25 05:16:45 +05:30
Yunus M
efb741cf35 feat: handle add ons 2025-06-25 05:16:45 +05:30
Yunus M
153f64067c feat: handle list panel type options 2025-06-25 05:16:45 +05:30
Yunus M
c83ae1a485 feat: pass index to query addons 2025-06-25 05:16:45 +05:30
Yunus M
bfd74fb906 feat: update qb elements based on panel type 2025-06-25 05:16:45 +05:30
Yunus M
5d56f05fab feat: hide extra qb elements 2025-06-25 05:16:45 +05:30
Yunus M
57ca53c74c feat: use qb-v2 in explorers and alerts 2025-06-25 05:16:42 +05:30
Yunus M
bde078472b feat: update explorer views 2025-06-25 05:16:09 +05:30
Yunus M
6deb75ff46 feat: update logs, metrics and traces qb 2025-06-25 05:10:59 +05:30
Yunus M
424fd0362d feat: query builder layout updates 2025-06-25 05:10:59 +05:30
Yunus M
1bc51102f6 fix: minor fixes 2025-06-25 05:10:58 +05:30
Yunus M
c1b70c05f1 feat: create separate containers for traces, logs and metrics qbs 2025-06-25 05:10:58 +05:30
Yunus M
8fce0ab1af feat: metrics qb 2025-06-25 05:10:57 +05:30
Yunus M
df1923a7c6 fix: update dropdown css 2025-06-25 05:10:05 +05:30
Yunus M
1e37ae2fd0 feat: remove () from suggestions 2025-06-25 05:10:05 +05:30
Yunus M
7b3ea5cc45 feat: handle parenthesis and conjunction operators 2025-06-25 05:10:05 +05:30
Yunus M
167ddc6c56 feat: support multiple having key value pairs 2025-06-25 05:10:05 +05:30
Yunus M
dbc1e1fc45 feat: move state to context 2025-06-25 05:10:05 +05:30
Yunus M
01e798f3c1 feat: handle having options creation 2025-06-25 05:10:05 +05:30
Yunus M
d9010fb3fc feat: hide already used variables 2025-06-25 05:10:05 +05:30
Yunus M
06363f2e5b fix: show operator suggestions only on manual trigger or valid key 2025-06-25 05:10:05 +05:30
Yunus M
f1853a6bca fix: handle autocomplete 2025-06-25 05:10:05 +05:30
Yunus M
97e9f5dc8d fix: update styles 2025-06-25 05:10:05 +05:30
Yunus M
3b959bd2f6 fix: update css 2025-06-25 05:10:05 +05:30
Yunus M
9662e43418 feat: handle multie select functions 2025-06-25 05:10:05 +05:30
Yunus M
736bb2ebfb feat: handle field suggestions for aggregate operators 2025-06-25 05:10:05 +05:30
Yunus M
879700ea7a feat: support aggregation function with values 2025-06-25 05:10:05 +05:30
Yunus M
438ffe45f2 feat: add groupBy, having, order by, limit and legend format 2025-06-25 05:10:05 +05:30
Yunus M
723b6b6b79 feat: handle multie select values better 2025-06-25 05:10:05 +05:30
Yunus M
d2df098bb3 feat: improve suggestions 2025-06-25 05:10:05 +05:30
Yunus M
196ae10f00 feat: console log context based on cursor position 2025-06-25 05:10:05 +05:30
Yunus M
00eba89e20 fix: handle . notation keywords better 2025-06-25 05:10:05 +05:30
Yunus M
1739a9e27b feat: remove card container above where clause 2025-06-25 05:10:05 +05:30
Yunus M
cfdf714ffa feat: use new qb in logs explorer 2025-06-25 05:10:04 +05:30
Yunus M
49e78b6998 feat: handle parenthesis 2025-06-25 05:10:04 +05:30
Yunus M
762c658c10 feat: handle value selection 2025-06-25 05:10:04 +05:30
Yunus M
48e7e33dea feat: styling updates 2025-06-25 05:10:04 +05:30
Yunus M
dc4996c127 feat: handle string and number values correctly 2025-06-25 05:10:04 +05:30
Yunus M
d95f7b976c feat: handle async value fetching 2025-06-25 05:10:04 +05:30
Yunus M
9a47883064 feat: update the context with additonal properties 2025-06-25 05:10:04 +05:30
Yunus M
39a90fd33c feat: styling updates 2025-06-25 05:10:04 +05:30
Yunus M
722c3482d2 feat: update theme and syntax highlighting 2025-06-25 05:10:04 +05:30
Yunus M
60e84e6681 feat: handle context switch 2025-06-25 05:10:04 +05:30
Yunus M
8d1fa84e6a feat: handle multiple spaces 2025-06-25 05:10:04 +05:30
Yunus M
6c22197bf4 feat: integrate the apis 2025-06-25 05:10:04 +05:30
Yunus M
f6c426d0cc feat: update context logic and return auto-suggestions based on context 2025-06-25 05:10:04 +05:30
Yunus M
e21757b2bd feat: add apis and hooks 2025-06-25 05:10:04 +05:30
Yunus M
a87fbabbe7 feat: update context to recognise conjunction operator 2025-06-25 05:10:04 +05:30
Yunus M
b2847cb05b feat: add codemirror 2025-06-25 05:10:00 +05:30
Yunus M
0b575b41a1 feat: add types, base components 2025-06-25 05:08:52 +05:30
Yunus M
0a3fd7a7dc feat: add antlr4, parser files and grammar 2025-06-25 05:08:52 +05:30
309 changed files with 5030 additions and 9229 deletions

View File

@@ -40,7 +40,7 @@ services:
timeout: 5s
retries: 3
schema-migrator-sync:
image: signoz/signoz-schema-migrator:v0.129.0
image: signoz/signoz-schema-migrator:v0.128.2
container_name: schema-migrator-sync
command:
- sync
@@ -53,7 +53,7 @@ services:
condition: service_healthy
restart: on-failure
schema-migrator-async:
image: signoz/signoz-schema-migrator:v0.129.0
image: signoz/signoz-schema-migrator:v0.128.2
container_name: schema-migrator-async
command:
- async

View File

@@ -1,29 +0,0 @@
services:
signoz-otel-collector:
image: signoz/signoz-otel-collector:v0.128.2
container_name: signoz-otel-collector-dev
command:
- --config=/etc/otel-collector-config.yaml
- --feature-gates=-pkg.translator.prometheus.NormalizeName
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- LOW_CARDINAL_EXCEPTION_GROUPING=false
ports:
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
- "13133:13133" # health check extension
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- localhost:13133
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"

View File

@@ -1,96 +0,0 @@
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-collector
processors:
batch:
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
resourcedetection:
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
detectors: [env, system]
timeout: 2s
signozspanmetrics/delta:
metrics_exporter: signozclickhousemetrics
metrics_flush_interval: 60s
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
dimensions_cache_size: 100000
aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA
enable_exp_histogram: true
dimensions:
- name: service.namespace
default: default
- name: deployment.environment
default: default
# This is added to ensure the uniqueness of the timeseries
# Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics
- name: signoz.collector.id
- name: service.version
- name: browser.platform
- name: browser.mobile
- name: k8s.cluster.name
- name: k8s.node.name
- name: k8s.namespace.name
- name: host.name
- name: host.type
- name: container.name
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 0.0.0.0:1777
exporters:
clickhousetraces:
datasource: tcp://host.docker.internal:9000/signoz_traces
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
use_new_schema: true
signozclickhousemetrics:
dsn: tcp://host.docker.internal:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://host.docker.internal:9000/signoz_logs
timeout: 10s
use_new_schema: true
service:
telemetry:
logs:
encoding: json
extensions:
- health_check
- pprof
pipelines:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter]

View File

@@ -61,17 +61,6 @@ devenv-postgres: ## Run postgres in devenv
@cd .devenv/docker/postgres; \
docker compose -f compose.yaml up -d
.PHONY: devenv-signoz-otel-collector
devenv-signoz-otel-collector: ## Run signoz-otel-collector in devenv (requires clickhouse to be running)
@cd .devenv/docker/signoz-otel-collector; \
docker compose -f compose.yaml up -d
.PHONY: devenv-up
devenv-up: devenv-clickhouse devenv-signoz-otel-collector ## Start both clickhouse and signoz-otel-collector for local development
@echo "Development environment is ready!"
@echo " - ClickHouse: http://localhost:8123"
@echo " - Signoz OTel Collector: grpc://localhost:4317, http://localhost:4318"
##############################################################
# go commands
##############################################################

View File

@@ -121,8 +121,6 @@ telemetrystore:
timeout_before_checking_execution_speed: 0
max_bytes_to_read: 0
max_result_rows: 0
ignore_data_skipping_indices: ""
secondary_indices_enable_bulk_filtering: false
##################### Prometheus #####################
prometheus:

View File

@@ -174,7 +174,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.92.1
image: signoz/signoz:v0.91.0
command:
- --config=/root/config/prometheus.yml
ports:
@@ -207,7 +207,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.129.0
image: signoz/signoz-otel-collector:v0.128.2
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -231,7 +231,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.129.0
image: signoz/signoz-schema-migrator:v0.128.2
deploy:
restart_policy:
condition: on-failure

View File

@@ -115,7 +115,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.92.1
image: signoz/signoz:v0.91.0
command:
- --config=/root/config/prometheus.yml
ports:
@@ -148,7 +148,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.129.0
image: signoz/signoz-otel-collector:v0.128.2
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -174,7 +174,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.129.0
image: signoz/signoz-schema-migrator:v0.128.2
deploy:
restart_policy:
condition: on-failure

View File

@@ -177,7 +177,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.92.1}
image: signoz/signoz:${VERSION:-v0.91.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -211,7 +211,7 @@ services:
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.0}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.2}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -237,7 +237,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
container_name: schema-migrator-sync
command:
- sync
@@ -248,7 +248,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
container_name: schema-migrator-async
command:
- async

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.92.1}
image: signoz/signoz:${VERSION:-v0.91.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -143,7 +143,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.0}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.2}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -165,7 +165,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
container_name: schema-migrator-sync
command:
- sync
@@ -177,7 +177,7 @@ services:
restart: on-failure
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
container_name: schema-migrator-async
command:
- async

View File

@@ -44,35 +44,20 @@ Before diving in, make sure you have these tools installed:
SigNoz has three main components: Clickhouse, Backend, and Frontend. Let's set them up one by one.
### 1. Setting up ClickHouse
### 1. Setting up Clickhouse
First, we need to get ClickHouse running:
First, we need to get Clickhouse running:
```bash
make devenv-clickhouse
```
This command:
- Starts ClickHouse in a single-shard, single-replica cluster
- Starts Clickhouse in a single-shard, single-replica cluster
- Sets up Zookeeper
- Runs the latest schema migrations
### 2. Setting up SigNoz OpenTelemetry Collector
Next, start the OpenTelemetry Collector to receive telemetry data:
```bash
make devenv-signoz-otel-collector
```
This command:
- Starts the SigNoz OpenTelemetry Collector
- Listens on port 4317 (gRPC) and 4318 (HTTP) for incoming telemetry data
- Forwards data to ClickHouse for storage
> 💡 **Quick Setup**: Use `make devenv-up` to start both ClickHouse and OTel Collector together
### 3. Starting the Backend
### 2. Starting the Backend
1. Run the backend server:
```bash
@@ -88,7 +73,7 @@ This command:
> 💡 **Tip**: The API server runs at `http://localhost:8080/` by default
### 4. Setting up the Frontend
### 3. Setting up the Frontend
1. Navigate to the frontend directory:
```bash
@@ -113,25 +98,3 @@ This command:
> 💡 **Tip**: `yarn dev` will automatically rebuild when you make changes to the code
Now you're all set to start developing! Happy coding! 🎉
## Verifying Your Setup
To verify everything is working correctly:
1. **Check ClickHouse**: `curl http://localhost:8123/ping` (should return "Ok.")
2. **Check OTel Collector**: `curl http://localhost:13133` (should return health status)
3. **Check Backend**: `curl http://localhost:8080/api/v1/health` (should return `{"status":"ok"}`)
4. **Check Frontend**: Open `http://localhost:3301` in your browser
## How to send test data?
You can now send telemetry data to your local SigNoz instance:
- **OTLP gRPC**: `localhost:4317`
- **OTLP HTTP**: `localhost:4318`
For example, using `curl` to send a test trace:
```bash
curl -X POST http://localhost:4318/v1/traces \
-H "Content-Type: application/json" \
-d '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"test-service"}}]},"scopeSpans":[{"spans":[{"traceId":"12345678901234567890123456789012","spanId":"1234567890123456","name":"test-span","startTimeUnixNano":"1609459200000000000","endTimeUnixNano":"1609459201000000000"}]}]}]}'
```

View File

@@ -46,8 +46,5 @@
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer",
"METER_EXPLORER_BASE": "SigNoz | Meter Explorer"
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
}

View File

@@ -69,8 +69,5 @@
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
"API_MONITORING": "SigNoz | External APIs",
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer",
"METER_EXPLORER_BASE": "SigNoz | Meter Explorer"
"API_MONITORING": "SigNoz | External APIs"
}

View File

@@ -1,6 +1,5 @@
import ROUTES from 'constants/routes';
import MessagingQueues from 'pages/MessagingQueues';
import MeterExplorer from 'pages/MeterExplorer';
import { RouteProps } from 'react-router-dom';
import {
@@ -435,28 +434,6 @@ const routes: AppRoutes[] = [
key: 'METRICS_EXPLORER_VIEWS',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER_BASE,
exact: true,
component: MeterExplorer,
key: 'METER_EXPLORER_BASE',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER,
exact: true,
component: MeterExplorer,
key: 'METER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER_VIEWS,
exact: true,
component: MeterExplorer,
key: 'METER_EXPLORER_VIEWS',
isPrivate: true,
},
{
path: ROUTES.API_MONITORING,
exact: true,

View File

@@ -1,34 +0,0 @@
import { ApiV5Instance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { QueryRangePayloadV5 } from 'api/v5/v5';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
interface ISubstituteVars {
compositeQuery: ICompositeMetricQuery;
}
export const getSubstituteVars = async (
props?: Partial<QueryRangePayloadV5>,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponseV2<ISubstituteVars>> => {
try {
const response = await ApiV5Instance.post<{ data: ISubstituteVars }>(
'/substitute_vars',
props,
{
signal,
headers,
},
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};

View File

@@ -2,7 +2,7 @@ import { ApiV3Instance, ApiV4Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ErrorResponse, SuccessResponse, Warning } from 'types/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MetricRangePayloadV3,
QueryRangePayload,
@@ -13,9 +13,7 @@ export const getMetricsQueryRange = async (
version: string,
signal: AbortSignal,
headers?: Record<string, string>,
): Promise<
(SuccessResponse<MetricRangePayloadV3> & { warning?: Warning }) | ErrorResponse
> => {
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
try {
if (version && version === ENTITY_VERSION_V4) {
const response = await ApiV4Instance.post('/query_range', props, {

View File

@@ -17,7 +17,6 @@ export const getAggregateAttribute = async ({
aggregateOperator,
searchText,
dataSource,
source,
}: IGetAggregateAttributePayload): Promise<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
> => {
@@ -28,7 +27,7 @@ export const getAggregateAttribute = async ({
`/autocomplete/aggregate_attributes?${createQueryParams({
aggregateOperator,
searchText,
dataSource: source === 'meter' ? 'meter' : dataSource,
dataSource,
})}`,
);

View File

@@ -14,7 +14,6 @@ export const getKeySuggestions = (
metricName = '',
fieldContext = '',
fieldDataType = '',
signalSource = '',
} = props;
const encodedSignal = encodeURIComponent(signal);
@@ -22,9 +21,8 @@ export const getKeySuggestions = (
const encodedMetricName = encodeURIComponent(metricName);
const encodedFieldContext = encodeURIComponent(fieldContext);
const encodedFieldDataType = encodeURIComponent(fieldDataType);
const encodedSource = encodeURIComponent(signalSource);
return axios.get(
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}`,
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}`,
);
};

View File

@@ -8,15 +8,13 @@ import {
export const getValueSuggestions = (
props: QueryKeyValueRequestProps,
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {
const { signal, key, searchText, signalSource, metricName } = props;
const { signal, key, searchText } = props;
const encodedSignal = encodeURIComponent(signal);
const encodedKey = encodeURIComponent(key);
const encodedMetricName = encodeURIComponent(metricName || '');
const encodedSearchText = encodeURIComponent(searchText);
const encodedSource = encodeURIComponent(signalSource || '');
return axios.get(
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&source=${encodedSource}`,
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}`,
);
};

View File

@@ -4,6 +4,6 @@ import { AllViewsProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
export const getAllViews = (
sourcepage: DataSource | 'meter',
sourcepage: DataSource,
): Promise<AxiosResponse<AllViewsProps>> =>
axios.get(`/explorer/views?sourcePage=${sourcepage}`);

View File

@@ -1,5 +1,5 @@
import { cloneDeep, isEmpty } from 'lodash-es';
import { SuccessResponse, Warning } from 'types/api';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
import {
DistributionData,
@@ -28,18 +28,14 @@ function getColName(
const aggregationsCount = aggregationPerQuery[col.queryName]?.length || 0;
const isSingleAggregation = aggregationsCount === 1;
if (aggregationsCount > 0) {
// Single aggregation: Priority is alias > legend > expression
if (isSingleAggregation) {
return alias || legend || expression || col.queryName;
}
// Multiple aggregations: Each follows single rules BUT never shows legend
// Priority: alias > expression (legend is ignored for multiple aggregations)
return alias || expression || col.queryName;
// Single aggregation: Priority is alias > legend > expression
if (isSingleAggregation) {
return alias || legend || expression;
}
return legend || col.queryName;
// Multiple aggregations: Each follows single rules BUT never shows legend
// Priority: alias > expression (legend is ignored for multiple aggregations)
return alias || expression;
}
function getColId(
@@ -52,14 +48,7 @@ function getColId(
const aggregation =
aggregationPerQuery?.[col.queryName]?.[col.aggregationIndex];
const expression = aggregation?.expression || '';
const aggregationsCount = aggregationPerQuery[col.queryName]?.length || 0;
const isMultipleAggregations = aggregationsCount > 1;
if (isMultipleAggregations && expression) {
return `${col.queryName}.${expression}`;
}
return col.queryName;
return `${col.queryName}.${expression}`;
}
/**
@@ -352,7 +341,7 @@ export function convertV5ResponseToLegacy(
v5Response: SuccessResponse<MetricRangePayloadV5>,
legendMap: Record<string, string>,
formatForWeb?: boolean,
): SuccessResponse<MetricRangePayloadV3> & { warning?: Warning } {
): SuccessResponse<MetricRangePayloadV3> {
const { payload, params } = v5Response;
const v5Data = payload?.data;
@@ -378,18 +367,14 @@ export function convertV5ResponseToLegacy(
legendMap,
aggregationPerQuery,
);
return {
...v5Response,
payload: {
data: {
resultType: 'scalar',
result: webTables,
warnings: v5Data?.data?.warning || [],
},
warning: v5Data?.warning || undefined,
},
warning: v5Data?.warning || undefined,
};
}
@@ -405,7 +390,6 @@ export function convertV5ResponseToLegacy(
...v5Response,
payload: {
data: convertedData,
warning: v5Response.payload?.data?.warning || undefined,
},
};

View File

@@ -5,7 +5,10 @@ import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEmpty } from 'lodash-es';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
FieldContext,
@@ -27,7 +30,6 @@ import {
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { normalizeFunctionName } from 'utils/functionNameNormalizer';
type PrepareQueryRangePayloadV5Result = {
queryPayload: QueryRangePayloadV5;
@@ -121,21 +123,17 @@ function createBaseSpec(
functions: isEmpty(queryData.functions)
? undefined
: queryData.functions.map(
(func: QueryFunction): QueryFunction => {
// Normalize function name to handle case sensitivity
const normalizedName = normalizeFunctionName(func?.name);
return {
name: normalizedName as FunctionName,
args: isEmpty(func.namedArgs)
? func.args?.map((arg) => ({
value: arg?.value,
}))
: Object.entries(func?.namedArgs || {}).map(([name, value]) => ({
name,
value,
})),
};
},
(func: QueryFunctionProps): QueryFunction => ({
name: func.name as FunctionName,
args: isEmpty(func.namedArgs)
? func.args.map((arg) => ({
value: arg,
}))
: Object.entries(func.namedArgs).map(([name, value]) => ({
name,
value,
})),
}),
),
selectFields: isEmpty(nonEmptySelectColumns)
? undefined
@@ -260,7 +258,6 @@ export function convertBuilderQueriesToV5(
spec = {
name: queryName,
signal: 'metrics' as const,
source: queryData.source || '',
...baseSpec,
aggregations: aggregations as MetricAggregation[],
// reduceTo: queryData.reduceTo,

View File

@@ -19,7 +19,6 @@ export interface NavigateToExplorerProps {
endTime?: number;
sameTab?: boolean;
shouldResolveQuery?: boolean;
widgetQuery?: Query;
}
export function useNavigateToExplorer(): (
@@ -31,34 +30,27 @@ export function useNavigateToExplorer(): (
);
const prepareQuery = useCallback(
(
selectedFilters: TagFilterItem[],
dataSource: DataSource,
query?: Query,
): Query => {
const widgetQuery = query || currentQuery;
return {
...widgetQuery,
builder: {
...widgetQuery.builder,
queryData: widgetQuery.builder.queryData
.map((item) => ({
...item,
dataSource,
aggregateOperator: MetricAggregateOperator.NOOP,
filters: {
...item.filters,
items: [...(item.filters?.items || []), ...selectedFilters],
op: item.filters?.op || 'AND',
},
groupBy: [],
disabled: false,
}))
.slice(0, 1),
queryFormulas: [],
},
};
},
(selectedFilters: TagFilterItem[], dataSource: DataSource): Query => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData
.map((item) => ({
...item,
dataSource,
aggregateOperator: MetricAggregateOperator.NOOP,
filters: {
...item.filters,
items: [...(item.filters?.items || []), ...selectedFilters],
op: item.filters?.op || 'AND',
},
groupBy: [],
disabled: false,
}))
.slice(0, 1),
queryFormulas: [],
},
}),
[currentQuery],
);
@@ -75,7 +67,6 @@ export function useNavigateToExplorer(): (
endTime,
sameTab,
shouldResolveQuery,
widgetQuery,
} = props;
const urlParams = new URLSearchParams();
if (startTime && endTime) {
@@ -86,7 +77,7 @@ export function useNavigateToExplorer(): (
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
}
let preparedQuery = prepareQuery(filters, dataSource, widgetQuery);
let preparedQuery = prepareQuery(filters, dataSource);
if (shouldResolveQuery) {
await getUpdatedQuery({

View File

@@ -1,33 +0,0 @@
.error-state-container {
height: 240px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
border-radius: 3px;
.error-state-container-content {
display: flex;
flex-direction: column;
gap: 8px;
.error-state-text {
font-size: 14px;
font-weight: 500;
}
.error-state-additional-messages {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 4px;
.error-state-additional-text {
font-size: 12px;
font-weight: 400;
margin-left: 8px;
}
}
}
}

View File

@@ -1,59 +0,0 @@
import './Common.styles.scss';
import { Typography } from 'antd';
import APIError from '../../types/api/error';
interface ErrorStateComponentProps {
message?: string;
error?: APIError;
}
const defaultProps: Partial<ErrorStateComponentProps> = {
message: undefined,
error: undefined,
};
function ErrorStateComponent({
message,
error,
}: ErrorStateComponentProps): JSX.Element {
// Handle API Error object
if (error) {
const mainMessage = error.getErrorMessage();
const additionalErrors = error.getErrorDetails().error.errors || [];
return (
<div className="error-state-container">
<div className="error-state-container-content">
<Typography className="error-state-text">{mainMessage}</Typography>
{additionalErrors.length > 0 && (
<div className="error-state-additional-messages">
{additionalErrors.map((additionalError) => (
<Typography
key={`error-${additionalError.message}`}
className="error-state-additional-text"
>
{additionalError.message}
</Typography>
))}
</div>
)}
</div>
</div>
);
}
// Handle simple string message (backwards compatibility)
return (
<div className="error-state-container">
<div className="error-state-container-content">
<Typography className="error-state-text">{message}</Typography>
</div>
</div>
);
}
ErrorStateComponent.defaultProps = defaultProps;
export default ErrorStateComponent;

View File

@@ -1,79 +0,0 @@
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import { ReactNode } from 'react';
import APIError from 'types/api/error';
interface ErrorInPlaceProps {
/** The error object to display */
error: APIError;
/** Custom class name */
className?: string;
/** Custom style */
style?: React.CSSProperties;
/** Whether to show a border */
bordered?: boolean;
/** Background color */
background?: string;
/** Padding */
padding?: string | number;
/** Height - defaults to 100% to take available space */
height?: string | number;
/** Width - defaults to 100% to take available space */
width?: string | number;
/** Custom content instead of ErrorContent */
children?: ReactNode;
}
/**
* ErrorInPlace - A component that renders error content directly in the available space
* of its parent container. Perfect for displaying errors in widgets, cards, or any
* container where you want the error to take up the full available space.
*
* @example
* <ErrorInPlace error={error} />
*
* @example
* <ErrorInPlace error={error} bordered background="#f5f5f5" padding={16} />
*/
function ErrorInPlace({
error,
className = '',
style,
bordered = false,
background,
padding = 16,
height = '100%',
width = '100%',
children,
}: ErrorInPlaceProps): JSX.Element {
const containerStyle: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
width,
height,
padding: typeof padding === 'number' ? `${padding}px` : padding,
backgroundColor: background,
border: bordered ? '1px solid var(--bg-slate-400, #374151)' : 'none',
borderRadius: bordered ? '4px' : '0',
overflow: 'auto',
...style,
};
return (
<div className={`error-in-place ${className}`.trim()} style={containerStyle}>
{children || <ErrorContent error={error} />}
</div>
);
}
ErrorInPlace.defaultProps = {
className: undefined,
style: undefined,
bordered: undefined,
background: undefined,
padding: undefined,
height: undefined,
width: undefined,
children: undefined,
};
export default ErrorInPlace;

View File

@@ -1,33 +0,0 @@
/* eslint-disable react/jsx-props-no-spreading */
import { Popover, PopoverProps } from 'antd';
import { ReactNode } from 'react';
interface ErrorPopoverProps extends Omit<PopoverProps, 'content'> {
/** Content to display in the popover */
content: ReactNode;
/** Element that triggers the popover */
children: ReactNode;
}
/**
* ErrorPopover - A clean wrapper around Ant Design's Popover
* that provides a simple interface for displaying content in a popover.
*
* @example
* <ErrorPopover content={<ErrorContent error={error} />}>
* <CircleX />
* </ErrorPopover>
*/
function ErrorPopover({
content,
children,
...popoverProps
}: ErrorPopoverProps): JSX.Element {
return (
<Popover content={content} {...popoverProps}>
{children}
</Popover>
);
}
export default ErrorPopover;

View File

@@ -169,6 +169,7 @@
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}

View File

@@ -1,19 +0,0 @@
.loading-panel-data {
padding: 24px 0;
height: 240px;
display: flex;
justify-content: center;
align-items: flex-start;
.loading-panel-data-content {
display: flex;
align-items: flex-start;
flex-direction: column;
.loading-gif {
height: 72px;
margin-left: -24px;
}
}
}

View File

@@ -1,19 +0,0 @@
import './PanelDataLoading.styles.scss';
import { Typography } from 'antd';
export function PanelDataLoading(): JSX.Element {
return (
<div className="loading-panel-data">
<div className="loading-panel-data-content">
<img
className="loading-gif"
src="/Icons/loading-plane.gif"
alt="wait-icon"
/>
<Typography.Text>Fetching data...</Typography.Text>
</div>
</div>
);
}

View File

@@ -131,7 +131,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
signalSource={config?.signalSource || ''}
/>
))}

View File

@@ -1,11 +1,4 @@
import {
createContext,
ReactNode,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import { createContext, ReactNode, useContext, useMemo, useState } from 'react';
// Types for the context state
export type AggregationOption = { func: string; arg: string };
@@ -13,12 +6,8 @@ export type AggregationOption = { func: string; arg: string };
interface QueryBuilderV2ContextType {
searchText: string;
setSearchText: (text: string) => void;
aggregationOptionsMap: Record<string, AggregationOption[]>;
setAggregationOptions: (
queryName: string,
options: AggregationOption[],
) => void;
getAggregationOptions: (queryName: string) => AggregationOption[];
aggregationOptions: AggregationOption[];
setAggregationOptions: (options: AggregationOption[]) => void;
aggregationInterval: string;
setAggregationInterval: (interval: string) => void;
queryAddValues: any; // Replace 'any' with a more specific type if available
@@ -35,50 +24,26 @@ export function QueryBuilderV2Provider({
children: ReactNode;
}): JSX.Element {
const [searchText, setSearchText] = useState('');
const [aggregationOptionsMap, setAggregationOptionsMap] = useState<
Record<string, AggregationOption[]>
>({});
const [aggregationOptions, setAggregationOptions] = useState<
AggregationOption[]
>([]);
const [aggregationInterval, setAggregationInterval] = useState('');
const [queryAddValues, setQueryAddValues] = useState<any>(null); // Replace 'any' if you have a type
const setAggregationOptions = useCallback(
(queryName: string, options: AggregationOption[]): void => {
setAggregationOptionsMap((prev) => ({
...prev,
[queryName]: options,
}));
},
[],
);
const getAggregationOptions = useCallback(
(queryName: string): AggregationOption[] =>
aggregationOptionsMap[queryName] || [],
[aggregationOptionsMap],
);
return (
<QueryBuilderV2Context.Provider
value={useMemo(
() => ({
searchText,
setSearchText,
aggregationOptionsMap,
aggregationOptions,
setAggregationOptions,
getAggregationOptions,
aggregationInterval,
setAggregationInterval,
queryAddValues,
setQueryAddValues,
}),
[
searchText,
aggregationOptionsMap,
aggregationInterval,
queryAddValues,
getAggregationOptions,
setAggregationOptions,
],
[searchText, aggregationOptions, aggregationInterval, queryAddValues],
)}
>
{children}

View File

@@ -7,6 +7,7 @@ import { ATTRIBUTE_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import SpaceAggregationOptions from 'container/QueryBuilder/components/SpaceAggregationOptions/SpaceAggregationOptions';
import { GroupByFilter, OperatorsSelect } from 'container/QueryBuilder/filters';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Info } from 'lucide-react';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
@@ -18,13 +19,11 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
index,
version,
panelType,
signalSource = '',
}: {
query: IBuilderQuery;
index: number;
version: string;
panelType: PANEL_TYPES | null;
signalSource: string;
}): JSX.Element {
const { setAggregationOptions } = useQueryBuilderV2Context();
const {
@@ -51,17 +50,17 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
);
useEffect(() => {
setAggregationOptions(query.queryName, [
setAggregationOptions([
{
func: queryAggregation.spaceAggregation || 'count',
arg: queryAggregation.metricName || '',
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
queryAggregation.spaceAggregation,
queryAggregation.metricName,
query.queryName,
setAggregationOptions,
query,
]);
const handleChangeGroupByKeys = useCallback(
@@ -101,22 +100,12 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
<div className="metrics-time-aggregation-section">
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<Tooltip
title={
<a
href="https://signoz.io/docs/metrics-management/types-and-aggregation/#aggregation"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
Learn more about temporal aggregation
</a>
}
>
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE WITHIN TIME SERIES{' '}
</div>
</Tooltip>
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE BY TIME{' '}
<Tooltip title="AGGREGATE BY TIME">
<Info size={12} />
</Tooltip>
</div>
<div className="metrics-aggregation-section-content-item-value">
<OperatorsSelect
value={queryAggregation.timeAggregation || ''}
@@ -129,30 +118,9 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
{showAggregationInterval && (
<div className="metrics-aggregation-section-content-item">
<Tooltip
title={
<div>
Set the time interval for aggregation
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
Learn about step intervals
</a>
</div>
}
placement="top"
>
<div
className="metrics-aggregation-section-content-item-label"
style={{ cursor: 'help' }}
>
every
</div>
</Tooltip>
<div className="metrics-aggregation-section-content-item-label">
every
</div>
<div className="metrics-aggregation-section-content-item-value">
<InputWithLabel
@@ -170,22 +138,12 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
<div className="metrics-space-aggregation-section">
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<Tooltip
title={
<a
href="https://signoz.io/docs/metrics-management/types-and-aggregation/#aggregation"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
Learn more about spatial aggregation
</a>
}
>
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE ACROSS TIME SERIES
</div>
</Tooltip>
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE LABELS
<Tooltip title="AGGREGATE LABELS">
<Info size={12} />
</Tooltip>
</div>
<div className="metrics-aggregation-section-content-item-value">
<SpaceAggregationOptions
panelType={panelType}
@@ -210,7 +168,6 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
disabled={!queryAggregation.metricName}
query={query}
onChange={handleChangeGroupByKeys}
signalSource={signalSource}
/>
</div>
</div>
@@ -247,35 +204,13 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
disabled={!queryAggregation.metricName}
query={query}
onChange={handleChangeGroupByKeys}
signalSource={signalSource}
/>
</div>
</div>
<div className="metrics-aggregation-section-content-item">
<Tooltip
title={
<div>
Set the time interval for aggregation
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
Learn about step intervals
</a>
</div>
}
placement="top"
>
<div
className="metrics-aggregation-section-content-item-label"
style={{ cursor: 'help' }}
>
every
</div>
</Tooltip>
<div className="metrics-aggregation-section-content-item-label">
every
</div>
<div className="metrics-aggregation-section-content-item-value">
<InputWithLabel

View File

@@ -9,12 +9,10 @@ export const MetricsSelect = memo(function MetricsSelect({
query,
index,
version,
signalSource,
}: {
query: IBuilderQuery;
index: number;
version: string;
signalSource: 'meter' | '';
}): JSX.Element {
const { handleChangeAggregatorAttribute } = useQueryOperations({
index,
@@ -24,12 +22,7 @@ export const MetricsSelect = memo(function MetricsSelect({
return (
<div className="metrics-select-container">
<AggregatorFilter
onChange={handleChangeAggregatorAttribute}
query={query}
index={index}
signalSource={signalSource || ''}
/>
<AggregatorFilter onChange={handleChangeAggregatorAttribute} query={query} />
</div>
);
});

View File

@@ -95,8 +95,7 @@ function HavingFilter({
queryData: IBuilderQuery;
}): JSX.Element {
const isDarkMode = useIsDarkMode();
const { getAggregationOptions } = useQueryBuilderV2Context();
const aggregationOptions = getAggregationOptions(queryData.queryName);
const { aggregationOptions } = useQueryBuilderV2Context();
const having = queryData?.having as Having;
const [input, setInput] = useState(having?.expression || '');

View File

@@ -111,13 +111,17 @@
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: -2px !important;
width: 100% !important;
position: absolute !important;
top: calc(100% + 6px) !important;
top: 38px !important;
left: 0px !important;
right: 0px !important;
border-radius: 4px;
border: 1px solid var(--bg-slate-200, #1d212d);
border-top: none !important;
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
@@ -125,9 +129,7 @@
) !important;
backdrop-filter: blur(20px);
box-sizing: border-box;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6);
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
ul {
width: 100% !important;
@@ -163,6 +165,7 @@
overflow: hidden;
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
.cm-completionIcon {
display: none !important;
@@ -327,18 +330,16 @@
background: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
ul {
li {
color: var(--bg-ink-300) !important;
&:hover {
background: var(--bg-vanilla-300) !important;
}
&[aria-selected='true'] {
color: var(--bg-ink-500) !important;
background: var(--bg-vanilla-300) !important;
font-weight: 600 !important;
}
}
}

View File

@@ -1,7 +1,6 @@
/* eslint-disable react/require-default-props */
import './QueryAddOns.styles.scss';
import { Button, Radio, RadioChangeEvent, Tooltip } from 'antd';
import { Button, Radio, RadioChangeEvent } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
@@ -10,7 +9,7 @@ import { ReduceToFilter } from 'container/QueryBuilder/filters/ReduceToFilter/Re
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { isEmpty } from 'lodash-es';
import { BarChart2, ChevronUp, ExternalLink, ScrollText } from 'lucide-react';
import { BarChart2, ChevronUp, ScrollText } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
@@ -22,8 +21,6 @@ interface AddOn {
icon: React.ReactNode;
label: string;
key: string;
description?: string;
docLink?: string;
}
const ADD_ONS_KEYS = {
@@ -39,45 +36,26 @@ const ADD_ONS = [
icon: <BarChart2 size={14} />,
label: 'Group By',
key: 'group_by',
description:
'Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments.',
docLink: 'https://signoz.io/docs/userguide/query-builder-v5/#grouping',
},
{
icon: <ScrollText size={14} />,
label: 'Having',
key: 'having',
description:
'Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#conditional-filtering-with-having',
},
{
icon: <ScrollText size={14} />,
label: 'Order By',
key: 'order_by',
description:
'Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers.',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting',
},
{
icon: <ScrollText size={14} />,
label: 'Limit',
key: 'limit',
description:
'Show only the top/bottom N results. Perfect for focusing on outliers, reducing noise, and improving dashboard performance.',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting',
},
{
icon: <ScrollText size={14} />,
label: 'Legend format',
key: 'legend_format',
description:
'Customize series labels using variables like {{service.name}}-{{endpoint}}. Makes charts readable at a glance during incident investigation.',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#legend-formatting',
},
];
@@ -85,58 +63,8 @@ const REDUCE_TO = {
icon: <ScrollText size={14} />,
label: 'Reduce to',
key: 'reduce_to',
description:
'Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value.',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations',
};
// Custom tooltip content component
function TooltipContent({
label,
description,
docLink,
}: {
label: string;
description?: string;
docLink?: string;
}): JSX.Element {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
maxWidth: '300px',
}}
>
<strong style={{ fontSize: '14px' }}>{label}</strong>
{description && (
<span style={{ fontSize: '12px', lineHeight: '1.5' }}>{description}</span>
)}
{docLink && (
<a
href={docLink}
target="_blank"
rel="noopener noreferrer"
onClick={(e): void => e.stopPropagation()}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
color: '#4096ff',
fontSize: '12px',
marginTop: '4px',
}}
>
Learn more
<ExternalLink size={12} />
</a>
)}
</div>
);
}
function QueryAddOns({
query,
version,
@@ -284,21 +212,7 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'group_by') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<Tooltip
title={
<TooltipContent
label="Group By"
description="Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments."
docLink="https://signoz.io/docs/userguide/query-builder-v5/#grouping"
/>
}
placement="top"
mouseEnterDelay={0.5}
>
<div className="label" style={{ cursor: 'help' }}>
Group By
</div>
</Tooltip>
<div className="label">Group By</div>
<div className="input">
<GroupByFilter
disabled={
@@ -320,21 +234,7 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'having') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<Tooltip
title={
<TooltipContent
label="Having"
description="Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500"
docLink="https://signoz.io/docs/userguide/query-builder-v5/#conditional-filtering-with-having"
/>
}
placement="top"
mouseEnterDelay={0.5}
>
<div className="label" style={{ cursor: 'help' }}>
Having
</div>
</Tooltip>
<div className="label">Having</div>
<div className="input">
<HavingFilter
onClose={(): void => {
@@ -366,21 +266,7 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'order_by') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<Tooltip
title={
<TooltipContent
label="Order By"
description="Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers."
docLink="https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting"
/>
}
placement="top"
mouseEnterDelay={0.5}
>
<div className="label" style={{ cursor: 'help' }}>
Order By
</div>
</Tooltip>
<div className="label">Order By</div>
<div className="input">
<OrderByFilter
entityVersion={version}
@@ -404,21 +290,7 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<Tooltip
title={
<TooltipContent
label="Reduce to"
description="Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value."
docLink="https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations"
/>
}
placement="top"
mouseEnterDelay={0.5}
>
<div className="label" style={{ cursor: 'help' }}>
Reduce to
</div>
</Tooltip>
<div className="label">Reduce to</div>
<div className="input">
<ReduceToFilter query={query} onChange={handleChangeReduceToV5} />
</div>
@@ -458,32 +330,20 @@ function QueryAddOns({
value={selectedViews}
>
{addOns.map((addOn) => (
<Tooltip
key={addOn.key}
title={
<TooltipContent
label={addOn.label}
description={addOn.description}
docLink={addOn.docLink}
/>
<Radio.Button
key={addOn.label}
className={
selectedViews.find((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
}
placement="top"
mouseEnterDelay={0.5}
value={addOn}
>
<Radio.Button
className={
selectedViews.find((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
}
value={addOn}
>
<div className="add-on-tab-title">
{addOn.icon}
{addOn.label}
</div>
</Radio.Button>
</Tooltip>
<div className="add-on-tab-title">
{addOn.icon}
{addOn.label}
</div>
</Radio.Button>
))}
</Radio.Group>
</div>

View File

@@ -63,14 +63,17 @@
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: 8px !important;
min-width: 400px !important;
position: absolute !important;
top: calc(100% + 6px) !important;
left: 0px !important;
right: 0px !important;
width: 100% !important;
border-radius: 4px;
border: 1px solid var(--bg-slate-200, #1d212d);
border-top: none !important;
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
@@ -78,7 +81,6 @@
) !important;
backdrop-filter: blur(20px);
box-sizing: border-box;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6);
font-family: 'Space Mono', monospace !important;
ul {
@@ -267,17 +269,19 @@
background: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
ul {
li {
color: var(--bg-ink-300) !important;
&:hover {
background-color: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600;
}
&:hover,
&[aria-selected='true'] {
background: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600 !important;
font-weight: 600;
}
}
}

View File

@@ -1,6 +1,5 @@
import './QueryAggregation.styles.scss';
import { Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useMemo } from 'react';
@@ -54,31 +53,7 @@ function QueryAggregationOptions({
{showAggregationInterval && (
<div className="query-aggregation-interval">
<Tooltip
title={
<div>
Set the time interval for aggregation
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
Learn about step intervals
</a>
</div>
}
placement="top"
>
<div
className="metrics-aggregation-section-content-item-label"
style={{ cursor: 'help' }}
>
every
</div>
</Tooltip>
<div className="query-aggregation-interval-label">every</div>
<div className="query-aggregation-interval-input-container">
<InputWithLabel
initialValue={

View File

@@ -27,13 +27,13 @@ import CodeMirror, {
ViewPlugin,
ViewUpdate,
} from '@uiw/react-codemirror';
import { Button, Popover, Tooltip } from 'antd';
import { Button, Popover } from 'antd';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { QUERY_BUILDER_KEY_TYPES } from 'constants/antlrQueryConstants';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Info, TriangleAlert } from 'lucide-react';
import { TriangleAlert } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -263,7 +263,7 @@ function QueryAggregationSelect({
setValidationError(validateAggregations());
setFunctionArgPairs(pairs);
setAggregationOptions(queryData.queryName, pairs);
setAggregationOptions(pairs);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [input, maxAggregations, validFunctions]);
@@ -639,50 +639,6 @@ function QueryAggregationSelect({
}
}}
/>
<Tooltip
title={
<div>
Aggregation functions:
<br />
<span style={{ fontSize: '12px', lineHeight: '1.4' }}>
<strong>count</strong> - number of occurrences
<br /> <strong>sum/avg</strong> - sum/average of values
<br /> <strong>min/max</strong> - minimum/maximum value
<br /> <strong>p50/p90/p99</strong> - percentiles
<br /> <strong>count_distinct</strong> - unique values
<br /> <strong>rate</strong> - per-interval rate
</span>
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#core-aggregation-functions"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
View documentation
</a>
</div>
}
placement="left"
>
<div
style={{
position: 'absolute',
top: '8px', // Match the error icon's top position
right: validationError ? '40px' : '8px', // Move left when error icon is shown
cursor: 'help',
zIndex: 10,
transition: 'right 0.2s ease',
}}
>
<Info
size={14}
style={{ opacity: 0.9, color: isDarkMode ? '#ffffff' : '#000000' }}
/>
</div>
</Tooltip>
{validationError && (
<div className="query-aggregation-error-container">
<Popover

View File

@@ -12,7 +12,22 @@ export default function QueryFooter({
<div className="qb-footer">
<div className="qb-footer-container">
<div className="qb-add-new-query">
<Tooltip title={<div style={{ textAlign: 'center' }}>Add New Query</div>}>
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add New Query
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=query-builder#multiple-queries-and-functions"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
<Button
className="add-new-query-button periscope-btn secondary"
type="text"
@@ -28,7 +43,7 @@ export default function QueryFooter({
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
href="https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=query-builder#multiple-queries-and-functions"
target="_blank"
style={{ textDecoration: 'underline' }}
>

View File

@@ -48,7 +48,7 @@
.cm-editor {
border-radius: 2px;
// overflow: hidden;
overflow: hidden;
background-color: transparent !important;
&:focus-within {
@@ -75,11 +75,11 @@
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: -2px !important;
min-width: 400px !important;
position: absolute !important;
top: calc(100% + 6px) !important;
position: relative !important;
top: 0px !important;
left: 0px !important;
right: 0px !important;
border-radius: 4px;
border: 0px;
@@ -91,8 +91,6 @@
backdrop-filter: blur(20px);
box-sizing: border-box;
font-family: 'Space Mono', monospace !important;
border: 1px solid var(--bg-slate-200);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6);
ul {
width: 100% !important;
@@ -573,9 +571,9 @@
.cm-tooltip-autocomplete {
background: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300);
border: 0px;
backdrop-filter: blur(20px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
ul {
li {
@@ -585,7 +583,7 @@
&:hover,
&[aria-selected='true'] {
background-color: var(--bg-vanilla-300) !important;
font-weight: 600 !important;
font-weight: 600;
}
}
}

View File

@@ -16,13 +16,12 @@ import { Color } from '@signozhq/design-tokens';
import { copilot } from '@uiw/codemirror-theme-copilot';
import { githubLight } from '@uiw/codemirror-theme-github';
import CodeMirror, { EditorView, keymap, Prec } from '@uiw/react-codemirror';
import { Button, Card, Collapse, Popover, Tag, Tooltip } from 'antd';
import { Button, Card, Collapse, Popover, Tag } from 'antd';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import cx from 'classnames';
import {
negationQueryOperatorSuggestions,
OPERATORS,
QUERY_BUILDER_KEY_TYPES,
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
queryOperatorSuggestions,
@@ -31,7 +30,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebounce from 'hooks/useDebounce';
import { debounce, isNull } from 'lodash-es';
import { Info, TriangleAlert } from 'lucide-react';
import { TriangleAlert } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
IDetailedError,
@@ -41,11 +40,11 @@ import {
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { DataSource } from 'types/common/queryBuilder';
import { validateQuery } from 'utils/antlrQueryUtils';
import {
getCurrentValueIndexAtCursor,
getQueryContextAtCursor,
} from 'utils/queryContextUtils';
import { validateQuery } from 'utils/queryValidationUtils';
import { unquote } from 'utils/stringUtils';
import { queryExamples } from './constants';
@@ -81,17 +80,18 @@ function QuerySearch({
queryData,
dataSource,
onRun,
signalSource,
}: {
onChange: (value: string) => void;
queryData: IBuilderQuery;
dataSource: DataSource;
signalSource?: string;
onRun?: (query: string) => void;
}): JSX.Element {
const isDarkMode = useIsDarkMode();
const [query, setQuery] = useState<string>(queryData.filter?.expression || '');
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
const [valueSuggestions, setValueSuggestions] = useState<any[]>([
{ label: 'error', type: 'value' },
{ label: 'frontend', type: 'value' },
]);
const [activeKey, setActiveKey] = useState<string>('');
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
const [queryContext, setQueryContext] = useState<IQueryContext | null>(null);
@@ -114,27 +114,9 @@ function QuerySearch({
}
};
// Track if the query was changed externally (from queryData) vs internally (user input)
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
const [lastExternalQuery, setLastExternalQuery] = useState<string>('');
useEffect(() => {
const newQuery = queryData.filter?.expression || '';
// Only mark as external change if the query actually changed from external source
if (newQuery !== lastExternalQuery) {
setQuery(newQuery);
setIsExternalQueryChange(true);
setLastExternalQuery(newQuery);
}
}, [queryData.filter?.expression, lastExternalQuery]);
// Validate query when it changes externally (from queryData)
useEffect(() => {
if (isExternalQueryChange && query) {
handleQueryValidation(query);
setIsExternalQueryChange(false);
}
}, [isExternalQueryChange, query]);
setQuery(queryData.filter?.expression || '');
}, [queryData.filter?.expression]);
const [keySuggestions, setKeySuggestions] = useState<
QueryKeyDataSuggestionsProps[] | null
@@ -145,6 +127,7 @@ function QuerySearch({
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
const [isFocused, setIsFocused] = useState(false);
const [isCompleteKeysList, setIsCompleteKeysList] = useState(false);
const [
isFetchingCompleteValuesList,
setIsFetchingCompleteValuesList,
@@ -155,7 +138,6 @@ function QuerySearch({
// Reference to the editor view for programmatic autocompletion
const editorRef = useRef<EditorView | null>(null);
const lastKeyRef = useRef<string>('');
const lastFetchedKeyRef = useRef<string>('');
const lastValueRef = useRef<string>('');
const isMountedRef = useRef<boolean>(true);
@@ -188,78 +170,36 @@ function QuerySearch({
500,
);
const toggleSuggestions = useCallback(
(timeout?: number) => {
const timeoutId = setTimeout(() => {
if (!editorRef.current) return;
if (isFocused) {
startCompletion(editorRef.current);
} else {
closeCompletion(editorRef.current);
}
}, timeout);
const fetchKeySuggestions = async (searchText?: string): Promise<void> => {
if (dataSource === DataSource.METRICS && !queryData.aggregateAttribute?.key) {
setKeySuggestions([]);
return;
}
const response = await getKeySuggestions({
signal: dataSource,
searchText: searchText || '',
metricName: debouncedMetricName ?? undefined,
});
return (): void => clearTimeout(timeoutId);
},
[isFocused],
);
const fetchKeySuggestions = useCallback(
async (searchText?: string): Promise<void> => {
if (
dataSource === DataSource.METRICS &&
!queryData.aggregateAttribute?.key
) {
setKeySuggestions([]);
return;
if (response.data.data) {
const { complete, keys } = response.data.data;
const options = generateOptions(keys);
// Use a Map to deduplicate by label and preserve order: new options take precedence
const merged = new Map<string, QueryKeyDataSuggestionsProps>();
options.forEach((opt) => merged.set(opt.label, opt));
if (searchText && lastKeyRef.current !== searchText) {
(keySuggestions || []).forEach((opt) => {
if (!merged.has(opt.label)) merged.set(opt.label, opt);
});
}
lastFetchedKeyRef.current = searchText || '';
const response = await getKeySuggestions({
signal: dataSource,
searchText: searchText || '',
metricName: debouncedMetricName ?? undefined,
signalSource: signalSource as 'meter' | '',
});
if (response.data.data) {
const { keys } = response.data.data;
const options = generateOptions(keys);
// Use a Map to deduplicate by label and preserve order: new options take precedence
const merged = new Map<string, QueryKeyDataSuggestionsProps>();
options.forEach((opt) => merged.set(opt.label, opt));
if (searchText && lastKeyRef.current !== searchText) {
(keySuggestions || []).forEach((opt) => {
if (!merged.has(opt.label)) merged.set(opt.label, opt);
});
}
setKeySuggestions(Array.from(merged.values()));
// Force reopen the completion if editor is available and focused
if (editorRef.current) {
toggleSuggestions(10);
}
}
},
[
dataSource,
debouncedMetricName,
keySuggestions,
toggleSuggestions,
queryData.aggregateAttribute?.key,
signalSource,
],
);
const debouncedFetchKeySuggestions = useMemo(
() => debounce(fetchKeySuggestions, 300),
[fetchKeySuggestions],
);
setKeySuggestions(Array.from(merged.values()));
setIsCompleteKeysList(complete);
}
};
useEffect(() => {
setKeySuggestions([]);
debouncedFetchKeySuggestions();
fetchKeySuggestions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataSource, debouncedMetricName]);
@@ -370,11 +310,6 @@ function QuerySearch({
},
]);
// Force reopen the completion if editor is available and focused
if (editorRef.current) {
toggleSuggestions(10);
}
const sanitizedSearchText = searchText ? searchText?.trim() : '';
try {
@@ -382,8 +317,6 @@ function QuerySearch({
key,
searchText: sanitizedSearchText,
signal: dataSource,
signalSource: signalSource as 'meter' | '',
metricName: debouncedMetricName ?? undefined,
});
// Skip updates if component unmounted or key changed
@@ -449,9 +382,13 @@ function QuerySearch({
]);
}
// Force reopen the completion if editor is available and focused
// Force reopen the completion if editor is available
if (editorRef.current) {
toggleSuggestions(10);
setTimeout(() => {
if (isMountedRef.current && editorRef.current) {
startCompletion(editorRef.current);
}
}, 10);
}
}
} catch (error) {
@@ -471,14 +408,7 @@ function QuerySearch({
setIsFetchingCompleteValuesList(false);
}
},
[
activeKey,
dataSource,
isLoadingSuggestions,
debouncedMetricName,
signalSource,
toggleSuggestions,
],
[activeKey, dataSource, isLoadingSuggestions],
);
const debouncedFetchValueSuggestions = useMemo(
@@ -538,13 +468,14 @@ function QuerySearch({
}
}, []);
const handleQueryChange = useCallback(async (newQuery: string) => {
setQuery(newQuery);
}, []);
const handleChange = (value: string): void => {
setQuery(value);
handleQueryChange(value);
onChange(value);
// Mark as internal change to avoid triggering external validation
setIsExternalQueryChange(false);
// Update lastExternalQuery to prevent external validation trigger
setLastExternalQuery(value);
};
const handleBlur = (): void => {
@@ -552,27 +483,24 @@ function QuerySearch({
setIsFocused(false);
};
useEffect(
() => (): void => {
useEffect(() => {
if (query) {
handleQueryValidation(query);
}
return (): void => {
if (debouncedFetchValueSuggestions) {
debouncedFetchValueSuggestions.cancel();
}
if (debouncedFetchKeySuggestions) {
debouncedFetchKeySuggestions.cancel();
}
},
};
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
}, []);
const handleExampleClick = (exampleQuery: string): void => {
// If there's an existing query, append the example with AND
const newQuery = query ? `${query} AND ${exampleQuery}` : exampleQuery;
setQuery(newQuery);
// Mark as internal change to avoid triggering external validation
setIsExternalQueryChange(false);
// Update lastExternalQuery to prevent external validation trigger
setLastExternalQuery(newQuery);
handleQueryChange(newQuery);
};
// Helper function to render a badge for the current context mode
@@ -815,14 +743,16 @@ function QuerySearch({
}
if (queryContext.isInKey) {
const searchText = word?.text.toLowerCase().trim() ?? '';
const searchText = word?.text.toLowerCase() ?? '';
options = (keySuggestions || []).filter((option) =>
option.label.toLowerCase().includes(searchText),
);
if (options.length === 0 && lastFetchedKeyRef.current !== searchText) {
debouncedFetchKeySuggestions(searchText);
if (!isCompleteKeysList && options.length === 0) {
setTimeout(() => {
fetchKeySuggestions(searchText);
}, 300);
}
// If we have previous pairs, we can prioritize keys that haven't been used yet
@@ -897,32 +827,12 @@ function QuerySearch({
QUERY_BUILDER_KEY_TYPES.STRING
].includes(op.label),
)
.map((op) => {
if (op.label === OPERATORS['=']) {
return {
...op,
boost: 200,
};
}
if (
[
OPERATORS['!='],
OPERATORS.LIKE,
OPERATORS.ILIKE,
OPERATORS.CONTAINS,
OPERATORS.IN,
].includes(op.label)
) {
return {
...op,
boost: 100,
};
}
return {
...op,
boost: 0,
};
});
.map((op) => ({
...op,
boost: ['=', '!=', 'LIKE', 'ILIKE', 'CONTAINS', 'IN'].includes(op.label)
? 100
: 0,
}));
} else if (keyType === QUERY_BUILDER_KEY_TYPES.BOOLEAN) {
// Prioritize boolean operators
options = options
@@ -931,24 +841,10 @@ function QuerySearch({
QUERY_BUILDER_KEY_TYPES.BOOLEAN
].includes(op.label),
)
.map((op) => {
if (op.label === OPERATORS['=']) {
return {
...op,
boost: 200,
};
}
if (op.label === OPERATORS['!=']) {
return {
...op,
boost: 100,
};
}
return {
...op,
boost: 0,
};
});
.map((op) => ({
...op,
boost: ['=', '!='].includes(op.label) ? 100 : 0,
}));
}
}
}
@@ -1138,15 +1034,26 @@ function QuerySearch({
// Effect to handle focus state and trigger suggestions
useEffect(() => {
const clearTimeout = toggleSuggestions(10);
return (): void => clearTimeout();
}, [isFocused, toggleSuggestions]);
if (editorRef.current) {
if (!isFocused) {
closeCompletion(editorRef.current);
} else {
startCompletion(editorRef.current);
}
}
}, [isFocused]);
useEffect(() => {
if (!queryContext) return;
// Trigger suggestions based on context
if (editorRef.current) {
toggleSuggestions(10);
// Small delay to ensure the context is fully updated
setTimeout(() => {
if (editorRef.current) {
startCompletion(editorRef.current);
}
}, 50);
}
// Handle value suggestions for value context
@@ -1159,28 +1066,7 @@ function QuerySearch({
fetchValueSuggestions({ key });
}
}
}, [
queryContext,
toggleSuggestions,
isLoadingSuggestions,
activeKey,
fetchValueSuggestions,
]);
const getTooltipContent = (): JSX.Element => (
<div>
Need help with search syntax?
<br />
<a
href="https://signoz.io/docs/userguide/search-syntax/"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
View documentation
</a>
</div>
);
}, [queryContext, activeKey, isLoadingSuggestions, fetchValueSuggestions]);
return (
<div className="code-mirror-where-clause">
@@ -1223,31 +1109,6 @@ function QuerySearch({
)}
<div className="query-where-clause-editor-container">
<Tooltip title={getTooltipContent()} placement="left">
<a
href="https://signoz.io/docs/userguide/search-syntax/"
target="_blank"
rel="noopener noreferrer"
style={{
position: 'absolute',
top: 8,
right: validation.isValid === false && query ? 40 : 8, // Move left when error shown
cursor: 'help',
zIndex: 10,
transition: 'right 0.2s ease',
display: 'inline-flex',
alignItems: 'center',
color: '#8c8c8c',
}}
onClick={(e): void => e.stopPropagation()}
>
<Info
size={14}
style={{ opacity: 0.9, color: isDarkMode ? '#ffffff' : '#000000' }}
/>
</a>
</Tooltip>
<CodeMirror
value={query}
theme={isDarkMode ? copilot : githubLight}
@@ -1306,7 +1167,7 @@ function QuerySearch({
]),
),
]}
placeholder="Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')"
placeholder="Enter your filter query (e.g., status = 'error' AND service = 'frontend')"
basicSetup={{
lineNumbers: false,
}}
@@ -1452,7 +1313,6 @@ function QuerySearch({
QuerySearch.defaultProps = {
onRun: undefined,
signalSource: '',
};
export default QuerySearch;

View File

@@ -28,7 +28,6 @@ export const QueryV2 = memo(function QueryV2({
isListViewPanel = false,
version,
showOnlyWhereClause = false,
signalSource = '',
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
const { cloneQuery, panelType } = useQueryBuilder();
@@ -176,7 +175,6 @@ export const QueryV2 = memo(function QueryV2({
query={query}
index={index}
version={ENTITY_VERSION_V5}
signalSource={signalSource as 'meter' | ''}
/>
</div>
)}
@@ -188,7 +186,6 @@ export const QueryV2 = memo(function QueryV2({
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
@@ -221,7 +218,6 @@ export const QueryV2 = memo(function QueryV2({
index={index}
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
version="v4"
signalSource={signalSource as 'meter' | ''}
/>
)}

View File

@@ -21,7 +21,6 @@ import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import { unquote } from 'utils/stringUtils';
import { isFunctionOperator } from 'utils/tokenUtils';
import { v4 as uuid } from 'uuid';
/**
@@ -87,10 +86,6 @@ export const convertFiltersToExpression = (
return '';
}
if (isFunctionOperator(op)) {
return `${op}(${key.key}, ${value})`;
}
const formattedValue = formatValueForExpression(value, op);
return `${key.key} ${op} ${formattedValue}`;
})
@@ -544,16 +539,43 @@ export const convertAggregationToExpression = (
];
};
export const getQueryTitles = (currentQuery: Query): string[] => {
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
const queryTitles: string[] = [];
// Handle builder queries with multiple aggregations
currentQuery.builder.queryData.forEach((q) => {
const aggregationCount = q.aggregations?.length || 1;
if (aggregationCount > 1) {
// If multiple aggregations, create titles like A.0, A.1, A.2
for (let i = 0; i < aggregationCount; i++) {
queryTitles.push(`${q.queryName}.${i}`);
}
} else {
// Single aggregation, just use query name
queryTitles.push(q.queryName);
}
});
// Handle formulas (they don't have aggregations, so just use query name)
const formulas = currentQuery.builder.queryFormulas.map((q) => q.queryName);
return [...queryTitles, ...formulas];
}
if (currentQuery.queryType === EQueryType.CLICKHOUSE) {
return currentQuery.clickhouse_sql.map((q) => q.name);
}
return currentQuery.promql.map((q) => q.name);
};
function getColId(
queryName: string,
aggregation: { alias?: string; expression?: string },
isMultipleAggregations: boolean,
): string {
if (isMultipleAggregations && aggregation.expression) {
return `${queryName}.${aggregation.expression}`;
}
return queryName;
return `${queryName}.${aggregation.expression}`;
}
// function to give you label value for query name taking multiaggregation into account
@@ -577,7 +599,7 @@ export function getQueryLabelWithAggregation(
const isMultipleAggregations = aggregations.length > 1;
aggregations.forEach((agg: any, index: number) => {
const columnId = getColId(queryName, agg, isMultipleAggregations);
const columnId = getColId(queryName, agg);
// For display purposes, show the aggregation index for multiple aggregations
const displayLabel = isMultipleAggregations

View File

@@ -17,7 +17,6 @@ import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
import { ChevronDown, ChevronRight } from 'lucide-react';
@@ -74,59 +73,18 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
searchText: searchText ?? '',
},
{
enabled: isOpen && source !== QuickFiltersSource.METER_EXPLORER,
enabled: isOpen,
keepPreviousData: true,
},
);
const {
data: keyValueSuggestions,
isLoading: isLoadingKeyValueSuggestions,
} = useGetQueryKeyValueSuggestions({
key: filter.attributeKey.key,
signal: filter.dataSource || DataSource.LOGS,
signalSource: 'meter',
options: {
enabled: isOpen && source === QuickFiltersSource.METER_EXPLORER,
keepPreviousData: true,
},
});
const attributeValues: string[] = useMemo(() => {
const dataType = filter.attributeKey.dataType || DataTypes.String;
if (source === QuickFiltersSource.METER_EXPLORER && keyValueSuggestions) {
// Process the response data
const responseData = keyValueSuggestions?.data as any;
const values = responseData.data?.values || {};
const stringValues = values.stringValues || [];
const numberValues = values.numberValues || [];
// Generate options from string values - explicitly handle empty strings
const stringOptions = stringValues
// Strict filtering for empty string - we'll handle it as a special case if needed
.filter(
(value: string | null | undefined): value is string =>
value !== null && value !== undefined && value !== '',
);
// Generate options from number values
const numberOptions = numberValues
.filter(
(value: number | null | undefined): value is number =>
value !== null && value !== undefined,
)
.map((value: number) => value.toString());
// Combine all options and make sure we don't have duplicate labels
return [...stringOptions, ...numberOptions];
}
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
return (data?.payload?.[key] || []).filter(
(val) => val !== undefined && val !== null,
);
}, [data?.payload, filter.attributeKey.dataType, keyValueSuggestions, source]);
}, [data?.payload, filter.attributeKey.dataType]);
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
@@ -520,14 +478,12 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
)}
</section>
</section>
{isOpen &&
(isLoading || isLoadingKeyValueSuggestions) &&
!attributeValues.length && (
<section className="loading">
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && !isLoading && !isLoadingKeyValueSuggestions && (
{isOpen && isLoading && !attributeValues.length && (
<section className="loading">
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && !isLoading && (
<>
{!isEmptyStateWithDocsEnabled && (
<section className="search">

View File

@@ -1,8 +1,6 @@
.quick-filters-container {
display: flex;
height: 100%;
position: relative;
.quick-filters-settings-container {
position: relative;
}
@@ -104,37 +102,6 @@
margin: 8px 12px;
}
}
.no-filters-container {
display: flex;
height: 100%;
gap: 8px;
align-items: center;
padding: 8px;
}
}
.perilin-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle, #fff 10%, transparent 0);
background-size: 12px 12px;
opacity: 1;
mask-image: radial-gradient(
circle at 50% 0,
rgba(11, 12, 14, 0.1) 0,
rgba(11, 12, 14, 0) 100%
);
-webkit-mask-image: radial-gradient(
circle at 50% 0,
rgba(11, 12, 14, 0.1) 0,
rgba(11, 12, 14, 0) 100%
);
}
.lightMode {

View File

@@ -15,7 +15,7 @@ import { LOCALSTORAGE } from 'constants/localStorage';
import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isFunction, isNull } from 'lodash-es';
import { Frown, Settings2 as SettingsIcon } from 'lucide-react';
import { Settings2 as SettingsIcon } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useMemo, useState } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -236,13 +236,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
);
}
})}
{filterConfig.length === 0 && (
<div className="no-filters-container">
<Frown size={16} />
<Typography.Text>No filters found</Typography.Text>
</div>
)}
</section>
</>
</OverlayScrollbar>

View File

@@ -6,5 +6,4 @@ export const SIGNAL_DATA_SOURCE_MAP = {
[SignalType.TRACES]: DataSource.TRACES,
[SignalType.EXCEPTIONS]: DataSource.TRACES,
[SignalType.API_MONITORING]: DataSource.TRACES,
[SignalType.METER_EXPLORER]: DataSource.METRICS,
};

View File

@@ -54,7 +54,6 @@ const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/${SIGNAL}`;
const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`;
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`;
const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`;
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
const FILTER_OS_DESCRIPTION = 'os.description';
const FILTER_K8S_DEPLOYMENT_NAME = 'k8s.deployment.name';
@@ -78,11 +77,7 @@ const setupServer = (): void => {
putHandler(await req.json());
return res(ctx.status(200), ctx.json({}));
}),
rest.get(quickFiltersAttributeValuesURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
),
rest.get(fieldsValuesURL, (req, res, ctx) =>
rest.get(quickFiltersAttributeValuesURL, (_, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
),
);

View File

@@ -23,7 +23,6 @@ export enum SignalType {
LOGS = 'logs',
API_MONITORING = 'api_monitoring',
EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
}
export interface IQuickFiltersConfig {
@@ -54,5 +53,4 @@ export enum QuickFiltersSource {
TRACES_EXPLORER = 'traces-explorer',
API_MONITORING = 'api-monitoring',
EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
}

View File

@@ -4,7 +4,7 @@ import { Table } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import cx from 'classnames';
import { dragColumnParams } from 'hooks/useDragColumns/configs';
import { getColumnWidth, RowData } from 'lib/query/createTableColumnsFromQuery';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { debounce, set } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
@@ -110,14 +110,11 @@ function ResizeTable({
// Apply stored column widths from widget configuration
const columnsWithStoredWidths = columns.map((col) => {
const dataIndex = (col as RowData).dataIndex as string;
if (dataIndex && columnWidths) {
const width = getColumnWidth(dataIndex, columnWidths);
if (width) {
return {
...col,
width, // Apply stored width
};
}
if (dataIndex && columnWidths && columnWidths[dataIndex]) {
return {
...col,
width: columnWidths[dataIndex], // Apply stored width
};
}
return col;
});

View File

@@ -1,208 +0,0 @@
.warning-content {
display: flex;
flex-direction: column;
// === SECTION: Summary (Top)
&__summary-section {
display: flex;
flex-direction: column;
border-bottom: 1px solid var(--bg-slate-400);
}
&__summary {
display: flex;
justify-content: space-between;
padding: 16px;
}
&__summary-left {
display: flex;
align-items: baseline;
gap: 8px;
}
&__summary-text {
display: flex;
flex-direction: column;
gap: 6px;
}
&__warning-code {
color: var(--bg-vanilla-100);
margin: 0;
font-size: 16px;
font-weight: 500;
line-height: 24px; /* 150% */
letter-spacing: -0.08px;
}
&__warning-message {
margin: 0;
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
&__docs-button {
display: flex;
align-items: center;
gap: 6px;
padding: 9px 12.5px;
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: 0.12px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: none;
}
&__message-badge {
display: flex;
align-items: center;
gap: 12px;
padding: 0px 16px 16px;
.key-value-label {
width: fit-content;
border-color: var(--bg-slate-400);
border-radius: 20px;
overflow: hidden;
&__key {
padding-left: 8px;
padding-right: 8px;
}
&__value {
padding-right: 10px;
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 500;
line-height: 18px; /* 150% */
letter-spacing: 0.48px;
pointer-events: none;
}
}
&-label {
display: flex;
align-items: center;
gap: 6px;
&-dot {
height: 6px;
width: 6px;
background: var(--bg-sakura-500);
border-radius: 50%;
}
&-text {
color: var(--bg-vanilla-100);
font-size: 10px;
font-weight: 500;
line-height: 18px; /* 180% */
letter-spacing: 0.5px;
}
}
&-line {
flex: 1;
height: 8px;
background-image: radial-gradient(circle, #444c63 1px, transparent 2px);
background-size: 8px 11px;
background-position: top left;
padding: 6px;
}
}
// === SECTION: Message List (Bottom)
&__message-list-container {
position: relative;
}
&__message-list {
margin: 0;
padding: 0;
list-style: none;
max-height: 275px;
}
&__message-item {
position: relative;
margin-bottom: 4px;
color: var(--bg-vanilla-400);
font-family: Geist Mono;
font-size: 12px;
font-weight: 400;
line-height: 18px;
color: var(--bg-vanilla-400);
padding: 3px 12px;
padding-left: 26px;
}
&__message-item::before {
font-family: unset;
content: '';
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 2px;
height: 4px;
border-radius: 50px;
background: var(--bg-slate-400);
}
&__scroll-hint {
position: absolute;
bottom: 10px;
left: 0px;
right: 0px;
margin: auto;
width: fit-content;
display: inline-flex;
padding: 4px 12px 4px 10px;
justify-content: center;
align-items: center;
gap: 3px;
background: var(--bg-slate-200);
border-radius: 20px;
box-shadow: 0px 103px 12px 0px rgba(0, 0, 0, 0.01),
0px 66px 18px 0px rgba(0, 0, 0, 0.01), 0px 37px 22px 0px rgba(0, 0, 0, 0.03),
0px 17px 17px 0px rgba(0, 0, 0, 0.04), 0px 4px 9px 0px rgba(0, 0, 0, 0.04);
}
&__scroll-hint-text {
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
}
}
.lightMode {
.warning-content {
&__warning-code {
color: var(--bg-ink-100);
}
&__warning-message {
color: var(--bg-ink-400);
}
&__message-item {
color: var(--bg-ink-400);
}
&__message-badge {
&-label-text {
color: var(--bg-ink-400);
}
.key-value-label__value {
color: var(--bg-ink-400);
}
}
&__docs-button {
background: var(--bg-vanilla-100);
color: var(--bg-ink-100);
}
}
}

View File

@@ -1,142 +0,0 @@
/* eslint-disable react/jsx-props-no-spreading */
import './WarningPopover.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Popover, PopoverProps } from 'antd';
import ErrorIcon from 'assets/Error';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { BookOpenText, ChevronsDown, TriangleAlert } from 'lucide-react';
import KeyValueLabel from 'periscope/components/KeyValueLabel';
import { ReactNode } from 'react';
import { Warning } from 'types/api';
interface WarningContentProps {
warning: Warning;
}
export function WarningContent({ warning }: WarningContentProps): JSX.Element {
const {
url: warningUrl,
warnings: warningMessages,
code: warningCode,
message: warningMessage,
} = warning || {};
if (!warning) {
return <div />;
}
return (
<section className="warning-content">
{/* Summary Header */}
<section className="warning-content__summary-section">
<header className="warning-content__summary">
<div className="warning-content__summary-left">
<div className="warning-content__icon-wrapper">
<ErrorIcon />
</div>
<div className="warning-content__summary-text">
<h2 className="warning-content__warning-code">{warningCode}</h2>
<p className="warning-content__warning-message">{warningMessage}</p>
</div>
</div>
{warningUrl && (
<div className="warning-content__summary-right">
<Button
type="default"
className="warning-content__docs-button"
href={warningUrl}
target="_blank"
data-testid="warning-docs-button"
>
<BookOpenText size={14} />
Open Docs
</Button>
</div>
)}
</header>
{warningMessages?.length > 0 && (
<div className="warning-content__message-badge">
<KeyValueLabel
badgeKey={
<div className="warning-content__message-badge-label">
<div className="warning-content__message-badge-label-dot" />
<div className="warning-content__message-badge-label-text">
MESSAGES
</div>
</div>
}
badgeValue={warningMessages.length.toString()}
/>
<div className="warning-content__message-badge-line" />
</div>
)}
</section>
{/* Detailed Messages */}
<section className="warning-content__messages-section">
<div className="warning-content__message-list-container">
<OverlayScrollbar>
<ul className="warning-content__message-list">
{warningMessages?.map((warning) => (
<li className="warning-content__message-item" key={warning.message}>
{warning.message}
</li>
))}
</ul>
</OverlayScrollbar>
{warningMessages?.length > 10 && (
<div className="warning-content__scroll-hint">
<ChevronsDown
size={16}
color={Color.BG_VANILLA_100}
className="warning-content__scroll-hint-icon"
/>
<span className="warning-content__scroll-hint-text">
Scroll for more
</span>
</div>
)}
</div>
</section>
</section>
);
}
interface WarningPopoverProps extends PopoverProps {
children?: ReactNode;
warningData: Warning;
}
function WarningPopover({
children,
warningData,
...popoverProps
}: WarningPopoverProps): JSX.Element {
return (
<Popover
content={<WarningContent warning={warningData} />}
overlayStyle={{ padding: 0, maxWidth: '600px' }}
overlayInnerStyle={{ padding: 0 }}
autoAdjustOverflow
{...popoverProps}
>
{children || (
<TriangleAlert
size={16}
style={{ cursor: 'pointer' }}
color={Color.BG_AMBER_500}
/>
)}
</Popover>
);
}
WarningPopover.defaultProps = {
children: undefined,
};
export default WarningPopover;

View File

@@ -15,12 +15,6 @@ export const OPERATORS = {
'<': '<',
};
export const QUERY_BUILDER_FUNCTIONS = {
HAS: 'has',
HASANY: 'hasAny',
HASALL: 'hasAll',
};
export const NON_VALUE_OPERATORS = [OPERATORS.EXISTS];
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -82,15 +76,3 @@ export const queryOperatorSuggestions = [
{ label: OPERATORS.NOT, type: 'operator', info: 'Not' },
...negationQueryOperatorSuggestions,
];
export function negateOperator(operatorOrFunction: string): string {
// Special cases for equals/not equals
if (operatorOrFunction === OPERATORS['=']) {
return OPERATORS['!='];
}
if (operatorOrFunction === OPERATORS['!=']) {
return OPERATORS['='];
}
// For all other operators and functions, add NOT in front
return `${OPERATORS.NOT} ${operatorOrFunction}`;
}

View File

@@ -46,6 +46,7 @@ export enum QueryParams {
msgSystem = 'msgSystem',
destination = 'destination',
kindString = 'kindString',
summaryFilters = 'summaryFilters',
tab = 'tab',
thresholds = 'thresholds',
selectedExplorerView = 'selectedExplorerView',

View File

@@ -23,7 +23,6 @@ import {
BoolOperators,
DataSource,
LogsAggregatorOperator,
MeterAggregateOperator,
MetricAggregateOperator,
NumberOperators,
QueryAdditionalFilter,
@@ -37,7 +36,6 @@ import { v4 as uuid } from 'uuid';
import {
logsAggregateOperatorOptions,
meterAggregateOperatorOptions,
metricAggregateOperatorOptions,
metricsGaugeAggregateOperatorOptions,
metricsGaugeSpaceAggregateOperatorOptions,
@@ -81,7 +79,6 @@ export const mapOfOperators = {
metrics: metricAggregateOperatorOptions,
logs: logsAggregateOperatorOptions,
traces: tracesAggregateOperatorOptions,
meter: meterAggregateOperatorOptions,
};
export const metricsOperatorsByType = {
@@ -196,7 +193,6 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
groupBy: [],
legend: '',
reduceTo: 'avg',
source: '',
};
const initialQueryBuilderFormLogsValues: IBuilderQuery = {
@@ -213,39 +209,6 @@ const initialQueryBuilderFormTracesValues: IBuilderQuery = {
dataSource: DataSource.TRACES,
};
export const initialQueryBuilderFormMeterValues: IBuilderQuery = {
dataSource: DataSource.METRICS,
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
aggregateOperator: MeterAggregateOperator.COUNT,
aggregateAttribute: initialAutocompleteData,
timeAggregation: MeterAggregateOperator.RATE,
spaceAggregation: MeterAggregateOperator.SUM,
filter: { expression: '' },
aggregations: [
{
metricName: '',
temporality: '',
timeAggregation: MeterAggregateOperator.COUNT,
spaceAggregation: MeterAggregateOperator.SUM,
reduceTo: 'avg',
},
],
functions: [],
filters: { items: [], op: 'AND' },
expression: createNewBuilderItemName({
existNames: [],
sourceNames: alphabet,
}),
disabled: false,
stepInterval: undefined,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
};
export const initialQueryBuilderFormValuesMap: Record<
DataSource,
IBuilderQuery
@@ -322,19 +285,6 @@ export const initialQueriesMap: Record<DataSource, Query> = {
traces: initialQueryTracesWithType,
};
export const initialQueryMeterWithType: Query = {
...initialQueryWithType,
builder: {
...initialQueryWithType.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.metrics,
source: 'meter',
},
],
},
};
export const operatorsByTypes: Record<LocalDataType, string[]> = {
string: Object.values(StringOperators),
number: Object.values(NumberOperators),

View File

@@ -125,126 +125,6 @@ export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
},
];
export const meterAggregateOperatorOptions: SelectOption<string, string>[] = [
{
value: MetricAggregateOperator.COUNT,
label: 'Count',
},
{
value: MetricAggregateOperator.COUNT_DISTINCT,
// eslint-disable-next-line sonarjs/no-duplicate-string
label: 'Count Distinct',
},
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.P05,
label: 'P05',
},
{
value: MetricAggregateOperator.P10,
label: 'P10',
},
{
value: MetricAggregateOperator.P20,
label: 'P20',
},
{
value: MetricAggregateOperator.P25,
label: 'P25',
},
{
value: MetricAggregateOperator.P50,
label: 'P50',
},
{
value: MetricAggregateOperator.P75,
label: 'P75',
},
{
value: MetricAggregateOperator.P90,
label: 'P90',
},
{
value: MetricAggregateOperator.P95,
label: 'P95',
},
{
value: MetricAggregateOperator.P99,
label: 'P99',
},
{
value: MetricAggregateOperator.RATE,
label: 'Rate',
},
{
value: MetricAggregateOperator.SUM_RATE,
label: 'Sum_rate',
},
{
value: MetricAggregateOperator.AVG_RATE,
label: 'Avg_rate',
},
{
value: MetricAggregateOperator.MAX_RATE,
label: 'Max_rate',
},
{
value: MetricAggregateOperator.MIN_RATE,
label: 'Min_rate',
},
{
value: MetricAggregateOperator.RATE_SUM,
label: 'Rate_sum',
},
{
value: MetricAggregateOperator.RATE_AVG,
label: 'Rate_avg',
},
{
value: MetricAggregateOperator.RATE_MIN,
label: 'Rate_min',
},
{
value: MetricAggregateOperator.RATE_MAX,
label: 'Rate_max',
},
{
value: MetricAggregateOperator.HIST_QUANTILE_50,
label: 'Hist_quantile_50',
},
{
value: MetricAggregateOperator.HIST_QUANTILE_75,
label: 'Hist_quantile_75',
},
{
value: MetricAggregateOperator.HIST_QUANTILE_90,
label: 'Hist_quantile_90',
},
{
value: MetricAggregateOperator.HIST_QUANTILE_95,
label: 'Hist_quantile_95',
},
{
value: MetricAggregateOperator.HIST_QUANTILE_99,
label: 'Hist_quantile_99',
},
];
export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
{
value: TracesAggregatorOperator.COUNT,

View File

@@ -77,9 +77,6 @@ const ROUTES = {
API_MONITORING: '/api-monitoring/explorer',
METRICS_EXPLORER_BASE: '/metrics-explorer',
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
METER_EXPLORER_BASE: '/meter-explorer',
METER_EXPLORER: '/meter-explorer',
METER_EXPLORER_VIEWS: '/meter-explorer/views',
HOME_PAGE: '/',
} as const;

View File

@@ -6,7 +6,6 @@ export const GlobalShortcuts = {
NavigateToAlerts: 'a+shift',
NavigateToExceptions: 'e+shift',
NavigateToMessagingQueues: 'm+shift',
ToggleSidebar: 'b+shift',
};
export const GlobalShortcutsName = {
@@ -17,7 +16,6 @@ export const GlobalShortcutsName = {
NavigateToAlerts: 'shift+a',
NavigateToExceptions: 'shift+e',
NavigateToMessagingQueues: 'shift+m',
ToggleSidebar: 'shift+b',
};
export const GlobalShortcutsDescription = {
@@ -28,5 +26,4 @@ export const GlobalShortcutsDescription = {
NavigateToAlerts: 'Navigate to alerts page',
NavigateToExceptions: 'Navigate to Exceptions page',
NavigateToMessagingQueues: 'Navigate to Messaging Queues page',
ToggleSidebar: 'Toggle sidebar visibility',
};

View File

@@ -1,176 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import logEvent from 'api/common/logEvent';
import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts';
import { USER_PREFERENCES } from 'constants/userPreferences';
import {
KeyboardHotkeysProvider,
useKeyboardHotkeys,
} from 'hooks/hotkeys/useKeyboardHotkeys';
import { QueryClient, QueryClientProvider } from 'react-query';
// Mock dependencies
jest.mock('api/common/logEvent', () => jest.fn());
// Mock the AppContext
const mockUpdateUserPreferenceInContext = jest.fn();
const SHIFT_B_KEYBOARD_SHORTCUT = '{Shift>}b{/Shift}';
jest.mock('providers/App/App', () => ({
useAppContext: jest.fn(() => ({
userPreferences: [
{
name: USER_PREFERENCES.SIDENAV_PINNED,
value: false,
},
],
updateUserPreferenceInContext: mockUpdateUserPreferenceInContext,
})),
}));
function TestComponent({
mockHandleShortcut,
}: {
mockHandleShortcut: () => void;
}): JSX.Element {
const { registerShortcut } = useKeyboardHotkeys();
registerShortcut(GlobalShortcuts.ToggleSidebar, mockHandleShortcut);
return <div data-testid="test">Test</div>;
}
describe('Sidebar Toggle Shortcut', () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
});
jest.clearAllMocks();
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Global Shortcuts Constants', () => {
it('should have the correct shortcut key combination', () => {
expect(GlobalShortcuts.ToggleSidebar).toBe('b+shift');
});
});
describe('Keyboard Shortcut Registration', () => {
it('should register the sidebar toggle shortcut correctly', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn();
render(
<QueryClientProvider client={queryClient}>
<KeyboardHotkeysProvider>
<TestComponent mockHandleShortcut={mockHandleShortcut} />
</KeyboardHotkeysProvider>
</QueryClientProvider>,
);
// Trigger the shortcut
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
expect(mockHandleShortcut).toHaveBeenCalled();
});
it('should not trigger shortcut in input fields', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn();
function TestComponent(): JSX.Element {
const { registerShortcut } = useKeyboardHotkeys();
registerShortcut(GlobalShortcuts.ToggleSidebar, mockHandleShortcut);
return (
<div>
<input data-testid="input-field" />
<div data-testid="test">Test</div>
</div>
);
}
render(
<QueryClientProvider client={queryClient}>
<KeyboardHotkeysProvider>
<TestComponent />
</KeyboardHotkeysProvider>
</QueryClientProvider>,
);
// Focus on input field
const inputField = screen.getByTestId('input-field');
await user.click(inputField);
// Try to trigger shortcut while focused on input
await user.keyboard('{Shift>}b{/Shift}');
// Should not trigger the shortcut
expect(mockHandleShortcut).not.toHaveBeenCalled();
});
});
describe('Sidebar Toggle Functionality', () => {
it('should log the toggle event with correct parameters', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn(() => {
logEvent('Global Shortcut: Sidebar Toggle', {
previousState: false,
newState: true,
});
});
render(
<QueryClientProvider client={queryClient}>
<KeyboardHotkeysProvider>
<TestComponent mockHandleShortcut={mockHandleShortcut} />
</KeyboardHotkeysProvider>
</QueryClientProvider>,
);
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
expect(logEvent).toHaveBeenCalledWith('Global Shortcut: Sidebar Toggle', {
previousState: false,
newState: true,
});
});
it('should update user preference in context', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn(() => {
const save = {
name: USER_PREFERENCES.SIDENAV_PINNED,
value: true,
};
mockUpdateUserPreferenceInContext(save);
});
render(
<QueryClientProvider client={queryClient}>
<KeyboardHotkeysProvider>
<TestComponent mockHandleShortcut={mockHandleShortcut} />
</KeyboardHotkeysProvider>
</QueryClientProvider>,
);
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
expect(mockUpdateUserPreferenceInContext).toHaveBeenCalledWith({
name: USER_PREFERENCES.SIDENAV_PINNED,
value: true,
});
});
});
});

View File

@@ -10,10 +10,8 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
import getChangelogByVersion from 'api/changelog/getChangelogByVersion';
import logEvent from 'api/common/logEvent';
import manageCreditCardApi from 'api/v1/portal/create';
import updateUserPreference from 'api/v1/user/preferences/name/update';
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
import getUserVersion from 'api/v1/version/getVersion';
import { AxiosError } from 'axios';
import cx from 'classnames';
import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
@@ -24,12 +22,10 @@ import { Events } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts';
import { USER_PREFERENCES } from 'constants/userPreferences';
import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav';
import dayjs from 'dayjs';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
@@ -72,10 +68,8 @@ import {
LicensePlatform,
LicenseState,
} from 'types/api/licensesV3/getActive';
import { UserPreference } from 'types/api/preferences/preference';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { showErrorNotification } from 'utils/error';
import { eventEmitter } from 'utils/getEventEmitter';
import {
getFormattedDate,
@@ -668,85 +662,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</div>
);
const sideNavPinnedPreference = userPreferences?.find(
const sideNavPinned = userPreferences?.find(
(preference) => preference.name === USER_PREFERENCES.SIDENAV_PINNED,
)?.value as boolean;
// Add loading state to prevent layout shift during initial load
const [isSidebarLoaded, setIsSidebarLoaded] = useState(false);
// Get sidebar state from localStorage as fallback until preferences are loaded
const getSidebarStateFromLocalStorage = useCallback((): boolean => {
try {
const storedValue = getLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED);
return storedValue === 'true';
} catch {
return false;
}
}, []);
// Set sidebar as loaded after user preferences are fetched
useEffect(() => {
if (userPreferences !== null) {
setIsSidebarLoaded(true);
}
}, [userPreferences]);
// Use localStorage value as fallback until preferences are loaded
const isSideNavPinned = isSidebarLoaded
? sideNavPinnedPreference
: getSidebarStateFromLocalStorage();
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const { updateUserPreferenceInContext } = useAppContext();
const { mutate: updateUserPreferenceMutation } = useMutation(
updateUserPreference,
{
onError: (error) => {
showErrorNotification(notifications, error as AxiosError);
},
},
);
const handleToggleSidebar = useCallback((): void => {
const newState = !isSideNavPinned;
logEvent('Global Shortcut: Sidebar Toggle', {
previousState: isSideNavPinned,
newState,
});
// Save to localStorage immediately for instant feedback
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, newState.toString());
// Update the context immediately
const save = {
name: USER_PREFERENCES.SIDENAV_PINNED,
value: newState,
};
updateUserPreferenceInContext(save as UserPreference);
// Make the API call in the background
updateUserPreferenceMutation({
name: USER_PREFERENCES.SIDENAV_PINNED,
value: newState,
});
}, [
isSideNavPinned,
updateUserPreferenceInContext,
updateUserPreferenceMutation,
]);
// Register the sidebar toggle shortcut
useEffect(() => {
registerShortcut(GlobalShortcuts.ToggleSidebar, handleToggleSidebar);
return (): void => {
deregisterShortcut(GlobalShortcuts.ToggleSidebar);
};
}, [registerShortcut, deregisterShortcut, handleToggleSidebar]);
const SHOW_TRIAL_EXPIRY_BANNER =
showTrialExpiryBanner && !showPaymentFailedWarning;
const SHOW_WORKSPACE_RESTRICTED_BANNER = showWorkspaceRestricted;
@@ -820,14 +739,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
className={cx(
'app-layout',
isDarkMode ? 'darkMode dark' : 'lightMode',
isSideNavPinned ? 'side-nav-pinned' : '',
sideNavPinned ? 'side-nav-pinned' : '',
SHOW_WORKSPACE_RESTRICTED_BANNER ? 'isWorkspaceRestricted' : '',
SHOW_TRIAL_EXPIRY_BANNER ? 'isTrialExpired' : '',
SHOW_PAYMENT_FAILED_BANNER ? 'isPaymentFailed' : '',
)}
>
{isToDisplayLayout && !renderFullScreen && (
<SideNav isPinned={isSideNavPinned} />
<SideNav isPinned={sideNavPinned} />
)}
<div
className={cx('app-content', {

View File

@@ -1,4 +1,4 @@
import { ENTITY_VERSION_V5 } from 'constants/app';
import { ENTITY_VERSION_V4 } from 'constants/app';
import {
initialQueryBuilderFormValuesMap,
initialQueryPromQLData,
@@ -28,7 +28,7 @@ const defaultAnnotations = {
export const alertDefaults: AlertDef = {
alertType: AlertTypes.METRICS_BASED_ALERT,
version: ENTITY_VERSION_V5,
version: ENTITY_VERSION_V4,
condition: {
compositeQuery: {
builderQueries: {
@@ -62,7 +62,7 @@ export const alertDefaults: AlertDef = {
export const anamolyAlertDefaults: AlertDef = {
alertType: AlertTypes.METRICS_BASED_ALERT,
version: ENTITY_VERSION_V5,
version: ENTITY_VERSION_V4,
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
condition: {
compositeQuery: {
@@ -107,7 +107,7 @@ export const anamolyAlertDefaults: AlertDef = {
export const logAlertDefaults: AlertDef = {
alertType: AlertTypes.LOGS_BASED_ALERT,
version: ENTITY_VERSION_V5,
version: ENTITY_VERSION_V4,
condition: {
compositeQuery: {
builderQueries: {
@@ -139,7 +139,7 @@ export const logAlertDefaults: AlertDef = {
export const traceAlertDefaults: AlertDef = {
alertType: AlertTypes.TRACES_BASED_ALERT,
version: ENTITY_VERSION_V5,
version: ENTITY_VERSION_V4,
condition: {
compositeQuery: {
builderQueries: {
@@ -171,7 +171,7 @@ export const traceAlertDefaults: AlertDef = {
export const exceptionAlertDefaults: AlertDef = {
alertType: AlertTypes.EXCEPTIONS_BASED_ALERT,
version: ENTITY_VERSION_V5,
version: ENTITY_VERSION_V4,
condition: {
compositeQuery: {
builderQueries: {

View File

@@ -1,6 +1,6 @@
import { Form, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { QueryParams } from 'constants/query';
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
@@ -71,14 +71,14 @@ function CreateRules(): JSX.Element {
case AlertTypes.ANOMALY_BASED_ALERT:
setInitValues({
...anamolyAlertDefaults,
version: version || ENTITY_VERSION_V5,
version: version || ENTITY_VERSION_V4,
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
});
break;
default:
setInitValues({
...alertDefaults,
version: version || ENTITY_VERSION_V5,
version: version || ENTITY_VERSION_V4,
ruleType: AlertDetectionTypes.THRESHOLD_ALERT,
});
}

View File

@@ -16,7 +16,6 @@ function ExplorerOptionWrapper({
sourcepage,
isOneChartPerQuery,
splitedQueries,
signalSource,
}: ExplorerOptionsWrapperProps): JSX.Element {
const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false);
@@ -33,7 +32,6 @@ function ExplorerOptionWrapper({
isLoading={isLoading}
onExport={onExport}
sourcepage={sourcepage}
signalSource={signalSource}
isExplorerOptionHidden={isExplorerOptionHidden}
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
isOneChartPerQuery={isOneChartPerQuery}

View File

@@ -1,12 +1,12 @@
.explorer-options-container {
position: fixed;
bottom: 8px;
bottom: 24px;
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
display: flex;
gap: 8px;
gap: 16px;
background-color: transparent;
.multi-alert-button,
@@ -33,12 +33,11 @@
display: inline-flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
padding: 10px 10px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.6);
border: 1px solid var(--bg-slate-500);
border-radius: 4px;
backdrop-filter: blur(20px);
box-sizing: border-box;
.action-icon {
display: flex;
@@ -65,9 +64,9 @@
.explorer-options {
padding: 10px 12px;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
border: 1px solid var(--bg-slate-400);
border-radius: 50px;
background: rgba(22, 24, 29, 0.6);
backdrop-filter: blur(20px);
cursor: default;

View File

@@ -93,7 +93,6 @@ function ExplorerOptions({
onExport,
query,
sourcepage,
signalSource,
isExplorerOptionHidden = false,
setIsExplorerOptionHidden,
isOneChartPerQuery = false,
@@ -111,7 +110,6 @@ function ExplorerOptions({
const isLogsExplorer = sourcepage === DataSource.LOGS;
const isMetricsExplorer = sourcepage === DataSource.METRICS;
const isMeterExplorer = signalSource === 'meter';
const PRESERVED_VIEW_LOCAL_STORAGE_KEY = LOCALSTORAGE.LAST_USED_SAVED_VIEWS;
@@ -122,11 +120,8 @@ function ExplorerOptions({
if (isMetricsExplorer) {
return PreservedViewsTypes.METRICS;
}
if (isMeterExplorer) {
return PreservedViewsTypes.METER;
}
return PreservedViewsTypes.TRACES;
}, [isLogsExplorer, isMetricsExplorer, isMeterExplorer]);
}, [isLogsExplorer, isMetricsExplorer]);
const onModalToggle = useCallback((value: boolean) => {
setIsExport(value);
@@ -155,10 +150,6 @@ function ExplorerOptions({
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
panelType,
});
} else if (isMeterExplorer) {
logEvent('Meter Explorer: Save view clicked', {
panelType,
});
}
setIsSaveModalOpen(!isSaveModalOpen);
};
@@ -252,7 +243,7 @@ function ExplorerOptions({
error,
isRefetching,
refetch: refetchAllView,
} = useGetAllViews(isMeterExplorer ? 'meter' : sourcepage);
} = useGetAllViews(sourcepage);
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
@@ -325,7 +316,7 @@ function ExplorerOptions({
compositeQuery,
viewKey,
extraData: updatedExtraData,
sourcePage: isMeterExplorer ? 'meter' : sourcepage,
sourcePage: sourcepage,
viewName,
});
@@ -341,7 +332,7 @@ function ExplorerOptions({
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey,
extraData: updatedExtraData,
sourcePage: isMeterExplorer ? 'meter' : sourcepage,
sourcePage: sourcepage,
viewName,
},
{
@@ -468,11 +459,6 @@ function ExplorerOptions({
panelType,
viewName: option?.value,
});
} else if (isMeterExplorer) {
logEvent('Meter Explorer: Select view', {
panelType,
viewName: option?.value,
});
}
updatePreservedViewInLocalStorage(option);
@@ -519,11 +505,6 @@ function ExplorerOptions({
: defaultLogsSelectedColumns,
});
if (signalSource === 'meter') {
history.replace(ROUTES.METER_EXPLORER);
return;
}
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
};
@@ -568,7 +549,7 @@ function ExplorerOptions({
redirectWithQueryBuilderData,
refetchAllView,
saveViewAsync,
sourcePage: isMeterExplorer ? 'meter' : sourcepage,
sourcePage: sourcepage,
viewName: newViewName,
setNewViewName,
});
@@ -687,7 +668,7 @@ function ExplorerOptions({
return `Query ${query.builder.queryData[0].queryName}`;
};
const CreateAlertButton = useMemo(() => {
const alertButton = useMemo(() => {
if (isOneChartPerQuery) {
const selectLabel = (
<Button
@@ -740,7 +721,7 @@ function ExplorerOptions({
splitedQueries,
]);
const AddToDashboardButton = useMemo(() => {
const dashboardButton = useMemo(() => {
if (isOneChartPerQuery) {
const selectLabel = (
<Button
@@ -848,7 +829,7 @@ function ExplorerOptions({
style={{
background: extraData
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
: 'initial',
: 'transparent',
}}
>
<div className="view-options">
@@ -903,13 +884,10 @@ function ExplorerOptions({
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
{signalSource !== 'meter' && (
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
{CreateAlertButton}
{AddToDashboardButton}
</div>
)}
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
{alertButton}
{dashboardButton}
</div>
<div className="actions">
{/* Hide the info icon for metrics explorer until we get the docs link */}
{!isMetricsExplorer && (
@@ -1015,7 +993,6 @@ export interface ExplorerOptionsProps {
query: Query | null;
disabled: boolean;
sourcepage: DataSource;
signalSource?: string;
isExplorerOptionHidden?: boolean;
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
isOneChartPerQuery?: boolean;
@@ -1028,7 +1005,6 @@ ExplorerOptions.defaultProps = {
setIsExplorerOptionHidden: undefined,
isOneChartPerQuery: false,
splitedQueries: [],
signalSource: '',
};
export default ExplorerOptions;

View File

@@ -2,5 +2,4 @@ export enum PreservedViewsTypes {
LOGS = 'logs',
TRACES = 'traces',
METRICS = 'metrics',
METER = 'meter',
}

View File

@@ -13,7 +13,7 @@ import { PreservedViewsTypes } from './constants';
export interface SaveNewViewHandlerProps {
viewName: string;
compositeQuery: ICompositeMetricQuery;
sourcePage: DataSource | 'meter';
sourcePage: DataSource;
extraData: SaveViewProps['extraData'];
panelType: PANEL_TYPES | null;
notifications: NotificationInstance;
@@ -32,8 +32,7 @@ export interface SaveNewViewHandlerProps {
export type PreservedViewType =
| PreservedViewsTypes.LOGS
| PreservedViewsTypes.TRACES
| PreservedViewsTypes.METRICS
| PreservedViewsTypes.METER;
| PreservedViewsTypes.METRICS;
export type PreservedViewsInLocalStorage = Partial<
Record<PreservedViewType, { key: string; value: string }>

View File

@@ -37,7 +37,7 @@ export const saveNewViewHandler = ({
{
viewName,
compositeQuery,
sourcePage: sourcePage as DataSource,
sourcePage,
extraData,
},
{

View File

@@ -2,12 +2,6 @@
height: 57vh;
width: 100%;
.chart-preview-header {
display: flex;
align-items: center;
gap: 8px;
}
.threshold-alert-uplot-chart-container {
height: calc(100% - 24px);
}

View File

@@ -1,14 +1,12 @@
import './ChartPreview.styles.scss';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import { InfoCircleOutlined } from '@ant-design/icons';
import Spinner from 'components/Spinner';
import WarningPopover from 'components/WarningPopover/WarningPopover';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
@@ -28,7 +26,6 @@ import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { isEmpty } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -37,10 +34,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Warning } from 'types/api';
import { AlertDef } from 'types/api/alerts/def';
import { LegendPosition } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -50,7 +44,7 @@ import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import { AlertDetectionTypes } from '..';
import { ChartContainer } from './styles';
import { ChartContainer, FailedMessageContainer } from './styles';
import { getThresholdLabel } from './utils';
export interface ChartPreviewProps {
@@ -86,7 +80,6 @@ function ChartPreview({
const threshold = alertDef?.condition.target || 0;
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
const { currentQuery } = useQueryBuilder();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
@@ -178,19 +171,6 @@ function ChartPreview({
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, queryResponse, setQueryStatus]);
// Initialize graph visibility from localStorage
useEffect(() => {
if (queryResponse?.data?.payload?.data?.result) {
const {
graphVisibilityStates: localStoredVisibilityState,
} = getLocalStorageGraphVisibilityState({
apiResponse: queryResponse.data.payload.data.result,
name: 'alert-chart-preview',
});
setGraphVisibility(localStoredVisibilityState);
}
}, [queryResponse?.data?.payload?.data?.result]);
if (queryResponse.data && graphType === PANEL_TYPES.BAR) {
const sortedSeriesData = getSortedSeriesData(
queryResponse.data?.payload.data.result,
@@ -277,10 +257,6 @@ function ChartPreview({
timezone: timezone.value,
currentQuery,
query: query || currentQuery,
graphsVisibilityStates: graphVisibility,
setGraphsVisibilityStates: setGraphVisibility,
enhancedLegend: true,
legendPosition: LegendPosition.BOTTOM,
}),
[
yAxisUnit,
@@ -298,7 +274,6 @@ function ChartPreview({
timezone.value,
currentQuery,
query,
graphVisibility,
],
);
@@ -314,23 +289,20 @@ function ChartPreview({
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
?.active || false;
const isWarning = !isEmpty(queryResponse.data?.warning);
return (
<div className="alert-chart-container" ref={graphRef}>
<ChartContainer>
<div className="chart-preview-header">
{headline}
{isWarning && (
<WarningPopover warningData={queryResponse.data?.warning as Warning} />
)}
</div>
{headline}
<div className="threshold-alert-uplot-chart-container">
{queryResponse.isLoading && (
<Spinner size="large" tip="Loading..." height="100%" />
)}
{(queryResponse?.isError || queryResponse?.error) && (
<ErrorInPlace error={queryResponse.error as APIError} />
<FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />
{queryResponse.error.message || t('preview_chart_unexpected_error')}
</FailedMessageContainer>
)}
{chartDataAvailable && !isAnomalyDetectionAlert && (

View File

@@ -37,8 +37,11 @@ import {
defaultEvalWindow,
defaultMatchType,
} from 'types/api/alerts/def';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryFunction } from 'types/api/v5/queryRange';
import {
IBuilderQuery,
Query,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -179,17 +182,12 @@ function FormAlertRules({
setDetectionMethod(value);
};
const updateFunctions = (data: IBuilderQuery): QueryFunction[] => {
const anomalyFunction: QueryFunction = {
name: 'anomaly' as any,
args: [
{
name: 'z_score_threshold',
value: alertDef.condition.target || 3,
},
],
const updateFunctions = (data: IBuilderQuery): QueryFunctionProps[] => {
const anomalyFunction = {
name: 'anomaly',
args: [],
namedArgs: { z_score_threshold: alertDef.condition.target || 3 },
};
const functions = data.functions || [];
if (alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
@@ -240,18 +238,8 @@ function FormAlertRules({
const queryData = currentQuery.builder.queryData[index];
const updatedFunctions = updateFunctions(queryData);
// Only update if functions actually changed to avoid resetting aggregateAttribute
const currentFunctions = queryData.functions || [];
const functionsChanged = !isEqual(currentFunctions, updatedFunctions);
if (functionsChanged) {
const updatedQueryData = {
...queryData,
functions: updatedFunctions,
};
handleSetQueryData(index, updatedQueryData);
}
queryData.functions = updatedFunctions;
handleSetQueryData(index, queryData);
}
};

View File

@@ -4,7 +4,9 @@
overflow-y: hidden;
.full-view-header-container {
height: 40px;
display: flex;
flex-direction: column;
gap: 16px;
}
.graph-container {

View File

@@ -1,3 +1,4 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './WidgetFullView.styles.scss';
import {
@@ -8,18 +9,23 @@ import {
import { Button, Input, Spin } from 'antd';
import cx from 'classnames';
import { ToggleGraphProps } from 'components/Graph/types';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import useDrilldown from 'container/GridCardLayout/GridCard/FullView/useDrilldown';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import {
timeItems,
timePreferance,
} from 'container/NewWidget/RightContainer/timeItems';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useChartMutable } from 'hooks/useChartMutable';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
@@ -52,6 +58,7 @@ function FullView({
onClickHandler,
customOnDragSelect,
setCurrentGraphRef,
enableDrillDown = false,
}: FullViewProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { selectedTime: globalSelectedTime } = useSelector<
@@ -63,6 +70,7 @@ function FullView({
const location = useLocation();
const fullViewRef = useRef<HTMLDivElement>(null);
const { handleRunQuery } = useQueryBuilder();
useEffect(() => {
setCurrentGraphRef(fullViewRef);
@@ -114,6 +122,13 @@ function FullView({
};
});
const { dashboardEditView, handleResetQuery, showResetQuery } = useDrilldown({
enableDrillDown,
widget,
setRequestData,
selectedDashboard,
});
useEffect(() => {
setRequestData((prev) => ({
...prev,
@@ -204,71 +219,115 @@ function FullView({
return (
<div className="full-view-container">
<div className="full-view-header-container">
{fullViewOptions && (
<TimeContainer $panelType={widget.panelTypes}>
{response.isFetching && (
<Spin spinning indicator={<LoadingOutlined spin />} />
<OverlayScrollbar>
<>
<div className="full-view-header-container">
{fullViewOptions && (
<TimeContainer $panelType={widget.panelTypes}>
{enableDrillDown && (
<div className="drildown-options-container">
{showResetQuery && (
<Button type="link" onClick={handleResetQuery}>
Reset Query
</Button>
)}
<Button
className="switch-edit-btn"
disabled={response.isFetching || response.isLoading}
onClick={(): void => {
if (dashboardEditView) {
safeNavigate(dashboardEditView);
}
}}
>
Switch to Edit Mode
</Button>
</div>
)}
<div className="time-container">
{response.isFetching && (
<Spin spinning indicator={<LoadingOutlined spin />} />
)}
<TimePreference
selectedTime={selectedTime}
setSelectedTime={setSelectedTime}
/>
<Button
style={{
marginLeft: '4px',
}}
onClick={(): void => {
response.refetch();
}}
type="primary"
icon={<SyncOutlined />}
/>
</div>
</TimeContainer>
)}
<TimePreference
selectedTime={selectedTime}
setSelectedTime={setSelectedTime}
/>
<Button
style={{
marginLeft: '4px',
}}
onClick={(): void => {
response.refetch();
}}
type="primary"
icon={<SyncOutlined />}
/>
</TimeContainer>
)}
</div>
{enableDrillDown && (
<>
<QueryBuilderV2
panelType={widget.panelTypes}
version={selectedDashboard?.data?.version || 'v3'}
isListViewPanel={widget.panelTypes === PANEL_TYPES.LIST}
// filterConfigs={filterConfigs}
// queryComponents={queryComponents}
/>
<RightToolbarActions
onStageRunQuery={(): void => {
handleRunQuery(true, true);
}}
/>
</>
)}
</div>
<div
className={cx('graph-container', {
disabled: isDashboardLocked,
'height-widget': widget?.mergeAllActiveQueries || widget?.stackedBarChart,
'list-graph-container': isListView,
})}
ref={fullViewRef}
>
<GraphContainer
style={{
height: isListView ? '100%' : '90%',
}}
isGraphLegendToggleAvailable={canModifyChart}
>
{isTablePanel && (
<Input
addonBefore={<SearchOutlined size={14} />}
className="global-search"
placeholder="Search..."
allowClear
key={widget.id}
onChange={(e): void => {
setSearchTerm(e.target.value || '');
<div
className={cx('graph-container', {
disabled: isDashboardLocked,
'height-widget':
widget?.mergeAllActiveQueries || widget?.stackedBarChart,
'list-graph-container': isListView,
})}
ref={fullViewRef}
>
<GraphContainer
style={{
height: isListView ? '100%' : '90%',
}}
/>
)}
<PanelWrapper
queryResponse={response}
widget={widget}
setRequestData={setRequestData}
isFullViewMode
onToggleModelHandler={onToggleModelHandler}
setGraphVisibility={setGraphsVisibilityStates}
graphVisibility={graphsVisibilityStates}
onDragSelect={customOnDragSelect ?? onDragSelect}
tableProcessedDataRef={tableProcessedDataRef}
searchTerm={searchTerm}
onClickHandler={onClickHandler}
/>
</GraphContainer>
</div>
isGraphLegendToggleAvailable={canModifyChart}
>
{isTablePanel && (
<Input
addonBefore={<SearchOutlined size={14} />}
className="global-search"
placeholder="Search..."
allowClear
key={widget.id}
onChange={(e): void => {
setSearchTerm(e.target.value || '');
}}
/>
)}
<PanelWrapper
queryResponse={response}
widget={widget}
setRequestData={setRequestData}
isFullViewMode
onToggleModelHandler={onToggleModelHandler}
setGraphVisibility={setGraphsVisibilityStates}
graphVisibility={graphsVisibilityStates}
onDragSelect={customOnDragSelect ?? onDragSelect}
tableProcessedDataRef={tableProcessedDataRef}
searchTerm={searchTerm}
onClickHandler={onClickHandler}
enableDrillDown={enableDrillDown}
/>
</GraphContainer>
</div>
</>
</OverlayScrollbar>
</div>
);
}

View File

@@ -18,6 +18,7 @@ export const NotFoundContainer = styled.div`
export const TimeContainer = styled.div<Props>`
display: flex;
justify-content: flex-end;
gap: 16px;
align-items: center;
${({ $panelType }): FlattenSimpleInterpolation =>
$panelType === PANEL_TYPES.TABLE
@@ -25,6 +26,10 @@ export const TimeContainer = styled.div<Props>`
margin-bottom: 1rem;
`
: css``}
.time-container {
display: flex;
}
`;
export const GraphContainer = styled.div<GraphContainerProps>`

View File

@@ -59,6 +59,7 @@ export interface FullViewProps {
isDependedDataLoaded?: boolean;
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
setCurrentGraphRef: Dispatch<SetStateAction<RefObject<HTMLDivElement> | null>>;
enableDrillDown?: boolean;
}
export interface GraphManagerProps extends UplotProps {

View File

@@ -0,0 +1,84 @@
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
export interface DrilldownQueryProps {
widget: Widgets;
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
enableDrillDown: boolean;
selectedDashboard: Dashboard | undefined;
}
export interface UseDrilldownReturn {
dashboardEditView: string;
handleResetQuery: () => void;
showResetQuery: boolean;
}
const useDrilldown = ({
enableDrillDown,
widget,
setRequestData,
selectedDashboard,
}: DrilldownQueryProps): UseDrilldownReturn => {
const isMounted = useRef(false);
const { redirectWithQueryBuilderData, currentQuery } = useQueryBuilder();
const compositeQuery = useGetCompositeQueryParam();
useEffect(() => {
if (enableDrillDown && !!compositeQuery) {
setRequestData((prev) => ({
...prev,
query: compositeQuery,
}));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentQuery, compositeQuery]);
// update composite query with widget query if composite query is not present in url.
// Composite query should be in the url if switch to edit mode is clicked or drilldown happens from dashboard.
useEffect(() => {
if (enableDrillDown && !isMounted.current) {
redirectWithQueryBuilderData(compositeQuery || widget.query);
}
isMounted.current = true;
}, [widget, enableDrillDown, compositeQuery, redirectWithQueryBuilderData]);
const dashboardEditView = selectedDashboard?.id
? generateExportToDashboardLink({
query: currentQuery,
panelType: widget.panelTypes,
dashboardId: selectedDashboard?.id || '',
widgetId: widget.id,
})
: '';
const showResetQuery = useMemo(
() =>
JSON.stringify(widget.query?.builder) !==
JSON.stringify(compositeQuery?.builder),
[widget.query, compositeQuery],
);
const handleResetQuery = useCallback((): void => {
redirectWithQueryBuilderData(widget.query);
}, [redirectWithQueryBuilderData, widget.query]);
return {
dashboardEditView,
handleResetQuery,
showResetQuery,
};
};
export default useDrilldown;

View File

@@ -30,7 +30,6 @@ import {
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { Widgets } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
import { DataSource } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
@@ -46,6 +45,7 @@ import { getLocalStorageGraphVisibilityState, handleGraphClick } from './utils';
function WidgetGraphComponent({
widget,
queryResponse,
errorMessage,
version,
threshold,
headerMenuList,
@@ -62,6 +62,7 @@ function WidgetGraphComponent({
customErrorMessage,
customOnRowClick,
customTimeRangeWindowForCoRelation,
enableDrillDown,
}: WidgetGraphComponentProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [deleteModal, setDeleteModal] = useState(false);
@@ -184,19 +185,9 @@ function WidgetGraphComponent({
notifications.success({
message: 'Panel cloned successfully, redirecting to new copy.',
});
const clonedWidget = updatedDashboard.data?.data?.widgets?.find(
(w) => w.id === uuid,
) as Widgets;
const queryParams = {
[QueryParams.graphType]: clonedWidget?.panelTypes,
[QueryParams.widgetId]: uuid,
...(clonedWidget?.query && {
[QueryParams.compositeQuery]: encodeURIComponent(
JSON.stringify(clonedWidget.query),
),
}),
graphType: widget?.panelTypes,
widgetId: uuid,
};
safeNavigate(`${pathname}/new?${createQueryParams(queryParams)}`);
},
@@ -236,6 +227,7 @@ function WidgetGraphComponent({
const onToggleModelHandler = (): void => {
const existingSearchParams = new URLSearchParams(search);
existingSearchParams.delete(QueryParams.expandedWidgetId);
existingSearchParams.delete(QueryParams.compositeQuery);
const updatedQueryParams = Object.fromEntries(existingSearchParams.entries());
if (queryResponse.data?.payload) {
const {
@@ -364,6 +356,7 @@ function WidgetGraphComponent({
onClickHandler={onClickHandler ?? graphClickHandler}
customOnDragSelect={customOnDragSelect}
setCurrentGraphRef={setCurrentGraphRef}
enableDrillDown={enableDrillDown}
/>
</Modal>
@@ -376,6 +369,7 @@ function WidgetGraphComponent({
onDelete={handleOnDelete}
onClone={onCloneHandler}
queryResponse={queryResponse}
errorMessage={errorMessage}
threshold={threshold}
headerMenuList={headerMenuList}
isWarning={isWarning}
@@ -414,6 +408,7 @@ function WidgetGraphComponent({
onOpenTraceBtnClick={onOpenTraceBtnClick}
customSeries={customSeries}
customOnRowClick={customOnRowClick}
enableDrillDown={enableDrillDown}
/>
</div>
)}
@@ -426,6 +421,7 @@ WidgetGraphComponent.defaultProps = {
setLayout: undefined,
onClickHandler: undefined,
customTimeRangeWindowForCoRelation: undefined,
enableDrillDown: false,
};
export default WidgetGraphComponent;

View File

@@ -53,6 +53,7 @@ function GridCardGraph({
customTimeRange,
customOnRowClick,
customTimeRangeWindowForCoRelation,
enableDrillDown,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
@@ -267,6 +268,7 @@ function GridCardGraph({
getGraphData?.(data?.payload?.data);
setDashboardQueryRangeCalled(true);
},
showErrorModal: false,
},
);
@@ -301,7 +303,7 @@ function GridCardGraph({
widget={widget}
queryResponse={queryResponse}
errorMessage={errorMessage}
isWarning={!isEmpty(queryResponse.data?.warning)}
isWarning={false}
version={version}
threshold={threshold}
headerMenuList={menuList}
@@ -317,6 +319,7 @@ function GridCardGraph({
customErrorMessage={isInternalServerError ? customErrorMessage : undefined}
customOnRowClick={customOnRowClick}
customTimeRangeWindowForCoRelation={customTimeRangeWindowForCoRelation}
enableDrillDown={enableDrillDown}
/>
)}
</div>
@@ -332,6 +335,7 @@ GridCardGraph.defaultProps = {
version: 'v3',
analyticsEvent: undefined,
customTimeRangeWindowForCoRelation: undefined,
enableDrillDown: false,
};
export default memo(GridCardGraph);

View File

@@ -41,6 +41,7 @@ export interface WidgetGraphComponentProps {
customErrorMessage?: string;
customOnRowClick?: (record: RowData) => void;
customTimeRangeWindowForCoRelation?: string | undefined;
enableDrillDown?: boolean;
}
export interface GridCardGraphProps {
@@ -69,6 +70,7 @@ export interface GridCardGraphProps {
};
customOnRowClick?: (record: RowData) => void;
customTimeRangeWindowForCoRelation?: string | undefined;
enableDrillDown?: boolean;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@@ -235,7 +235,6 @@ export const handleGraphClick = async ({
? customTracesTimeRange?.end
: xValue + (stepInterval ?? 60),
shouldResolveQuery: true,
widgetQuery: widget?.query,
}),
}));

View File

@@ -53,11 +53,12 @@ import { WidgetRowHeader } from './WidgetRow';
interface GraphLayoutProps {
handle: FullScreenHandle;
enableDrillDown?: boolean;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function GraphLayout(props: GraphLayoutProps): JSX.Element {
const { handle } = props;
const { handle, enableDrillDown = false } = props;
const { safeNavigate } = useSafeNavigate();
const {
selectedDashboard,
@@ -584,6 +585,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
version={ENTITY_VERSION_V5}
onDragSelect={onDragSelect}
dataAvailable={checkIfDataExists}
enableDrillDown={enableDrillDown}
/>
</Card>
</CardContainer>
@@ -670,3 +672,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
}
export default GraphLayout;
GraphLayout.defaultProps = {
enableDrillDown: false,
};

View File

@@ -10,13 +10,10 @@ import {
InfoCircleOutlined,
MoreOutlined,
SearchOutlined,
WarningOutlined,
} from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Dropdown, Input, MenuProps, Tooltip, Typography } from 'antd';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import ErrorPopover from 'components/ErrorPopover/ErrorPopover';
import Spinner from 'components/Spinner';
import WarningPopover from 'components/WarningPopover/WarningPopover';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import useGetResolvedText from 'hooks/dashboard/useGetResolvedText';
@@ -31,12 +28,11 @@ import { unparse } from 'papaparse';
import { useAppContext } from 'providers/App/App';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse, Warning } from 'types/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { errorTooltipPosition } from './config';
import { errorTooltipPosition, WARNING_MESSAGE } from './config';
import { MENUITEM_KEYS_VS_LABELS, MenuItemKeys } from './contants';
import { MenuItem } from './types';
import { generateMenuList, isTWidgetOptions } from './utils';
@@ -49,11 +45,9 @@ interface IWidgetHeaderProps {
onClone?: VoidFunction;
parentHover: boolean;
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown> & {
warning?: Warning;
},
Error
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
>;
errorMessage: string | undefined;
threshold?: ReactNode;
headerMenuList?: MenuItemKeys[];
isWarning: boolean;
@@ -70,6 +64,7 @@ function WidgetHeader({
onClone,
parentHover,
queryResponse,
errorMessage,
threshold,
headerMenuList,
isWarning,
@@ -217,8 +212,12 @@ function WidgetHeader({
});
const renderErrorMessage = useMemo(
() => <ErrorContent error={queryResponse.error as APIError} />,
[queryResponse.error],
() =>
errorMessage
?.split('\n')
// eslint-disable-next-line react/no-array-index-key
.map((item, i) => <p key={i}>{item}</p>),
[errorMessage],
);
if (widget.id === PANEL_TYPES.EMPTY_WIDGET) {
@@ -279,23 +278,23 @@ function WidgetHeader({
<Spinner style={{ paddingRight: '0.25rem' }} />
)}
{queryResponse.isError && (
<ErrorPopover
content={renderErrorMessage}
<Tooltip
title={renderErrorMessage}
placement={errorTooltipPosition}
overlayStyle={{ padding: 0, maxWidth: '600px' }}
overlayInnerStyle={{ padding: 0 }}
autoAdjustOverflow
className="widget-api-actions"
>
<CircleX
size={16}
style={{ cursor: 'pointer' }}
color={Color.BG_CHERRY_500}
/>
</ErrorPopover>
<CircleX size={20} />
</Tooltip>
)}
{isWarning && queryResponse.data?.warning && (
<WarningPopover warningData={queryResponse.data?.warning as Warning} />
{isWarning && (
<Tooltip
title={WARNING_MESSAGE}
placement={errorTooltipPosition}
className="widget-api-actions"
>
<WarningOutlined />
</Tooltip>
)}
{globalSearchAvailable && (
<SearchOutlined

View File

@@ -4,10 +4,17 @@ import GraphLayoutContainer from './GridCardLayout';
interface GridGraphProps {
handle: FullScreenHandle;
enableDrillDown?: boolean;
}
function GridGraph(props: GridGraphProps): JSX.Element {
const { handle } = props;
return <GraphLayoutContainer handle={handle} />;
const { handle, enableDrillDown = false } = props;
return (
<GraphLayoutContainer handle={handle} enableDrillDown={enableDrillDown} />
);
}
export default GridGraph;
GridGraph.defaultProps = {
enableDrillDown: false,
};

View File

@@ -1,8 +1,8 @@
import { getSubstituteVars } from 'api/dashboard/substitute_vars';
import { prepareQueryRangePayloadV5 } from 'api/v5/v5';
import { getQueryRangeFormat } from 'api/dashboard/queryRangeFormat';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { useCallback } from 'react';
import { useMutation } from 'react-query';
@@ -32,7 +32,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
GlobalReducer
>((state) => state.globalTime);
const queryRangeMutation = useMutation(getSubstituteVars);
const queryRangeMutation = useMutation(getQueryRangeFormat);
const getUpdatedQuery = useCallback(
async ({
@@ -40,7 +40,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
selectedDashboard,
}: UseUpdatedQueryOptions): Promise<Query> => {
// Prepare query payload with resolved variables
const { queryPayload } = prepareQueryRangePayloadV5({
const { queryPayload } = prepareQueryRangePayload({
query: widgetConfig.query,
graphType: getGraphType(widgetConfig.panelTypes),
selectedTime: widgetConfig.timePreferance,
@@ -53,10 +53,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
const queryResult = await queryRangeMutation.mutateAsync(queryPayload);
// Map query data from API response
return mapQueryDataFromApi(
queryResult.data.compositeQuery,
widgetConfig?.query,
);
return mapQueryDataFromApi(queryResult.compositeQuery, widgetConfig?.query);
},
[globalSelectedInterval, queryRangeMutation],
);

View File

@@ -7,7 +7,7 @@ import { ColumnType } from 'antd/es/table';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { Events } from 'constants/events';
import { QueryTable } from 'container/QueryTable';
import { getColumnUnit, RowData } from 'lib/query/createTableColumnsFromQuery';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { cloneDeep, get, isEmpty } from 'lodash-es';
import { Compass } from 'lucide-react';
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
@@ -84,11 +84,10 @@ function GridTableComponent({
(val): RowData => {
const newValue = { ...val };
Object.keys(val).forEach((k) => {
const unit = getColumnUnit(k, columnUnits);
if (unit) {
if (columnUnits[k]) {
// the check below takes care of not adding units for rows that have n/a or null values
if (val[k] !== 'n/a' && val[k] !== null) {
newValue[k] = getYAxisFormattedValue(String(val[k]), unit);
newValue[k] = getYAxisFormattedValue(String(val[k]), columnUnits[k]);
} else if (val[k] === null) {
newValue[k] = 'n/a';
}
@@ -122,8 +121,7 @@ function GridTableComponent({
render: (text: string, ...rest: any): ReactNode => {
let textForThreshold = text;
const dataIndex = (e as ColumnType<RowData>)?.dataIndex || e.title;
const unit = getColumnUnit(dataIndex as string, columnUnits || {});
if (unit) {
if (columnUnits && columnUnits?.[dataIndex as string]) {
textForThreshold = rest[0][`${dataIndex}_without_unit`];
}
const isNumber = !Number.isNaN(Number(textForThreshold));
@@ -133,7 +131,7 @@ function GridTableComponent({
thresholds,
dataIndex as string,
Number(textForThreshold),
unit,
columnUnits?.[dataIndex as string],
);
const idx = thresholds.findIndex(

View File

@@ -22,6 +22,7 @@ export type GridTableComponentProps = {
widgetId?: string;
renderColumnCell?: QueryTableProps['renderColumnCell'];
customColTitles?: Record<string, string>;
enableDrillDown?: boolean;
} & Pick<LogsExplorerTableProps, 'data'> &
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;

View File

@@ -1,5 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { ColumnsType, ColumnType } from 'antd/es/table';
import { ColumnType } from 'antd/es/table';
import { convertUnit } from 'container/NewWidget/RightContainer/dataFormatCategories';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
@@ -9,6 +9,12 @@ import { isEmpty, isNaN } from 'lodash-es';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
// Custom column type that extends ColumnType to include isValueColumn
export interface CustomDataColumnType<T> extends ColumnType<T> {
isValueColumn?: boolean;
queryName?: string;
}
// Helper function to evaluate the condition based on the operator
function evaluateCondition(
operator: string | undefined,
@@ -180,9 +186,9 @@ export function createColumnsAndDataSource(
data: TableData,
currentQuery: Query,
renderColumnCell?: QueryTableProps['renderColumnCell'],
): { columns: ColumnsType<RowData>; dataSource: RowData[] } {
const columns: ColumnsType<RowData> =
data.columns?.reduce<ColumnsType<RowData>>((acc, item) => {
): { columns: CustomDataColumnType<RowData>[]; dataSource: RowData[] } {
const columns: CustomDataColumnType<RowData>[] =
data.columns?.reduce<CustomDataColumnType<RowData>[]>((acc, item) => {
// is the column is the value column then we need to check for the available legend
const legend = item.isValueColumn
? getQueryLegend(currentQuery, item.queryName)
@@ -193,11 +199,13 @@ export function createColumnsAndDataSource(
(query) => query.queryName === item.queryName,
)?.aggregations?.length || 0;
const column: ColumnType<RowData> = {
const column: CustomDataColumnType<RowData> = {
dataIndex: item.id || item.name,
// if no legend present then rely on the column name value
title: !isNewAggregation && !isEmpty(legend) ? legend : item.name,
width: QUERY_TABLE_CONFIG.width,
isValueColumn: item.isValueColumn,
queryName: item.queryName,
render: renderColumnCell && renderColumnCell[item.name],
sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item),
};

View File

@@ -1,11 +1,7 @@
import { orange } from '@ant-design/colors';
import { SettingOutlined } from '@ant-design/icons';
import { Dropdown, MenuProps } from 'antd';
import {
negateOperator,
OPERATORS,
QUERY_BUILDER_FUNCTIONS,
} from 'constants/antlrQueryConstants';
import { OPERATORS } from 'constants/queryBuilder';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { TitleWrapper } from './BodyTitleRenderer.styles';
@@ -33,9 +29,7 @@ function BodyTitleRenderer({
getDataTypes(value),
),
`${value}`,
isFilterIn
? QUERY_BUILDER_FUNCTIONS.HAS
: negateOperator(QUERY_BUILDER_FUNCTIONS.HAS),
isFilterIn ? OPERATORS.HAS : OPERATORS.NHAS,
true,
parentIsArray ? getDataTypes([value]) : getDataTypes(value),
);

View File

@@ -73,8 +73,6 @@ export default function TableRow({
{tableColumns.map((column) => {
if (!column.render) return <td>Empty</td>;
if (!column.key) return null;
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
log[column.key as keyof Record<string, unknown>],
log,
@@ -99,7 +97,6 @@ export default function TableRow({
fontSize={fontSize}
columnKey={column.key as string}
onClick={handleShowLogDetails}
className={column.key as string}
>
{cloneElement(children, props)}
</TableCellStyled>

View File

@@ -136,7 +136,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
key={column.key}
fontSize={tableViewProps?.fontSize}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(isDragColumn && { className: `dragHandler ${column.key}` })}
{...(isDragColumn && { className: 'dragHandler' })}
columnKey={column.key as string}
>
{(column.title as string).replace(/^\w/, (c) => c.toUpperCase())}

View File

@@ -1,4 +1,3 @@
import APIError from 'types/api/error';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -9,6 +8,5 @@ export type LogsExplorerListProps = {
logs: ILog[];
onEndReached: (index: number) => void;
isError: boolean;
error?: Error | APIError;
isFilterApplied: boolean;
};

View File

@@ -141,7 +141,6 @@ describe('LogsExplorerList - empty states', () => {
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
setWarning={(): void => {}}
/>
</PreferenceContextProvider>
</QueryBuilderContext.Provider>,
@@ -206,7 +205,6 @@ describe('LogsExplorerList - empty states', () => {
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
setWarning={(): void => {}}
/>
</PreferenceContextProvider>
</QueryBuilderContext.Provider>,

View File

@@ -2,7 +2,6 @@ import './LogsExplorerList.style.scss';
import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
// components
@@ -13,6 +12,7 @@ import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card';
import { LOCALSTORAGE } from 'constants/localStorage';
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { useOptionsMenu } from 'container/OptionsMenu';
import { FontSize } from 'container/OptionsMenu/types';
@@ -21,7 +21,6 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import APIError from 'types/api/error';
// interfaces
import { ILog } from 'types/api/logs/log';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
@@ -46,11 +45,9 @@ function LogsExplorerList({
logs,
onEndReached,
isError,
error,
isFilterApplied,
}: LogsExplorerListProps): JSX.Element {
const ref = useRef<VirtuosoHandle>(null);
const { activeLogId } = useCopyLogLink();
const {
@@ -258,9 +255,7 @@ function LogsExplorerList({
/>
)}
{isError && !isLoading && !isFetching && error && (
<ErrorInPlace error={error as APIError} />
)}
{isError && !isLoading && !isFetching && <LogsError />}
{!isLoading && !isError && logs.length > 0 && (
<>

View File

@@ -1,5 +1,4 @@
import { TelemetryFieldKey } from 'api/v5/v5';
import { isEmpty } from 'lodash-es';
import { IField } from 'types/api/logs/fields';
import {
IBuilderQuery,
@@ -9,13 +8,11 @@ import {
export const convertKeysToColumnFields = (
keys: TelemetryFieldKey[],
): IField[] =>
keys
.filter((item) => !isEmpty(item.name))
.map((item) => ({
dataType: item.fieldDataType ?? '',
name: item.name,
type: item.fieldContext ?? '',
}));
keys.map((item) => ({
dataType: item.fieldDataType ?? '',
name: item.name,
type: item.fieldContext ?? '',
}));
/**
* Determines if a query represents a trace-to-logs navigation
* by checking for the presence of a trace_id filter.

View File

@@ -1,9 +1,7 @@
import APIError from 'types/api/error';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
export type LogsExplorerTableProps = {
data: QueryDataV3[];
isLoading: boolean;
isError: boolean;
error?: Error | APIError;
};

View File

@@ -1,12 +1,11 @@
import './LogsExplorerTable.styles.scss';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import { initialQueriesMap } from 'constants/queryBuilder';
import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { QueryTable } from 'container/QueryTable';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo } from 'react';
import APIError from 'types/api/error';
import { LogsExplorerTableProps } from './LogsExplorerTable.interfaces';
@@ -14,7 +13,6 @@ function LogsExplorerTable({
data,
isLoading,
isError,
error,
}: LogsExplorerTableProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
@@ -22,8 +20,8 @@ function LogsExplorerTable({
return <LogsLoading />;
}
if (isError && error) {
return <ErrorInPlace error={error as APIError} />;
if (isError) {
return <LogsError />;
}
return (

View File

@@ -51,10 +51,8 @@ import { ArrowUp10, Minus, Sliders } from 'lucide-react';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { useTimezone } from 'providers/Timezone';
import {
Dispatch,
memo,
MutableRefObject,
SetStateAction,
useCallback,
useEffect,
useMemo,
@@ -64,9 +62,7 @@ import {
import { useDispatch, useSelector } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Warning } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@@ -91,7 +87,6 @@ function LogsExplorerViewsContainer({
setIsLoadingQueries,
listQueryKeyRef,
chartQueryKeyRef,
setWarning,
}: {
selectedView: ExplorerViews;
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
@@ -99,7 +94,6 @@ function LogsExplorerViewsContainer({
listQueryKeyRef: MutableRefObject<any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
chartQueryKeyRef: MutableRefObject<any>;
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
}): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const dispatch = useDispatch();
@@ -275,7 +269,6 @@ function LogsExplorerViewsContainer({
isFetching,
isError,
isSuccess,
error,
} = useGetExplorerQueryRange(
requestData,
panelType,
@@ -379,13 +372,6 @@ function LogsExplorerViewsContainer({
[activeLogId, orderBy, listQuery, selectedView],
);
useEffect(() => {
if (data?.payload) {
setWarning(data?.warning);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.payload, data?.warning]);
const handleEndReached = useCallback(() => {
if (!listQuery) return;
@@ -785,7 +771,6 @@ function LogsExplorerViewsContainer({
logs={logs}
onEndReached={handleEndReached}
isError={isError}
error={error as APIError}
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
/>
)}
@@ -795,10 +780,8 @@ function LogsExplorerViewsContainer({
isLoading={isLoading || isFetching}
data={data}
isError={isError}
error={error as APIError}
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
dataSource={DataSource.LOGS}
setWarning={setWarning}
/>
)}
@@ -811,7 +794,6 @@ function LogsExplorerViewsContainer({
}
isLoading={isLoading || isFetching}
isError={isError}
error={error as APIError}
/>
)}
</div>

View File

@@ -82,48 +82,6 @@ jest.mock('hooks/queryBuilder/useGetExplorerQueryRange', () => ({
useGetExplorerQueryRange: jest.fn(),
}));
// Mock ErrorStateComponent to handle APIError properly
jest.mock(
'components/Common/ErrorStateComponent',
() =>
function MockErrorStateComponent({ error, message }: any): JSX.Element {
if (error) {
// Mock the getErrorMessage and getErrorDetails methods
const getErrorMessage = jest
.fn()
.mockReturnValue(
error.error?.message ||
'Something went wrong. Please try again or contact support.',
);
const getErrorDetails = jest.fn().mockReturnValue(error);
// Add the methods to the error object
const errorWithMethods = {
...error,
getErrorMessage,
getErrorDetails,
};
return (
<div data-testid="error-state-component">
<div>{errorWithMethods.getErrorMessage()}</div>
{errorWithMethods.getErrorDetails().error?.errors?.map((err: any) => (
<div key={`error-${err.message}`}> {err.message}</div>
))}
</div>
);
}
return (
<div data-testid="error-state-component">
<div>
{message || 'Something went wrong. Please try again or contact support.'}
</div>
</div>
);
},
);
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
@@ -173,7 +131,6 @@ const renderer = (): RenderResult =>
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
setWarning={(): void => {}}
/>
</PreferenceContextProvider>
</VirtuosoMockContext.Provider>,
@@ -215,6 +172,21 @@ describe('LogsExplorerViews -', () => {
expect(queryByText('pending_data_placeholder')).toBeInTheDocument();
});
it('check error state', async () => {
lodsQueryServerRequest();
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
data: { payload: logsQueryRangeSuccessNewFormatResponse },
isLoading: false,
isFetching: false,
isError: true,
});
const { queryByText } = renderer();
expect(
queryByText('Something went wrong. Please try again or contact support.'),
).toBeInTheDocument();
});
it('should add activeLogId filter when present in URL', async () => {
// Mock useCopyLogLink to return an activeLogId
(useCopyLogLink as jest.Mock).mockReturnValue({
@@ -234,7 +206,6 @@ describe('LogsExplorerViews -', () => {
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
setWarning={(): void => {}}
/>
</PreferenceContextProvider>
</QueryBuilderContext.Provider>,

View File

@@ -1,195 +0,0 @@
.meter-explorer-container {
display: flex;
flex-direction: row;
.meter-explorer-quick-filters-section {
width: 280px;
border-right: 1px solid var(--bg-slate-500);
&.hidden {
display: none;
}
}
.meter-explorer-content-section {
width: 100%;
.explore-header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 4px 0;
padding: 0 8px;
.explore-header-left-actions {
display: flex;
align-items: flex-start;
gap: 10px;
}
.explore-header-right-actions {
display: flex;
align-items: flex-end;
gap: 10px;
}
}
.query-section {
max-height: 450px;
overflow-y: auto;
.rc-virtual-list-holder {
height: 150px;
}
}
.explore-tabs {
margin: 15px 0;
.tab {
background-color: var(--bg-slate-500);
border-color: var(--bg-ink-200);
width: 180px;
padding: 16px 0;
display: flex;
justify-content: center;
align-items: center;
}
.tab:first-of-type {
border-top-left-radius: 2px;
}
.tab:last-of-type {
border-top-right-radius: 2px;
}
.selected-view {
background: var(--bg-ink-500);
}
}
.explore-content {
.ant-space {
margin-top: 10px;
margin-bottom: 20px;
}
.empty-meter-search {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.time-series-view-panel {
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
padding: 8px !important;
margin: 8px;
}
.time-series-container {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(100%, calc(50% - 8px)), 1fr)
);
gap: 16px;
width: 100%;
height: fit-content;
}
}
}
&.quick-filters-open {
.meter-explorer-content-section {
width: calc(100% - 280px);
}
}
padding-bottom: 80px;
}
.meter-time-series-container {
display: flex;
flex-direction: column;
gap: 10px;
.builder-units-filter {
padding: 0 8px;
margin-bottom: 0px !important;
.builder-units-filter-label {
margin-bottom: 0px !important;
font-size: 13px;
}
}
}
.lightMode {
.meter-explorer-container {
.explore-tabs {
.tab {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-400);
}
.selected-view {
background: var(--bg-vanilla-500);
}
}
}
}
.dashboards-and-alerts-popover-container {
display: flex;
gap: 16px;
.dashboards-and-alerts-popover {
border-radius: 20px;
padding: 4px 8px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
&:hover {
opacity: 0.8;
}
}
.dashboards-popover {
border: 1px solid var(--bg-sienna-500);
.ant-typography {
color: var(--bg-sienna-500);
}
}
.alerts-popover {
border: 1px solid var(--bg-sakura-500);
.ant-typography {
color: var(--bg-sakura-500);
}
}
}
.no-data-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
gap: 16px;
.no-data-text {
color: var(--text-vanilla-500);
font-size: 14px;
line-height: 20px;
text-align: center;
}
}

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