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
806 changed files with 9328 additions and 49137 deletions

View File

@@ -24,7 +24,7 @@ services:
depends_on:
- zookeeper
zookeeper:
image: signoz/zookeeper:3.7.1
image: bitnami/zookeeper:3.7.1
container_name: zookeeper
volumes:
- ${PWD}/fs/tmp/zookeeper:/bitnami/zookeeper
@@ -40,7 +40,7 @@ services:
timeout: 5s
retries: 3
schema-migrator-sync:
image: signoz/signoz-schema-migrator:v0.129.2
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.2
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]

4
.github/CODEOWNERS vendored
View File

@@ -42,7 +42,3 @@
/pkg/telemetrymetadata/ @srikanthccv
/pkg/telemetrymetrics/ @srikanthccv
/pkg/telemetrytraces/ @srikanthccv
# AuthN / AuthZ Owners
/pkg/authz/ @vikrantgupta25 @grandwizard28

View File

@@ -62,7 +62,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.23
GO_NAME: signoz-community
GO_INPUT_ARTIFACT_CACHE_KEY: community-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build

View File

@@ -93,7 +93,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.23
GO_INPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
GO_BUILD_CONTEXT: ./cmd/enterprise

View File

@@ -92,7 +92,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.23
GO_INPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
GO_BUILD_CONTEXT: ./cmd/enterprise

View File

@@ -18,7 +18,7 @@ jobs:
with:
PRIMUS_REF: main
GO_TEST_CONTEXT: ./...
GO_VERSION: 1.24
GO_VERSION: 1.23
fmt:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -27,7 +27,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.23
lint:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -36,7 +36,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.23
deps:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -45,7 +45,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.23
build:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -57,7 +57,7 @@ jobs:
- name: go-install
uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version: "1.23"
- name: qemu-install
uses: docker/setup-qemu-action@v3
- name: aarch64-install

View File

@@ -58,7 +58,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version: "1.23"
- name: cross-compilation-tools
if: matrix.os == 'ubuntu-latest'
run: |
@@ -122,7 +122,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version: "1.23"
# copy the caches from build
- name: get-sha

View File

@@ -72,7 +72,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version: "1.23"
- name: cross-compilation-tools
if: matrix.os == 'ubuntu-latest'
run: |
@@ -135,7 +135,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version: "1.23"
# copy the caches from build
- name: get-sha

2
.gitignore vendored
View File

@@ -86,8 +86,6 @@ queries.active
.devenv/**/tmp/**
.qodo
.dev
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/

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

@@ -2,11 +2,10 @@ FROM node:18-bullseye AS build
WORKDIR /opt/
COPY ./frontend/ ./
ENV NODE_OPTIONS=--max-old-space-size=8192
RUN CI=1 yarn install
RUN CI=1 yarn build
FROM golang:1.24-bullseye
FROM golang:1.23-bullseye
ARG OS="linux"
ARG TARGETARCH

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

@@ -39,7 +39,7 @@ x-clickhouse-defaults: &clickhouse-defaults
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
image: bitnami/zookeeper:3.7.1
user: root
deploy:
labels:
@@ -174,7 +174,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.93.0
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.2
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.2
image: signoz/signoz-schema-migrator:v0.128.2
deploy:
restart_policy:
condition: on-failure

View File

@@ -38,7 +38,7 @@ x-clickhouse-defaults: &clickhouse-defaults
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
image: bitnami/zookeeper:3.7.1
user: root
deploy:
labels:
@@ -115,7 +115,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.93.0
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.2
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.2
image: signoz/signoz-schema-migrator:v0.128.2
deploy:
restart_policy:
condition: on-failure

View File

@@ -42,7 +42,7 @@ x-clickhouse-defaults: &clickhouse-defaults
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
image: bitnami/zookeeper:3.7.1
user: root
labels:
signoz.io/scrape: "true"
@@ -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.93.0}
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.2}
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.2}
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.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
container_name: schema-migrator-async
command:
- async

View File

@@ -38,7 +38,7 @@ x-clickhouse-defaults: &clickhouse-defaults
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
image: bitnami/zookeeper:3.7.1
user: root
labels:
signoz.io/scrape: "true"
@@ -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.93.0}
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.2}
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.2}
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.2}
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

@@ -13,11 +13,11 @@ import (
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/user"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/google/uuid"
"github.com/gorilla/mux"
"go.uber.org/zap"
)
@@ -192,14 +192,14 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
))
}
password := types.MustGenerateFactorPassword(newUser.ID.StringValue())
password, err := types.NewFactorPassword(uuid.NewString())
err = ah.Signoz.Modules.User.CreateUser(ctx, newUser, user.WithFactorPassword(password))
integrationUser, err := ah.Signoz.Modules.User.CreateUserWithPassword(ctx, newUser, password)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
}
return newUser, nil
return integrationUser, nil
}
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (

View File

@@ -257,7 +257,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
s.config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
r.Use(middleware.NewComment().Wrap)
apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)

View File

@@ -1,7 +1,7 @@
package model
import (
"errors"
"fmt"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
)
@@ -57,7 +57,7 @@ func Unauthorized(err error) *ApiError {
func BadRequestStr(s string) *ApiError {
return &ApiError{
Typ: basemodel.ErrorBadData,
Err: errors.New(s),
Err: fmt.Errorf(s),
}
}
@@ -73,7 +73,7 @@ func InternalError(err error) *ApiError {
func InternalErrorStr(s string) *ApiError {
return &ApiError{
Typ: basemodel.ErrorInternal,
Err: errors.New(s),
Err: fmt.Errorf(s),
}
}

View File

@@ -1,5 +1,4 @@
node_modules
build
*.typegen.ts
i18-generate-hash.js
src/parser/TraceOperatorParser/**
i18-generate-hash.js

View File

@@ -10,6 +10,4 @@ public/
**/*.json
# Ignore all files in parser folder:
src/parser/**
src/TraceOperator/parser/**
src/parser/**

View File

@@ -16,7 +16,6 @@ const config: Config.InitialOptions = {
'ts-jest': {
useESM: true,
isolatedModules: true,
tsconfig: '<rootDir>/tsconfig.jest.json',
},
},
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
@@ -26,7 +25,7 @@ const config: Config.InitialOptions = {
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -43,19 +43,11 @@
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "8.41.0",
"@sentry/webpack-plugin": "2.22.6",
"@signozhq/badge": "0.0.2",
"@signozhq/calendar": "0.0.0",
"@signozhq/callout": "0.0.2",
"@signozhq/design-tokens": "1.1.4",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
"@signozhq/sonner": "0.1.0",
"@signozhq/table": "0.3.7",
"@signozhq/tooltip": "0.0.2",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/codemirror-theme-github": "4.24.1",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/react-codemirror": "4.23.10",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
@@ -100,7 +92,6 @@
"i18next-http-backend": "^1.3.2",
"jest": "^27.5.1",
"js-base64": "^3.7.2",
"kbar": "0.1.0-beta.48",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",

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 Views",
"METER": "SigNoz | Meter"
"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 Views",
"METER": "SigNoz | Meter"
"API_MONITORING": "SigNoz | External APIs"
}

View File

@@ -4,7 +4,6 @@ import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import AppLoading from 'components/AppLoading/AppLoading';
import KBarCommandPalette from 'components/KBarCommandPalette/KBarCommandPalette';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import UserpilotRouteTracker from 'components/UserpilotRouteTracker/UserpilotRouteTracker';
@@ -26,7 +25,6 @@ import { useAppContext } from 'providers/App/App';
import { IUser } from 'providers/App/types';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { KBarCommandPaletteProvider } from 'providers/KBarCommandPaletteProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
@@ -370,42 +368,39 @@ function App(): JSX.Element {
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<CompatRouter>
<KBarCommandPaletteProvider>
<UserpilotRouteTracker />
<KBarCommandPalette />
<NotificationProvider>
<ErrorModalProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</ErrorModalProvider>
</NotificationProvider>
</KBarCommandPaletteProvider>
<UserpilotRouteTracker />
<NotificationProvider>
<ErrorModalProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</ErrorModalProvider>
</NotificationProvider>
</CompatRouter>
</Router>
</ConfigProvider>

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,
exact: true,
component: MeterExplorer,
key: 'METER',
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

@@ -1,115 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { ApiBaseInstance } from 'api';
import { getFieldKeys } from '../getFieldKeys';
// Mock the API instance
jest.mock('api', () => ({
ApiBaseInstance: {
get: jest.fn(),
},
}));
describe('getFieldKeys API', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const mockSuccessResponse = {
status: 200,
data: {
status: 'success',
data: {
keys: {
'service.name': [],
'http.status_code': [],
},
complete: true,
},
},
};
it('should call API with correct parameters when no args provided', async () => {
// Mock successful API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
// Call function with no parameters
await getFieldKeys();
// Verify API was called correctly with empty params object
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
params: {},
});
});
it('should call API with signal parameter when provided', async () => {
// Mock successful API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
// Call function with signal parameter
await getFieldKeys('traces');
// Verify API was called with signal parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
params: { signal: 'traces' },
});
});
it('should call API with name parameter when provided', async () => {
// Mock successful API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
data: {
keys: { service: [] },
complete: false,
},
},
});
// Call function with name parameter
await getFieldKeys(undefined, 'service');
// Verify API was called with name parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
params: { name: 'service' },
});
});
it('should call API with both signal and name when provided', async () => {
// Mock successful API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
data: {
keys: { service: [] },
complete: false,
},
},
});
// Call function with both parameters
await getFieldKeys('logs', 'service');
// Verify API was called with both parameters
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
params: { signal: 'logs', name: 'service' },
});
});
it('should return properly formatted response', async () => {
// Mock API to return our response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
// Call the function
const result = await getFieldKeys('traces');
// Verify the returned structure matches SuccessResponseV2 format
expect(result).toEqual({
httpStatusCode: 200,
data: mockSuccessResponse.data.data,
});
});
});

View File

@@ -1,214 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { ApiBaseInstance } from 'api';
import { getFieldValues } from '../getFieldValues';
// Mock the API instance
jest.mock('api', () => ({
ApiBaseInstance: {
get: jest.fn(),
},
}));
describe('getFieldValues API', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should call the API with correct parameters (no options)', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
});
// Call function without parameters
await getFieldValues();
// Verify API was called correctly with empty params
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: {},
});
});
it('should call the API with signal parameter', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
});
// Call function with signal parameter
await getFieldValues('traces');
// Verify API was called with signal parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: { signal: 'traces' },
});
});
it('should call the API with name parameter', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
});
// Call function with name parameter
await getFieldValues(undefined, 'service.name');
// Verify API was called with name parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: { name: 'service.name' },
});
});
it('should call the API with value parameter', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend'],
},
complete: false,
},
},
});
// Call function with value parameter
await getFieldValues(undefined, 'service.name', 'front');
// Verify API was called with value parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: { name: 'service.name', searchText: 'front' },
});
});
it('should call the API with time range parameters', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
});
// Call function with time range parameters
const startUnixMilli = 1625097600000000; // Note: nanoseconds
const endUnixMilli = 1625184000000000;
await getFieldValues(
'logs',
'service.name',
undefined,
startUnixMilli,
endUnixMilli,
);
// Verify API was called with time range parameters (converted to milliseconds)
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: {
signal: 'logs',
name: 'service.name',
startUnixMilli: '1625097600', // Should be converted to seconds (divided by 1000000)
endUnixMilli: '1625184000', // Should be converted to seconds (divided by 1000000)
},
});
});
it('should normalize the response values', async () => {
// Mock API response with multiple value types
const mockResponse = {
status: 200,
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
numberValues: [200, 404],
boolValues: [true, false],
},
complete: true,
},
},
};
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockResponse);
// Call the function
const result = await getFieldValues('traces', 'mixed.values');
// Verify the response has normalized values array
expect(result.data?.normalizedValues).toContain('frontend');
expect(result.data?.normalizedValues).toContain('backend');
expect(result.data?.normalizedValues).toContain('200');
expect(result.data?.normalizedValues).toContain('404');
expect(result.data?.normalizedValues).toContain('true');
expect(result.data?.normalizedValues).toContain('false');
expect(result.data?.normalizedValues?.length).toBe(6);
});
it('should return a properly formatted success response', async () => {
// Create mock response
const mockApiResponse = {
status: 200,
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
};
// Mock API to return our response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockApiResponse);
// Call the function
const result = await getFieldValues('traces', 'service.name');
// Verify the returned structure matches SuccessResponseV2 format
expect(result).toEqual({
httpStatusCode: 200,
data: expect.objectContaining({
values: expect.any(Object),
normalizedValues: expect.any(Array),
complete: true,
}),
});
});
});

View File

@@ -1,38 +0,0 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { FieldKeyResponse } from 'types/api/dynamicVariables/getFieldKeys';
/**
* Get field keys for a given signal type
* @param signal Type of signal (traces, logs, metrics)
* @param name Optional search text
*/
export const getFieldKeys = async (
signal?: 'traces' | 'logs' | 'metrics',
name?: string,
): Promise<SuccessResponseV2<FieldKeyResponse>> => {
const params: Record<string, string> = {};
if (signal) {
params.signal = encodeURIComponent(signal);
}
if (name) {
params.name = encodeURIComponent(name);
}
try {
const response = await ApiBaseInstance.get('/fields/keys', { params });
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getFieldKeys;

View File

@@ -1,87 +0,0 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { FieldValueResponse } from 'types/api/dynamicVariables/getFieldValues';
/**
* Get field values for a given signal type and field name
* @param signal Type of signal (traces, logs, metrics)
* @param name Name of the attribute for which values are being fetched
* @param value Optional search text
* @param existingQuery Optional existing query - across all present dynamic variables
*/
export const getFieldValues = async (
signal?: 'traces' | 'logs' | 'metrics',
name?: string,
searchText?: string,
startUnixMilli?: number,
endUnixMilli?: number,
existingQuery?: string,
): Promise<SuccessResponseV2<FieldValueResponse>> => {
const params: Record<string, string> = {};
if (signal) {
params.signal = encodeURIComponent(signal);
}
if (name) {
params.name = encodeURIComponent(name);
}
if (searchText) {
params.searchText = encodeURIComponent(searchText);
}
if (startUnixMilli) {
params.startUnixMilli = Math.floor(startUnixMilli / 1000000).toString();
}
if (endUnixMilli) {
params.endUnixMilli = Math.floor(endUnixMilli / 1000000).toString();
}
if (existingQuery) {
params.existingQuery = existingQuery;
}
try {
const response = await ApiBaseInstance.get('/fields/values', { params });
// Normalize values from different types (stringValues, boolValues, etc.)
if (response.data?.data?.values) {
const allValues: string[] = [];
Object.entries(response.data?.data?.values).forEach(
([key, valueArray]: [string, any]) => {
// Skip RelatedValues as they should be kept separate
if (key === 'relatedValues') {
return;
}
if (Array.isArray(valueArray)) {
allValues.push(...valueArray.map(String));
}
},
);
// Add a normalized values array to the response
response.data.data.normalizedValues = allValues;
// Add relatedValues to the response as per FieldValueResponse
if (response.data?.data?.values?.relatedValues) {
response.data.data.relatedValues =
response.data?.data?.values?.relatedValues;
}
}
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getFieldValues;

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,25 +0,0 @@
import { ApiV2Instance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/settings/getRetention';
// Only works for logs
const getRetentionV2 = async (): Promise<
SuccessResponseV2<PayloadProps<'logs'>>
> => {
try {
const response = await ApiV2Instance.get<PayloadProps<'logs'>>(
`/settings/ttl`,
);
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getRetentionV2;

View File

@@ -1,14 +1,14 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadPropsV2, Props } from 'types/api/settings/setRetention';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/settings/setRetention';
const setRetention = async (
props: Props,
): Promise<SuccessResponseV2<PayloadPropsV2>> => {
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post<PayloadPropsV2>(
const response = await axios.post<PayloadProps>(
`/settings/ttl?duration=${props.totalDuration}&type=${props.type}${
props.coldStorage
? `&coldStorage=${props.coldStorage}&toColdDuration=${props.toColdDuration}`
@@ -17,11 +17,13 @@ const setRetention = async (
);
return {
httpStatusCode: response.status,
data: response.data,
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,32 +0,0 @@
import { ApiV2Instance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadPropsV2, PropsV2 } from 'types/api/settings/setRetention';
const setRetentionV2 = async ({
type,
defaultTTLDays,
coldStorageVolume,
coldStorageDuration,
ttlConditions,
}: PropsV2): Promise<SuccessResponseV2<PayloadPropsV2>> => {
try {
const response = await ApiV2Instance.post<PayloadPropsV2>(`/settings/ttl`, {
type,
defaultTTLDays,
coldStorageVolume,
coldStorageDuration,
ttlConditions,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default setRetentionV2;

View File

@@ -1,284 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { SuccessResponse } from 'types/api';
import {
MetricRangePayloadV5,
QueryBuilderFormula,
QueryRangeRequestV5,
QueryRangeResponseV5,
RequestType,
ScalarData,
TelemetryFieldKey,
TimeSeries,
TimeSeriesData,
TimeSeriesValue,
} from 'types/api/v5/queryRange';
import { convertV5ResponseToLegacy } from './convertV5Response';
describe('convertV5ResponseToLegacy', () => {
function makeBaseSuccess<T>(
payload: T,
params: QueryRangeRequestV5,
): SuccessResponse<T, QueryRangeRequestV5> {
return {
statusCode: 200,
message: 'success',
payload,
error: null,
params,
};
}
function makeBaseParams(
requestType: RequestType,
queries: QueryRangeRequestV5['compositeQuery']['queries'],
): QueryRangeRequestV5 {
return {
schemaVersion: 'v1',
start: 1,
end: 2,
requestType,
compositeQuery: { queries },
variables: {},
formatOptions: { formatTableResultForUI: false, fillGaps: false },
};
}
it('converts time_series response into legacy series structure', () => {
const timeSeries: TimeSeriesData = {
queryName: 'A',
aggregations: [
{
index: 0,
alias: '__result_0',
meta: {},
series: [
({
labels: [
{
key: ({ name: 'service.name' } as unknown) as TelemetryFieldKey,
value: 'adservice',
},
],
values: [
({ timestamp: 1000, value: 10 } as unknown) as TimeSeriesValue,
({ timestamp: 2000, value: 12 } as unknown) as TimeSeriesValue,
],
} as unknown) as TimeSeries,
],
},
],
};
const v5Data: QueryRangeResponseV5 = {
type: 'time_series',
data: { results: [timeSeries] },
meta: { rowsScanned: 0, bytesScanned: 0, durationMs: 0 },
};
const params = makeBaseParams('time_series', [
{
type: 'builder_query',
spec: {
name: 'A',
signal: 'traces',
stepInterval: 60,
disabled: false,
aggregations: [{ expression: 'count()' }],
},
},
]);
const input: SuccessResponse<
MetricRangePayloadV5,
QueryRangeRequestV5
> = makeBaseSuccess({ data: v5Data }, params);
const legendMap = { A: '{{service.name}}' };
const result = convertV5ResponseToLegacy(input, legendMap, false);
expect(result.payload.data.resultType).toBe('time_series');
expect(result.payload.data.result).toHaveLength(1);
const q = result.payload.data.result[0];
expect(q.queryName).toBe('A');
expect(q.legend).toBe('{{service.name}}');
expect(q.series?.[0]).toEqual(
expect.objectContaining({
labels: { 'service.name': 'adservice' },
values: [
{ timestamp: 1000, value: '10' },
{ timestamp: 2000, value: '12' },
],
metaData: expect.objectContaining({
alias: '__result_0',
index: 0,
queryName: 'A',
}),
}),
);
});
it('converts scalar to legacy table (formatForWeb=false) with names/ids resolved from aggregations', () => {
const scalar: ScalarData = {
columns: [
// group column
({
name: 'service.name',
queryName: 'A',
aggregationIndex: 0,
columnType: 'group',
} as unknown) as ScalarData['columns'][number],
// aggregation 0
({
name: '__result_0',
queryName: 'A',
aggregationIndex: 0,
columnType: 'aggregation',
} as unknown) as ScalarData['columns'][number],
// aggregation 1
({
name: '__result_1',
queryName: 'A',
aggregationIndex: 1,
columnType: 'aggregation',
} as unknown) as ScalarData['columns'][number],
// formula F1
({
name: '__result',
queryName: 'F1',
aggregationIndex: 0,
columnType: 'aggregation',
} as unknown) as ScalarData['columns'][number],
],
data: [['adservice', 606, 1.452, 151.5]],
};
const v5Data: QueryRangeResponseV5 = {
type: 'scalar',
data: { results: [scalar] },
meta: { rowsScanned: 0, bytesScanned: 0, durationMs: 0 },
};
const params = makeBaseParams('scalar', [
{
type: 'builder_query',
spec: {
name: 'A',
signal: 'traces',
stepInterval: 60,
disabled: false,
aggregations: [
{ expression: 'count()' },
{ expression: 'avg(app.ads.count)', alias: 'avg' },
],
},
},
{
type: 'builder_formula',
spec: ({
name: 'F1',
expression: 'A * 0.25',
} as unknown) as QueryBuilderFormula,
},
]);
const input: SuccessResponse<
MetricRangePayloadV5,
QueryRangeRequestV5
> = makeBaseSuccess({ data: v5Data }, params);
const legendMap = { A: '{{service.name}}', F1: '' };
const result = convertV5ResponseToLegacy(input, legendMap, false);
expect(result.payload.data.resultType).toBe('scalar');
const [tableEntry] = result.payload.data.result;
expect(tableEntry.table?.columns).toEqual([
{
name: 'service.name',
queryName: 'A',
isValueColumn: false,
id: 'service.name',
},
{ name: 'count()', queryName: 'A', isValueColumn: true, id: 'A.count()' },
{
name: 'avg',
queryName: 'A',
isValueColumn: true,
id: 'A.avg(app.ads.count)',
},
{ name: 'F1', queryName: 'F1', isValueColumn: true, id: 'F1' },
]);
expect(tableEntry.table?.rows?.[0]).toEqual({
data: {
'service.name': 'adservice',
'A.count()': 606,
'A.avg(app.ads.count)': 1.452,
F1: 151.5,
},
});
});
it('converts scalar with formatForWeb=true to UI-friendly table', () => {
const scalar: ScalarData = {
columns: [
{
name: 'service.name',
queryName: 'A',
aggregationIndex: 0,
columnType: 'group',
} as any,
{
name: '__result_0',
queryName: 'A',
aggregationIndex: 0,
columnType: 'aggregation',
} as any,
],
data: [['adservice', 580]],
};
const v5Data: QueryRangeResponseV5 = {
type: 'scalar',
data: { results: [scalar] },
meta: { rowsScanned: 0, bytesScanned: 0, durationMs: 0 },
};
const params = makeBaseParams('scalar', [
{
type: 'builder_query',
spec: {
name: 'A',
signal: 'traces',
stepInterval: 60,
disabled: false,
aggregations: [{ expression: 'count()' }],
},
},
]);
const input: SuccessResponse<
MetricRangePayloadV5,
QueryRangeRequestV5
> = makeBaseSuccess({ data: v5Data }, params);
const legendMap = { A: '{{service.name}}' };
const result = convertV5ResponseToLegacy(input, legendMap, true);
expect(result.payload.data.resultType).toBe('scalar');
const [tableEntry] = result.payload.data.result;
expect(tableEntry.table?.columns).toEqual([
{
name: 'service.name',
queryName: 'A',
isValueColumn: false,
id: 'service.name',
},
// Single aggregation: name resolves to legend, id resolves to queryName
{ name: '{{service.name}}', queryName: 'A', isValueColumn: true, id: 'A' },
]);
expect(tableEntry.table?.rows?.[0]).toEqual({
data: {
'service.name': 'adservice',
A: 580,
},
});
});
});

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

@@ -1,637 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string, simple-import-sort/imports, @typescript-eslint/indent, no-mixed-spaces-and-tabs */
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
IBuilderFormula,
IBuilderQuery,
} from 'types/api/queryBuilder/queryBuilderData';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import {
ClickHouseQuery,
LogAggregation,
LogBuilderQuery,
MetricBuilderQuery,
PromQuery,
QueryBuilderFormula as V5QueryBuilderFormula,
QueryEnvelope,
QueryRangePayloadV5,
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { prepareQueryRangePayloadV5 } from './prepareQueryRangePayloadV5';
jest.mock('lib/getStartEndRangeTime', () => ({
__esModule: true,
default: jest.fn(() => ({ start: '100', end: '200' })),
}));
describe('prepareQueryRangePayloadV5', () => {
const start = 1_710_000_000; // seconds
const end = 1_710_000_600; // seconds
const baseBuilderQuery = (
overrides?: Partial<IBuilderQuery>,
): IBuilderQuery => ({
queryName: 'A',
dataSource: DataSource.METRICS,
aggregations: [
{
metricName: 'cpu_usage',
temporality: '',
timeAggregation: 'sum',
spaceAggregation: 'avg',
reduceTo: 'avg',
},
],
timeAggregation: 'sum',
spaceAggregation: 'avg',
temporality: '',
functions: [
{
name: 'timeShift',
args: [{ value: '5m' }],
},
],
filter: { expression: '' },
filters: { items: [], op: 'AND' },
groupBy: [],
expression: 'A',
disabled: false,
having: [],
limit: null,
stepInterval: 600,
orderBy: [],
reduceTo: 'avg',
legend: 'Legend A',
...overrides,
});
const baseFormula = (
overrides?: Partial<IBuilderFormula>,
): IBuilderFormula => ({
expression: 'A + 1',
disabled: false,
queryName: 'F1',
legend: 'Formula Legend',
limit: undefined,
having: [],
stepInterval: undefined,
orderBy: [],
...overrides,
});
it('builds payload for builder queries with formulas and variables', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'q1',
unit: undefined,
promql: [],
clickhouse_sql: [],
builder: {
queryData: [baseBuilderQuery()],
queryFormulas: [baseFormula()],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
start,
end,
variables: { svc: 'api', count: 5, flag: true },
fillGaps: true,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'Legend A', F1: 'Formula Legend' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: expect.arrayContaining([
expect.objectContaining({
type: 'builder_query',
spec: expect.objectContaining({
name: 'A',
signal: 'metrics',
stepInterval: 600,
functions: [{ name: 'timeShift', args: [{ value: '5m' }] }],
aggregations: [
expect.objectContaining({
metricName: 'cpu_usage',
timeAggregation: 'sum',
spaceAggregation: 'avg',
reduceTo: undefined,
}),
],
}),
}),
expect.objectContaining({
type: 'builder_formula',
spec: expect.objectContaining({
name: 'F1',
expression: 'A + 1',
legend: 'Formula Legend',
}),
}),
]),
}),
requestType: 'time_series',
formatOptions: expect.objectContaining({
formatTableResultForUI: false,
fillGaps: true,
}),
start: start * 1000,
end: end * 1000,
variables: expect.objectContaining({
svc: { value: 'api' },
count: { value: 5 },
flag: { value: true },
}),
}),
}),
);
// Legend map combines builder and formulas
expect(result.legendMap).toEqual({ A: 'Legend A', F1: 'Formula Legend' });
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.schemaVersion).toBe('v1');
expect(payload.start).toBe(start * 1000);
expect(payload.end).toBe(end * 1000);
expect(payload.requestType).toBe('time_series');
expect(payload.formatOptions?.formatTableResultForUI).toBe(false);
expect(payload.formatOptions?.fillGaps).toBe(true);
// Variables mapped as { key: { value } }
expect(payload.variables).toEqual({
svc: { value: 'api' },
count: { value: 5 },
flag: { value: true },
});
// Queries include one builder_query and one builder_formula
expect(payload.compositeQuery.queries).toHaveLength(2);
const builderQuery = payload.compositeQuery.queries.find(
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const builderSpec = builderQuery.spec as MetricBuilderQuery;
expect(builderSpec.name).toBe('A');
expect(builderSpec.signal).toBe('metrics');
expect(builderSpec.aggregations?.[0]).toMatchObject({
metricName: 'cpu_usage',
timeAggregation: 'sum',
spaceAggregation: 'avg',
});
// reduceTo should not be present for non-scalar panels
expect(builderSpec.aggregations?.[0].reduceTo).toBeUndefined();
// functions should be preserved/normalized
expect(builderSpec.functions?.[0]?.name).toBe('timeShift');
const formulaQuery = payload.compositeQuery.queries.find(
(q) => q.type === 'builder_formula',
) as QueryEnvelope;
const formulaSpec = formulaQuery.spec as V5QueryBuilderFormula;
expect(formulaSpec.name).toBe('F1');
expect(formulaSpec.expression).toBe('A + 1');
expect(formulaSpec.legend).toBe('Formula Legend');
});
it('builds payload for PromQL queries and respects originalGraphType for formatting', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.PROM,
id: 'q2',
unit: undefined,
promql: [
{
name: 'A',
query: 'up',
disabled: false,
legend: 'LP',
},
],
clickhouse_sql: [],
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
},
graphType: PANEL_TYPES.TIME_SERIES,
originalGraphType: PANEL_TYPES.TABLE,
selectedTime: 'GLOBAL_TIME',
start,
end,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'LP' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: [
{
type: 'promql',
spec: expect.objectContaining({
name: 'A',
query: 'up',
legend: 'LP',
stats: false,
}),
},
],
}),
requestType: 'time_series',
formatOptions: expect.objectContaining({
formatTableResultForUI: true,
fillGaps: false,
}),
start: start * 1000,
end: end * 1000,
variables: {},
}),
}),
);
expect(result.legendMap).toEqual({ A: 'LP' });
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.requestType).toBe('time_series');
expect(payload.formatOptions?.formatTableResultForUI).toBe(true);
expect(payload.compositeQuery.queries).toHaveLength(1);
const prom = payload.compositeQuery.queries[0];
expect(prom.type).toBe('promql');
const promSpec = prom.spec as PromQuery;
expect(promSpec.name).toBe('A');
expect(promSpec.query).toBe('up');
expect(promSpec.legend).toBe('LP');
expect(promSpec.stats).toBe(false);
});
it('builds payload for ClickHouse queries and maps requestType from panel', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.CLICKHOUSE,
id: 'q3',
unit: undefined,
promql: [],
clickhouse_sql: [
{
name: 'Q',
query: 'SELECT 1',
disabled: false,
legend: 'LC',
},
],
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
},
graphType: PANEL_TYPES.TABLE,
selectedTime: 'GLOBAL_TIME',
start,
end,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { Q: 'LC' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: [
{
type: 'clickhouse_sql',
spec: expect.objectContaining({
name: 'Q',
query: 'SELECT 1',
legend: 'LC',
}),
},
],
}),
requestType: 'scalar',
formatOptions: expect.objectContaining({
formatTableResultForUI: true,
fillGaps: false,
}),
start: start * 1000,
end: end * 1000,
variables: {},
}),
}),
);
expect(result.legendMap).toEqual({ Q: 'LC' });
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.requestType).toBe('scalar');
expect(payload.compositeQuery.queries).toHaveLength(1);
const ch = payload.compositeQuery.queries[0];
expect(ch.type).toBe('clickhouse_sql');
const chSpec = ch.spec as ClickHouseQuery;
expect(chSpec.name).toBe('Q');
expect(chSpec.query).toBe('SELECT 1');
expect(chSpec.legend).toBe('LC');
});
it('uses getStartEndRangeTime when start/end are not provided', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'q4',
unit: undefined,
promql: [],
clickhouse_sql: [],
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
},
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: {},
queryPayload: expect.objectContaining({
compositeQuery: { queries: [] },
requestType: 'time_series',
formatOptions: expect.objectContaining({
formatTableResultForUI: false,
fillGaps: false,
}),
start: 100 * 1000,
end: 200 * 1000,
variables: {},
}),
}),
);
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.start).toBe(100 * 1000);
expect(payload.end).toBe(200 * 1000);
});
it('includes reduceTo for metrics in scalar panels (TABLE)', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'q5',
unit: undefined,
promql: [],
clickhouse_sql: [],
builder: {
queryData: [baseBuilderQuery()],
queryFormulas: [],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.TABLE,
selectedTime: 'GLOBAL_TIME',
start,
end,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'Legend A' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: [
{
type: 'builder_query',
spec: expect.objectContaining({
name: 'A',
signal: 'metrics',
stepInterval: 600,
functions: [{ name: 'timeShift', args: [{ value: '5m' }] }],
aggregations: [
expect.objectContaining({
metricName: 'cpu_usage',
timeAggregation: 'sum',
spaceAggregation: 'avg',
reduceTo: 'avg',
temporality: undefined,
}),
],
}),
},
],
}),
requestType: 'scalar',
formatOptions: expect.objectContaining({
formatTableResultForUI: true,
fillGaps: false,
}),
start: start * 1000,
end: end * 1000,
variables: {},
}),
}),
);
const payload: QueryRangePayloadV5 = result.queryPayload;
const builderQuery = payload.compositeQuery.queries.find(
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const builderSpec = builderQuery.spec as MetricBuilderQuery;
expect(builderSpec.aggregations?.[0].reduceTo).toBe('avg');
});
it('omits aggregations for raw request type (LIST panel)', () => {
const logAgg: LogAggregation[] = [{ expression: 'count()' }];
const logsQuery = baseBuilderQuery({
dataSource: DataSource.LOGS,
aggregations: logAgg,
} as Partial<IBuilderQuery>);
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'q6',
unit: undefined,
promql: [],
clickhouse_sql: [],
builder: {
queryData: [logsQuery],
queryFormulas: [],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
start,
end,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'Legend A' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: [
{
type: 'builder_query',
spec: expect.objectContaining({
name: 'A',
signal: 'logs',
stepInterval: 600,
functions: [{ name: 'timeShift', args: [{ value: '5m' }] }],
aggregations: undefined,
}),
},
],
}),
requestType: 'raw',
formatOptions: expect.objectContaining({
formatTableResultForUI: false,
fillGaps: false,
}),
start: start * 1000,
end: end * 1000,
variables: {},
}),
}),
);
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.requestType).toBe('raw');
const builderQuery = payload.compositeQuery.queries.find(
(q) => q.type === 'builder_query',
) as QueryEnvelope;
// For RAW request type, aggregations should be omitted
const logSpec = builderQuery.spec as LogBuilderQuery;
expect(logSpec.aggregations).toBeUndefined();
});
it('maps groupBy, order, having, aggregations and filter for logs builder query', () => {
const getStartEndRangeTime = jest.requireMock('lib/getStartEndRangeTime')
.default as jest.Mock;
getStartEndRangeTime.mockReturnValueOnce({
start: '1754623641',
end: '1754645241',
});
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'e643e387-1996-4449-97b6-9ef4498a0573',
unit: undefined,
promql: [{ name: 'A', query: '', legend: '', disabled: false }],
clickhouse_sql: [{ name: 'A', legend: '', disabled: false, query: '' }],
builder: {
queryData: [
{
dataSource: DataSource.LOGS,
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
key: '',
dataType: DataTypes.EMPTY,
type: '',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
filter: { expression: "service.name = 'adservice'" },
aggregations: [
{ expression: 'count() as cnt avg(code.lineno) ' } as LogAggregation,
],
functions: [],
filters: {
items: [
{
id: '14c790ec-54d1-42f0-a889-3b4f0fb79852',
op: '=',
key: { id: 'service.name', key: 'service.name', type: '' },
value: 'adservice',
},
],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 80,
having: { expression: 'count() > 0' },
limit: 600,
orderBy: [{ columnName: 'service.name', order: 'desc' }],
groupBy: [
{
key: 'service.name',
type: '',
},
],
legend: '{{service.name}}',
reduceTo: 'avg',
offset: 0,
pageSize: 100,
},
],
queryFormulas: [],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: 'custom' as never,
variables: {},
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: '{{service.name}}' },
queryPayload: expect.objectContaining({
schemaVersion: 'v1',
start: 1754623641000,
end: 1754645241000,
requestType: 'time_series',
compositeQuery: expect.objectContaining({
queries: [
{
type: 'builder_query',
spec: expect.objectContaining({
name: 'A',
signal: 'logs',
stepInterval: 80,
disabled: false,
filter: { expression: "service.name = 'adservice'" },
groupBy: [
{
name: 'service.name',
fieldDataType: '',
fieldContext: '',
},
],
limit: 600,
order: [
{
key: { name: 'service.name' },
direction: 'desc',
},
],
legend: '{{service.name}}',
having: { expression: 'count() > 0' },
aggregations: [
{ expression: 'count()', alias: 'cnt' },
{ expression: 'avg(code.lineno)' },
],
}),
},
],
}),
formatOptions: { formatTableResultForUI: false, fillGaps: false },
variables: {},
}),
}),
);
});
});

View File

@@ -1,5 +1,4 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable sonarjs/no-identical-functions */
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
@@ -8,7 +7,7 @@ import { isEmpty } from 'lodash-es';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
IBuilderTraceOperator,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
@@ -28,11 +27,9 @@ import {
TelemetryFieldKey,
TraceAggregation,
VariableItem,
VariableType,
} 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;
@@ -71,46 +68,9 @@ function getSignalType(dataSource: string): 'traces' | 'logs' | 'metrics' {
return 'metrics';
}
function isDeprecatedField(fieldName: string): boolean {
const deprecatedIntrinsicFields = [
'traceID',
'spanID',
'parentSpanID',
'spanKind',
'durationNano',
'statusCode',
'statusMessage',
'statusCodeString',
];
const deprecatedCalculatedFields = [
'responseStatusCode',
'externalHttpUrl',
'httpUrl',
'externalHttpMethod',
'httpMethod',
'httpHost',
'dbName',
'dbOperation',
'hasError',
'isRemote',
'serviceName',
'httpRoute',
'msgSystem',
'msgOperation',
'dbSystem',
'rpcSystem',
'rpcService',
'rpcMethod',
'peerService',
];
return (
deprecatedIntrinsicFields.includes(fieldName) ||
deprecatedCalculatedFields.includes(fieldName)
);
}
/**
* Creates base spec for builder queries
*/
function createBaseSpec(
queryData: IBuilderQuery,
requestType: RequestType,
@@ -122,7 +82,7 @@ function createBaseSpec(
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
return {
stepInterval: queryData?.stepInterval || null,
stepInterval: queryData?.stepInterval || undefined,
disabled: queryData.disabled,
filter: queryData?.filter?.expression ? queryData.filter : undefined,
groupBy:
@@ -130,8 +90,8 @@ function createBaseSpec(
? queryData.groupBy.map(
(item: any): GroupByKey => ({
name: item.key,
fieldDataType: item?.dataType || '',
fieldContext: item?.type || '',
fieldDataType: item?.dataType,
fieldContext: item?.type,
description: item?.description,
unit: item?.unit,
signal: item?.signal,
@@ -163,52 +123,34 @@ 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
: nonEmptySelectColumns?.map(
(column: any): TelemetryFieldKey => {
const fieldName = column.name ?? column.key;
const isDeprecated = isDeprecatedField(fieldName);
const fieldObj: TelemetryFieldKey = {
name: fieldName,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
signal: column?.signal ?? undefined,
};
// Only add fieldContext if the field is NOT deprecated
if (!isDeprecated && fieldName !== 'name') {
fieldObj.fieldContext =
column?.fieldContext ?? (column?.type as FieldContext);
}
return fieldObj;
},
(column: any): TelemetryFieldKey => ({
name: column.name ?? column.key,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
signal: column?.signal ?? undefined,
}),
),
};
}
// Utility to parse aggregation expressions with optional alias
export function parseAggregations(
expression: string,
availableAlias?: string,
): { expression: string; alias?: string }[] {
const result: { expression: string; alias?: string }[] = [];
// Matches function calls like "count()" or "sum(field)" with optional alias like "as 'alias'"
@@ -217,7 +159,7 @@ export function parseAggregations(
let match = regex.exec(expression);
while (match !== null) {
const expr = match[1];
let alias = match[2] || availableAlias; // Use provided alias or availableAlias if not matched
let alias = match[2];
if (alias) {
// Remove quotes if present
alias = alias.replace(/^['"]|['"]$/g, '');
@@ -268,14 +210,9 @@ export function createAggregation(
}
if (queryData.aggregations?.length > 0) {
return queryData.aggregations.flatMap(
(agg: { expression: string; alias?: string }) => {
const parsedAggregations = parseAggregations(agg.expression, agg?.alias);
return isEmpty(parsedAggregations)
? [{ expression: 'count()' }]
: parsedAggregations;
},
);
return isEmpty(parseAggregations(queryData.aggregations?.[0].expression))
? [{ expression: 'count()' }]
: parseAggregations(queryData.aggregations?.[0].expression);
}
return [{ expression: 'count()' }];
@@ -321,7 +258,6 @@ export function convertBuilderQueriesToV5(
spec = {
name: queryName,
signal: 'metrics' as const,
source: queryData.source || '',
...baseSpec,
aggregations: aggregations as MetricAggregation[],
// reduceTo: queryData.reduceTo,
@@ -337,109 +273,6 @@ export function convertBuilderQueriesToV5(
);
}
function createTraceOperatorBaseSpec(
queryData: IBuilderTraceOperator,
requestType: RequestType,
panelType?: PANEL_TYPES,
): BaseBuilderQuery {
const nonEmptySelectColumns = (queryData.selectColumns as (
| BaseAutocompleteData
| TelemetryFieldKey
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
const {
stepInterval,
groupBy,
limit,
offset,
legend,
having,
orderBy,
pageSize,
} = queryData;
return {
stepInterval: stepInterval || undefined,
groupBy:
groupBy?.length > 0
? groupBy.map(
(item: any): GroupByKey => ({
name: item.key,
fieldDataType: item?.dataType,
fieldContext: item?.type,
description: item?.description,
unit: item?.unit,
signal: item?.signal,
materialized: item?.materialized,
}),
)
: undefined,
limit:
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
? limit || pageSize || undefined
: limit || undefined,
offset: requestType === 'raw' || requestType === 'trace' ? offset : undefined,
order:
orderBy?.length > 0
? orderBy.map(
(order: any): OrderBy => ({
key: {
name: order.columnName,
},
direction: order.order,
}),
)
: undefined,
legend: isEmpty(legend) ? undefined : legend,
having: isEmpty(having) ? undefined : (having as Having),
selectFields: isEmpty(nonEmptySelectColumns)
? undefined
: nonEmptySelectColumns?.map(
(column: any): TelemetryFieldKey => ({
name: column.name ?? column.key,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
signal: column?.signal ?? undefined,
}),
),
};
}
export function convertTraceOperatorToV5(
traceOperator: Record<string, IBuilderTraceOperator>,
requestType: RequestType,
panelType?: PANEL_TYPES,
): QueryEnvelope[] {
return Object.entries(traceOperator).map(
([queryName, traceOperatorData]): QueryEnvelope => {
const baseSpec = createTraceOperatorBaseSpec(
traceOperatorData,
requestType,
panelType,
);
// Skip aggregation for raw request type
const aggregations =
requestType === 'raw'
? undefined
: createAggregation(traceOperatorData, panelType);
const spec: QueryEnvelope['spec'] = {
name: queryName,
...baseSpec,
expression: traceOperatorData.expression || '',
aggregations: aggregations as TraceAggregation[],
};
return {
type: 'builder_trace_operator' as QueryType,
spec,
};
},
);
}
/**
* Converts PromQL queries to V5 format
*/
@@ -514,7 +347,6 @@ export const prepareQueryRangePayloadV5 = ({
formatForWeb,
originalGraphType,
fillGaps,
dynamicVariables,
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
let legendMap: Record<string, string> = {};
const requestType = mapPanelTypeToRequestType(graphType);
@@ -522,28 +354,14 @@ export const prepareQueryRangePayloadV5 = ({
switch (query.queryType) {
case EQueryType.QUERY_BUILDER: {
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
const { queryData: data, queryFormulas } = query.builder;
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
const filteredTraceOperator =
queryTraceOperator && queryTraceOperator.length > 0
? queryTraceOperator.filter((traceOperator) =>
Boolean(traceOperator.expression.trim()),
)
: [];
const currentTraceOperator = mapQueryDataToApi(
filteredTraceOperator,
'queryName',
tableParams,
);
// Combine legend maps
legendMap = {
...currentQueryData.newLegendMap,
...currentFormulas.newLegendMap,
...currentTraceOperator.newLegendMap,
};
// Convert builder queries
@@ -576,14 +394,8 @@ export const prepareQueryRangePayloadV5 = ({
}),
);
const traceOperatorQueries = convertTraceOperatorToV5(
currentTraceOperator.data,
requestType,
graphType,
);
// Combine all query types
queries = [...builderQueries, ...formulaQueries, ...traceOperatorQueries];
// Combine both types
queries = [...builderQueries, ...formulaQueries];
break;
}
case EQueryType.PROM: {
@@ -626,12 +438,7 @@ export const prepareQueryRangePayloadV5 = ({
fillGaps: fillGaps || false,
},
variables: Object.entries(variables).reduce((acc, [key, value]) => {
acc[key] = {
value,
type: dynamicVariables
?.find((v) => v.name === key)
?.type?.toLowerCase() as VariableType,
};
acc[key] = { value };
return acc;
}, {} as Record<string, VariableItem>),
};

View File

@@ -1,16 +1,14 @@
import { render, screen } from '@testing-library/react';
import getLocal from '../../../api/browser/localstorage/get';
import AppLoading from '../AppLoading';
jest.mock('../../../api/browser/localstorage/get', () => ({
// Mock the localStorage API
const mockGet = jest.fn();
jest.mock('api/browser/localstorage/get', () => ({
__esModule: true,
default: jest.fn(),
default: mockGet,
}));
// Access the mocked function
const mockGet = (getLocal as unknown) as jest.Mock;
describe('AppLoading', () => {
const SIGNOZ_TEXT = 'SigNoz';
const TAGLINE_TEXT =

View File

@@ -20,15 +20,13 @@
.ant-card-body {
height: calc(100% - 18px);
.widget-graph-component-container {
.widget-graph-container {
&.bar-panel-container {
height: calc(100% - 110px);
}
.widget-graph-container {
&.bar {
height: calc(100% - 110px);
}
&.graph-panel-container {
height: calc(100% - 80px);
}
&.graph {
height: calc(100% - 80px);
}
}
}
@@ -84,11 +82,9 @@
.ant-card-body {
height: calc(100% - 18px);
.widget-graph-component-container {
.widget-graph-container {
&.bar-panel-container {
height: calc(100% - 110px);
}
.widget-graph-container {
&.bar {
height: calc(100% - 110px);
}
}
}

View File

@@ -32,6 +32,8 @@ export const celeryAllStateWidgetData = (
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
@@ -48,6 +50,8 @@ export const celeryAllStateWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -84,6 +88,7 @@ export const celeryRetryStateWidgetData = (
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
key: '',
type: '',
},
@@ -98,6 +103,8 @@ export const celeryRetryStateWidgetData = (
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -112,6 +119,8 @@ export const celeryRetryStateWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
@@ -144,6 +153,8 @@ export const celeryFailedStateWidgetData = (
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
@@ -158,6 +169,8 @@ export const celeryFailedStateWidgetData = (
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -172,6 +185,8 @@ export const celeryFailedStateWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
@@ -204,6 +219,8 @@ export const celerySuccessStateWidgetData = (
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
@@ -218,6 +235,8 @@ export const celerySuccessStateWidgetData = (
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -232,6 +251,8 @@ export const celerySuccessStateWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
@@ -263,6 +284,7 @@ export const celeryTasksByWorkerWidgetData = (
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
key: '',
type: '',
},
@@ -279,6 +301,8 @@ export const celeryTasksByWorkerWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
@@ -314,6 +338,8 @@ export const celeryErrorByWorkerWidgetData = (
aggregateAttribute: {
dataType: 'string',
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
@@ -327,6 +353,8 @@ export const celeryErrorByWorkerWidgetData = (
key: {
dataType: DataTypes.bool,
id: 'has_error--bool----true',
isColumn: true,
isJSON: false,
key: 'has_error',
type: '',
},
@@ -345,6 +373,8 @@ export const celeryErrorByWorkerWidgetData = (
groupBy: [
{
dataType: DataTypes.String,
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
id: 'celery.hostname--string--tag--false',
@@ -360,6 +390,8 @@ export const celeryErrorByWorkerWidgetData = (
aggregateAttribute: {
dataType: 'string',
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
@@ -379,6 +411,8 @@ export const celeryErrorByWorkerWidgetData = (
groupBy: [
{
dataType: DataTypes.String,
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
id: 'celery.hostname--string--tag--false',
@@ -411,6 +445,8 @@ export const celeryLatencyByWorkerWidgetData = (
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
@@ -427,6 +463,8 @@ export const celeryLatencyByWorkerWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
@@ -460,6 +498,8 @@ export const celeryActiveTasksWidgetData = (
dataType: DataTypes.Float64,
id:
'flower_worker_number_of_currently_executing_tasks--float64--Gauge--true',
isColumn: true,
isJSON: false,
key: 'flower_worker_number_of_currently_executing_tasks',
type: 'Gauge',
},
@@ -476,6 +516,8 @@ export const celeryActiveTasksWidgetData = (
{
dataType: DataTypes.String,
id: 'worker--string--tag--false',
isColumn: false,
isJSON: false,
key: 'worker',
type: 'tag',
},
@@ -509,6 +551,8 @@ export const celeryTaskLatencyWidgetData = (
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
@@ -525,6 +569,8 @@ export const celeryTaskLatencyWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
@@ -560,6 +606,8 @@ export const celerySlowestTasksTableWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
@@ -576,6 +624,8 @@ export const celerySlowestTasksTableWidgetData = getWidgetQueryBuilder(
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
@@ -610,6 +660,8 @@ export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
@@ -624,6 +676,8 @@ export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -638,6 +692,8 @@ export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
@@ -673,6 +729,8 @@ export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
@@ -687,6 +745,8 @@ export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -701,6 +761,8 @@ export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
@@ -734,6 +796,8 @@ export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
@@ -748,6 +812,8 @@ export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -762,6 +828,8 @@ export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
@@ -801,6 +869,8 @@ export const celeryTimeSeriesTablesWidgetData = (
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
@@ -815,6 +885,8 @@ export const celeryTimeSeriesTablesWidgetData = (
key: {
dataType: DataTypes.String,
id: `${entity}--string--tag--false`,
isColumn: false,
isJSON: false,
key: `${entity}`,
type: 'tag',
},
@@ -829,6 +901,8 @@ export const celeryTimeSeriesTablesWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
@@ -859,6 +933,8 @@ export const celeryAllStateCountWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
@@ -896,6 +972,8 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
@@ -910,6 +988,8 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -945,6 +1025,8 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
@@ -959,6 +1041,8 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -994,6 +1078,7 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
key: 'span_id',
type: '',
},
@@ -1008,6 +1093,8 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},

View File

@@ -39,6 +39,8 @@ export function getFiltersFromQueryParams(
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',
@@ -98,7 +100,8 @@ export const createFiltersFromData = (
key: string;
dataType: DataTypes;
type: string;
isColumn: boolean;
isJSON: boolean;
id: string;
};
op: string;
@@ -116,6 +119,8 @@ export const createFiltersFromData = (
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',

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

@@ -137,11 +137,5 @@
h6 {
color: var(--text-ink-500);
}
code {
background-color: var(--bg-vanilla-300);
border: 1px solid var(--bg-vanilla-300);
color: var(--text-ink-500);
}
}
}

View File

@@ -241,6 +241,8 @@ function ClientSideQBSearch(
key: 'body',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
id: 'body--string----true',
},
op: OPERATORS.CONTAINS,

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,16 +1,6 @@
.custom-time-picker {
display: flex;
flex-direction: column;
.timeSelection-input {
&:hover {
border-color: #1d212d !important;
}
}
.time-input-suffix {
display: flex;
}
}
.time-options-container {
@@ -145,7 +135,6 @@
align-items: center;
color: var(--bg-vanilla-400);
gap: 6px;
.timezone {
display: flex;
align-items: center;
@@ -174,52 +163,6 @@
cursor: pointer;
}
.time-input-prefix {
.live-dot-icon {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: var(--bg-forest-500);
animation: ripple 1s infinite;
margin-right: 4px;
margin-left: 4px;
}
}
@keyframes ripple {
0% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4);
}
70% {
box-shadow: 0 0 0 6px rgba(245, 158, 11, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
}
}
.time-input-suffix-icon-badge {
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
cursor: pointer;
height: 20px;
width: 20px;
&:hover {
background: rgba(171, 189, 255, 0.08);
}
}
.lightMode {
.date-time-popover__footer {
border-color: var(--bg-vanilla-400);
@@ -237,26 +180,8 @@
}
}
}
.custom-time-picker {
.timeSelection-input {
&:hover {
border-color: var(--bg-vanilla-300) !important;
}
}
}
.timezone-badge {
color: var(--bg-ink-100);
background: rgb(179 179 179 / 15%);
}
.time-input-suffix-icon-badge {
color: var(--bg-ink-100);
background: rgb(179 179 179 / 15%);
&:hover {
background: rgb(179 179 179 / 20%);
}
}
}

View File

@@ -5,12 +5,13 @@ import './CustomTimePicker.styles.scss';
import { Input, Popover, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
CustomTimeType,
FixedDurationSuggestionOptions,
Options,
RelativeDurationSuggestionOptions,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { isValidTimeFormat } from 'lib/getMinMax';
@@ -27,10 +28,7 @@ import {
useMemo,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { popupContainer } from 'utils/selectPopupContainer';
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
@@ -59,9 +57,11 @@ interface CustomTimePickerProps {
customDateTimeVisible?: boolean;
setCustomDTPickerVisible?: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler?: (dateTimeRange: DateTimeRangeType) => void;
showLiveLogs?: boolean;
onGoLive?: () => void;
onExitLiveLogs?: () => void;
handleGoLive?: () => void;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}
function CustomTimePicker({
@@ -78,19 +78,14 @@ function CustomTimePicker({
customDateTimeVisible,
setCustomDTPickerVisible,
onCustomDateHandler,
onGoLive,
onExitLiveLogs,
showLiveLogs,
handleGoLive,
onTimeChange,
}: CustomTimePickerProps): JSX.Element {
const [
selectedTimePlaceholderValue,
setSelectedTimePlaceholderValue,
] = useState('Select / Enter Time Range');
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [inputValue, setInputValue] = useState('');
const [inputStatus, setInputStatus] = useState<'' | 'error' | 'success'>('');
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
@@ -169,13 +164,9 @@ function CustomTimePicker({
};
useEffect(() => {
if (showLiveLogs) {
setSelectedTimePlaceholderValue('Live');
} else {
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setSelectedTimePlaceholderValue(value);
}
}, [selectedTime, selectedValue, showLiveLogs]);
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setSelectedTimePlaceholderValue(value);
}, [selectedTime, selectedValue]);
const hide = (): void => {
setOpen(false);
@@ -265,11 +256,6 @@ function CustomTimePicker({
};
const handleSelect = (label: string, value: string): void => {
if (label === 'Custom') {
setCustomDTPickerVisible?.(true);
return;
}
onSelect(value);
setSelectedTimePlaceholderValue(label);
setInputStatus('');
@@ -332,118 +318,84 @@ function CustomTimePicker({
);
};
const getTooltipTitle = (): string => {
if (selectedTime === 'custom' && inputValue === '' && !open) {
return `${dayjs(minTime / 1000_000)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)} - ${dayjs(
maxTime / 1000_000,
)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)}`;
}
return '';
};
const getInputPrefix = (): JSX.Element => {
if (showLiveLogs) {
return (
<div className="time-input-prefix">
<div className="live-dot-icon" />
</div>
);
}
return (
<div className="time-input-prefix">
{inputValue && inputStatus === 'success' ? (
<CheckCircle size={14} color="#51E7A8" />
) : (
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
<Clock size={14} className="cursor-pointer" />
</Tooltip>
)}
</div>
);
};
return (
<div className="custom-time-picker">
<Tooltip title={getTooltipTitle()} placement="top">
<Popover
className={cx(
'timeSelection-input-container',
selectedTime === 'custom' && inputValue === '' ? 'custom-time' : '',
)}
placement="bottomRight"
getPopupContainer={popupContainer}
rootClassName="date-time-root"
content={
newPopover ? (
<CustomTimePickerPopoverContent
setIsOpen={setOpen}
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
onSelectHandler={handleSelect}
onGoLive={defaultTo(onGoLive, noop)}
onExitLiveLogs={defaultTo(onExitLiveLogs, noop)}
options={items}
selectedTime={selectedTime}
activeView={activeView}
setActiveView={setActiveView}
setIsOpenedFromFooter={setIsOpenedFromFooter}
isOpenedFromFooter={isOpenedFromFooter}
/>
<Popover
className={cx(
'timeSelection-input-container',
selectedTime === 'custom' && inputValue === '' ? 'custom-time' : '',
)}
placement="bottomRight"
getPopupContainer={popupContainer}
rootClassName="date-time-root"
content={
newPopover ? (
<CustomTimePickerPopoverContent
setIsOpen={setOpen}
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
onSelectHandler={handleSelect}
handleGoLive={defaultTo(handleGoLive, noop)}
options={items}
selectedTime={selectedTime}
activeView={activeView}
setActiveView={setActiveView}
setIsOpenedFromFooter={setIsOpenedFromFooter}
isOpenedFromFooter={isOpenedFromFooter}
onTimeChange={onTimeChange}
/>
) : (
content
)
}
arrow={false}
trigger="click"
open={open}
onOpenChange={handleOpenChange}
style={{
padding: 0,
}}
>
<Input
className="timeSelection-input"
type="text"
status={inputValue && inputStatus === 'error' ? 'error' : ''}
placeholder={
isInputFocused
? 'Time Format (1m or 2h or 3d or 4w)'
: selectedTimePlaceholderValue
}
value={inputValue}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={handleInputChange}
data-1p-ignore
prefix={
inputValue && inputStatus === 'success' ? (
<CheckCircle size={14} color="#51E7A8" />
) : (
content
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
<Clock size={14} />
</Tooltip>
)
}
arrow={false}
trigger="click"
open={open}
onOpenChange={handleOpenChange}
style={{
padding: 0,
}}
>
<Input
className="timeSelection-input"
type="text"
status={inputValue && inputStatus === 'error' ? 'error' : ''}
placeholder={
isInputFocused
? 'Time Format (1m or 2h or 3d or 4w)'
: selectedTimePlaceholderValue
}
value={inputValue}
onFocus={handleFocus}
onClick={handleFocus}
onBlur={handleBlur}
onChange={handleInputChange}
data-1p-ignore
prefix={getInputPrefix()}
suffix={
<div className="time-input-suffix">
{!!isTimezoneOverridden && activeTimezoneOffset && (
<div className="timezone-badge" onClick={handleTimezoneHintClick}>
<span>{activeTimezoneOffset}</span>
</div>
)}
<ChevronDown
size={14}
className="cursor-pointer time-input-suffix-icon-badge"
onClick={(e): void => {
e.stopPropagation();
handleViewChange('datetime');
}}
/>
</div>
}
/>
</Popover>
</Tooltip>
suffix={
<>
{!!isTimezoneOverridden && activeTimezoneOffset && (
<div className="timezone-badge" onClick={handleTimezoneHintClick}>
<span>{activeTimezoneOffset}</span>
</div>
)}
<ChevronDown
size={14}
onClick={(): void => handleViewChange('datetime')}
/>
</>
}
/>
</Popover>
{inputStatus === 'error' && inputErrorMessage && (
<Typography.Title level={5} className="valid-format-error">
{inputErrorMessage}
@@ -460,8 +412,7 @@ CustomTimePicker.defaultProps = {
customDateTimeVisible: false,
setCustomDTPickerVisible: noop,
onCustomDateHandler: noop,
onGoLive: noop,
handleGoLive: noop,
onCustomTimeStatusUpdate: noop,
onExitLiveLogs: noop,
showLiveLogs: false,
onTimeChange: undefined,
};

View File

@@ -4,30 +4,21 @@ import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import DatePickerV2 from 'components/DatePickerV2/DatePickerV2';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
CustomTimeType,
LexicalContext,
Option,
RelativeDurationSuggestionOptions,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { Clock, PenLine } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { getCustomTimeRanges } from 'utils/customTimeRangeUtils';
import RangePickerModal from './RangePickerModal';
import TimezonePicker from './TimezonePicker';
interface CustomTimePickerPopoverContentProps {
@@ -40,21 +31,16 @@ interface CustomTimePickerPopoverContentProps {
lexicalContext?: LexicalContext,
) => void;
onSelectHandler: (label: string, value: string) => void;
onGoLive: () => void;
handleGoLive: () => void;
selectedTime: string;
activeView: 'datetime' | 'timezone';
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
isOpenedFromFooter: boolean;
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
onExitLiveLogs: () => void;
}
interface RecentlyUsedDateTimeRange {
label: string;
value: number;
timestamp: number;
from: string;
to: string;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -65,68 +51,22 @@ function CustomTimePickerPopoverContent({
setCustomDTPickerVisible,
onCustomDateHandler,
onSelectHandler,
onGoLive,
handleGoLive,
selectedTime,
activeView,
setActiveView,
isOpenedFromFooter,
setIsOpenedFromFooter,
onExitLiveLogs,
onTimeChange,
}: CustomTimePickerPopoverContentProps): JSX.Element {
const { pathname } = useLocation();
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname,
]);
const url = new URLSearchParams(window.location.search);
let panelTypeFromURL = url.get(QueryParams.panelTypes);
try {
panelTypeFromURL = JSON.parse(panelTypeFromURL as string);
} catch {
// fallback → leave as-is
}
const isLogsListView =
panelTypeFromURL !== 'table' && panelTypeFromURL !== 'graph'; // we do not select list view in the url
const { timezone } = useTimezone();
const activeTimezoneOffset = timezone.offset;
const [recentlyUsedTimeRanges, setRecentlyUsedTimeRanges] = useState<
RecentlyUsedDateTimeRange[]
>([]);
const handleExitLiveLogs = useCallback((): void => {
if (isLogsExplorerPage) {
onExitLiveLogs();
}
}, [isLogsExplorerPage, onExitLiveLogs]);
useEffect(() => {
if (!customDateTimeVisible) {
const customTimeRanges = getCustomTimeRanges();
const formattedCustomTimeRanges: RecentlyUsedDateTimeRange[] = customTimeRanges.map(
(range) => ({
label: `${dayjs(range.from)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)} - ${dayjs(range.to)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)}`,
from: range.from,
to: range.to,
value: range.timestamp,
timestamp: range.timestamp,
}),
);
setRecentlyUsedTimeRanges(formattedCustomTimeRanges);
}
}, [customDateTimeVisible, timezone.value]);
function getTimeChips(options: Option[]): JSX.Element {
return (
<div className="relative-date-time-section">
@@ -136,7 +76,6 @@ function CustomTimePickerPopoverContent({
className="time-btns"
key={option.label + option.value}
onClick={(): void => {
handleExitLiveLogs();
onSelectHandler(option.label, option.value);
}}
>
@@ -170,87 +109,53 @@ function CustomTimePickerPopoverContent({
);
}
const handleGoLive = (): void => {
onGoLive();
setIsOpen(false);
};
return (
<>
<div className="date-time-popover">
{!customDateTimeVisible && (
<div className="date-time-options">
{isLogsExplorerPage && isLogsListView && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
handleExitLiveLogs();
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
</div>
)}
<div className="date-time-options">
{isLogsExplorerPage && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
</div>
<div
className={cx(
'relative-date-time',
customDateTimeVisible ? 'date-picker' : 'relative-times',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
)}
>
{customDateTimeVisible ? (
<DatePickerV2
onSetCustomDTPickerVisible={setCustomDTPickerVisible}
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
onTimeChange={onTimeChange}
/>
) : (
<div className="time-selector-container">
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
<div className="recently-used-container">
<div className="time-heading">RECENTLY USED</div>
<div className="recently-used-range">
{recentlyUsedTimeRanges.map((range: RecentlyUsedDateTimeRange) => (
<div
className="recently-used-range-item"
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
handleExitLiveLogs();
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
setIsOpen(false);
}
}}
key={range.value}
onClick={(): void => {
handleExitLiveLogs();
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
setIsOpen(false);
}}
>
{range.label}
</div>
))}
</div>
</div>
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
)}
</div>
@@ -284,4 +189,8 @@ function CustomTimePickerPopoverContent({
);
}
CustomTimePickerPopoverContent.defaultProps = {
onTimeChange: undefined,
};
export default CustomTimePickerPopoverContent;

View File

@@ -1,114 +0,0 @@
.date-picker-v2-container {
display: flex;
flex-direction: row;
}
.custom-date-time-picker-v2 {
padding: 12px;
.periscope-calendar {
border-radius: 4px;
border: none !important;
background: none !important;
padding: 8px 0 !important;
}
.periscope-calendar-day {
background: none !important;
&.periscope-calendar-today {
&.text-accent-foreground {
color: var(--bg-vanilla-100) !important;
}
}
button {
&:hover {
background-color: var(--bg-robin-500) !important;
color: var(--bg-vanilla-100) !important;
}
}
}
.custom-time-selector {
display: flex;
flex-direction: row;
gap: 16px;
align-items: center;
justify-content: space-between;
.time-input {
border-radius: 4px;
border: none !important;
background: none !important;
padding: 8px 4px !important;
color: var(--bg-vanilla-100) !important;
&::-webkit-calendar-picker-indicator {
display: none !important;
-webkit-appearance: none;
appearance: none;
}
&:focus {
border: none !important;
outline: none !important;
box-shadow: none !important;
}
&:focus-visible {
border: none !important;
outline: none !important;
box-shadow: none !important;
}
}
}
.custom-date-time-picker-footer {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
justify-content: flex-end;
margin-top: 16px;
.next-btn {
width: 80px;
}
.clear-btn {
width: 80px;
}
}
}
.invalid-date-range-tooltip {
.ant-tooltip-inner {
color: var(--bg-sakura-500) !important;
}
}
.lightMode {
.custom-date-time-picker-v2 {
.periscope-calendar-day {
&.periscope-calendar-today {
&.text-accent-foreground {
color: var(--bg-ink-500) !important;
}
}
button {
&:hover {
background-color: var(--bg-robin-500) !important;
color: var(--bg-ink-500) !important;
}
}
}
.custom-time-selector {
.time-input {
color: var(--bg-ink-500) !important;
}
}
}
}

View File

@@ -1,311 +0,0 @@
import './DatePickerV2.styles.scss';
import { Calendar } from '@signozhq/calendar';
import { Input } from '@signozhq/input';
import { Button, Tooltip } from 'antd';
import cx from 'classnames';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs, { Dayjs } from 'dayjs';
import { CornerUpLeft, MoveRight } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { addCustomTimeRange } from 'utils/customTimeRangeUtils';
function DatePickerV2({
onSetCustomDTPickerVisible,
setIsOpen,
onCustomDateHandler,
}: {
onSetCustomDTPickerVisible: (visible: boolean) => void;
setIsOpen: (isOpen: boolean) => void;
onCustomDateHandler: (
dateTimeRange: DateTimeRangeType,
lexicalContext?: LexicalContext,
) => void;
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const timeInputRef = useRef<HTMLInputElement>(null);
const { timezone } = useTimezone();
const [selectedDateTimeFor, setSelectedDateTimeFor] = useState<'to' | 'from'>(
'from',
);
const [selectedFromDateTime, setSelectedFromDateTime] = useState<Dayjs | null>(
dayjs(minTime / 1000_000).tz(timezone.value),
);
const [selectedToDateTime, setSelectedToDateTime] = useState<Dayjs | null>(
dayjs(maxTime / 1000_000).tz(timezone.value),
);
const handleNext = (): void => {
if (selectedDateTimeFor === 'to') {
onCustomDateHandler([selectedFromDateTime, selectedToDateTime]);
addCustomTimeRange([selectedFromDateTime, selectedToDateTime]);
setIsOpen(false);
onSetCustomDTPickerVisible(false);
setSelectedDateTimeFor('from');
} else {
setSelectedDateTimeFor('to');
}
};
const handleDateChange = (date: Date | undefined): void => {
if (!date) {
return;
}
if (selectedDateTimeFor === 'from') {
const prevFromDateTime = selectedFromDateTime;
const newDate = dayjs(date);
const updatedFromDateTime = prevFromDateTime
? prevFromDateTime
.year(newDate.year())
.month(newDate.month())
.date(newDate.date())
: dayjs(date).tz(timezone.value);
setSelectedFromDateTime(updatedFromDateTime);
} else {
// eslint-disable-next-line sonarjs/no-identical-functions
setSelectedToDateTime((prev) => {
const newDate = dayjs(date);
// Update only the date part, keeping time from existing state
return prev
? prev.year(newDate.year()).month(newDate.month()).date(newDate.date())
: dayjs(date).tz(timezone.value);
});
}
// focus the time input
timeInputRef?.current?.focus();
};
const handleTimeChange = (time: string): void => {
// time should have format HH:mm:ss
if (!/^\d{2}:\d{2}:\d{2}$/.test(time)) {
return;
}
if (selectedDateTimeFor === 'from') {
setSelectedFromDateTime((prev) => {
if (prev) {
return prev
.set('hour', parseInt(time.split(':')[0], 10))
.set('minute', parseInt(time.split(':')[1], 10))
.set('second', parseInt(time.split(':')[2], 10));
}
return prev;
});
}
if (selectedDateTimeFor === 'to') {
// eslint-disable-next-line sonarjs/no-identical-functions
setSelectedToDateTime((prev) => {
if (prev) {
return prev
.set('hour', parseInt(time.split(':')[0], 10))
.set('minute', parseInt(time.split(':')[1], 10))
.set('second', parseInt(time.split(':')[2], 10));
}
return prev;
});
}
};
const getDefaultMonth = (): Date => {
let defaultDate = null;
if (selectedDateTimeFor === 'from') {
defaultDate = selectedFromDateTime?.toDate();
} else if (selectedDateTimeFor === 'to') {
defaultDate = selectedToDateTime?.toDate();
}
return defaultDate ?? new Date();
};
const isValidRange = (): boolean => {
if (selectedDateTimeFor === 'to') {
return selectedToDateTime?.isAfter(selectedFromDateTime) ?? false;
}
return true;
};
const handleBack = (): void => {
setSelectedDateTimeFor('from');
};
const handleHideCustomDTPicker = (): void => {
onSetCustomDTPickerVisible(false);
};
const handleSelectDateTimeFor = (selectedDateTimeFor: 'to' | 'from'): void => {
setSelectedDateTimeFor(selectedDateTimeFor);
};
return (
<div className="date-picker-v2-container">
<div className="date-time-custom-options-container">
<div
className="back-btn"
onClick={handleHideCustomDTPicker}
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
handleHideCustomDTPicker();
}
}}
>
<CornerUpLeft size={16} />
<span>Back</span>
</div>
<div className="date-time-custom-options">
<div
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
handleSelectDateTimeFor('from');
}
}}
className={cx(
'date-time-custom-option-from',
selectedDateTimeFor === 'from' && 'active',
)}
onClick={(): void => {
handleSelectDateTimeFor('from');
}}
>
<div className="date-time-custom-option-from-title">FROM</div>
<div className="date-time-custom-option-from-value">
{selectedFromDateTime?.format('YYYY-MM-DD HH:mm:ss')}
</div>
</div>
<div
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
handleSelectDateTimeFor('to');
}
}}
className={cx(
'date-time-custom-option-to',
selectedDateTimeFor === 'to' && 'active',
)}
onClick={(): void => {
handleSelectDateTimeFor('to');
}}
>
<div className="date-time-custom-option-to-title">TO</div>
<div className="date-time-custom-option-to-value">
{selectedToDateTime?.format('YYYY-MM-DD HH:mm:ss')}
</div>
</div>
</div>
</div>
<div className="custom-date-time-picker-v2">
<Calendar
mode="single"
required
selected={
selectedDateTimeFor === 'from'
? selectedFromDateTime?.toDate()
: selectedToDateTime?.toDate()
}
key={selectedDateTimeFor + selectedDateTimeFor}
onSelect={handleDateChange}
defaultMonth={getDefaultMonth()}
disabled={(current): boolean => {
if (selectedDateTimeFor === 'to') {
// disable dates after today and before selectedFromDateTime
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs()) || false;
}
if (selectedDateTimeFor === 'from') {
// disable dates after selectedToDateTime
return dayjs(current).isAfter(dayjs()) || false;
}
return false;
}}
className="rounded-md border"
navLayout="after"
/>
<div className="custom-time-selector">
<label className="text-xs font-normal block" htmlFor="time-picker">
Timestamp
</label>
<MoveRight size={16} />
<div className="time-input-container">
<Input
type="time"
ref={timeInputRef}
className="time-input"
value={
selectedDateTimeFor === 'from'
? selectedFromDateTime?.format('HH:mm:ss')
: selectedToDateTime?.format('HH:mm:ss')
}
onChange={(e): void => handleTimeChange(e.target.value)}
step="1"
/>
</div>
</div>
<div className="custom-date-time-picker-footer">
{selectedDateTimeFor === 'to' && (
<Button
className="periscope-btn secondary clear-btn"
type="default"
onClick={handleBack}
>
Back
</Button>
)}
<Tooltip
title={
!isValidRange() ? 'Invalid range: TO date should be after FROM date' : ''
}
overlayClassName="invalid-date-range-tooltip"
>
<Button
className="periscope-btn primary next-btn"
type="primary"
onClick={handleNext}
disabled={!isValidRange()}
>
{selectedDateTimeFor === 'from' ? 'Next' : 'Apply'}
</Button>
</Tooltip>
</div>
</div>
</div>
);
}
export default DatePickerV2;

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

@@ -55,31 +55,37 @@ export const selectedColumns: BaseAutocompleteData[] = [
key: 'timestamp',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'serviceName',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'name',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'durationNano',
dataType: DataTypes.Float64,
type: 'tag',
isColumn: true,
},
{
key: 'httpMethod',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'responseStatusCode',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
];
@@ -102,7 +108,9 @@ export const getHostTracesQueryPayload = (
id: '------false',
dataType: DataTypes.EMPTY,
key: '',
isColumn: false,
type: '',
isJSON: false,
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
@@ -125,7 +133,6 @@ export const getHostTracesQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
queryType: EQueryType.QUERY_BUILDER,
@@ -147,6 +154,8 @@ export const getHostTracesQueryPayload = (
key: 'serviceName',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
isIndexed: false,
},
@@ -154,6 +163,8 @@ export const getHostTracesQueryPayload = (
key: 'name',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
isIndexed: false,
},
@@ -161,6 +172,8 @@ export const getHostTracesQueryPayload = (
key: 'durationNano',
dataType: 'float64',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
isIndexed: false,
},
@@ -168,6 +181,8 @@ export const getHostTracesQueryPayload = (
key: 'httpMethod',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
isIndexed: false,
},
@@ -175,6 +190,8 @@ export const getHostTracesQueryPayload = (
key: 'responseStatusCode',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
isIndexed: false,
},

View File

@@ -119,6 +119,8 @@ function HostMetricsDetails({
key: 'host.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'host.name--string--resource--false',
},
op: '=',

View File

@@ -26,7 +26,9 @@ export const getHostLogsQueryPayload = (
id: '------false',
dataType: DataTypes.String,
key: '',
isColumn: false,
type: '',
isJSON: false,
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
@@ -51,7 +53,6 @@ export const getHostLogsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
id: uuidv4(),
queryType: EQueryType.QUERY_BUILDER,

View File

@@ -78,7 +78,7 @@ function Metrics({
signal,
}: QueryFunctionContext): Promise<
SuccessResponse<MetricRangePayloadProps>
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, undefined, signal),
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, signal),
enabled: !!payload && visibilities[index],
keepPreviousData: true,
})),

View File

@@ -1,50 +0,0 @@
import { Badge } from '@signozhq/badge';
type BadgeColor =
| 'vanilla'
| 'robin'
| 'forest'
| 'amber'
| 'sienna'
| 'cherry'
| 'sakura'
| 'aqua';
interface HttpStatusBadgeProps {
statusCode: string | number;
}
function getStatusCodeColor(statusCode: number): BadgeColor {
if (statusCode >= 200 && statusCode < 300) {
return 'forest'; // Success - green
}
if (statusCode >= 300 && statusCode < 400) {
return 'robin'; // Redirect - blue
}
if (statusCode >= 400 && statusCode < 500) {
return 'amber'; // Client error - amber
}
if (statusCode >= 500) {
return 'cherry'; // Server error - red
}
if (statusCode >= 100 && statusCode < 200) {
return 'vanilla'; // Informational - neutral
}
return 'robin'; // Default fallback
}
function HttpStatusBadge({
statusCode,
}: HttpStatusBadgeProps): JSX.Element | null {
const numericStatusCode = Number(statusCode);
if (!numericStatusCode || numericStatusCode <= 0) {
return null;
}
const color = getStatusCodeColor(numericStatusCode);
return <Badge color={color}>{statusCode}</Badge>;
}
export default HttpStatusBadge;

View File

@@ -17,7 +17,7 @@ function InputWithLabel({
closeIcon,
}: {
label: string;
initialValue?: string | number | null;
initialValue?: string | number;
placeholder: string;
type?: string;
onClose?: () => void;

View File

@@ -1,152 +0,0 @@
.kbar-command-palette__positioner {
position: fixed;
inset: 0;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 1rem;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(6px);
z-index: 50;
}
.kbar-command-palette__animator {
width: 100%;
max-width: 600px;
}
.kbar-command-palette__card {
background: var(--bg-ink-500);
color: var(--text-vanilla-100);
border-radius: 3px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
overflow: hidden;
display: flex;
flex-direction: column;
}
.kbar-command-palette__search {
padding: 12px 16px;
font-size: 13px;
border: none;
border-bottom: 1px solid var(--border-ink-200);
color: var(--text-vanilla-100);
outline: none;
background-color: var(--bg-ink-500);
}
.kbar-command-palette__section {
padding: 8px 16px 4px;
font-size: 12px;
font-weight: 600;
color: var(--text-robin-500);
font-family: 'Inter', sans-serif;
text-transform: uppercase;
letter-spacing: 0.02em;
}
.kbar-command-palette__item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
font-size: 13px;
cursor: pointer;
transition: background 0.15s ease;
}
.kbar-command-palette__item:hover,
.kbar-command-palette__item--active {
background: var(--bg-ink-400);
}
.kbar-command-palette__icon {
flex-shrink: 0;
width: 18px;
height: 18px;
color: #444;
}
.kbar-command-palette__shortcut {
margin-left: auto;
display: flex;
gap: 4px;
}
.kbar-command-palette__key {
padding: 2px 6px;
font-size: 12px;
border-radius: 4px;
background: var(--bg-ink-300);
color: var(--text-vanilla-300);
text-transform: uppercase;
font-family: 'Space Mono', monospace;
}
.kbar-command-palette__results-container {
div {
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}
}
.lightMode {
.kbar-command-palette__positioner {
background: rgba(0, 0, 0, 0.5);
}
.kbar-command-palette__card {
background: #fff;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
}
.kbar-command-palette__search {
border-bottom: 1px solid #e5e5e5;
color: var(--text-ink-500);
background-color: var(--bg-vanilla-100);
}
.kbar-command-palette__item {
color: var(--text-ink-500);
}
.kbar-command-palette__item:hover,
.kbar-command-palette__item--active {
background: #f5f5f5;
}
.kbar-command-palette__icon {
color: #444;
}
.kbar-command-palette__key {
background: #eee;
color: #555;
}
.kbar-command-palette__results-container {
div {
&::-webkit-scrollbar-thumb {
background: var(--bg-vanilla-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-vanilla-300);
}
}
}
}

View File

@@ -1,69 +0,0 @@
import './KBarCommandPalette.scss';
import {
KBarAnimator,
KBarPortal,
KBarPositioner,
KBarResults,
KBarSearch,
useMatches,
} from 'kbar';
function Results(): JSX.Element {
const { results } = useMatches();
const renderResults = ({
item,
active,
}: {
item: any;
active: boolean;
}): JSX.Element =>
typeof item === 'string' ? (
<div className="kbar-command-palette__section">{item}</div>
) : (
<div
className={`kbar-command-palette__item ${
active ? 'kbar-command-palette__item--active' : ''
}`}
>
{item.icon}
<span>{item.name}</span>
{item.shortcut?.length ? (
<span className="kbar-command-palette__shortcut">
{item.shortcut.map((sc: string) => (
<kbd key={sc} className="kbar-command-palette__key">
{sc}
</kbd>
))}
</span>
) : null}
</div>
);
return (
<div className="kbar-command-palette__results-container">
<KBarResults items={results} onRender={renderResults} />
</div>
);
}
function KBarCommandPalette(): JSX.Element {
return (
<KBarPortal>
<KBarPositioner className="kbar-command-palette__positioner">
<KBarAnimator className="kbar-command-palette__animator">
<div className="kbar-command-palette__card">
<KBarSearch
className="kbar-command-palette__search"
placeholder="Search or type a command..."
/>
<Results />
</div>
</KBarAnimator>
</KBarPositioner>
</KBarPortal>
);
}
export default KBarCommandPalette;

View File

@@ -10,7 +10,11 @@ import { VIEWS } from './constants';
export type LogDetailProps = {
log: ILog | null;
selectedTab: VIEWS;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
onGroupByAttribute?: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &

View File

@@ -23,7 +23,6 @@ import {
} from 'container/LogDetailedView/utils';
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
@@ -40,7 +39,7 @@ import {
TextSelect,
X,
} from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useCopyToClipboard, useLocation } from 'react-use';
import { AppState } from 'store/reducers';
@@ -95,8 +94,6 @@ function LogDetailInner({
const { notifications } = useNotifications();
const { onLogCopy } = useCopyLogLink(log?.id);
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
const handleModeChange = (e: RadioChangeEvent): void => {
@@ -149,34 +146,6 @@ function LogDetailInner({
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
};
const handleQueryExpressionChange = useCallback(
(value: string, queryIndex: number) => {
// update the query at the given index
setContextQuery((prev) => {
if (!prev) return prev;
return {
...prev,
builder: {
...prev.builder,
queryData: prev.builder.queryData.map((query, idx) =>
idx === queryIndex
? {
...query,
filter: {
...query.filter,
expression: value,
},
}
: query,
),
},
};
});
},
[],
);
const handleRunQuery = (expression: string): void => {
let updatedContextQuery = cloneDeep(contextQuery);
@@ -336,19 +305,11 @@ function LogDetailInner({
onClick={handleFilterVisible}
/>
)}
<Tooltip title="Copy Log Link" placement="left" aria-label="Copy Log Link">
<Button
className="action-btn"
icon={<Copy size={16} />}
onClick={onLogCopy}
/>
</Tooltip>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
<QuerySearch
onChange={(value): void => handleQueryExpressionChange(value, 0)}
onChange={(): void => {}}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}

View File

@@ -17,7 +17,7 @@ function AddToQueryHOC({
}: AddToQueryHOCProps): JSX.Element {
const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => {
event.stopPropagation();
onAddToQuery(fieldKey, fieldValue, OPERATORS['='], dataType);
onAddToQuery(fieldKey, fieldValue, OPERATORS['='], undefined, dataType);
};
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
@@ -41,6 +41,7 @@ export interface AddToQueryHOCProps {
fieldKey: string,
fieldValue: string,
operator: string,
isJSON?: boolean,
dataType?: DataTypes,
) => void;
fontSize: FontSize;

View File

@@ -56,8 +56,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
.map(({ name }) => ({
title: name,
dataIndex: name,
accessorKey: name,
id: name.toLowerCase().replace(/\./g, '_'),
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
@@ -85,10 +83,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
// We do not need any title and data index for the log state indicator
title: '',
dataIndex: '',
// eslint-disable-next-line sonarjs/no-duplicate-string
key: 'state-indicator',
accessorKey: 'state-indicator',
id: 'state-indicator',
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
children: (
<div className={cx('state-indicator', fontSize)}>
@@ -106,8 +101,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
title: 'timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
accessorKey: 'timestamp',
id: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (
field: string | number,
@@ -142,8 +135,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
title: 'body',
dataIndex: 'body',
key: 'body',
accessorKey: 'body',
id: 'body',
render: (
field: string | number,
): ColumnTypeRender<Record<string, unknown>> => ({

View File

@@ -1,5 +1,3 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable no-nested-ternary */
@@ -14,11 +12,9 @@ import {
import { Color } from '@signozhq/design-tokens';
import { Button, Checkbox, Select, Typography } from 'antd';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip/TextToolTip';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { capitalize, isEmpty } from 'lodash-es';
import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, Info } from 'lucide-react';
import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp } from 'lucide-react';
import type { BaseSelectRef } from 'rc-select';
import React, {
useCallback,
@@ -27,13 +23,11 @@ import React, {
useRef,
useState,
} from 'react';
import { Virtuoso } from 'react-virtuoso';
import { popupContainer } from 'utils/selectPopupContainer';
import { CustomMultiSelectProps, CustomTagProps, OptionData } from './types';
import {
filterOptionsBySearch,
handleScrollToBottom,
prioritizeOrAddOptionForMultiSelect,
SPACEKEY,
} from './utils';
@@ -43,7 +37,7 @@ enum ToggleTagValue {
All = 'All',
}
const ALL_SELECTED_VALUE = '__ALL__'; // Constant for the special value
const ALL_SELECTED_VALUE = '__all__'; // Constant for the special value
const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
placeholder = 'Search...',
@@ -68,12 +62,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
allowClear = false,
onRetry,
maxTagTextLength,
onDropdownVisibleChange,
showIncompleteDataMessage = false,
showLabels = false,
enableRegexOption = false,
isDynamicVariable = false,
showRetryButton = true,
...rest
}) => {
// ===== State & Refs =====
@@ -90,10 +78,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({});
const [visibleOptions, setVisibleOptions] = useState<OptionData[]>([]);
const isClickInsideDropdownRef = useRef(false);
const justOpenedRef = useRef<boolean>(false);
const [isScrolledToBottom, setIsScrolledToBottom] = useState(false);
const isDarkMode = useIsDarkMode();
// Convert single string value to array for consistency
const selectedValues = useMemo(
@@ -140,12 +124,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
return allAvailableValues.every((val) => selectedValues.includes(val));
}, [selectedValues, allAvailableValues, enableAllSelection]);
// Define allOptionShown earlier in the code
const allOptionShown = useMemo(
() => value === ALL_SELECTED_VALUE || value === 'ALL',
[value],
);
// Value passed to the underlying Ant Select component
const displayValue = useMemo(
() => (isAllSelected ? [ALL_SELECTED_VALUE] : selectedValues),
@@ -154,18 +132,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// ===== Internal onChange Handler =====
const handleInternalChange = useCallback(
(newValue: string | string[], directCaller?: boolean): void => {
(newValue: string | string[]): void => {
// Ensure newValue is an array
const currentNewValue = Array.isArray(newValue) ? newValue : [];
if (
(allOptionShown || isAllSelected) &&
!directCaller &&
currentNewValue.length === 0
) {
return;
}
if (!onChange) return;
// Case 1: Cleared (empty array or undefined)
@@ -174,7 +144,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
return;
}
// Case 2: "__ALL__" is selected (means select all actual values)
// Case 2: "__all__" is selected (means select all actual values)
if (currentNewValue.includes(ALL_SELECTED_VALUE)) {
const allActualOptions = allAvailableValues.map(
(v) => options.flat().find((o) => o.value === v) || { label: v, value: v },
@@ -205,14 +175,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}
}
},
[
allOptionShown,
isAllSelected,
onChange,
allAvailableValues,
options,
enableAllSelection,
],
[onChange, allAvailableValues, options, enableAllSelection],
);
// ===== Existing Callbacks (potentially needing adjustment later) =====
@@ -309,8 +272,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
: filteredOptions,
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filteredOptions, searchText, options]);
}, [filteredOptions, searchText, options, selectedValues]);
// ===== Text Selection Utilities =====
@@ -548,46 +510,13 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}
// Normal single value handling
const trimmedValue = value.trim();
setSearchText(trimmedValue);
setSearchText(value.trim());
if (!isOpen) {
setIsOpen(true);
justOpenedRef.current = true;
}
// Reset active index when search changes if dropdown is open
if (isOpen && trimmedValue) {
setActiveIndex(-1);
// see if the trimmed value matched any option and set that active index
const matchedOption = filteredOptions.find(
(option) =>
option.label.toLowerCase() === trimmedValue.toLowerCase() ||
option.value?.toLowerCase() === trimmedValue.toLowerCase(),
);
if (matchedOption) {
setActiveIndex(1);
} else {
// check if the trimmed value is a regex pattern and set that active index
const isRegex =
trimmedValue.startsWith('.*') && trimmedValue.endsWith('.*');
if (isRegex && enableRegexOption) {
setActiveIndex(0);
} else {
setActiveIndex(enableRegexOption ? 1 : 0);
}
}
}
if (onSearch) onSearch(trimmedValue);
if (onSearch) onSearch(value.trim());
},
[
onSearch,
isOpen,
selectedValues,
onChange,
filteredOptions,
enableRegexOption,
],
[onSearch, isOpen, selectedValues, onChange],
);
// ===== UI & Rendering Functions =====
@@ -599,34 +528,28 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
(text: string, searchQuery: string): React.ReactNode => {
if (!searchQuery || !highlightSearch) return text;
try {
const parts = text.split(
new RegExp(
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
'gi',
),
);
return (
<>
{parts.map((part, i) => {
// Create a unique key that doesn't rely on array index
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
const parts = text.split(
new RegExp(
`(${searchQuery.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')})`,
'gi',
),
);
return (
<>
{parts.map((part, i) => {
// Create a unique key that doesn't rely on array index
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">
{part}
</span>
) : (
part
);
})}
</>
);
} catch (error) {
// If regex fails, return the original text without highlighting
console.error('Error in text highlighting:', error);
return text;
}
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">
{part}
</span>
) : (
part
);
})}
</>
);
},
[highlightSearch],
);
@@ -637,10 +560,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
if (isAllSelected) {
// If all are selected, deselect all
handleInternalChange([], true);
handleInternalChange([]);
} else {
// Otherwise, select all
handleInternalChange([ALL_SELECTED_VALUE], true);
handleInternalChange([ALL_SELECTED_VALUE]);
}
}, [options, isAllSelected, handleInternalChange]);
@@ -815,26 +738,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// Enhanced keyboard navigation with support for maxTagCount
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLElement>): void => {
// Simple early return if ALL is selected - block all possible keyboard interactions
// that could remove the ALL tag, but still allow dropdown navigation and search
if (
(allOptionShown || isAllSelected) &&
(e.key === 'Backspace' || e.key === 'Delete')
) {
// Only prevent default if the input is empty or cursor is at start position
const activeElement = document.activeElement as HTMLInputElement;
const isInputActive = activeElement?.tagName === 'INPUT';
const isInputEmpty = isInputActive && !activeElement?.value;
const isCursorAtStart =
isInputActive && activeElement?.selectionStart === 0;
if (isInputEmpty || isCursorAtStart) {
e.preventDefault();
e.stopPropagation();
return;
}
}
// Get flattened list of all selectable options
const getFlatOptions = (): OptionData[] => {
if (!visibleOptions) return [];
@@ -849,13 +752,13 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
if (hasAll) {
flatList.push({
label: 'ALL',
value: ALL_SELECTED_VALUE, // Special value for the ALL option
value: '__all__', // Special value for the ALL option
type: 'defined',
});
}
// Add Regex to flat list
if (!isEmpty(searchText) && enableRegexOption) {
if (!isEmpty(searchText)) {
// Only add regex wrapper if it doesn't already look like a regex pattern
const isAlreadyRegex =
searchText.startsWith('.*') && searchText.endsWith('.*');
@@ -881,17 +784,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const flatOptions = getFlatOptions();
// If we just opened the dropdown and have options, set first option as active
if (justOpenedRef.current && flatOptions.length > 0) {
setActiveIndex(0);
justOpenedRef.current = false;
}
// If no option is active but we have options and dropdown is open, activate the first one
if (isOpen && activeIndex === -1 && flatOptions.length > 0) {
setActiveIndex(0);
}
// Get the active input element to check cursor position
const activeElement = document.activeElement as HTMLInputElement;
const isInputActive = activeElement?.tagName === 'INPUT';
@@ -1237,7 +1129,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// If there's an active option in the dropdown, prioritize selecting it
if (activeIndex >= 0 && activeIndex < flatOptions.length) {
const selectedOption = flatOptions[activeIndex];
if (selectedOption.value === ALL_SELECTED_VALUE) {
if (selectedOption.value === '__all__') {
handleSelectAll();
} else if (selectedOption.value && onChange) {
const newValues = selectedValues.includes(selectedOption.value)
@@ -1267,10 +1159,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
e.preventDefault();
setIsOpen(false);
setActiveIndex(-1);
// Call onDropdownVisibleChange when Escape is pressed to close dropdown
if (onDropdownVisibleChange) {
onDropdownVisibleChange(false);
}
break;
case SPACEKEY:
@@ -1280,7 +1168,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const selectedOption = flatOptions[activeIndex];
// Check if it's the ALL option
if (selectedOption.value === ALL_SELECTED_VALUE) {
if (selectedOption.value === '__all__') {
handleSelectAll();
} else if (selectedOption.value && onChange) {
const newValues = selectedValues.includes(selectedOption.value)
@@ -1326,7 +1214,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
e.stopPropagation();
e.preventDefault();
setIsOpen(true);
justOpenedRef.current = true; // Set flag to initialize active option on next render
setActiveIndex(0);
setActiveChipIndex(-1);
break;
@@ -1372,14 +1260,9 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}
},
[
allOptionShown,
isAllSelected,
isOpen,
activeIndex,
getVisibleChipIndices,
getLastVisibleChipIndex,
selectedChips,
isSelectionMode,
isOpen,
activeChipIndex,
selectedValues,
visibleOptions,
@@ -1395,9 +1278,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
startSelection,
selectionEnd,
extendSelection,
onDropdownVisibleChange,
activeIndex,
handleSelectAll,
enableRegexOption,
getVisibleChipIndices,
getLastVisibleChipIndex,
],
);
@@ -1422,14 +1306,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
setIsOpen(false);
}, []);
// Add a scroll handler for the dropdown
const handleDropdownScroll = useCallback(
(e: React.UIEvent<HTMLDivElement>): void => {
setIsScrolledToBottom(handleScrollToBottom(e));
},
[],
);
// Custom dropdown render with sections support
const customDropdownRender = useCallback((): React.ReactElement => {
// Process options based on current search
@@ -1448,7 +1324,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const customOptions: OptionData[] = [];
// add regex options first since they appear first in the UI
if (!isEmpty(searchText) && enableRegexOption) {
if (!isEmpty(searchText)) {
// Only add regex wrapper if it doesn't already look like a regex pattern
const isAlreadyRegex =
searchText.startsWith('.*') && searchText.endsWith('.*');
@@ -1471,17 +1347,8 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
});
}
// Now add all custom options at the beginning, removing duplicates based on value
const allOptions = [...customOptions, ...nonSectionOptions];
const seenValues = new Set<string>();
const enhancedNonSectionOptions = allOptions.filter((option) => {
const value = option.value || '';
if (seenValues.has(value)) {
return false;
}
seenValues.add(value);
return true;
});
// Now add all custom options at the beginning
const enhancedNonSectionOptions = [...customOptions, ...nonSectionOptions];
const allOptionValues = getAllAvailableValues(processedOptions);
const allOptionsSelected =
@@ -1515,7 +1382,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
onMouseDown={handleDropdownMouseDown}
onClick={handleDropdownClick}
onKeyDown={handleKeyDown}
onScroll={handleDropdownScroll}
onBlur={handleBlur}
role="listbox"
aria-multiselectable="true"
@@ -1557,39 +1423,14 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}
}}
>
<div style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Checkbox checked={allOptionsSelected} className="option-checkbox">
<div className="option-content">
<div className="all-option-text">ALL</div>
</div>
</Checkbox>
<div
onClick={(e): void => {
e.stopPropagation();
}}
onMouseDown={(e): void => {
e.stopPropagation();
}}
>
{isDynamicVariable && (
<TextToolTip
text="ALL in dynamic variable = No filter applied (unlike other variable types where ALL sends all selected values). Learn more"
url="https://signoz.io/docs/userguide/manage-variables/#note-about-all"
urlText="here"
useFilledIcon={false}
outlinedIcon={
<Info
size={14}
style={{
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
marginLeft: 5,
}}
/>
}
/>
)}
<Checkbox
checked={allOptionsSelected}
style={{ width: '100%', height: '100%' }}
>
<div className="option-content">
<div>ALL</div>
</div>
</div>
</Checkbox>
</div>
<div className="divider" />
</>
@@ -1598,19 +1439,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
{/* Non-section options when not searching */}
{enhancedNonSectionOptions.length > 0 && (
<div className="no-section-options">
<Virtuoso
style={{
minHeight: Math.min(300, enhancedNonSectionOptions.length * 40),
maxHeight: enhancedNonSectionOptions.length * 40,
}}
data={enhancedNonSectionOptions}
itemContent={(index, item): React.ReactNode =>
(mapOptions([item]) as unknown) as React.ReactElement
}
totalCount={enhancedNonSectionOptions.length}
itemSize={(): number => 40}
overscan={5}
/>
{mapOptions(enhancedNonSectionOptions)}
</div>
)}
@@ -1621,65 +1450,31 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
<div className="select-group" key={section.label}>
<div className="group-label" role="heading" aria-level={2}>
{section.label}
{isDynamicVariable && (
<TextToolTip
text="Related values: Filtered by other variable selections. All values: Unfiltered complete list. Learn more"
url="https://signoz.io/docs/userguide/manage-variables/#dynamic-variable-dropdowns-display-values-in-two-sections"
urlText="here"
useFilledIcon={false}
outlinedIcon={
<Info
size={14}
style={{
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
marginTop: 1,
}}
/>
}
/>
)}
</div>
<div role="group" aria-label={`${section.label} options`}>
<Virtuoso
style={{
minHeight: Math.min(300, (section.options?.length || 0) * 40),
maxHeight: (section.options?.length || 0) * 40,
}}
data={section.options || []}
itemContent={(index, item): React.ReactNode =>
(mapOptions([item]) as unknown) as React.ReactElement
}
totalCount={section.options?.length || 0}
itemSize={(): number => 40}
overscan={5}
/>
{section.options && mapOptions(section.options)}
</div>
</div>
) : (
<div key={section.label} />
),
) : null,
)}
{/* Navigation help footer */}
<div className="navigation-footer" role="note">
{!loading &&
!errorMessage &&
!noDataMessage &&
!(showIncompleteDataMessage && isScrolledToBottom) && (
<section className="navigate">
<ArrowDown size={8} className="icons" />
<ArrowUp size={8} className="icons" />
<ArrowLeft size={8} className="icons" />
<ArrowRight size={8} className="icons" />
<span className="keyboard-text">to navigate</span>
</section>
)}
{!loading && !errorMessage && !noDataMessage && (
<section className="navigate">
<ArrowDown size={8} className="icons" />
<ArrowUp size={8} className="icons" />
<ArrowLeft size={8} className="icons" />
<ArrowRight size={8} className="icons" />
<span className="keyboard-text">to navigate</span>
</section>
)}
{loading && (
<div className="navigation-loading">
<div className="navigation-icons">
<LoadingOutlined />
</div>
<div className="navigation-text">Refreshing values...</div>
<div className="navigation-text">We are updating the values...</div>
</div>
)}
{errorMessage && !loading && (
@@ -1687,33 +1482,21 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
<div className="navigation-text">
{errorMessage || SOMETHING_WENT_WRONG}
</div>
{onRetry && showRetryButton && (
<div className="navigation-icons">
<ReloadOutlined
twoToneColor={Color.BG_CHERRY_400}
onClick={(e): void => {
e.stopPropagation();
onRetry();
}}
/>
</div>
)}
<div className="navigation-icons">
<ReloadOutlined
twoToneColor={Color.BG_CHERRY_400}
onClick={(e): void => {
e.stopPropagation();
if (onRetry) onRetry();
}}
/>
</div>
</div>
)}
{showIncompleteDataMessage &&
isScrolledToBottom &&
!loading &&
!errorMessage && (
<div className="navigation-text-incomplete">
Don&apos;t see the value? Use search
</div>
)}
{noDataMessage &&
!loading &&
!(showIncompleteDataMessage && isScrolledToBottom) &&
!errorMessage && <div className="navigation-text">{noDataMessage}</div>}
{noDataMessage && !loading && (
<div className="navigation-text">{noDataMessage}</div>
)}
</div>
</div>
);
@@ -1730,7 +1513,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
handleDropdownMouseDown,
handleDropdownClick,
handleKeyDown,
handleDropdownScroll,
handleBlur,
activeIndex,
loading,
@@ -1740,35 +1522,8 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
renderOptionWithIndex,
handleSelectAll,
onRetry,
showIncompleteDataMessage,
isScrolledToBottom,
enableRegexOption,
isDarkMode,
isDynamicVariable,
showRetryButton,
]);
// Custom handler for dropdown visibility changes
const handleDropdownVisibleChange = useCallback(
(visible: boolean): void => {
setIsOpen(visible);
if (visible) {
justOpenedRef.current = true;
setActiveIndex(0);
setActiveChipIndex(-1);
} else {
setSearchText('');
setActiveIndex(-1);
// Don't clear activeChipIndex when dropdown closes to maintain tag focus
}
// Pass through to the parent component's handler if provided
if (onDropdownVisibleChange) {
onDropdownVisibleChange(visible);
}
},
[onDropdownVisibleChange],
);
// ===== Side Effects =====
// Clear search when dropdown closes
@@ -1830,16 +1585,55 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// Custom Tag Render (needs significant updates)
const tagRender = useCallback(
(props: CustomTagProps): React.ReactElement => {
const { label: labelProp, value, closable, onClose } = props;
const label = showLabels
? options.find((option) => option.value === value)?.label || labelProp
: labelProp;
const { label, value, closable, onClose } = props;
// If the display value is the special ALL value, render the ALL tag
if (allOptionShown) {
// Don't render a visible tag - will be shown as placeholder
return <div style={{ display: 'none' }} />;
if (value === ALL_SELECTED_VALUE && isAllSelected) {
const handleAllTagClose = (
e: React.MouseEvent | React.KeyboardEvent,
): void => {
e.stopPropagation();
e.preventDefault();
handleInternalChange([]); // Clear selection when ALL tag is closed
};
const handleAllTagKeyDown = (e: React.KeyboardEvent): void => {
if (e.key === 'Enter' || e.key === SPACEKEY) {
handleAllTagClose(e);
}
// Prevent Backspace/Delete propagation if needed, handle in main keydown handler
};
return (
<div
className={cx('ant-select-selection-item', {
'ant-select-selection-item-active': activeChipIndex === 0, // Treat ALL tag as index 0 when active
'ant-select-selection-item-selected': selectedChips.includes(0),
})}
style={
activeChipIndex === 0 || selectedChips.includes(0)
? {
borderColor: Color.BG_ROBIN_500,
backgroundColor: Color.BG_SLATE_400,
}
: undefined
}
>
<span className="ant-select-selection-item-content">ALL</span>
{closable && (
<span
className="ant-select-selection-item-remove"
onClick={handleAllTagClose}
onKeyDown={handleAllTagKeyDown}
role="button"
tabIndex={0}
aria-label="Remove ALL tag (deselect all)"
>
×
</span>
)}
</div>
);
}
// If not isAllSelected, render individual tags using previous logic
@@ -1919,69 +1713,52 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// Fallback for safety, should not be reached
return <div />;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[isAllSelected, activeChipIndex, selectedChips, selectedValues, maxTagCount],
[
isAllSelected,
handleInternalChange,
activeChipIndex,
selectedChips,
selectedValues,
maxTagCount,
],
);
// Simple onClear handler to prevent clearing ALL
const onClearHandler = useCallback((): void => {
// Skip clearing if ALL is selected
if (allOptionShown || isAllSelected) {
return;
}
// Normal clear behavior
handleInternalChange([], true);
if (onClear) onClear();
}, [onClear, handleInternalChange, allOptionShown, isAllSelected]);
// ===== Component Rendering =====
return (
<div
className={cx('custom-multiselect-wrapper', {
'all-selected': allOptionShown || isAllSelected,
<Select
ref={selectRef}
className={cx('custom-multiselect', className, {
'has-selection': selectedChips.length > 0 && !isAllSelected,
'is-all-selected': isAllSelected,
})}
>
{(allOptionShown || isAllSelected) && !searchText && (
<div className="all-text">ALL</div>
)}
<Select
ref={selectRef}
className={cx('custom-multiselect', className, {
'has-selection': selectedChips.length > 0 && !isAllSelected,
'is-all-selected': isAllSelected,
})}
placeholder={placeholder}
mode="multiple"
showSearch
filterOption={false}
onSearch={handleSearch}
value={displayValue}
onChange={(newValue): void => {
handleInternalChange(newValue, false);
}}
onClear={onClearHandler}
onDropdownVisibleChange={handleDropdownVisibleChange}
open={isOpen}
defaultActiveFirstOption={defaultActiveFirstOption}
popupMatchSelectWidth={dropdownMatchSelectWidth}
allowClear={allowClear}
getPopupContainer={getPopupContainer ?? popupContainer}
suffixIcon={<DownOutlined style={{ cursor: 'default' }} />}
dropdownRender={customDropdownRender}
menuItemSelectedIcon={null}
popupClassName={cx('custom-multiselect-dropdown-container', popupClassName)}
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
onKeyDown={handleKeyDown}
tagRender={tagRender as any}
placement={placement}
listHeight={300}
searchValue={searchText}
maxTagTextLength={maxTagTextLength}
maxTagCount={isAllSelected ? undefined : maxTagCount}
{...rest}
/>
</div>
placeholder={placeholder}
mode="multiple"
showSearch
filterOption={false}
onSearch={handleSearch}
value={displayValue}
onChange={handleInternalChange}
onClear={(): void => handleInternalChange([])}
onDropdownVisibleChange={setIsOpen}
open={isOpen}
defaultActiveFirstOption={defaultActiveFirstOption}
popupMatchSelectWidth={dropdownMatchSelectWidth}
allowClear={allowClear}
getPopupContainer={getPopupContainer ?? popupContainer}
suffixIcon={<DownOutlined style={{ cursor: 'default' }} />}
dropdownRender={customDropdownRender}
menuItemSelectedIcon={null}
popupClassName={cx('custom-multiselect-dropdown-container', popupClassName)}
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
onKeyDown={handleKeyDown}
tagRender={tagRender as any}
placement={placement}
listHeight={300}
searchValue={searchText}
maxTagTextLength={maxTagTextLength}
maxTagCount={isAllSelected ? 1 : maxTagCount}
{...rest}
/>
);
};

View File

@@ -13,11 +13,9 @@ import {
import { Color } from '@signozhq/design-tokens';
import { Select } from 'antd';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { capitalize, isEmpty } from 'lodash-es';
import { ArrowDown, ArrowUp, Info } from 'lucide-react';
import { ArrowDown, ArrowUp } from 'lucide-react';
import type { BaseSelectRef } from 'rc-select';
import React, {
useCallback,
@@ -31,7 +29,6 @@ import { popupContainer } from 'utils/selectPopupContainer';
import { CustomSelectProps, OptionData } from './types';
import {
filterOptionsBySearch,
handleScrollToBottom,
prioritizeOrAddOptionForSingleSelect,
SPACEKEY,
} from './utils';
@@ -60,33 +57,17 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
errorMessage,
allowClear = false,
onRetry,
showIncompleteDataMessage = false,
showRetryButton = true,
isDynamicVariable = false,
...rest
}) => {
// ===== State & Refs =====
const [isOpen, setIsOpen] = useState(false);
const [searchText, setSearchText] = useState('');
const [activeOptionIndex, setActiveOptionIndex] = useState<number>(-1);
const [isScrolledToBottom, setIsScrolledToBottom] = useState(false);
const isDarkMode = useIsDarkMode();
// Refs for element access and scroll behavior
const selectRef = useRef<BaseSelectRef>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({});
// Flag to track if dropdown just opened
const justOpenedRef = useRef<boolean>(false);
// Add a scroll handler for the dropdown
const handleDropdownScroll = useCallback(
(e: React.UIEvent<HTMLDivElement>): void => {
setIsScrolledToBottom(handleScrollToBottom(e));
},
[],
);
// ===== Option Filtering & Processing Utilities =====
@@ -149,33 +130,23 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
(text: string, searchQuery: string): React.ReactNode => {
if (!searchQuery || !highlightSearch) return text;
try {
const parts = text.split(
new RegExp(
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
'gi',
),
);
return (
<>
{parts.map((part, i) => {
// Create a deterministic but unique key
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
const parts = text.split(new RegExp(`(${searchQuery})`, 'gi'));
return (
<>
{parts.map((part, i) => {
// Create a deterministic but unique key
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">
{part}
</span>
) : (
part
);
})}
</>
);
} catch (error) {
console.error('Error in text highlighting:', error);
return text;
}
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">
{part}
</span>
) : (
part
);
})}
</>
);
},
[highlightSearch],
);
@@ -275,14 +246,9 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
const trimmedValue = value.trim();
setSearchText(trimmedValue);
// Reset active option index when search changes
if (isOpen) {
setActiveOptionIndex(0);
}
if (onSearch) onSearch(trimmedValue);
},
[onSearch, isOpen],
[onSearch],
);
/**
@@ -306,23 +272,14 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
const flatList: OptionData[] = [];
// Process options
let processedOptions = isEmpty(value)
? filteredOptions
: prioritizeOrAddOptionForSingleSelect(filteredOptions, value);
if (!isEmpty(searchText)) {
processedOptions = filterOptionsBySearch(processedOptions, searchText);
}
const { sectionOptions, nonSectionOptions } = splitOptions(
processedOptions,
isEmpty(value)
? filteredOptions
: prioritizeOrAddOptionForSingleSelect(filteredOptions, value),
);
// Add custom option if needed
if (
!isEmpty(searchText) &&
!isLabelPresent(processedOptions, searchText)
) {
if (!isEmpty(searchText) && !isLabelPresent(filteredOptions, searchText)) {
flatList.push({
label: searchText,
value: searchText,
@@ -343,52 +300,33 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
const options = getFlatOptions();
// If we just opened the dropdown and have options, set first option as active
if (justOpenedRef.current && options.length > 0) {
setActiveOptionIndex(0);
justOpenedRef.current = false;
}
// If no option is active but we have options, activate the first one
if (activeOptionIndex === -1 && options.length > 0) {
setActiveOptionIndex(0);
}
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (options.length > 0) {
setActiveOptionIndex((prev) =>
prev < options.length - 1 ? prev + 1 : 0,
);
}
setActiveOptionIndex((prev) =>
prev < options.length - 1 ? prev + 1 : 0,
);
break;
case 'ArrowUp':
e.preventDefault();
if (options.length > 0) {
setActiveOptionIndex((prev) =>
prev > 0 ? prev - 1 : options.length - 1,
);
}
setActiveOptionIndex((prev) =>
prev > 0 ? prev - 1 : options.length - 1,
);
break;
case 'Tab':
// Tab navigation with Shift key support
if (e.shiftKey) {
e.preventDefault();
if (options.length > 0) {
setActiveOptionIndex((prev) =>
prev > 0 ? prev - 1 : options.length - 1,
);
}
setActiveOptionIndex((prev) =>
prev > 0 ? prev - 1 : options.length - 1,
);
} else {
e.preventDefault();
if (options.length > 0) {
setActiveOptionIndex((prev) =>
prev < options.length - 1 ? prev + 1 : 0,
);
}
setActiveOptionIndex((prev) =>
prev < options.length - 1 ? prev + 1 : 0,
);
}
break;
@@ -401,7 +339,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
onChange(selectedOption.value, selectedOption);
setIsOpen(false);
setActiveOptionIndex(-1);
setSearchText('');
}
} else if (!isEmpty(searchText)) {
// Add custom value when no option is focused
@@ -414,7 +351,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
onChange(customOption.value, customOption);
setIsOpen(false);
setActiveOptionIndex(-1);
setSearchText('');
}
}
break;
@@ -423,7 +359,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
e.preventDefault();
setIsOpen(false);
setActiveOptionIndex(-1);
setSearchText('');
break;
case ' ': // Space key
@@ -434,7 +369,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
onChange(selectedOption.value, selectedOption);
setIsOpen(false);
setActiveOptionIndex(-1);
setSearchText('');
}
}
break;
@@ -445,7 +379,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
// Open dropdown when Down or Tab is pressed while closed
e.preventDefault();
setIsOpen(true);
justOpenedRef.current = true; // Set flag to initialize active option on next render
setActiveOptionIndex(0);
}
},
[
@@ -510,7 +444,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
className="custom-select-dropdown"
onClick={handleDropdownClick}
onKeyDown={handleKeyDown}
onScroll={handleDropdownScroll}
role="listbox"
tabIndex={-1}
aria-activedescendant={
@@ -521,6 +454,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
<div className="no-section-options">
{nonSectionOptions.length > 0 && mapOptions(nonSectionOptions)}
</div>
{/* Section options */}
{sectionOptions.length > 0 &&
sectionOptions.map((section) =>
@@ -528,23 +462,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
<div className="select-group" key={section.label}>
<div className="group-label" role="heading" aria-level={2}>
{section.label}
{isDynamicVariable && (
<TextToolTip
text="Related values: Filtered by other variable selections. All values: Unfiltered complete list. Learn more"
url="https://signoz.io/docs/userguide/manage-variables/#dynamic-variable-dropdowns-display-values-in-two-sections"
urlText="here"
useFilledIcon={false}
outlinedIcon={
<Info
size={14}
style={{
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
marginTop: 1,
}}
/>
}
/>
)}
</div>
<div role="group" aria-label={`${section.label} options`}>
{section.options && mapOptions(section.options)}
@@ -555,22 +472,19 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
{/* Navigation help footer */}
<div className="navigation-footer" role="note">
{!loading &&
!errorMessage &&
!noDataMessage &&
!(showIncompleteDataMessage && isScrolledToBottom) && (
<section className="navigate">
<ArrowDown size={8} className="icons" />
<ArrowUp size={8} className="icons" />
<span className="keyboard-text">to navigate</span>
</section>
)}
{!loading && !errorMessage && !noDataMessage && (
<section className="navigate">
<ArrowDown size={8} className="icons" />
<ArrowUp size={8} className="icons" />
<span className="keyboard-text">to navigate</span>
</section>
)}
{loading && (
<div className="navigation-loading">
<div className="navigation-icons">
<LoadingOutlined />
</div>
<div className="navigation-text">Refreshing values...</div>
<div className="navigation-text">We are updating the values...</div>
</div>
)}
{errorMessage && !loading && (
@@ -578,33 +492,21 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
<div className="navigation-text">
{errorMessage || SOMETHING_WENT_WRONG}
</div>
{onRetry && showRetryButton && (
<div className="navigation-icons">
<ReloadOutlined
twoToneColor={Color.BG_CHERRY_400}
onClick={(e): void => {
e.stopPropagation();
onRetry();
}}
/>
</div>
)}
<div className="navigation-icons">
<ReloadOutlined
twoToneColor={Color.BG_CHERRY_400}
onClick={(e): void => {
e.stopPropagation();
if (onRetry) onRetry();
}}
/>
</div>
</div>
)}
{showIncompleteDataMessage &&
isScrolledToBottom &&
!loading &&
!errorMessage && (
<div className="navigation-text-incomplete">
Don&apos;t see the value? Use search
</div>
)}
{noDataMessage &&
!loading &&
!(showIncompleteDataMessage && isScrolledToBottom) &&
!errorMessage && <div className="navigation-text">{noDataMessage}</div>}
{noDataMessage && !loading && (
<div className="navigation-text">{noDataMessage}</div>
)}
</div>
</div>
);
@@ -618,7 +520,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
isLabelPresent,
handleDropdownClick,
handleKeyDown,
handleDropdownScroll,
activeOptionIndex,
loading,
errorMessage,
@@ -626,25 +527,8 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
dropdownRender,
renderOptionWithIndex,
onRetry,
showIncompleteDataMessage,
isScrolledToBottom,
showRetryButton,
isDarkMode,
isDynamicVariable,
]);
// Handle dropdown visibility changes
const handleDropdownVisibleChange = useCallback((visible: boolean): void => {
setIsOpen(visible);
if (visible) {
justOpenedRef.current = true;
setActiveOptionIndex(0);
} else {
setSearchText('');
setActiveOptionIndex(-1);
}
}, []);
// ===== Side Effects =====
// Clear search text when dropdown closes
@@ -698,7 +582,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
onSearch={handleSearch}
value={value}
onChange={onChange}
onDropdownVisibleChange={handleDropdownVisibleChange}
onDropdownVisibleChange={setIsOpen}
open={isOpen}
options={optionsWithHighlight}
defaultActiveFirstOption={defaultActiveFirstOption}

View File

@@ -1,127 +0,0 @@
import {
fireEvent,
render,
RenderResult,
screen,
waitFor,
} from '@testing-library/react';
import { VirtuosoMockContext } from 'react-virtuoso';
import CustomMultiSelect from '../CustomMultiSelect';
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Helper function to render with VirtuosoMockContext
const renderWithVirtuoso = (component: React.ReactElement): RenderResult =>
render(
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
{component}
</VirtuosoMockContext.Provider>,
);
// Mock options data
const mockOptions = [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
];
// CSS selector for retry button
const RETRY_BUTTON_SELECTOR = '.navigation-icons .anticon-reload';
describe('CustomMultiSelect - Retry Functionality', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should show retry button when 5xx error occurs and error message is displayed', async () => {
const mockOnRetry = jest.fn();
const errorMessage = 'Internal Server Error (500)';
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
errorMessage={errorMessage}
onRetry={mockOnRetry}
showRetryButton
loading={false}
/>,
);
// Open dropdown to see error state
const selectElement = screen.getByRole('combobox');
fireEvent.mouseDown(selectElement);
// Wait for dropdown to appear with error message
await waitFor(() => {
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
// Check that retry button (ReloadOutlined icon) is present
const retryButton = document.querySelector(RETRY_BUTTON_SELECTOR);
expect(retryButton).toBeInTheDocument();
});
it('should show retry button when 4xx error occurs and error message is displayed (current behavior)', async () => {
const mockOnRetry = jest.fn();
const errorMessage = 'Bad Request (400)';
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
errorMessage={errorMessage}
onRetry={mockOnRetry}
showRetryButton={false}
loading={false}
/>,
);
// Open dropdown
const selectElement = screen.getByRole('combobox');
fireEvent.mouseDown(selectElement);
// Wait for dropdown to appear with error message
await waitFor(() => {
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
const retryButton = document.querySelector(RETRY_BUTTON_SELECTOR);
expect(retryButton).not.toBeInTheDocument();
});
it('should call onRetry function when retry button is clicked', async () => {
const mockOnRetry = jest.fn();
const errorMessage = 'Internal Server Error (500)';
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
errorMessage={errorMessage}
onRetry={mockOnRetry}
showRetryButton
loading={false}
/>,
);
// Open dropdown
const selectElement = screen.getByRole('combobox');
fireEvent.mouseDown(selectElement);
// Wait for dropdown to appear
await waitFor(() => {
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
// Find and click the retry button
const retryButton = document.querySelector(RETRY_BUTTON_SELECTOR);
expect(retryButton).toBeInTheDocument();
fireEvent.click(retryButton as Element);
// Verify onRetry was called
expect(mockOnRetry).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,27 +1,10 @@
import {
fireEvent,
render,
RenderResult,
screen,
waitFor,
} from '@testing-library/react';
import { VirtuosoMockContext } from 'react-virtuoso';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import CustomMultiSelect from '../CustomMultiSelect';
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Helper function to render with VirtuosoMockContext
const renderWithVirtuoso = (component: React.ReactElement): RenderResult =>
render(
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
{component}
</VirtuosoMockContext.Provider>,
);
// Mock options data
const mockOptions = [
{ label: 'Option 1', value: 'option1' },
@@ -49,7 +32,7 @@ const mockGroupedOptions = [
describe('CustomMultiSelect Component', () => {
it('renders with placeholder', () => {
const handleChange = jest.fn();
renderWithVirtuoso(
render(
<CustomMultiSelect
placeholder="Select multiple options"
options={mockOptions}
@@ -64,9 +47,7 @@ describe('CustomMultiSelect Component', () => {
it('opens dropdown when clicked', async () => {
const handleChange = jest.fn();
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={handleChange} />,
);
render(<CustomMultiSelect options={mockOptions} onChange={handleChange} />);
// Click to open the dropdown
const selectElement = screen.getByRole('combobox');
@@ -85,7 +66,7 @@ describe('CustomMultiSelect Component', () => {
const handleChange = jest.fn();
// Start with option1 already selected
renderWithVirtuoso(
render(
<CustomMultiSelect
options={mockOptions}
onChange={handleChange}
@@ -112,7 +93,7 @@ describe('CustomMultiSelect Component', () => {
it('selects ALL options when ALL is clicked', async () => {
const handleChange = jest.fn();
renderWithVirtuoso(
render(
<CustomMultiSelect
options={mockOptions}
onChange={handleChange}
@@ -145,7 +126,7 @@ describe('CustomMultiSelect Component', () => {
});
it('displays selected options as tags', async () => {
renderWithVirtuoso(
render(
<CustomMultiSelect options={mockOptions} value={['option1', 'option2']} />,
);
@@ -156,7 +137,7 @@ describe('CustomMultiSelect Component', () => {
it('removes a tag when clicked', async () => {
const handleChange = jest.fn();
renderWithVirtuoso(
render(
<CustomMultiSelect
options={mockOptions}
value={['option1', 'option2']}
@@ -178,7 +159,7 @@ describe('CustomMultiSelect Component', () => {
});
it('filters options when searching', async () => {
renderWithVirtuoso(<CustomMultiSelect options={mockOptions} />);
render(<CustomMultiSelect options={mockOptions} />);
// Open dropdown
const selectElement = screen.getByRole('combobox');
@@ -212,7 +193,7 @@ describe('CustomMultiSelect Component', () => {
});
it('renders grouped options correctly', async () => {
renderWithVirtuoso(<CustomMultiSelect options={mockGroupedOptions} />);
render(<CustomMultiSelect options={mockGroupedOptions} />);
// Open dropdown
const selectElement = screen.getByRole('combobox');
@@ -230,18 +211,18 @@ describe('CustomMultiSelect Component', () => {
});
it('shows loading state', () => {
renderWithVirtuoso(<CustomMultiSelect options={mockOptions} loading />);
render(<CustomMultiSelect options={mockOptions} loading />);
// Open dropdown
const selectElement = screen.getByRole('combobox');
fireEvent.mouseDown(selectElement);
// Check loading text is displayed
expect(screen.getByText('Refreshing values...')).toBeInTheDocument();
expect(screen.getByText('We are updating the values...')).toBeInTheDocument();
});
it('shows error message', () => {
renderWithVirtuoso(
render(
<CustomMultiSelect
options={mockOptions}
errorMessage="Test error message"
@@ -257,9 +238,7 @@ describe('CustomMultiSelect Component', () => {
});
it('shows no data message', () => {
renderWithVirtuoso(
<CustomMultiSelect options={[]} noDataMessage="No data available" />,
);
render(<CustomMultiSelect options={[]} noDataMessage="No data available" />);
// Open dropdown
const selectElement = screen.getByRole('combobox');
@@ -270,7 +249,7 @@ describe('CustomMultiSelect Component', () => {
});
it('shows "ALL" tag when all options are selected', () => {
renderWithVirtuoso(
render(
<CustomMultiSelect
options={mockOptions}
value={['option1', 'option2', 'option3']}

View File

@@ -140,7 +140,7 @@ describe('CustomSelect Component', () => {
fireEvent.mouseDown(selectElement);
// Check loading text is displayed
expect(screen.getByText('Refreshing values...')).toBeInTheDocument();
expect(screen.getByText('We are updating the values...')).toBeInTheDocument();
});
it('shows error message', () => {

View File

@@ -1,624 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { VirtuosoMockContext } from 'react-virtuoso';
import configureStore from 'redux-mock-store';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import VariableItem from '../../../container/NewDashboard/DashboardVariablesSelection/VariableItem';
// Mock the dashboard variables query
jest.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
__esModule: true,
default: jest.fn(() =>
Promise.resolve({
payload: {
variableValues: ['option1', 'option2', 'option3', 'option4'],
},
}),
),
}));
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Constants
const TEST_VARIABLE_NAME = 'test_variable';
const TEST_VARIABLE_ID = 'test-var-id';
// Create a mock store
const mockStore = configureStore([])({
globalTime: {
minTime: Date.now() - 3600000, // 1 hour ago
maxTime: Date.now(),
},
});
// Test data
const createMockVariable = (
overrides: Partial<IDashboardVariable> = {},
): IDashboardVariable => ({
id: TEST_VARIABLE_ID,
name: TEST_VARIABLE_NAME,
description: 'Test variable description',
type: 'QUERY',
queryValue: 'SELECT DISTINCT value FROM table',
customValue: '',
sort: 'ASC',
multiSelect: false,
showALLOption: true,
selectedValue: [],
allSelected: false,
...overrides,
});
function TestWrapper({ children }: { children: React.ReactNode }): JSX.Element {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return (
<Provider store={mockStore}>
<QueryClientProvider client={queryClient}>
<VirtuosoMockContext.Provider
// eslint-disable-next-line react/jsx-no-constructed-context-values
value={{ viewportHeight: 300, itemHeight: 40 }}
>
{children}
</VirtuosoMockContext.Provider>
</QueryClientProvider>
</Provider>
);
}
describe('VariableItem Integration Tests', () => {
let user: ReturnType<typeof userEvent.setup>;
let mockOnValueUpdate: jest.Mock;
let mockSetVariablesToGetUpdated: jest.Mock;
beforeEach(() => {
user = userEvent.setup();
mockOnValueUpdate = jest.fn();
mockSetVariablesToGetUpdated = jest.fn();
jest.clearAllMocks();
});
// ===== 1. INTEGRATION WITH CUSTOMSELECT =====
describe('CustomSelect Integration (VI)', () => {
test('VI-01: Single select variable integration', async () => {
const variable = createMockVariable({
multiSelect: false,
type: 'CUSTOM',
customValue: 'option1,option2,option3',
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Should render with CustomSelect
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
await user.click(combobox);
await waitFor(() => {
expect(screen.getByText('option1')).toBeInTheDocument();
expect(screen.getByText('option2')).toBeInTheDocument();
expect(screen.getByText('option3')).toBeInTheDocument();
});
// Select an option
const option1 = screen.getByText('option1');
await user.click(option1);
expect(mockOnValueUpdate).toHaveBeenCalledWith(
TEST_VARIABLE_NAME,
TEST_VARIABLE_ID,
'option1',
false,
);
});
});
// ===== 2. INTEGRATION WITH CUSTOMMULTISELECT =====
describe('CustomMultiSelect Integration (VI)', () => {
test('VI-02: Multi select variable integration', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
customValue: 'option1,option2,option3,option4',
showALLOption: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Should render with CustomMultiSelect
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
await user.click(combobox);
await waitFor(() => {
// Should show ALL option
expect(screen.getByText('ALL')).toBeInTheDocument();
});
// Wait for Virtuoso to render the custom options
await waitFor(
() => {
expect(screen.getByText('option1')).toBeInTheDocument();
expect(screen.getByText('option2')).toBeInTheDocument();
expect(screen.getByText('option3')).toBeInTheDocument();
expect(screen.getByText('option4')).toBeInTheDocument();
},
{ timeout: 5000 },
);
});
});
// ===== 3. TEXTBOX VARIABLE TYPE =====
describe('Textbox Variable Integration', () => {
test('VI-03: Textbox variable handling', async () => {
const variable = createMockVariable({
type: 'TEXTBOX',
selectedValue: 'initial-value',
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Should render a regular input
const textInput = screen.getByDisplayValue('initial-value');
expect(textInput).toBeInTheDocument();
expect(textInput.tagName).toBe('INPUT');
// Clear and type new value
await user.clear(textInput);
await user.type(textInput, 'new-text-value');
// Should call onValueUpdate after debounce
await waitFor(
() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
TEST_VARIABLE_NAME,
TEST_VARIABLE_ID,
'new-text-value',
false,
);
},
{ timeout: 1000 },
);
});
});
// ===== 4. VALUE PERSISTENCE AND STATE MANAGEMENT =====
describe('Value Persistence and State Management', () => {
test('VI-04: All selected state handling', () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
customValue: 'service1,service2,service3',
selectedValue: ['service1', 'service2', 'service3'],
allSelected: true,
showALLOption: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Should show "ALL" instead of individual values
expect(screen.getByText('ALL')).toBeInTheDocument();
});
test('VI-05: Dropdown behavior with temporary selections', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
customValue: 'item1,item2,item3',
selectedValue: ['item1'],
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
const combobox = screen.getByRole('combobox');
await user.click(combobox);
// Wait for dropdown to open
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toBeInTheDocument();
});
// Verify the component renders without crashing
expect(combobox).toBeInTheDocument();
});
});
// ===== 6. ACCESSIBILITY AND USER EXPERIENCE =====
describe('Accessibility and User Experience', () => {
test('VI-06: Variable description tooltip', async () => {
const variable = createMockVariable({
description: 'This variable controls the service selection',
type: 'CUSTOM',
customValue: 'service1,service2',
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Should show info icon
const infoIcon = document.querySelector('.info-icon');
expect(infoIcon).toBeInTheDocument();
// Hover to show tooltip
if (infoIcon) {
await user.hover(infoIcon);
}
await waitFor(() => {
expect(
screen.getByText('This variable controls the service selection'),
).toBeInTheDocument();
});
});
test('VI-07: Variable name display', () => {
const variable = createMockVariable({
name: 'service_name',
type: 'CUSTOM',
customValue: 'service1,service2',
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Should show variable name with $ prefix
expect(screen.getByText('$service_name')).toBeInTheDocument();
});
test('VI-08: Max tag count behavior', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
customValue: 'tag1,tag2,tag3,tag4,tag5,tag6,tag7',
selectedValue: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'],
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Wait for component to render
await waitFor(() => {
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
});
// Should show limited number of tags with "+ X more"
const tags = document.querySelectorAll('.ant-select-selection-item');
// The component should render without crashing
expect(tags.length).toBeGreaterThanOrEqual(0);
});
});
// ===== 8. SEARCH INTERACTION TESTS =====
describe('Search Interaction Tests', () => {
test('VI-14: Search persistence across dropdown open/close', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',
multiSelect: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
const combobox = screen.getByRole('combobox');
await user.click(combobox);
await waitFor(() => {
expect(screen.getByText('ALL')).toBeInTheDocument();
});
const searchInput = document.querySelector(
'.ant-select-selection-search-input',
);
expect(searchInput).toBeInTheDocument();
if (searchInput) {
await user.type(searchInput, 'search-text');
}
// Verify search text is in input
await waitFor(() => {
expect(searchInput).toHaveValue('search-text');
});
// Press Escape to close dropdown
await user.keyboard('{Escape}');
// Dropdown should close and search text should be cleared
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expect(searchInput).toHaveValue('');
});
});
});
// ===== 9. ADVANCED KEYBOARD NAVIGATION =====
describe('Advanced Keyboard Navigation (VI)', () => {
test('VI-15: Shift + Arrow + Del chip deletion in multiselect', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',
multiSelect: true,
selectedValue: ['option1', 'option2', 'option3'],
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
const combobox = screen.getByRole('combobox');
await user.click(combobox);
// Navigate to chips using arrow keys
await user.keyboard('{ArrowLeft}');
// Use Shift + Arrow to navigate between chips
await user.keyboard('{Shift>}{ArrowLeft}{/Shift}');
// Use Del to delete the active chip
await user.keyboard('{Delete}');
// Note: The component may not immediately call onValueUpdate
// This test verifies the chip deletion behavior
await waitFor(() => {
// Check if a chip was removed from the selection
const selectionItems = document.querySelectorAll(
'.ant-select-selection-item',
);
expect(selectionItems.length).toBeLessThan(3);
});
});
});
// ===== 11. ADVANCED UI STATES =====
describe('Advanced UI States (VI)', () => {
test('VI-19: No data with previous value selected in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: '',
multiSelect: true,
selectedValue: ['previous-value'],
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Wait for component to initialize
await waitFor(() => {
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
});
const combobox = screen.getByRole('combobox');
await user.click(combobox);
// Should show no data message (the component may not show this exact text)
await waitFor(() => {
// Check if dropdown is empty or shows no data indication
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toBeInTheDocument();
});
// Verify the component renders without crashing
expect(combobox).toBeInTheDocument();
});
test('VI-20: Always editable accessibility in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2',
multiSelect: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
const combobox = screen.getByRole('combobox');
// Should be editable
expect(combobox).not.toBeDisabled();
await user.click(combobox);
expect(combobox).toHaveFocus();
// Should still be interactive
expect(combobox).not.toBeDisabled();
});
});
// ===== 13. DROPDOWN PERSISTENCE =====
describe('Dropdown Persistence (VI)', () => {
test('VI-24: Dropdown stays open for non-save actions in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',
multiSelect: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Wait for component to initialize
await waitFor(() => {
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
});
const combobox = screen.getByRole('combobox');
await user.click(combobox);
await waitFor(() => {
expect(screen.getByText('ALL')).toBeInTheDocument();
});
// Navigate with arrow keys (non-save action)
await user.keyboard('{ArrowDown}');
await user.keyboard('{ArrowDown}');
// Dropdown should still be open
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toBeInTheDocument();
});
// Verify the component renders without crashing
expect(combobox).toBeInTheDocument();
// Only ESC should close the dropdown
await user.keyboard('{Escape}');
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
});
});
});
});

View File

@@ -35,50 +35,12 @@ $custom-border-color: #2c3044;
width: 100%;
position: relative;
&.is-all-selected {
.ant-select-selection-search-input {
caret-color: transparent;
}
.ant-select-selection-placeholder {
opacity: 1 !important;
color: var(--bg-vanilla-400) !important;
font-weight: 500;
visibility: visible !important;
pointer-events: none;
z-index: 2;
.lightMode & {
color: rgba(0, 0, 0, 0.85) !important;
}
}
&.ant-select-focused .ant-select-selection-placeholder {
opacity: 0.45 !important;
}
}
.all-selected-text {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--bg-vanilla-400);
z-index: 1;
pointer-events: none;
.lightMode & {
color: rgba(0, 0, 0, 0.85);
}
}
.ant-select-selector {
max-height: 200px;
overflow: auto;
scrollbar-width: thin;
background-color: var(--bg-ink-400);
border-color: var(--bg-slate-400);
cursor: text;
&::-webkit-scrollbar {
width: 6px;
@@ -94,16 +56,6 @@ $custom-border-color: #2c3044;
}
}
// Ensure adequate space for input area
.ant-select-selection-search {
min-width: 60px !important;
flex: 1 1 auto;
.ant-select-selection-search-input {
min-width: 60px !important;
cursor: text;
}
}
&.ant-select-focused {
.ant-select-selector {
border-color: var(--bg-robin-500);
@@ -206,7 +158,7 @@ $custom-border-color: #2c3044;
// Custom dropdown styles for single select
.custom-select-dropdown {
padding: 8px 0 0 0;
max-height: 300px;
max-height: 500px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
@@ -324,10 +276,6 @@ $custom-border-color: #2c3044;
font-size: 12px;
}
.navigation-text-incomplete {
color: var(--bg-amber-600) !important;
}
.navigation-error {
.navigation-text,
.navigation-icons {
@@ -374,7 +322,7 @@ $custom-border-color: #2c3044;
// Custom dropdown styles for multi-select
.custom-multiselect-dropdown {
padding: 8px 0 0 0;
max-height: 350px;
max-height: 500px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
@@ -407,13 +355,8 @@ $custom-border-color: #2c3044;
.select-group {
margin-bottom: 12px;
overflow: hidden;
margin-top: 4px;
.group-label {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 500;
padding: 4px 12px;
font-size: 13px;
@@ -461,13 +404,6 @@ $custom-border-color: #2c3044;
width: 100%;
}
.all-option-text {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.option-content {
display: flex;
justify-content: space-between;
@@ -701,7 +637,6 @@ $custom-border-color: #2c3044;
.ant-select-selector {
background-color: var(--bg-vanilla-100);
border-color: #e9e9e9;
cursor: text; // Make entire selector clickable for input focus
&::-webkit-scrollbar-thumb {
background-color: #ccc;
@@ -712,20 +647,6 @@ $custom-border-color: #2c3044;
}
}
.ant-select-selection-search {
min-width: 60px !important;
flex: 1 1 auto;
.ant-select-selection-search-input {
min-width: 60px !important;
cursor: text;
}
}
.ant-select-selector {
cursor: text;
}
.ant-select-selection-placeholder {
color: rgba(0, 0, 0, 0.45);
}
@@ -735,10 +656,6 @@ $custom-border-color: #2c3044;
border: 1px solid #e8e8e8;
color: rgba(0, 0, 0, 0.85);
font-size: 12px !important;
height: 20px;
line-height: 18px;
.ant-select-selection-item-content {
color: rgba(0, 0, 0, 0.85);
}
@@ -801,10 +718,6 @@ $custom-border-color: #2c3044;
.select-group {
.group-label {
display: flex;
align-items: center;
justify-content: space-between;
color: rgba(0, 0, 0, 0.85);
background-color: #fafafa;
border-bottom: 1px solid #f0f0f0;
@@ -923,38 +836,3 @@ $custom-border-color: #2c3044;
}
}
}
.custom-multiselect-wrapper {
position: relative;
width: 100%;
&.all-selected {
.all-text {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--bg-vanilla-400);
font-weight: 500;
z-index: 2;
pointer-events: none;
transition: opacity 0.2s ease, visibility 0.2s ease;
.lightMode & {
color: rgba(0, 0, 0, 0.85);
}
}
&:focus-within .all-text {
opacity: 0.45;
}
.ant-select-selection-search-input {
caret-color: auto;
}
.ant-select-selection-placeholder {
display: none;
}
}
}

View File

@@ -24,12 +24,9 @@ export interface CustomSelectProps extends Omit<SelectProps, 'options'> {
highlightSearch?: boolean;
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
popupMatchSelectWidth?: boolean;
errorMessage?: string | null;
errorMessage?: string;
allowClear?: SelectProps['allowClear'];
onRetry?: () => void;
showIncompleteDataMessage?: boolean;
showRetryButton?: boolean;
isDynamicVariable?: boolean;
}
export interface CustomTagProps {
@@ -54,16 +51,10 @@ export interface CustomMultiSelectProps
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
dropdownRender?: (menu: React.ReactElement) => React.ReactElement;
highlightSearch?: boolean;
errorMessage?: string | null;
errorMessage?: string;
popupClassName?: string;
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
maxTagCount?: number;
allowClear?: SelectProps['allowClear'];
onRetry?: () => void;
maxTagTextLength?: number;
showIncompleteDataMessage?: boolean;
showLabels?: boolean;
enableRegexOption?: boolean;
isDynamicVariable?: boolean;
showRetryButton?: boolean;
}

View File

@@ -1,6 +1,4 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { uniqueOptions } from 'container/NewDashboard/DashboardVariablesSelection/util';
import { OptionData } from './types';
export const SPACEKEY = ' ';
@@ -100,10 +98,8 @@ export const prioritizeOrAddOptionForMultiSelect = (
label: labels?.[value] ?? value, // Use provided label or default to value
}));
const flatOutSelectedOptions = uniqueOptions([...newOptions, ...foundOptions]);
// Add found & new options to the top
return [...flatOutSelectedOptions, ...filteredOptions];
return [...newOptions, ...foundOptions, ...filteredOptions];
};
/**
@@ -137,15 +133,3 @@ export const filterOptionsBySearch = (
})
.filter(Boolean) as OptionData[];
};
/**
* Utility function to handle dropdown scroll and detect when scrolled to bottom
* Returns true when scrolled to within 20px of the bottom
*/
export const handleScrollToBottom = (
e: React.UIEvent<HTMLDivElement>,
): boolean => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
// Consider "scrolled to bottom" when within 20px of the bottom or at the bottom
return scrollHeight - scrollTop - clientHeight < 20;
};

View File

@@ -102,7 +102,7 @@ function ListViewOrderBy({
onChange={onChange}
onSearch={handleSearch}
notFoundContent={<Loader isLoading={isLoading} />}
placeholder="Select a field"
placeholder="Select an attribute"
style={{ width: 200 }}
options={selectOptions}
filterOption={(input, option): boolean =>

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

@@ -22,10 +22,6 @@
flex: 1;
position: relative;
.qb-trace-view-selector-container {
padding: 12px 8px 8px 8px;
}
}
.qb-content-section {
@@ -183,7 +179,7 @@
flex-direction: column;
gap: 8px;
margin-left: 26px;
margin-left: 32px;
padding-bottom: 16px;
padding-left: 8px;
@@ -199,8 +195,8 @@
}
.formula-container {
padding: 8px;
margin-left: 74px;
margin-left: 82px;
padding: 4px 0px;
.ant-col {
&::before {
@@ -295,13 +291,6 @@
);
}
}
.qb-trace-operator-button-container {
&-text {
display: flex;
align-items: center;
gap: 8px;
}
}
}
}
@@ -342,12 +331,6 @@
);
left: 15px;
}
&.has-trace-operator {
&::before {
height: 0px;
}
}
}
.formula-name {
@@ -364,7 +347,7 @@
&::before {
content: '';
height: 128px;
height: 65px;
content: '';
position: absolute;
left: 0;
@@ -404,7 +387,6 @@
}
.qb-search-filter-container {
flex: 1;
display: flex;
flex-direction: row;
align-items: flex-start;

View File

@@ -5,13 +5,11 @@ import { Formula } from 'container/QueryBuilder/components/Formula';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useEffect, useMemo, useRef } from 'react';
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
import { QueryV2 } from './QueryV2/QueryV2';
import TraceOperator from './QueryV2/TraceOperator/TraceOperator';
export const QueryBuilderV2 = memo(function QueryBuilderV2({
config,
@@ -20,7 +18,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryComponents,
isListViewPanel = false,
showOnlyWhereClause = false,
showTraceOperator = false,
version,
}: QueryBuilderProps): JSX.Element {
const {
@@ -28,7 +25,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
addNewBuilderQuery,
addNewFormula,
handleSetConfig,
addTraceOperator,
panelType,
initialDataSource,
} = useQueryBuilder();
@@ -58,11 +54,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
newPanelType,
]);
const isMultiQueryAllowed = useMemo(
() => !isListViewPanel || showTraceOperator,
[showTraceOperator, isListViewPanel],
);
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: true, isDisabled: true },
@@ -106,60 +97,11 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
listViewTracesFilterConfigs,
]);
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
if (
currentQuery.builder.queryTraceOperator &&
currentQuery.builder.queryTraceOperator.length > 0
) {
return currentQuery.builder.queryTraceOperator[0];
}
return undefined;
}, [currentQuery.builder.queryTraceOperator]);
const hasAtLeastOneTraceQuery = useMemo(
() =>
currentQuery.builder.queryData.some(
(query) => query.dataSource === DataSource.TRACES,
),
[currentQuery.builder.queryData],
);
const hasTraceOperator = useMemo(
() => showTraceOperator && hasAtLeastOneTraceQuery && Boolean(traceOperator),
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
);
const shouldShowFooter = useMemo(
() =>
(!showOnlyWhereClause && !isListViewPanel) ||
(currentDataSource === DataSource.TRACES && showTraceOperator),
[isListViewPanel, showTraceOperator, showOnlyWhereClause, currentDataSource],
);
const showQueryList = useMemo(
() => (!showOnlyWhereClause && !isListViewPanel) || showTraceOperator,
[isListViewPanel, showOnlyWhereClause, showTraceOperator],
);
const showFormula = useMemo(() => {
if (currentDataSource === DataSource.TRACES) {
return !isListViewPanel;
}
return true;
}, [isListViewPanel, currentDataSource]);
const showAddTraceOperator = useMemo(
() => showTraceOperator && !traceOperator && hasAtLeastOneTraceQuery,
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
);
return (
<QueryBuilderV2Provider>
<div className="query-builder-v2">
<div className="qb-content-container">
{!isMultiQueryAllowed ? (
{isListViewPanel && (
<QueryV2
ref={containerRef}
key={currentQuery.builder.queryData[0].queryName}
@@ -167,16 +109,15 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
query={currentQuery.builder.queryData[0]}
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
isMultiQueryAllowed={isMultiQueryAllowed}
showTraceOperator={showTraceOperator}
hasTraceOperator={hasTraceOperator}
version={version}
isAvailableToDisable={false}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
/>
) : (
)}
{!isListViewPanel &&
currentQuery.builder.queryData.map((query, index) => (
<QueryV2
ref={containerRef}
@@ -186,17 +127,12 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
version={version}
isMultiQueryAllowed={isMultiQueryAllowed}
isAvailableToDisable={false}
showTraceOperator={showTraceOperator}
hasTraceOperator={hasTraceOperator}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
signalSource={config?.signalSource || ''}
/>
))
)}
))}
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
<div className="qb-formulas-container">
@@ -221,25 +157,15 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
</div>
)}
{shouldShowFooter && (
{!showOnlyWhereClause && !isListViewPanel && (
<QueryFooter
showAddFormula={showFormula}
addNewBuilderQuery={addNewBuilderQuery}
addNewFormula={addNewFormula}
addTraceOperator={addTraceOperator}
showAddTraceOperator={showAddTraceOperator}
/>
)}
{hasTraceOperator && (
<TraceOperator
isListViewPanel={isListViewPanel}
traceOperator={traceOperator as IBuilderTraceOperator}
/>
)}
</div>
{showQueryList && (
{!showOnlyWhereClause && !isListViewPanel && (
<div className="query-names-section">
{currentQuery.builder.queryData.map((query) => (
<div key={query.queryName} className="query-name">

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
@@ -160,7 +128,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
label="Seconds"
placeholder="Auto"
labelAfter
initialValue={query?.stepInterval ?? null}
initialValue={query?.stepInterval ?? undefined}
/>
</div>
</div>
@@ -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
@@ -283,7 +218,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
label="Seconds"
placeholder="Auto"
labelAfter
initialValue={query?.stepInterval ?? null}
initialValue={query?.stepInterval ?? undefined}
className="histogram-every-input"
/>
</div>

View File

@@ -44,14 +44,13 @@
.lightMode {
.metrics-select-container {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300) !important;
border: 1px solid var(--bg-slate-300) !important;
background: var(--bg-vanilla-100);
color: var(--text-ink-100);
}
.ant-select-dropdown {
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300) !important;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
backdrop-filter: none;

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

@@ -1,11 +1,7 @@
.query-add-ons {
width: 100%;
}
.add-ons-list {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
.add-ons-tabs {
display: flex;
@@ -115,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%,
@@ -129,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;
@@ -167,6 +165,7 @@
overflow: hidden;
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
.cm-completionIcon {
display: none !important;
@@ -331,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,
@@ -144,7 +72,6 @@ function QueryAddOns({
showReduceTo,
panelType,
index,
isForTraceOperator = false,
}: {
query: IBuilderQuery;
version: string;
@@ -152,7 +79,6 @@ function QueryAddOns({
showReduceTo: boolean;
panelType: PANEL_TYPES | null;
index: number;
isForTraceOperator?: boolean;
}): JSX.Element {
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
@@ -162,7 +88,6 @@ function QueryAddOns({
index,
query,
entityVersion: '',
isForTraceOperator,
});
const { handleSetQueryData } = useQueryBuilder();
@@ -287,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={
@@ -323,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 => {
@@ -369,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}
@@ -407,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>
@@ -461,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,13 +1,9 @@
import './QueryAggregation.styles.scss';
import { Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useMemo } from 'react';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import QueryAggregationSelect from './QueryAggregationSelect';
@@ -23,7 +19,7 @@ function QueryAggregationOptions({
panelType?: string;
onAggregationIntervalChange: (value: number) => void;
onChange?: (value: string) => void;
queryData: IBuilderQuery | IBuilderTraceOperator;
queryData: IBuilderQuery;
}): JSX.Element {
const showAggregationInterval = useMemo(() => {
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
@@ -57,34 +53,12 @@ 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={queryData?.stepInterval ? queryData?.stepInterval : null}
initialValue={
queryData?.stepInterval ? queryData?.stepInterval : undefined
}
className="query-aggregation-interval-input"
label="Seconds"
placeholder="Auto"

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