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
738 changed files with 11137 additions and 33891 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

@@ -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,
@@ -31,7 +30,6 @@ import {
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { normalizeFunctionName } from 'utils/functionNameNormalizer';
type PrepareQueryRangePayloadV5Result = {
queryPayload: QueryRangePayloadV5;
@@ -70,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,
@@ -121,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:
@@ -129,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,
@@ -162,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'"
@@ -216,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, '');
@@ -267,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()' }];
@@ -320,7 +258,6 @@ export function convertBuilderQueriesToV5(
spec = {
name: queryName,
signal: 'metrics' as const,
source: queryData.source || '',
...baseSpec,
aggregations: aggregations as MetricAggregation[],
// reduceTo: queryData.reduceTo,
@@ -336,101 +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));
return {
stepInterval: queryData?.stepInterval || undefined,
groupBy:
queryData.groupBy?.length > 0
? queryData.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
? queryData.limit || queryData.pageSize || undefined
: queryData.limit || undefined,
offset:
requestType === 'raw' || requestType === 'trace'
? queryData.offset
: undefined,
order:
queryData.orderBy?.length > 0
? queryData.orderBy.map(
(order: any): OrderBy => ({
key: {
name: order.columnName,
},
direction: order.order,
}),
)
: undefined,
legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
having: isEmpty(queryData.having) ? undefined : (queryData?.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
*/
@@ -512,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
@@ -566,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: {

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

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

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

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

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"

View File

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

View File

@@ -1,26 +1,33 @@
/* eslint-disable react/require-default-props */
import { Button, Tooltip, Typography } from 'antd';
import { DraftingCompass, Plus, Sigma } from 'lucide-react';
import BetaTag from 'periscope/components/BetaTag/BetaTag';
import { Plus, Sigma } from 'lucide-react';
export default function QueryFooter({
addNewBuilderQuery,
addNewFormula,
addTraceOperator,
showAddFormula = true,
showAddTraceOperator = false,
}: {
addNewBuilderQuery: () => void;
addNewFormula: () => void;
addTraceOperator?: () => void;
showAddTraceOperator: boolean;
showAddFormula?: boolean;
}): JSX.Element {
return (
<div className="qb-footer">
<div className="qb-footer-container">
<div className="qb-add-new-query">
<Tooltip title={<div style={{ textAlign: 'center' }}>Add New Query</div>}>
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add New Query
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=query-builder#multiple-queries-and-functions"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
<Button
className="add-new-query-button periscope-btn secondary"
type="text"
@@ -30,65 +37,32 @@ export default function QueryFooter({
</Tooltip>
</div>
{showAddFormula && (
<div className="qb-add-formula">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
<div className="qb-add-formula">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=query-builder#multiple-queries-and-functions"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
<Button
className="add-formula-button periscope-btn secondary"
icon={<Sigma size={16} />}
onClick={addNewFormula}
>
<Button
className="add-formula-button periscope-btn secondary"
icon={<Sigma size={16} />}
onClick={addNewFormula}
>
Add Formula
</Button>
</Tooltip>
</div>
)}
{showAddTraceOperator && (
<div className="qb-trace-operator-button-container">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add Trace Matching
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-trace-operators"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
<Button
className="add-trace-operator-button periscope-btn secondary"
icon={<DraftingCompass size={16} />}
onClick={(): void => addTraceOperator?.()}
>
<div className="qb-trace-operator-button-container-text">
Add Trace Matching
<BetaTag />
</div>
</Button>
</Tooltip>
</div>
)}
Add Formula
</Button>
</Tooltip>
</div>
</div>
</div>
);

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { Dropdown } from 'antd';
import cx from 'classnames';
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
@@ -27,12 +26,8 @@ export const QueryV2 = memo(function QueryV2({
query,
filterConfigs,
isListViewPanel = false,
showTraceOperator = false,
hasTraceOperator = false,
version,
showOnlyWhereClause = false,
signalSource = '',
isMultiQueryAllowed = false,
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
const { cloneQuery, panelType } = useQueryBuilder();
@@ -79,15 +74,6 @@ export const QueryV2 = memo(function QueryV2({
dataSource,
]);
const showInlineQuerySearch = useMemo(() => {
if (!showTraceOperator) {
return false;
}
return (
dataSource === DataSource.TRACES && (hasTraceOperator || isListViewPanel)
);
}, [hasTraceOperator, isListViewPanel, showTraceOperator, dataSource]);
const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => {
handleChangeQueryData('stepInterval', value);
@@ -121,12 +107,11 @@ export const QueryV2 = memo(function QueryV2({
ref={ref}
>
<div className="qb-content-section">
{(!showOnlyWhereClause || showTraceOperator) && (
{!showOnlyWhereClause && (
<div className="qb-header-container">
<div className="query-actions-container">
<div className="query-actions-left-container">
<QBEntityOptions
hasTraceOperator={hasTraceOperator}
isMetricsDataSource={dataSource === DataSource.METRICS}
showFunctions={
(version && version === ENTITY_VERSION_V4) ||
@@ -136,7 +121,6 @@ export const QueryV2 = memo(function QueryV2({
false
}
isCollapsed={isCollapsed}
showTraceOperator={showTraceOperator}
entityType="query"
entityData={query}
onToggleVisibility={handleToggleDisableQuery}
@@ -154,28 +138,7 @@ export const QueryV2 = memo(function QueryV2({
/>
</div>
{!isCollapsed && showInlineQuerySearch && (
<div className="qb-search-filter-container" style={{ flex: 1 }}>
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
</div>
)}
{isMultiQueryAllowed && (
{!isListViewPanel && (
<Dropdown
className="query-actions-dropdown"
menu={{
@@ -212,36 +175,31 @@ export const QueryV2 = memo(function QueryV2({
query={query}
index={index}
version={ENTITY_VERSION_V5}
signalSource={signalSource as 'meter' | ''}
/>
</div>
)}
{!showInlineQuerySearch && (
<div className="qb-search-filter-container">
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
<div className="qb-search-filter-container">
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
/>
</div>
)}
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
</div>
</div>
{!showOnlyWhereClause &&
!isListViewPanel &&
!(hasTraceOperator && dataSource === DataSource.TRACES) &&
dataSource !== DataSource.METRICS && (
<QueryAggregation
dataSource={dataSource}
@@ -260,21 +218,19 @@ export const QueryV2 = memo(function QueryV2({
index={index}
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
version="v4"
signalSource={signalSource as 'meter' | ''}
/>
)}
{!showOnlyWhereClause &&
!(hasTraceOperator && query.dataSource === DataSource.TRACES) && (
<QueryAddOns
index={index}
query={query}
version="v3"
isListViewPanel={isListViewPanel}
showReduceTo={showReduceTo}
panelType={panelType}
/>
)}
{!showOnlyWhereClause && (
<QueryAddOns
index={index}
query={query}
version="v3"
isListViewPanel={isListViewPanel}
showReduceTo={showReduceTo}
panelType={panelType}
/>
)}
</div>
)}
</div>

View File

@@ -1,185 +0,0 @@
.qb-trace-operator {
padding: 8px;
display: flex;
gap: 8px;
&.non-list-view {
padding-left: 40px;
position: relative;
&::before {
content: '';
position: absolute;
top: 24px;
left: 12px;
height: 88px;
width: 1px;
background: repeating-linear-gradient(
to bottom,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
}
&-span-source-label {
display: flex;
align-items: center;
gap: 8px;
height: 24px;
&-query {
font-size: 14px;
font-weight: 400;
color: var(--bg-vanilla-100);
}
&-query-name {
width: 18px;
height: 18px;
display: grid;
place-content: center;
padding: 2px;
border-radius: 2px;
border: 1px solid rgba(242, 71, 105, 0.2);
background: rgba(242, 71, 105, 0.1);
color: var(--Sakura-400, #f56c87);
font-size: 12px;
}
}
&-arrow {
position: relative;
&::before {
content: '';
position: absolute;
top: 16px;
transform: translateY(-50%);
left: -26px;
height: 1px;
width: 20px;
background: repeating-linear-gradient(
to right,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
&::after {
content: '';
position: absolute;
top: 16px;
left: -10px;
transform: translateY(-50%);
height: 4px;
width: 4px;
border-radius: 50%;
background-color: var(--bg-slate-400);
}
}
&-input {
width: 100%;
}
&-container {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
&-aggregation-container {
display: flex;
flex-direction: column;
gap: 8px;
}
&-add-ons-container {
width: 100%;
display: flex;
flex-direction: row;
gap: 16px;
}
&-label-with-input {
position: relative;
display: flex;
align-items: center;
flex-direction: row;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
.qb-trace-operator-editor-container {
flex: 1;
}
&.arrow-left {
&::before {
content: '';
position: absolute;
left: -16px;
top: 50%;
height: 1px;
width: 16px;
background-color: var(--bg-slate-400);
}
}
.label {
color: var(--bg-vanilla-400);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0px 8px;
border-right: 1px solid var(--bg-slate-400);
}
}
}
.lightMode {
.qb-trace-operator {
&-arrow {
&::before {
background: repeating-linear-gradient(
to right,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
&::after {
background-color: var(--bg-vanilla-300);
}
}
&.non-list-view {
&::before {
background: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
}
&-label-with-input {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
.label {
color: var(--bg-ink-500) !important;
border-right: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
}
}

View File

@@ -1,119 +0,0 @@
/* eslint-disable react/require-default-props */
/* eslint-disable sonarjs/no-duplicate-string */
import './TraceOperator.styles.scss';
import { Button, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Trash2 } from 'lucide-react';
import { useCallback } from 'react';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import QueryAddOns from '../QueryAddOns/QueryAddOns';
import QueryAggregation from '../QueryAggregation/QueryAggregation';
import TraceOperatorEditor from './TraceOperatorEditor';
export default function TraceOperator({
traceOperator,
isListViewPanel = false,
}: {
traceOperator: IBuilderTraceOperator;
isListViewPanel?: boolean;
}): JSX.Element {
const { panelType, removeTraceOperator } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: traceOperator,
entityVersion: '',
isForTraceOperator: true,
});
const handleTraceOperatorChange = useCallback(
(traceOperatorExpression: string) => {
handleChangeQueryData('expression', traceOperatorExpression);
},
[handleChangeQueryData],
);
const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => {
handleChangeQueryData('stepInterval', value);
},
[handleChangeQueryData],
);
const handleChangeAggregation = useCallback(
(value: string) => {
handleChangeQueryData('aggregations', [
{
expression: value,
},
]);
},
[handleChangeQueryData],
);
return (
<div className={cx('qb-trace-operator', !isListViewPanel && 'non-list-view')}>
<div className="qb-trace-operator-container">
<div
className={cx(
'qb-trace-operator-label-with-input',
!isListViewPanel && 'qb-trace-operator-arrow',
)}
>
<Typography.Text className="label">TRACE OPERATOR</Typography.Text>
<div className="qb-trace-operator-editor-container">
<TraceOperatorEditor
value={traceOperator?.expression || ''}
traceOperator={traceOperator}
onChange={handleTraceOperatorChange}
/>
</div>
</div>
{!isListViewPanel && (
<div className="qb-trace-operator-aggregation-container">
<div className={cx(!isListViewPanel && 'qb-trace-operator-arrow')}>
<QueryAggregation
dataSource={DataSource.TRACES}
key={`query-search-${traceOperator.queryName}`}
panelType={panelType || undefined}
onAggregationIntervalChange={handleChangeAggregateEvery}
onChange={handleChangeAggregation}
queryData={traceOperator}
/>
</div>
<div
className={cx(
'qb-trace-operator-add-ons-container',
!isListViewPanel && 'qb-trace-operator-arrow',
)}
>
<QueryAddOns
index={0}
query={traceOperator}
version="v3"
isForTraceOperator
isListViewPanel={false}
showReduceTo={false}
panelType={panelType}
/>
</div>
</div>
)}
</div>
<Tooltip title="Remove Trace Operator" placement="topLeft">
<Button className="periscope-btn ghost" onClick={removeTraceOperator}>
<Trash2 size={14} />
</Button>
</Tooltip>
</div>
);
}

View File

@@ -1,491 +0,0 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable sonarjs/no-identical-functions */
import '../QuerySearch/QuerySearch.styles.scss';
import { CheckCircleFilled } from '@ant-design/icons';
import {
autocompletion,
closeCompletion,
CompletionContext,
completionKeymap,
CompletionResult,
startCompletion,
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { Color } from '@signozhq/design-tokens';
import { copilot } from '@uiw/codemirror-theme-copilot';
import { githubLight } from '@uiw/codemirror-theme-github';
import CodeMirror, { EditorView, keymap, Prec } from '@uiw/react-codemirror';
import { Button, Popover } from 'antd';
import cx from 'classnames';
import {
TRACE_OPERATOR_OPERATORS,
TRACE_OPERATOR_OPERATORS_LABELS,
TRACE_OPERATOR_OPERATORS_WITH_PRIORITY,
} from 'constants/antlrQueryConstants';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { TriangleAlert } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { IDetailedError, IValidationResult } from 'types/antlrQueryTypes';
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { validateTraceOperatorQuery } from 'utils/queryValidationUtils';
import { getTraceOperatorContextAtCursor } from './utils/traceOperatorContextUtils';
import { getInvolvedQueriesInTraceOperator } from './utils/utils';
// Custom extension to stop events
const stopEventsExtension = EditorView.domEventHandlers({
keydown: (event) => {
// Stop all keyboard events from propagating to global shortcuts
event.stopPropagation();
event.stopImmediatePropagation();
return false; // Important for CM to know you handled it
},
input: (event) => {
event.stopPropagation();
return false;
},
focus: (event) => {
// Ensure focus events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
blur: (event) => {
// Ensure blur events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
});
interface TraceOperatorEditorProps {
value: string;
traceOperator: IBuilderTraceOperator;
onChange: (value: string) => void;
placeholder?: string;
onRun?: (query: string) => void;
}
function TraceOperatorEditor({
value,
onChange,
traceOperator,
placeholder = 'Enter your trace operator query',
onRun,
}: TraceOperatorEditorProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [isFocused, setIsFocused] = useState(false);
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
const editorRef = useRef<EditorView | null>(null);
const [validation, setValidation] = useState<IValidationResult>({
isValid: false,
message: '',
errors: [],
});
// Track if the query was changed externally (from props) vs internally (user input)
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
const [lastExternalValue, setLastExternalValue] = useState<string>('');
const { currentQuery, handleRunQuery } = useQueryBuilder();
const queryOptions = useMemo(
() =>
currentQuery.builder.queryData
.filter((query) => query.dataSource === DataSource.TRACES) // Only show trace queries
.map((query) => ({
label: query.queryName,
type: 'atom',
apply: query.queryName,
})),
[currentQuery.builder.queryData],
);
const toggleSuggestions = useCallback(
(timeout?: number) => {
const timeoutId = setTimeout(() => {
if (!editorRef.current) return;
if (isFocused) {
startCompletion(editorRef.current);
} else {
closeCompletion(editorRef.current);
}
}, timeout);
return (): void => clearTimeout(timeoutId);
},
[isFocused],
);
const handleQueryValidation = (newQuery: string): void => {
try {
const validationResponse = validateTraceOperatorQuery(newQuery);
setValidation(validationResponse);
} catch (error) {
setValidation({
isValid: false,
message: 'Failed to process trace operator',
errors: [error as IDetailedError],
});
}
};
// Detect external value changes and mark for validation
useEffect(() => {
const newValue = value || '';
if (newValue !== lastExternalValue) {
setIsExternalQueryChange(true);
setLastExternalValue(newValue);
}
}, [value, lastExternalValue]);
// Validate when the value changes externally (including on mount)
useEffect(() => {
if (isExternalQueryChange && value) {
handleQueryValidation(value);
setIsExternalQueryChange(false);
}
}, [isExternalQueryChange, value]);
// Enhanced autosuggestion function with context awareness
function autoSuggestions(context: CompletionContext): CompletionResult | null {
// This matches words before the cursor position
// eslint-disable-next-line no-useless-escape
const word = context.matchBefore(/[a-zA-Z0-9_.:/?&=#%\-\[\]]*/);
if (word?.from === word?.to && !context.explicit) return null;
// Get the trace operator context at the cursor position
const queryContext = getTraceOperatorContextAtCursor(value, cursorPos.ch);
// Define autocomplete options based on the context
let options: {
label: string;
type: string;
info?: string;
apply:
| string
| ((view: EditorView, completion: any, from: number, to: number) => void);
detail?: string;
boost?: number;
}[] = [];
// Helper function to add space after selection
const addSpaceAfterSelection = (
view: EditorView,
completion: any,
from: number,
to: number,
shouldAddSpace = true,
): void => {
view.dispatch({
changes: {
from,
to,
insert: shouldAddSpace ? `${completion.apply} ` : `${completion.apply}`,
},
selection: {
anchor:
from +
(shouldAddSpace ? completion.apply.length + 1 : completion.apply.length),
},
});
// Do not reopen here; onUpdate will handle reopening via toggleSuggestions
};
// Helper function to add space after selection to options
const addSpaceToOptions = (opts: typeof options): typeof options =>
opts.map((option) => {
const originalApply = option.apply || option.label;
return {
...option,
apply: (
view: EditorView,
completion: any,
from: number,
to: number,
): void => {
addSpaceAfterSelection(view, { apply: originalApply }, from, to);
},
};
});
if (queryContext.isInAtom) {
// Suggest atoms (identifiers) for trace operators
const involvedQueries = getInvolvedQueriesInTraceOperator([traceOperator]);
options = queryOptions.map((option) => ({
...option,
boost: !involvedQueries.includes(option.apply as string) ? 100 : -99,
}));
// Filter options based on what user is typing
const searchText = word?.text.toLowerCase().trim() ?? '';
options = options.filter((option) =>
option.label.toLowerCase().includes(searchText),
);
// Add space after selection for atoms
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
to: word?.to ?? cursorPos.ch,
options: optionsWithSpace,
};
}
if (queryContext.isInOperator) {
// Suggest operators for trace operators
const operators = Object.values(TRACE_OPERATOR_OPERATORS);
options = operators.map((operator) => ({
label: TRACE_OPERATOR_OPERATORS_LABELS[operator]
? `${operator} (${TRACE_OPERATOR_OPERATORS_LABELS[operator]})`
: operator,
type: 'operator',
apply: operator,
boost: TRACE_OPERATOR_OPERATORS_WITH_PRIORITY[operator] * -10,
}));
// Add space after selection for operators
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
to: word?.to ?? cursorPos.ch,
options: optionsWithSpace,
};
}
if (queryContext.isInParenthesis) {
// Different suggestions based on the context within parenthesis
const curChar = value.charAt(cursorPos.ch - 1) || '';
if (curChar === '(') {
// Right after opening parenthesis, suggest atoms or nested expressions
options = [
{ label: '(', type: 'parenthesis', apply: '(' },
...queryOptions,
];
// Add space after selection for opening parenthesis context
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
options: optionsWithSpace,
};
}
if (curChar === ')') {
// After closing parenthesis, suggest operators
const operators = Object.values(TRACE_OPERATOR_OPERATORS);
options = operators.map((operator) => ({
label: TRACE_OPERATOR_OPERATORS_LABELS[operator]
? `${operator} (${TRACE_OPERATOR_OPERATORS_LABELS[operator]})`
: operator,
type: 'operator',
apply: operator,
boost: TRACE_OPERATOR_OPERATORS_WITH_PRIORITY[operator] * -10,
}));
// Add space after selection for closing parenthesis context
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
options: optionsWithSpace,
};
}
}
// Default: suggest atoms if no specific context
options = [
...queryOptions,
{
label: '(',
type: 'parenthesis',
apply: '(',
},
];
// Filter options based on what user is typing
const searchText = word?.text.toLowerCase().trim() ?? '';
options = options.filter((option) =>
option.label.toLowerCase().includes(searchText),
);
// Add space after selection
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
to: word?.to ?? context.pos,
options: optionsWithSpace,
};
}
const handleUpdate = useCallback(
(viewUpdate: { view: EditorView }): void => {
if (!editorRef.current) {
editorRef.current = viewUpdate.view;
}
const selection = viewUpdate.view.state.selection.main;
const pos = selection.head;
const lineInfo = viewUpdate.view.state.doc.lineAt(pos);
const newPos = {
line: lineInfo.number,
ch: pos - lineInfo.from,
};
if (newPos.line !== cursorPos.line || newPos.ch !== cursorPos.ch) {
setCursorPos(newPos);
// Trigger suggestions on context update
toggleSuggestions(10);
}
},
[cursorPos, toggleSuggestions],
);
const handleChange = (newValue: string): void => {
// Mark as internal change to avoid triggering external validation
setIsExternalQueryChange(false);
setLastExternalValue(newValue);
onChange(newValue);
};
const handleBlur = (): void => {
handleQueryValidation(value);
setIsFocused(false);
};
// Effect to handle focus state and trigger suggestions on focus
useEffect(() => {
const clearTimeout = toggleSuggestions(10);
return (): void => clearTimeout();
}, [isFocused, toggleSuggestions]);
return (
<div className="code-mirror-where-clause">
<div className="query-where-clause-editor-container">
<CodeMirror
value={value}
theme={isDarkMode ? copilot : githubLight}
onChange={handleChange}
onUpdate={handleUpdate}
className={cx('query-where-clause-editor', {
isValid: validation.isValid === true,
hasErrors: validation.errors.length > 0,
})}
extensions={[
autocompletion({
override: [autoSuggestions],
defaultKeymap: true,
closeOnBlur: true,
activateOnTyping: true,
maxRenderedOptions: 50,
}),
javascript({ jsx: false, typescript: false }),
EditorView.lineWrapping,
stopEventsExtension,
Prec.highest(
keymap.of([
...completionKeymap,
{
key: 'Escape',
run: closeCompletion,
},
{
key: 'Enter',
preventDefault: true,
// Prevent default behavior of Enter to add new line
// and instead run a custom action
run: (): boolean => true,
},
{
key: 'Mod-Enter',
preventDefault: true,
run: (): boolean => {
if (onRun && typeof onRun === 'function') {
onRun(value);
} else {
handleRunQuery();
}
return true;
},
},
{
key: 'Shift-Enter',
preventDefault: true,
// Prevent default behavior of Shift-Enter to add new line
run: (): boolean => true,
},
]),
),
]}
placeholder={placeholder}
basicSetup={{
lineNumbers: false,
}}
onFocus={(): void => {
setIsFocused(true);
}}
onBlur={handleBlur}
/>
{value && validation.isValid === false && !isFocused && (
<div
className={cx('query-status-container', {
hasErrors: validation.errors.length > 0,
})}
>
<Popover
placement="bottomRight"
showArrow={false}
content={
<div className="query-status-content">
<div className="query-status-content-header">
<div className="query-validation">
<div className="query-validation-errors">
{validation.errors.map((error) => (
<div key={error.message} className="query-validation-error">
<div className="query-validation-error">
{error.line}:{error.column} - {error.message}
</div>
</div>
))}
</div>
</div>
</div>
</div>
}
overlayClassName="query-status-popover"
>
{validation.isValid ? (
<Button
type="text"
icon={<CheckCircleFilled />}
className="periscope-btn ghost"
/>
) : (
<Button
type="text"
icon={<TriangleAlert size={14} color={Color.BG_CHERRY_500} />}
className="periscope-btn ghost"
/>
)}
</Popover>
</div>
)}
</div>
</div>
);
}
TraceOperatorEditor.defaultProps = {
onRun: undefined,
placeholder: 'Enter your trace operator query',
};
export default TraceOperatorEditor;

View File

@@ -1,425 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable sonarjs/cognitive-complexity */
import { Token } from 'antlr4';
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
import {
createTraceOperatorContext,
extractTraceExpressionPairs,
getTraceOperatorContextAtCursor,
} from '../utils/traceOperatorContextUtils';
describe('traceOperatorContextUtils', () => {
describe('createTraceOperatorContext', () => {
it('should create a context object with all required properties', () => {
const mockToken = {
type: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
stop: 3,
} as Token;
const context = createTraceOperatorContext(
mockToken,
true,
false,
false,
false,
'atom',
'operator',
[],
null,
);
expect(context).toEqual({
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
stop: 3,
currentToken: 'test',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
atomToken: 'atom',
operatorToken: 'operator',
expressionPairs: [],
currentPair: null,
});
});
it('should create a context object with default values', () => {
const mockToken = {
type: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
stop: 3,
} as Token;
const context = createTraceOperatorContext(
mockToken,
false,
true,
false,
false,
);
expect(context).toEqual({
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
stop: 3,
currentToken: 'test',
isInAtom: false,
isInOperator: true,
isInParenthesis: false,
isInExpression: false,
atomToken: undefined,
operatorToken: undefined,
expressionPairs: [],
currentPair: undefined,
});
});
});
describe('extractTraceExpressionPairs', () => {
it('should extract simple expression pair', () => {
const query = 'A => B';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(1);
expect(result[0].leftAtom).toBe('A');
expect(result[0].position.leftStart).toBe(0);
expect(result[0].position.leftEnd).toBe(0);
expect(result[0].operator).toBe('=>');
expect(result[0].position.operatorStart).toBe(2);
expect(result[0].position.operatorEnd).toBe(3);
expect(result[0].rightAtom).toBe('B');
expect(result[0].position.rightStart).toBe(5);
expect(result[0].position.rightEnd).toBe(5);
expect(result[0].isComplete).toBe(true);
});
it('should extract multiple expression pairs', () => {
const query = 'A => B && C => D';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(2);
// First pair: A => B
expect(result[0].leftAtom).toBe('A');
expect(result[0].operator).toBe('=>');
expect(result[0].rightAtom).toBe('B');
// Second pair: C => D
expect(result[1].leftAtom).toBe('C');
expect(result[1].operator).toBe('=>');
expect(result[1].rightAtom).toBe('D');
});
it('should handle NOT operator', () => {
const query = 'NOT A => B';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(1);
expect(result[0].leftAtom).toBe('A');
expect(result[0].operator).toBe('=>');
expect(result[0].rightAtom).toBe('B');
});
it('should handle parentheses', () => {
const query = '(A => B) && (C => D)';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(2);
expect(result[0].leftAtom).toBe('A');
expect(result[0].rightAtom).toBe('B');
expect(result[1].leftAtom).toBe('C');
expect(result[1].rightAtom).toBe('D');
});
it('should handle incomplete expressions', () => {
const query = 'A =>';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(1);
expect(result[0].leftAtom).toBe('A');
expect(result[0].operator).toBe('=>');
expect(result[0].rightAtom).toBeUndefined();
expect(result[0].isComplete).toBe(true);
});
it('should handle complex nested expressions', () => {
const query = 'A => B && (C => D || E => F)';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(3);
expect(result[0].leftAtom).toBe('A');
expect(result[0].rightAtom).toBe('B');
expect(result[1].leftAtom).toBe('C');
expect(result[1].rightAtom).toBe('D');
expect(result[2].leftAtom).toBe('E');
expect(result[2].rightAtom).toBe('F');
});
it('should handle whitespace variations', () => {
const query = 'A=>B';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(1);
expect(result[0].leftAtom).toBe('A');
expect(result[0].operator).toBe('=>');
expect(result[0].rightAtom).toBe('B');
});
it('should handle error cases gracefully', () => {
const query = 'invalid syntax @#$%';
const result = extractTraceExpressionPairs(query);
// Should return an array (even if empty or with partial results)
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThanOrEqual(0);
});
});
describe('getTraceOperatorContextAtCursor', () => {
beforeEach(() => {
// Reset console.error mock
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should return default context for empty query', () => {
const result = getTraceOperatorContextAtCursor('', 0);
expect(result).toEqual({
tokenType: -1,
text: '',
start: 0,
stop: 0,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
});
});
it('should return default context for null query', () => {
const result = getTraceOperatorContextAtCursor(null as any, 0);
expect(result).toEqual({
tokenType: -1,
text: '',
start: 0,
stop: 0,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
});
});
it('should return default context for undefined query', () => {
const result = getTraceOperatorContextAtCursor(undefined as any, 0);
expect(result).toEqual({
tokenType: -1,
text: '',
start: 0,
stop: 0,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
});
});
it('should identify atom context', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at 'A'
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(0);
expect(result.stop).toBe(0);
});
it('should identify operator context', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 2); // cursor at '='
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBeUndefined();
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(2);
expect(result.stop).toBe(2);
});
it('should identify parenthesis context', () => {
const query = '(A => B)';
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at '('
expect(result.atomToken).toBeUndefined();
expect(result.operatorToken).toBeUndefined();
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(true);
expect(result.start).toBe(0);
expect(result.stop).toBe(0);
});
it('should handle cursor at space', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 1); // cursor at space
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBeUndefined();
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
});
it('should handle cursor at end of query', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 5); // cursor at end
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(5);
expect(result.stop).toBe(5);
});
it('should handle complex query', () => {
const query = 'A => B && C => D';
const result = getTraceOperatorContextAtCursor(query, 8); // cursor at '&'
expect(result.atomToken).toBeUndefined();
expect(result.operatorToken).toBe('&&');
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(7);
expect(result.stop).toBe(8);
});
it('should identify operator position in complex query', () => {
const query = 'A => B && C => D';
const result = getTraceOperatorContextAtCursor(query, 10); // cursor at 'C'
expect(result.atomToken).toBe('C');
expect(result.operatorToken).toBe('&&');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(10);
expect(result.stop).toBe(10);
});
it('should identify atom position in complex query', () => {
const query = 'A => B && C => D';
const result = getTraceOperatorContextAtCursor(query, 13); // cursor at '>'
expect(result.atomToken).toBe('C');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(12);
expect(result.stop).toBe(13);
});
it('should handle transition points', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 4); // cursor at 'B'
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(4);
expect(result.stop).toBe(4);
});
it('should handle whitespace in complex queries', () => {
const query = 'A=>B && C=>D';
const result = getTraceOperatorContextAtCursor(query, 6); // cursor at '&'
expect(result.atomToken).toBeUndefined();
expect(result.operatorToken).toBe('&&');
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(5);
expect(result.stop).toBe(6);
});
it('should handle NOT operator context', () => {
const query = 'NOT A => B';
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at 'N'
expect(result.atomToken).toBeUndefined();
expect(result.operatorToken).toBeUndefined();
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(true);
});
it('should handle parentheses context', () => {
const query = '(A => B)';
const result = getTraceOperatorContextAtCursor(query, 1); // cursor at 'A'
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(true);
expect(result.start).toBe(0);
expect(result.stop).toBe(0);
});
it('should handle expression pairs context', () => {
const query = 'A => B && C => D';
const result = getTraceOperatorContextAtCursor(query, 5); // cursor at 'A' in "&&"
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
});
it('should handle various cursor positions', () => {
const query = 'A => B';
// Test cursor at each position
for (let i = 0; i < query.length; i++) {
const result = getTraceOperatorContextAtCursor(query, i);
expect(result).toBeDefined();
expect(typeof result.start).toBe('number');
expect(typeof result.stop).toBe('number');
}
});
});
});

View File

@@ -1,46 +0,0 @@
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { getInvolvedQueriesInTraceOperator } from '../utils/utils';
const makeTraceOperator = (expression: string): IBuilderTraceOperator =>
(({ expression } as unknown) as IBuilderTraceOperator);
describe('getInvolvedQueriesInTraceOperator', () => {
it('returns empty array for empty input', () => {
const result = getInvolvedQueriesInTraceOperator([]);
expect(result).toEqual([]);
});
it('extracts identifiers from expression', () => {
const result = getInvolvedQueriesInTraceOperator([
makeTraceOperator('A => B'),
]);
expect(result).toEqual(['A', 'B']);
});
it('extracts identifiers from complex expression', () => {
const result = getInvolvedQueriesInTraceOperator([
makeTraceOperator('A => (NOT B || C)'),
]);
expect(result).toEqual(['A', 'B', 'C']);
});
it('filters out querynames from complex expression', () => {
const result = getInvolvedQueriesInTraceOperator([
makeTraceOperator(
'(A1 && (NOT B2 || (C3 -> (D4 && E5)))) => ((F6 || G7) && (NOT (H8 -> I9)))',
),
]);
expect(result).toEqual([
'A1',
'B2',
'C3',
'D4',
'E5',
'F6',
'G7',
'H8',
'I9',
]);
});
});

View File

@@ -1,562 +0,0 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable no-continue */
import { CharStreams, CommonTokenStream, Token } from 'antlr4';
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
import { IToken } from 'types/antlrQueryTypes';
// Trace Operator Context Interface
export interface ITraceOperatorContext {
tokenType: number;
text: string;
start: number;
stop: number;
currentToken: string;
isInAtom: boolean;
isInOperator: boolean;
isInParenthesis: boolean;
isInExpression: boolean;
atomToken?: string;
operatorToken?: string;
expressionPairs: ITraceExpressionPair[];
currentPair?: ITraceExpressionPair | null;
}
// Trace Expression Pair Interface
export interface ITraceExpressionPair {
leftAtom: string;
operator: string;
rightAtom?: string;
rightExpression?: string;
position: {
leftStart: number;
leftEnd: number;
operatorStart: number;
operatorEnd: number;
rightStart?: number;
rightEnd?: number;
};
isComplete: boolean;
}
// Helper functions to determine token types
function isAtomToken(tokenType: number): boolean {
return tokenType === TraceOperatorGrammarLexer.IDENTIFIER;
}
function isOperatorToken(tokenType: number): boolean {
return [
TraceOperatorGrammarLexer.T__2, // '=>'
TraceOperatorGrammarLexer.T__3, // '&&'
TraceOperatorGrammarLexer.T__4, // '||'
TraceOperatorGrammarLexer.T__5, // 'NOT'
TraceOperatorGrammarLexer.T__6, // '->'
].includes(tokenType);
}
function isParenthesisToken(tokenType: number): boolean {
return (
tokenType === TraceOperatorGrammarLexer.T__0 ||
tokenType === TraceOperatorGrammarLexer.T__1
);
}
function isOpeningParenthesis(tokenType: number): boolean {
return tokenType === TraceOperatorGrammarLexer.T__0;
}
function isClosingParenthesis(tokenType: number): boolean {
return tokenType === TraceOperatorGrammarLexer.T__1;
}
// Function to create a context object
export function createTraceOperatorContext(
token: Token,
isInAtom: boolean,
isInOperator: boolean,
isInParenthesis: boolean,
isInExpression: boolean,
atomToken?: string,
operatorToken?: string,
expressionPairs?: ITraceExpressionPair[],
currentPair?: ITraceExpressionPair | null,
): ITraceOperatorContext {
return {
tokenType: token.type,
text: token.text || '',
start: token.start,
stop: token.stop,
currentToken: token.text || '',
isInAtom,
isInOperator,
isInParenthesis,
isInExpression,
atomToken,
operatorToken,
expressionPairs: expressionPairs || [],
currentPair,
};
}
// Helper to determine token context
function determineTraceTokenContext(
token: IToken,
): {
isInAtom: boolean;
isInOperator: boolean;
isInParenthesis: boolean;
isInExpression: boolean;
} {
const tokenType = token.type;
return {
isInAtom: isAtomToken(tokenType),
isInOperator: isOperatorToken(tokenType),
isInParenthesis: isParenthesisToken(tokenType),
isInExpression: false, // Will be determined by broader context
};
}
/**
* Extracts all expression pairs from a trace operator query string
* This parses the query according to the TraceOperatorGrammar.g4 grammar
*
* @param query The trace operator query string to parse
* @returns An array of ITraceExpressionPair objects representing the expression pairs
*/
export function extractTraceExpressionPairs(
query: string,
): ITraceExpressionPair[] {
try {
const input = query || '';
const chars = CharStreams.fromString(input);
const lexer = new TraceOperatorGrammarLexer(chars);
const tokenStream = new CommonTokenStream(lexer);
tokenStream.fill();
const allTokens = tokenStream.tokens as IToken[];
const expressionPairs: ITraceExpressionPair[] = [];
let currentPair: Partial<ITraceExpressionPair> | null = null;
let i = 0;
while (i < allTokens.length) {
const token = allTokens[i];
i++;
// Skip EOF and whitespace tokens
if (token.type === TraceOperatorGrammarLexer.EOF || token.channel !== 0) {
continue;
}
// If token is an IDENTIFIER (atom), start or continue a pair
if (isAtomToken(token.type)) {
// If we don't have a current pair, start one
if (!currentPair) {
currentPair = {
leftAtom: token.text,
position: {
leftStart: token.start,
leftEnd: token.stop,
operatorStart: 0,
operatorEnd: 0,
},
};
}
// If we have a current pair but no operator yet, this is still the left atom
else if (!currentPair.operator && currentPair.position) {
currentPair.leftAtom = token.text;
currentPair.position.leftStart = token.start;
currentPair.position.leftEnd = token.stop;
}
// If we have an operator, this is the right atom
else if (
currentPair.operator &&
!currentPair.rightAtom &&
currentPair.position
) {
currentPair.rightAtom = token.text;
currentPair.position.rightStart = token.start;
currentPair.position.rightEnd = token.stop;
currentPair.isComplete = true;
// Add the completed pair to the result
expressionPairs.push(currentPair as ITraceExpressionPair);
currentPair = null;
}
}
// If token is an operator and we have a left atom
else if (
isOperatorToken(token.type) &&
currentPair &&
currentPair.leftAtom &&
currentPair.position
) {
currentPair.operator = token.text;
currentPair.position.operatorStart = token.start;
currentPair.position.operatorEnd = token.stop;
// If this is a NOT operator, it might be followed by another operator
if (token.type === TraceOperatorGrammarLexer.T__5 && i < allTokens.length) {
// Look ahead for the next operator
const nextToken = allTokens[i];
if (isOperatorToken(nextToken.type) && nextToken.channel === 0) {
currentPair.operator = `${token.text} ${nextToken.text}`;
currentPair.position.operatorEnd = nextToken.stop;
i++; // Skip the next token since we've consumed it
}
}
}
// If token is an opening parenthesis after an operator, this is a right expression
else if (
isOpeningParenthesis(token.type) &&
currentPair &&
currentPair.operator &&
!currentPair.rightAtom &&
currentPair.position
) {
// Find the matching closing parenthesis
let parenCount = 1;
let j = i;
let rightExpression = '';
const rightStart = token.start;
let rightEnd = token.stop;
while (j < allTokens.length && parenCount > 0) {
const parenToken = allTokens[j];
if (parenToken.channel === 0) {
if (isOpeningParenthesis(parenToken.type)) {
parenCount++;
} else if (isClosingParenthesis(parenToken.type)) {
parenCount--;
if (parenCount === 0) {
rightEnd = parenToken.stop;
break;
}
}
}
rightExpression += parenToken.text;
j++;
}
if (parenCount === 0) {
currentPair.rightExpression = rightExpression;
currentPair.position.rightStart = rightStart;
currentPair.position.rightEnd = rightEnd;
currentPair.isComplete = true;
// Add the completed pair to the result
expressionPairs.push(currentPair as ITraceExpressionPair);
currentPair = null;
// Skip to the end of the expression
i = j;
}
}
}
// Add any remaining incomplete pair
if (currentPair && currentPair.leftAtom && currentPair.position) {
expressionPairs.push({
...currentPair,
isComplete: !!(currentPair.leftAtom && currentPair.operator),
} as ITraceExpressionPair);
}
return expressionPairs;
} catch (error) {
console.error('Error in extractTraceExpressionPairs:', error);
return [];
}
}
/**
* Gets the current expression pair at the cursor position
*
* @param expressionPairs An array of ITraceExpressionPair objects
* @param query The full query string
* @param cursorIndex The position of the cursor in the query
* @returns The expression pair at the cursor position, or null if not found
*/
export function getCurrentTraceExpressionPair(
expressionPairs: ITraceExpressionPair[],
cursorIndex: number,
): ITraceExpressionPair | null {
try {
if (expressionPairs.length === 0) {
return null;
}
// Find the rightmost pair whose end position is before or at the cursor
let bestMatch: ITraceExpressionPair | null = null;
// eslint-disable-next-line no-restricted-syntax
for (const pair of expressionPairs) {
const { position } = pair;
const pairEnd =
position.rightEnd || position.operatorEnd || position.leftEnd;
const pairStart = position.leftStart;
// If this pair ends at or before the cursor, and it's further right than our previous best match
if (
pairStart <= cursorIndex &&
cursorIndex <= pairEnd + 1 &&
(!bestMatch ||
pairEnd >
(bestMatch.position.rightEnd ||
bestMatch.position.operatorEnd ||
bestMatch.position.leftEnd))
) {
bestMatch = pair;
}
}
return bestMatch;
} catch (error) {
console.error('Error in getCurrentTraceExpressionPair:', error);
return null;
}
}
/**
* Gets the current trace operator context at the cursor position
* This is useful for determining what kind of suggestions to show
*
* @param query The trace operator query string
* @param cursorIndex The position of the cursor in the query
* @returns The trace operator context at the cursor position
*/
export function getTraceOperatorContextAtCursor(
query: string,
cursorIndex: number,
): ITraceOperatorContext {
try {
// Guard against infinite recursion
const stackTrace = new Error().stack || '';
const callCount = (stackTrace.match(/getTraceOperatorContextAtCursor/g) || [])
.length;
if (callCount > 3) {
console.warn(
'Potential infinite recursion detected in getTraceOperatorContextAtCursor',
);
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
};
}
// Create input stream and lexer
const input = query || '';
const chars = CharStreams.fromString(input);
const lexer = new TraceOperatorGrammarLexer(chars);
const tokenStream = new CommonTokenStream(lexer);
tokenStream.fill();
const allTokens = tokenStream.tokens as IToken[];
// Get expression pairs information
const expressionPairs = extractTraceExpressionPairs(query);
const currentPair = getCurrentTraceExpressionPair(
expressionPairs,
cursorIndex,
);
// Find the token at or just before the cursor
let lastTokenBeforeCursor: IToken | null = null;
for (let i = 0; i < allTokens.length; i++) {
const token = allTokens[i];
if (token.type === TraceOperatorGrammarLexer.EOF) continue;
if (token.stop < cursorIndex || token.stop + 1 === cursorIndex) {
lastTokenBeforeCursor = token;
}
if (token.start > cursorIndex) {
break;
}
}
// Find exact token at cursor
let exactToken: IToken | null = null;
for (let i = 0; i < allTokens.length; i++) {
const token = allTokens[i];
if (token.type === TraceOperatorGrammarLexer.EOF) continue;
if (token.start <= cursorIndex && cursorIndex <= token.stop + 1) {
exactToken = token;
break;
}
}
// If we don't have any tokens, return default context
if (!lastTokenBeforeCursor && !exactToken) {
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: '',
isInAtom: true, // Default to atom context when input is empty
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs,
currentPair: null,
};
}
// Check if cursor is at a space after a token (transition point)
const isAtSpace = cursorIndex < query.length && query[cursorIndex] === ' ';
const isAfterSpace = cursorIndex > 0 && query[cursorIndex - 1] === ' ';
const isAfterToken = cursorIndex > 0 && query[cursorIndex - 1] !== ' ';
const isTransitionPoint =
(isAtSpace && isAfterToken) ||
(cursorIndex === query.length && isAfterToken);
// If we're at a transition point after a token, progress the context
if (
lastTokenBeforeCursor &&
(isAtSpace || isAfterSpace || isTransitionPoint)
) {
const lastTokenContext = determineTraceTokenContext(lastTokenBeforeCursor);
// Apply context progression: atom → operator → atom/expression → operator → atom
if (lastTokenContext.isInAtom) {
// After atom + space, move to operator context
return {
tokenType: lastTokenBeforeCursor.type,
text: lastTokenBeforeCursor.text,
start: cursorIndex,
stop: cursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInAtom: false,
isInOperator: true,
isInParenthesis: false,
isInExpression: false,
atomToken: lastTokenBeforeCursor.text,
expressionPairs,
currentPair,
};
}
if (lastTokenContext.isInOperator) {
// After operator + space, move to atom/expression context
return {
tokenType: lastTokenBeforeCursor.type,
text: lastTokenBeforeCursor.text,
start: cursorIndex,
stop: cursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInAtom: true, // Expecting an atom or expression after operator
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
operatorToken: lastTokenBeforeCursor.text,
atomToken: currentPair?.leftAtom,
expressionPairs,
currentPair,
};
}
if (
lastTokenContext.isInParenthesis &&
isClosingParenthesis(lastTokenBeforeCursor.type)
) {
// After closing parenthesis, move to operator context
return {
tokenType: lastTokenBeforeCursor.type,
text: lastTokenBeforeCursor.text,
start: cursorIndex,
stop: cursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInAtom: false,
isInOperator: true,
isInParenthesis: false,
isInExpression: false,
expressionPairs,
currentPair,
};
}
}
// If cursor is at the end of a token, return the current token context
if (exactToken && cursorIndex === exactToken.stop + 1) {
const tokenContext = determineTraceTokenContext(exactToken);
return {
tokenType: exactToken.type,
text: exactToken.text,
start: exactToken.start,
stop: exactToken.stop,
currentToken: exactToken.text,
...tokenContext,
atomToken: tokenContext.isInAtom ? exactToken.text : currentPair?.leftAtom,
operatorToken: tokenContext.isInOperator
? exactToken.text
: currentPair?.operator,
expressionPairs,
currentPair,
};
}
// Regular token-based context detection
if (exactToken?.channel === 0) {
const tokenContext = determineTraceTokenContext(exactToken);
return {
tokenType: exactToken.type,
text: exactToken.text,
start: exactToken.start,
stop: exactToken.stop,
currentToken: exactToken.text,
...tokenContext,
atomToken: tokenContext.isInAtom ? exactToken.text : currentPair?.leftAtom,
operatorToken: tokenContext.isInOperator
? exactToken.text
: currentPair?.operator,
expressionPairs,
currentPair,
};
}
// Default fallback to atom context
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs,
currentPair,
};
} catch (error) {
console.error('Error in getTraceOperatorContextAtCursor:', error);
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
};
}
}

View File

@@ -1,22 +0,0 @@
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
export const getInvolvedQueriesInTraceOperator = (
traceOperators: IBuilderTraceOperator[],
): string[] => {
if (
!traceOperators ||
traceOperators.length === 0 ||
traceOperators.length > 1
)
return [];
const currentTraceOperator = traceOperators[0];
// Match any word starting with letter or underscore
const tokens =
currentTraceOperator.expression.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) || [];
// Filter out operator keywords
const operators = new Set(['NOT']);
return tokens.filter((t) => !operators.has(t));
};

View File

@@ -1,974 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable import/no-unresolved */
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import {
convertAggregationToExpression,
convertFiltersToExpression,
convertFiltersToExpressionWithExistingQuery,
} from '../utils';
describe('convertFiltersToExpression', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should handle empty, null, and undefined inputs', () => {
// Test null and undefined
expect(convertFiltersToExpression(null as any)).toEqual({ expression: '' });
expect(convertFiltersToExpression(undefined as any)).toEqual({
expression: '',
});
// Test empty filters
expect(convertFiltersToExpression({ items: [], op: 'AND' })).toEqual({
expression: '',
});
expect(
convertFiltersToExpression({ items: undefined, op: 'AND' } as any),
).toEqual({ expression: '' });
});
it('should convert basic comparison operators with proper value formatting', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: '=',
value: 'api-gateway',
},
{
id: '2',
key: { key: 'status', type: 'string' },
op: '!=',
value: 'error',
},
{
id: '3',
key: { key: 'duration', type: 'number' },
op: '>',
value: 100,
},
{
id: '4',
key: { key: 'count', type: 'number' },
op: '<=',
value: 50,
},
{
id: '5',
key: { key: 'is_active', type: 'boolean' },
op: '=',
value: true,
},
{
id: '6',
key: { key: 'enabled', type: 'boolean' },
op: '=',
value: false,
},
{
id: '7',
key: { key: 'count', type: 'number' },
op: '=',
value: 0,
},
{
id: '7',
key: { key: 'regex', type: 'string' },
op: 'regex',
value: '.*',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service = 'api-gateway' AND status != 'error' AND duration > 100 AND count <= 50 AND is_active = true AND enabled = false AND count = 0 AND regex REGEXP '.*'",
});
});
it('should handle string value formatting and escaping', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'message', type: 'string' },
op: '=',
value: "user's data",
},
{
id: '2',
key: { key: 'description', type: 'string' },
op: '=',
value: '',
},
{
id: '3',
key: { key: 'path', type: 'string' },
op: '=',
value: '/api/v1/users',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"message = 'user\\'s data' AND description = '' AND path = '/api/v1/users'",
});
});
it('should handle IN operator with various value types and array formatting', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: 'IN',
value: ['api-gateway', 'user-service', 'auth-service'],
},
{
id: '2',
key: { key: 'status', type: 'string' },
op: 'IN',
value: 'success', // Single value should be converted to array
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'IN',
value: [], // Empty array
},
{
id: '4',
key: { key: 'name', type: 'string' },
op: 'IN',
value: ["John's", "Mary's", 'Bob'], // Values with quotes
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service in ['api-gateway', 'user-service', 'auth-service'] AND status in ['success'] AND tags in [] AND name in ['John\\'s', 'Mary\\'s', 'Bob']",
});
});
it('should convert deprecated operators to their modern equivalents', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: 'nin',
value: ['api-gateway', 'user-service'],
},
{
id: '2',
key: { key: 'message', type: 'string' },
op: 'nlike',
value: 'error',
},
{
id: '3',
key: { key: 'path', type: 'string' },
op: 'nregex',
value: '/api/.*',
},
{
id: '4',
key: { key: 'service', type: 'string' },
op: 'NIN', // Test case insensitivity
value: ['api-gateway'],
},
{
id: '5',
key: { key: 'user_id', type: 'string' },
op: 'nexists',
value: '',
},
{
id: '6',
key: { key: 'description', type: 'string' },
op: 'ncontains',
value: 'error',
},
{
id: '7',
key: { key: 'tags', type: 'string' },
op: 'nhas',
value: 'production',
},
{
id: '8',
key: { key: 'labels', type: 'string' },
op: 'nhasany',
value: ['env:prod', 'service:api'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service NOT IN ['api-gateway', 'user-service'] AND message NOT LIKE 'error' AND path NOT REGEXP '/api/.*' AND service NOT IN ['api-gateway'] AND user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
});
});
it('should handle non-value operators and function operators', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'user_id', type: 'string' },
op: 'EXISTS',
value: '', // Value should be ignored for EXISTS
},
{
id: '2',
key: { key: 'user_id', type: 'string' },
op: 'EXISTS',
value: 'some-value', // Value should be ignored for EXISTS
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'has',
value: 'production',
},
{
id: '4',
key: { key: 'tags', type: 'string' },
op: 'hasAny',
value: ['production', 'staging'],
},
{
id: '5',
key: { key: 'tags', type: 'string' },
op: 'hasAll',
value: ['production', 'monitoring'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"user_id exists AND user_id exists AND has(tags, 'production') AND hasAny(tags, ['production', 'staging']) AND hasAll(tags, ['production', 'monitoring'])",
});
});
it('should filter out invalid filters and handle edge cases', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: '=',
value: 'api-gateway',
},
{
id: '2',
key: undefined, // Invalid filter - should be skipped
op: '=',
value: 'error',
},
{
id: '3',
key: { key: '', type: 'string' }, // Invalid filter with empty key - should be skipped
op: '=',
value: 'test',
},
{
id: '4',
key: { key: 'status', type: 'string' },
op: ' = ', // Test whitespace handling
value: 'success',
},
{
id: '5',
key: { key: 'service', type: 'string' },
op: 'In', // Test mixed case handling
value: ['api-gateway'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service = 'api-gateway' AND status = 'success' AND service in ['api-gateway']",
});
});
it('should handle complex mixed operator scenarios with proper joining', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: 'IN',
value: ['api-gateway', 'user-service'],
},
{
id: '2',
key: { key: 'user_id', type: 'string' },
op: 'EXISTS',
value: '',
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'has',
value: 'production',
},
{
id: '4',
key: { key: 'duration', type: 'number' },
op: '>',
value: 100,
},
{
id: '5',
key: { key: 'status', type: 'string' },
op: 'nin',
value: ['error', 'timeout'],
},
{
id: '6',
key: { key: 'method', type: 'string' },
op: '=',
value: 'POST',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service in ['api-gateway', 'user-service'] AND user_id exists AND has(tags, 'production') AND duration > 100 AND status NOT IN ['error', 'timeout'] AND method = 'POST'",
});
});
it('should handle all numeric comparison operators and edge cases', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'count', type: 'number' },
op: '=',
value: 0,
},
{
id: '2',
key: { key: 'score', type: 'number' },
op: '>',
value: 100,
},
{
id: '3',
key: { key: 'limit', type: 'number' },
op: '>=',
value: 50,
},
{
id: '4',
key: { key: 'threshold', type: 'number' },
op: '<',
value: 1000,
},
{
id: '5',
key: { key: 'max_value', type: 'number' },
op: '<=',
value: 999,
},
{
id: '6',
key: { key: 'values', type: 'string' },
op: 'IN',
value: ['1', '2', '3', '4', '5'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"count = 0 AND score > 100 AND limit >= 50 AND threshold < 1000 AND max_value <= 999 AND values in ['1', '2', '3', '4', '5']",
});
});
it('should handle boolean values and string comparisons with special characters', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'is_active', type: 'boolean' },
op: '=',
value: true,
},
{
id: '2',
key: { key: 'is_deleted', type: 'boolean' },
op: '=',
value: false,
},
{
id: '3',
key: { key: 'email', type: 'string' },
op: '=',
value: 'user@example.com',
},
{
id: '4',
key: { key: 'description', type: 'string' },
op: '=',
value: 'Contains "quotes" and \'apostrophes\'',
},
{
id: '5',
key: { key: 'path', type: 'string' },
op: '=',
value: '/api/v1/users/123?filter=true',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"is_active = true AND is_deleted = false AND email = 'user@example.com' AND description = 'Contains \"quotes\" and \\'apostrophes\\'' AND path = '/api/v1/users/123?filter=true'",
});
});
it('should handle all function operators and complex array scenarios', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'tags', type: 'string' },
op: 'has',
value: 'production',
},
{
id: '2',
key: { key: 'labels', type: 'string' },
op: 'hasAny',
value: ['env:prod', 'service:api'],
},
{
id: '3',
key: { key: 'metadata', type: 'string' },
op: 'hasAll',
value: ['version:1.0', 'team:backend'],
},
{
id: '4',
key: { key: 'services', type: 'string' },
op: 'IN',
value: ['api-gateway', 'user-service', 'auth-service', 'payment-service'],
},
{
id: '5',
key: { key: 'excluded_services', type: 'string' },
op: 'nin',
value: ['legacy-service', 'deprecated-service'],
},
{
id: '6',
key: { key: 'status_codes', type: 'string' },
op: 'IN',
value: ['200', '201', '400', '500'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"has(tags, 'production') AND hasAny(labels, ['env:prod', 'service:api']) AND hasAll(metadata, ['version:1.0', 'team:backend']) AND services in ['api-gateway', 'user-service', 'auth-service', 'payment-service'] AND excluded_services NOT IN ['legacy-service', 'deprecated-service'] AND status_codes in ['200', '201', '400', '500']",
});
});
it('should handle specific deprecated operators: nhas, ncontains, nexists', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'user_id', type: 'string' },
op: 'nexists',
value: '',
},
{
id: '2',
key: { key: 'description', type: 'string' },
op: 'ncontains',
value: 'error',
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'nhas',
value: 'production',
},
{
id: '4',
key: { key: 'labels', type: 'string' },
op: 'nhasany',
value: ['env:prod', 'service:api'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
});
});
it('should return filters with new expression when no existing query', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS['='],
value: 'test-service',
},
],
op: 'AND',
};
const result = convertFiltersToExpressionWithExistingQuery(
filters,
undefined,
);
expect(result.filters).toEqual(filters);
expect(result.filter.expression).toBe("service.name = 'test-service'");
});
it('should handle empty filters', () => {
const filters = {
items: [],
op: 'AND',
};
const result = convertFiltersToExpressionWithExistingQuery(
filters,
undefined,
);
expect(result.filters).toEqual(filters);
expect(result.filter.expression).toBe('');
});
it('should handle existing query with matching filters', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS['='],
value: 'updated-service',
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters).toBeDefined();
expect(result.filter).toBeDefined();
expect(result.filter.expression).toBe("service.name = 'updated-service'");
// Ensure parser can parse the existing query
expect(extractQueryPairs(existingQuery)).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: 'service.name',
operator: '=',
value: "'old-service'",
}),
]),
);
});
it('should handle IN operator with existing query', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS.IN,
value: ['service1', 'service2'],
},
],
op: 'AND',
};
const existingQuery = "service.name IN ['old-service']";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters).toBeDefined();
expect(result.filter).toBeDefined();
expect(result.filter.expression).toBe(
"service.name IN ['service1', 'service2']",
);
});
it('should handle IN operator conversion from equals', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS.IN,
value: ['service1', 'service2'],
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe(
"service.name IN ['service1', 'service2'] ",
);
});
it('should handle NOT IN operator conversion from not equals', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: negateOperator(OPERATORS.IN),
value: ['service1', 'service2'],
},
],
op: 'AND',
};
const existingQuery = "service.name != 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe(
"service.name NOT IN ['service1', 'service2'] ",
);
});
it('should add new filters when they do not exist in existing query', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'new.key', key: 'new.key', type: 'string' },
op: OPERATORS['='],
value: 'new-value',
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(2); // Original + new filter
expect(result.filter.expression).toBe(
"service.name = 'old-service' new.key = 'new-value'",
);
});
it('should handle simple value replacement', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'status', key: 'status', type: 'string' },
op: OPERATORS['='],
value: 'error',
},
],
op: 'AND',
};
const existingQuery = "status = 'success'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe("status = 'error'");
});
it('should handle filters with no key gracefully', () => {
const filters = {
items: [
{
id: '1',
key: undefined,
op: OPERATORS['='],
value: 'test-value',
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(2);
expect(result.filter.expression).toBe("service.name = 'old-service'");
});
});
describe('convertAggregationToExpression', () => {
const mockAttribute: BaseAutocompleteData = {
id: 'test-id',
key: 'test_metric',
type: 'string',
dataType: DataTypes.String,
};
it('should return undefined when no aggregateOperator is provided', () => {
const result = convertAggregationToExpression({
aggregateOperator: '',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
});
expect(result).toBeUndefined();
});
it('should convert metrics aggregation with required temporality field', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'sum',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
timeAggregation: 'avg',
spaceAggregation: 'max',
alias: 'test_alias',
reduceTo: 'sum',
temporality: 'delta',
});
expect(result).toEqual([
{
metricName: 'test_metric',
timeAggregation: 'avg',
spaceAggregation: 'max',
reduceTo: 'sum',
temporality: 'delta',
},
]);
});
it('should handle noop operators by converting to count', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'noop',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
timeAggregation: 'noop',
spaceAggregation: 'noop',
});
expect(result).toEqual([
{
metricName: 'test_metric',
timeAggregation: 'count',
spaceAggregation: 'count',
},
]);
});
it('should handle missing attribute key gracefully', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'sum',
aggregateAttribute: { ...mockAttribute, key: '' },
dataSource: DataSource.METRICS,
});
expect(result).toEqual([
{
metricName: '',
timeAggregation: 'sum',
spaceAggregation: 'sum',
},
]);
});
it('should convert traces aggregation to expression format', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'count',
aggregateAttribute: mockAttribute,
dataSource: DataSource.TRACES,
alias: 'trace_alias',
});
expect(result).toEqual([
{
expression: 'count(test_metric)',
alias: 'trace_alias',
},
]);
});
it('should convert logs aggregation to expression format', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'avg',
aggregateAttribute: mockAttribute,
dataSource: DataSource.LOGS,
alias: 'log_alias',
});
expect(result).toEqual([
{
expression: 'avg(test_metric)',
alias: 'log_alias',
},
]);
});
it('should handle aggregation without attribute key for traces/logs', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'count',
aggregateAttribute: { ...mockAttribute, key: '' },
dataSource: DataSource.TRACES,
});
expect(result).toEqual([
{
expression: 'count()',
},
]);
});
it('should handle missing alias for traces/logs', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'sum',
aggregateAttribute: mockAttribute,
dataSource: DataSource.LOGS,
});
expect(result).toEqual([
{
expression: 'sum(test_metric)',
},
]);
});
it('should use aggregateOperator as fallback for time and space aggregation', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'max',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
});
expect(result).toEqual([
{
metricName: 'test_metric',
timeAggregation: 'max',
spaceAggregation: 'max',
},
]);
});
it('should handle undefined aggregateAttribute parameter with metrics', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'sum',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
});
expect(result).toEqual([
{
metricName: 'test_metric',
timeAggregation: 'sum',
spaceAggregation: 'sum',
reduceTo: undefined,
temporality: undefined,
},
]);
});
it('should handle undefined aggregateAttribute parameter with traces', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'noop',
aggregateAttribute: (undefined as unknown) as BaseAutocompleteData,
dataSource: DataSource.TRACES,
});
expect(result).toEqual([
{
expression: 'count()',
},
]);
});
it('should handle undefined aggregateAttribute parameter with logs', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'noop',
aggregateAttribute: (undefined as unknown) as BaseAutocompleteData,
dataSource: DataSource.LOGS,
});
expect(result).toEqual([
{
expression: 'count()',
},
]);
});
});

View File

@@ -1,12 +1,8 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
import {
DEPRECATED_OPERATORS_MAP,
OPERATORS,
QUERY_BUILDER_FUNCTIONS,
} from 'constants/antlrQueryConstants';
import { OPERATORS } from 'constants/antlrQueryConstants';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { cloneDeep, isEqual, sortBy } from 'lodash-es';
import { cloneDeep } from 'lodash-es';
import { IQueryPair } from 'types/antlrQueryTypes';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@@ -22,10 +18,9 @@ import {
TraceAggregation,
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { DataSource } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import { unquote } from 'utils/stringUtils';
import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
import { v4 as uuid } from 'uuid';
/**
@@ -91,32 +86,8 @@ export const convertFiltersToExpression = (
return '';
}
let operator = op.trim().toLowerCase();
if (Object.keys(DEPRECATED_OPERATORS_MAP).includes(operator)) {
operator =
DEPRECATED_OPERATORS_MAP[
operator as keyof typeof DEPRECATED_OPERATORS_MAP
];
}
if (isNonValueOperator(operator)) {
return `${key.key} ${operator}`;
}
if (isFunctionOperator(operator)) {
// Get the proper function name from QUERY_BUILDER_FUNCTIONS
const functionOperators = Object.values(QUERY_BUILDER_FUNCTIONS);
const properFunctionName =
functionOperators.find(
(func: string) => func.toLowerCase() === operator.toLowerCase(),
) || operator;
const formattedValue = formatValueForExpression(value, operator);
return `${properFunctionName}(${key.key}, ${formattedValue})`;
}
const formattedValue = formatValueForExpression(value, operator);
return `${key.key} ${operator} ${formattedValue}`;
const formattedValue = formatValueForExpression(value, op);
return `${key.key} ${op} ${formattedValue}`;
})
.filter((expression) => expression !== ''); // Remove empty expressions
@@ -141,6 +112,7 @@ export const convertExpressionToFilters = (
if (!expression) return [];
const queryPairs = extractQueryPairs(expression);
const filters: TagFilterItem[] = [];
queryPairs.forEach((pair) => {
@@ -163,57 +135,39 @@ export const convertExpressionToFilters = (
return filters;
};
const getQueryPairsMap = (query: string): Map<string, IQueryPair> => {
const queryPairs = extractQueryPairs(query);
const queryPairsMap: Map<string, IQueryPair> = new Map();
queryPairs.forEach((pair) => {
const key = pair.hasNegation
? `${pair.key}-not ${pair.operator}`.trim().toLowerCase()
: `${pair.key}-${pair.operator}`.trim().toLowerCase();
queryPairsMap.set(key, pair);
});
return queryPairsMap;
};
export const convertFiltersToExpressionWithExistingQuery = (
filters: TagFilter,
existingQuery: string | undefined,
): { filters: TagFilter; filter: { expression: string } } => {
// Check for deprecated operators and replace them with new operators
const updatedFilters = cloneDeep(filters);
// Replace deprecated operators in filter items
if (updatedFilters?.items) {
updatedFilters.items = updatedFilters.items.map((item) => {
const opLower = item.op?.toLowerCase();
if (Object.keys(DEPRECATED_OPERATORS_MAP).includes(opLower)) {
return {
...item,
op: DEPRECATED_OPERATORS_MAP[
opLower as keyof typeof DEPRECATED_OPERATORS_MAP
].toLowerCase(),
};
}
return item;
});
}
if (!existingQuery) {
// If no existing query, return filters with a newly generated expression
return {
filters: updatedFilters,
filter: convertFiltersToExpression(updatedFilters),
filters,
filter: convertFiltersToExpression(filters),
};
}
// Extract query pairs from the existing query
const queryPairs = extractQueryPairs(existingQuery.trim());
let queryPairsMap: Map<string, IQueryPair> = new Map();
const updatedFilters = cloneDeep(filters); // Clone filters to avoid direct mutation
const nonExistingFilters: TagFilterItem[] = [];
let modifiedQuery = existingQuery; // We'll modify this query as we proceed
const visitedPairs: Set<string> = new Set(); // Set to track visited query pairs
// Map extracted query pairs to key-specific pair information for faster access
let queryPairsMap = getQueryPairsMap(existingQuery.trim());
if (queryPairs.length > 0) {
queryPairsMap = new Map(
queryPairs.map((pair) => {
const key = pair.hasNegation
? `${pair.key}-not ${pair.operator}`.trim().toLowerCase()
: `${pair.key}-${pair.operator}`.trim().toLowerCase();
return [key, pair];
}),
);
}
filters?.items?.forEach((filter) => {
const { key, op, value } = filter;
@@ -242,37 +196,10 @@ export const convertFiltersToExpressionWithExistingQuery = (
existingPair.position?.valueEnd
) {
visitedPairs.add(`${key.key}-${op}`.trim().toLowerCase());
// Check if existing values match current filter values (for array-based operators)
if (existingPair.valueList && filter.value && Array.isArray(filter.value)) {
// Clean quotes from string values for comparison
const cleanValues = (values: any[]): any[] =>
values.map((val) => (typeof val === 'string' ? unquote(val) : val));
const cleanExistingValues = cleanValues(existingPair.valueList);
const cleanFilterValues = cleanValues(filter.value);
// Compare arrays (order-independent) - if identical, keep existing value
const isSameValues =
cleanExistingValues.length === cleanFilterValues.length &&
isEqual(sortBy(cleanExistingValues), sortBy(cleanFilterValues));
if (isSameValues) {
// Values are identical, preserve existing formatting
modifiedQuery =
modifiedQuery.slice(0, existingPair.position.valueStart) +
existingPair.value +
modifiedQuery.slice(existingPair.position.valueEnd + 1);
return;
}
}
modifiedQuery =
modifiedQuery.slice(0, existingPair.position.valueStart) +
formattedValue +
modifiedQuery.slice(existingPair.position.valueEnd + 1);
queryPairsMap = getQueryPairsMap(modifiedQuery);
return;
}
@@ -298,7 +225,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
notInPair.position.valueEnd + 1,
)}`;
queryPairsMap = getQueryPairsMap(modifiedQuery.trim());
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
} else if (
@@ -315,7 +241,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
equalsPair.position.valueEnd + 1,
)}`;
queryPairsMap = getQueryPairsMap(modifiedQuery);
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
} else if (
@@ -332,7 +257,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
notEqualsPair.position.valueEnd + 1,
)}`;
queryPairsMap = getQueryPairsMap(modifiedQuery);
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
}
@@ -354,7 +278,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
} ${formattedValue} ${modifiedQuery.slice(
notEqualsPair.position.valueEnd + 1,
)}`;
queryPairsMap = getQueryPairsMap(modifiedQuery);
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
}
@@ -367,23 +290,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
if (
queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
) {
const existingPair = queryPairsMap.get(
`${filter.key?.key}-${filter.op}`.trim().toLowerCase(),
);
if (
existingPair &&
existingPair.position?.valueStart &&
existingPair.position?.valueEnd
) {
const formattedValue = formatValueForExpression(value, op);
// replace the value with the new value
modifiedQuery =
modifiedQuery.slice(0, existingPair.position.valueStart) +
formattedValue +
modifiedQuery.slice(existingPair.position.valueEnd + 1);
queryPairsMap = getQueryPairsMap(modifiedQuery);
}
visitedPairs.add(`${filter.key?.key}-${filter.op}`.trim().toLowerCase());
}
@@ -580,25 +486,14 @@ export const convertHavingToExpression = (
* @returns New aggregation format based on data source
*
*/
export const convertAggregationToExpression = ({
aggregateOperator,
aggregateAttribute,
dataSource,
timeAggregation,
spaceAggregation,
alias,
reduceTo,
temporality,
}: {
aggregateOperator: string;
aggregateAttribute: BaseAutocompleteData;
dataSource: DataSource;
timeAggregation?: string;
spaceAggregation?: string;
alias?: string;
reduceTo?: ReduceOperators;
temporality?: string;
}): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
export const convertAggregationToExpression = (
aggregateOperator: string,
aggregateAttribute: BaseAutocompleteData,
dataSource: DataSource,
timeAggregation?: string,
spaceAggregation?: string,
alias?: string,
): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
// Skip if no operator or attribute key
if (!aggregateOperator) {
return undefined;
@@ -616,9 +511,7 @@ export const convertAggregationToExpression = ({
if (dataSource === DataSource.METRICS) {
return [
{
metricName: aggregateAttribute?.key || '',
reduceTo,
temporality,
metricName: aggregateAttribute.key,
timeAggregation: (normalizedTimeAggregation || normalizedOperator) as any,
spaceAggregation: (normalizedSpaceAggregation || normalizedOperator) as any,
} as MetricAggregation,
@@ -626,9 +519,7 @@ export const convertAggregationToExpression = ({
}
// For traces and logs, use expression format
const expression = aggregateAttribute?.key
? `${normalizedOperator}(${aggregateAttribute?.key})`
: `${normalizedOperator}()`;
const expression = `${normalizedOperator}(${aggregateAttribute.key})`;
if (dataSource === DataSource.TRACES) {
return [
@@ -648,16 +539,43 @@ export const convertAggregationToExpression = ({
];
};
export const getQueryTitles = (currentQuery: Query): string[] => {
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
const queryTitles: string[] = [];
// Handle builder queries with multiple aggregations
currentQuery.builder.queryData.forEach((q) => {
const aggregationCount = q.aggregations?.length || 1;
if (aggregationCount > 1) {
// If multiple aggregations, create titles like A.0, A.1, A.2
for (let i = 0; i < aggregationCount; i++) {
queryTitles.push(`${q.queryName}.${i}`);
}
} else {
// Single aggregation, just use query name
queryTitles.push(q.queryName);
}
});
// Handle formulas (they don't have aggregations, so just use query name)
const formulas = currentQuery.builder.queryFormulas.map((q) => q.queryName);
return [...queryTitles, ...formulas];
}
if (currentQuery.queryType === EQueryType.CLICKHOUSE) {
return currentQuery.clickhouse_sql.map((q) => q.name);
}
return currentQuery.promql.map((q) => q.name);
};
function getColId(
queryName: string,
aggregation: { alias?: string; expression?: string },
isMultipleAggregations: boolean,
): string {
if (isMultipleAggregations && aggregation.expression) {
return `${queryName}.${aggregation.expression}`;
}
return queryName;
return `${queryName}.${aggregation.expression}`;
}
// function to give you label value for query name taking multiaggregation into account
@@ -681,7 +599,7 @@ export function getQueryLabelWithAggregation(
const isMultipleAggregations = aggregations.length > 1;
aggregations.forEach((agg: any, index: number) => {
const columnId = getColId(queryName, agg, isMultipleAggregations);
const columnId = getColId(queryName, agg);
// For display purposes, show the aggregation index for multiple aggregations
const displayLabel = isMultipleAggregations

View File

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

View File

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

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