Compare commits

..

323 Commits

Author SHA1 Message Date
ahrefabhi
45d27304e1 Merge branch 'fix/tsc-trace-operator' of https://github.com/SigNoz/signoz into demo/trace-operators 2025-09-05 18:17:59 +05:30
ahrefabhi
535eed828d Merge branch 'feat/trace-operator-dashboards' of https://github.com/SigNoz/signoz into demo/trace-operators 2025-09-05 18:17:35 +05:30
ahrefabhi
6b9ada2a1e Merge branch 'feature/trace-operators' of https://github.com/SigNoz/signoz into demo/trace-operators 2025-09-05 18:17:16 +05:30
Abhi kumar
d85c4cf9bd Merge branch 'feature/trace-operators' into feat/trace-operator-dashboards 2025-09-05 18:14:10 +05:30
Abhi kumar
298647cf79 Merge branch 'feature/trace-operators' into fix/tsc-trace-operator 2025-09-05 18:14:00 +05:30
ahrefabhi
254d174962 chore: added beta tag in trace opeartor 2025-09-05 18:13:24 +05:30
ahrefabhi
ec2c9f3d0a Merge branch 'feature/trace-operators' into feat/trace-operator-dashboards 2025-09-05 17:58:45 +05:30
ahrefabhi
ab26d6d3b2 Merge branch 'feature/trace-operators' into fix/tsc-trace-operator 2025-09-05 17:58:09 +05:30
ahrefabhi
3275af484b chore: updated file names + regenerated grammer 2025-09-05 17:56:54 +05:30
eKuG
f2525fb293 Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-09-05 18:05:31 +07:00
eKuG
41716e16a2 feat: fixed span count in trace view 2025-09-05 18:04:34 +07:00
eKuG
c6dcdb8ba8 Merge branch 'demo/trace-operators' of github.com:SigNoz/signoz into demo/trace_operators_backend 2025-09-05 12:27:19 +07:00
eKuG
b6b3b5d6a6 Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-09-05 12:26:51 +07:00
eKuG
1a770f3b98 feat: added postprocess for timeseries and added limits to memory 2025-09-05 12:25:57 +07:00
ahrefabhi
28129fbcaf Merge branch 'feature/trace-operators' of https://github.com/SigNoz/signoz into demo/trace-operators 2025-09-04 21:53:32 +05:30
ahrefabhi
dcba183872 chore: removed returnspansfrom field from traceoperator 2025-09-04 21:52:14 +05:30
ahrefabhi
ef0785fa69 Merge branch 'fix/tsc-trace-operator' of https://github.com/SigNoz/signoz into demo/trace-operators 2025-09-04 21:03:43 +05:30
ahrefabhi
4a2ea9907c fix: fixed tsc issue 2025-09-04 21:01:52 +05:30
ahrefabhi
2419927f02 Merge branch 'feat/trace-operator-dashboards' of https://github.com/SigNoz/signoz into demo/trace-operators 2025-09-04 20:56:34 +05:30
ahrefabhi
d2c94a82d6 Merge branch 'fix/tsc-trace-operator' of https://github.com/SigNoz/signoz into demo/trace-operators 2025-09-04 20:56:17 +05:30
ahrefabhi
824576e176 Merge branch 'feature/trace-operators' of https://github.com/SigNoz/signoz into demo/trace-operators 2025-09-04 20:56:09 +05:30
ahrefabhi
f97001df91 Merge branch 'feature/trace-operators' of https://github.com/SigNoz/signoz into feat/trace-operator-dashboards 2025-09-04 20:53:37 +05:30
ahrefabhi
448a2533bb Merge branch 'feature/trace-operators' of https://github.com/SigNoz/signoz into fix/tsc-trace-operator 2025-09-04 20:50:52 +05:30
ahrefabhi
b69583d017 Merge branch 'main' of https://github.com/SigNoz/signoz into feature/trace-operators 2025-09-04 20:50:17 +05:30
ahrefabhi
f02ffeb4ff chore: added changes to keep indirect descendent operator at bottom 2025-09-04 20:49:43 +05:30
ahrefabhi
d22211443d feat: added changes related to saved in views in trace opeartor 2025-09-04 20:43:45 +05:30
eKuG
b2cb00d993 feat: added comment for build all spans cte 2025-09-04 19:36:30 +05:30
Abhi kumar
abeadc7672 fix: backward compatibility for explorer in case of aggregateAttribute is not present (#9000) 2025-09-04 13:21:16 +00:00
SagarRajput-7
faadc60c74 fix: fixed table panels not sorting, due to mismatch in lookup (id vs name) for aggregations (#9002)
* fix: fixed table panels not sorting, due to mismatch in id for aggregations

* fix: added test cases for the sort and util for qbv5 aggregation
2025-09-04 18:44:38 +05:30
ahrefabhi
0896ed9da9 fix: added fix for order by in trace opeartor 2025-09-04 18:23:14 +05:30
eKuG
d900076a77 Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-09-04 18:10:50 +05:30
ahrefabhi
56b153adc2 chore: removed using spans from in trace opeartors 2025-09-04 18:10:13 +05:30
eKuG
4ae881f1e2 feat: refactored fingerprinting 2025-09-04 17:54:27 +05:30
eKuG
439889400b feat: refactored fingerprinting 2025-09-04 17:43:09 +05:30
eKuG
d33d4b2a2e feat: refactored fingerprinting 2025-09-04 17:25:17 +05:30
Vibhu Pandey
360e8309c8 feat(password): implement strong controls for password (#8983)
## 📄 Summary

implement strong controls for password. Now the password requirement is : 

password must be at least 12 characters long, should contain at least one uppercase letter [A-Z], one lowercase letter [a-z], one number [0-9], and one symbol
2025-09-04 17:22:28 +05:30
eKuG
7f3cf5e3c2 feat: refactored fingerprinting 2025-09-04 16:48:17 +05:30
eKuG
a28d94e790 feat: refactored fingerprinting 2025-09-04 16:47:01 +05:30
eKuG
b25d38e246 Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-09-04 16:11:07 +05:30
SagarRajput-7
27580b62ba fix: fixed full view height for table panel (#9004) 2025-09-04 10:40:30 +00:00
Ekansh Gupta
4ce41aa586 Merge branch 'main' into trace_operator_implementation 2025-09-04 16:09:43 +05:30
eKuG
c9e7a19cc8 feat: refactored fingerprinting 2025-09-04 16:09:16 +05:30
SagarRajput-7
bcd21cee74 fix: y axis unit not interactive in the panel edit mode (#9003) 2025-09-04 16:00:03 +05:30
Nityananda Gohain
b59c5af060 Merge branch 'main' into trace_operator_implementation 2025-09-04 14:20:45 +05:30
Vikrant Gupta
2dbe0777f4 feat(authz): add openfga authz middleware (#8990)
* feat(authz): add openfga authz middleware

* feat(authz): update the auth context

* feat(authz): update the auth context

* feat(authz): update check request

* feat(authz): update check request

* feat(authz): add lifecycle tests

* feat(authz): add lifecycle tests

* feat(authz): add start-stop tests
2025-09-04 08:37:11 +00:00
eKuG
0cdb0253cd feat: refactored fingerprinting 2025-09-04 12:35:27 +05:30
eKuG
33f05d0745 feat: added deep copy in ranged queries 2025-09-04 12:26:47 +05:30
eKuG
13aa670972 feat: fixed merge conflicts 2025-09-04 12:20:47 +05:30
eKuG
1185a981c3 feat: fixed merge conflicts 2025-09-04 12:15:24 +05:30
eKuG
9a3feea008 Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-09-04 12:08:14 +05:30
eKuG
14c1e522fa feat: fixed merge conflicts 2025-09-04 12:03:28 +05:30
Ekansh Gupta
3a349d096a Merge branch 'main' into trace_operator_implementation 2025-09-04 11:15:50 +05:30
SagarRajput-7
7602d863dd fix: fixed table panel no scroll issue due to style override from calender component (#8996) 2025-09-04 09:55:46 +05:30
ahrefabhi
dfcbb40b62 Merge branch 'fix/tsc-trace-operator' into demo/trace-operators 2025-09-03 21:19:14 +05:30
ahrefabhi
7cf0d841ea chore: tsc fix 2025-09-03 21:17:31 +05:30
ahrefabhi
020b6c79d3 Merge branch 'feat/trace-operator-dashboards' of https://github.com/SigNoz/signoz into demo/trace-operators 2025-09-03 20:02:04 +05:30
ahrefabhi
2b67faa794 Merge branch 'fix/tsc-trace-operator' of https://github.com/SigNoz/signoz into demo/trace-operators 2025-09-03 20:01:46 +05:30
ahrefabhi
55509ad5c4 Merge branch 'feature/trace-operators' of https://github.com/SigNoz/signoz into demo/trace-operators 2025-09-03 20:01:25 +05:30
Abhi kumar
e7ab38e947 Merge branch 'feature/trace-operators' into fix/tsc-trace-operator 2025-09-03 20:00:52 +05:30
Abhi kumar
711b85f607 Merge branch 'feature/trace-operators' into feat/trace-operator-dashboards 2025-09-03 20:00:44 +05:30
Abhi kumar
8c19228f87 Merge branch 'main' into feature/trace-operators 2025-09-03 19:59:49 +05:30
Abhi kumar
68d9c6c3cc fix: added fix for incorrect query in dashboard on panel change to list panel (#8994)
* fix: added fix for incorrect query in dashboard on panel change to list issue

* test: added test for handleQueryChange

* chore: pr reviews
2025-09-03 14:00:22 +00:00
ahrefabhi
1632ad0396 Merge branch 'feature/trace-operators' of https://github.com/SigNoz/signoz into fix/tsc-trace-operator 2025-09-03 19:23:14 +05:30
ahrefabhi
e1c14a1dab Merge branch 'main' of https://github.com/SigNoz/signoz into feature/trace-operators 2025-09-03 19:22:33 +05:30
manika-signoz
10c6e1fac7 feat: add delete button to invite user flow (#8993) 2025-09-03 19:07:35 +05:30
ahrefabhi
81e9b70842 chore: minor changes for list panel 2025-09-03 18:24:49 +05:30
ahrefabhi
45923f9a9c feat: added changes for supporting trace operators in dashboards 2025-09-03 18:14:43 +05:30
Shaheer Kochai
3999a64c64 feat: add pinning functionality for span attributes (#8769)
* refactor: adjust the attribute pinning changes based on trace actionables latest changes

* feat: persist pinned attributes on the BE, fallback to local storage

* chore: overall improvement

* chore: fix the failing tests

* fix: make the changes w.r.t. pinned attributes preferences in in preference.go
2025-09-03 16:36:10 +04:30
ahrefabhi
82f81879c1 chore: added callout message for multiple queries without trace operators 2025-09-03 16:00:24 +05:30
Ekansh Gupta
363fdfd646 Merge branch 'main' into demo/trace-operators 2025-09-03 12:27:55 +05:30
eKuG
31a917820c Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-09-03 12:26:17 +05:30
eKuG
8fc9c09914 feat: fixed merge conflicts 2025-09-03 12:25:09 +05:30
ahrefabhi
a1ca15fc81 chore: changed trace operator query name 2025-09-02 17:42:51 +05:30
Vikrant Gupta
729bfb31f1 feat(authz): implement the current usecases in openfga (#8982)
* feat(authz): implement the current usecases in openfga

* feat(authz): implement the current usecases in openfga

* feat(authz): extract out the schema and DI the same

* feat(authz): extract out the schema and DI the same
2025-09-02 11:00:47 +00:00
Nityananda Gohain
052fb8b703 fix: support canDefaultZero for logs and traces (#8973)
* fix: support canDefaultZero for logs and traces

* fix: remove increase

* fix: move changes to req.go

* fix: add tood

* fix: address comments
2025-09-02 09:54:55 +00:00
nikhilmantri0902
5d9247f591 Allow deletion of multiple panels for dashboard updates made with API key (#8903) 2025-09-02 09:30:22 +00:00
eKuG
8357716c0a Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-09-02 13:10:37 +05:30
ahrefabhi
ea54aae57a Merge branch 'feature/trace-operators' into demo/trace-operators 2025-09-02 13:08:04 +05:30
ahrefabhi
7ae2ca503f Merge branch 'main' of https://github.com/SigNoz/signoz into demo/trace-operators 2025-09-02 13:07:56 +05:30
Yunus M
c0a9948146 feat: handle active log flow (#8946)
* feat: handle active log flow

* feat: show live logs in logs explorer view

* feat: enable live logs in logs explorer

* feat: show live time option only in logs list view

* chore: pass showLiveLogs as false in test cases

* fix: handle live logs data format to open in log details

* fix: use current query state for frequency chart in live logs view

* fix: encode filter expression, show live option only in list view
2025-09-02 11:05:52 +05:30
eKuG
a2f9eccc8b feat: updated time series query 2025-09-02 01:16:53 +05:30
eKuG
e355c944c8 feat: replaced info to debug logs 2025-09-01 22:44:41 +05:30
eKuG
a805bdb637 feat: replaced info to debug logs 2025-09-01 22:43:13 +05:30
eKuG
fcfaf152b2 feat: replaced info to debug logs 2025-09-01 22:41:16 +05:30
eKuG
3ad600c4df feat: resolved conflicts 2025-09-01 22:38:41 +05:30
eKuG
7e3d17ce5f feat: resolved conflicts 2025-09-01 22:34:43 +05:30
Ekansh Gupta
bc888539e0 Merge branch 'main' into trace_operator_implementation 2025-09-01 22:25:25 +05:30
eKuG
688867b708 feat: resolved conflicts 2025-09-01 22:25:08 +05:30
eKuG
23948e72eb feat: resolved conflicts 2025-09-01 22:23:58 +05:30
Vikrant Gupta
f3569a9a02 chore(meter): remove the meter data validity message for non cloud users (#8972) 2025-09-01 16:18:47 +00:00
Vikrant Gupta
0df1ed3b57 fix(dashboard): remove context from dashboard types (#8971) 2025-09-01 16:08:25 +00:00
Nityananda Gohain
d0132f11ae fix: update clickhouse-sql-parser (#8970) 2025-09-01 13:25:27 +00:00
Vikrant Gupta
f61e859901 feat(authz): embed openfga server (#8966)
* feat(access-control): embed openfga in signoz

* feat(authz): rename access control to authz

* feat(authz): fix codeowners and go mod tidy

* feat(authz): fix lint

* feat(authz): update go version and move convertor to instrumentation

* feat(authz): some more lint issues

* feat(authz): some more lint issues

* feat(authz): some more lint issues

* feat(authz): fix more lint issues

* feat(authz): make logger converter interface
2025-09-01 17:10:13 +05:30
Ekansh Gupta
4daec45d98 feat: added custom retention for logs api (#8513)
* feat: added custom retention for logs api

* feat: added custom retention for logs api

* feat: added implementation of trace operators

* feat: added validation checks for resource keys

* feat: added validation checks for resource keys

* feat: added validation checks for resource keys

* feat: added validation checks for resource keys

* feat: added validation checks for resource keys

* feat: added validation checks for resource keys

* feat: added validation checks for resource keys

* feat: added validation checks for resource keys

* feat: added default checks for custom retention

* feat: added default checks for custom retention

* feat: added default checks for custom retention

* feat: added change for ttl

* feat: v2 api supports both v1 and v2

* feat: v2 api supports both v1 and v2

* feat: v2 api supports both v1 and v2

* feat: v2 api supports both v1 and v2

* feat: added default_ttl in v1

* feat: added set logs ttl v1 from v2

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts
2025-09-01 07:30:57 +00:00
Ekansh Gupta
382d9d4a87 Revert "3rd party API sem conv fix (Supports >1.26) (#8822)" (#8954)
This reverts commit 396e0cdc2d.
2025-09-01 07:20:56 +00:00
Nityananda Gohain
87ce197631 fix: don't skip resource filter in main table for OR queries (#8958)
* fix: don't skip resource filter in main table for OR queries

* fix: dont skip resource table

* fix: make check case insensitive

* fix: iterate over token stream
2025-08-30 23:16:56 +05:30
aniketio-ctrl
3cc5a24a4b fix: return 404 status code if rule not found (#8940)
* fix(rule-alert-state): added 404 status code for invalid rules
2025-08-30 12:32:03 +05:30
Yunus M
9b8a892079 chore: use infinity table for logs column view (#8953) 2025-08-29 16:12:27 +05:30
Ekansh Gupta
396e0cdc2d 3rd party API sem conv fix (Supports >1.26) (#8822)
* feat: adding support for 1.26 semconv

* feat: adding support for 1.26 semconv

* feat: adding support for 1.26 semconv

* feat: adding support for 1.26 semconv

* feat: added native support for 1.26

* feat: added native support for 1.26

* feat: adding support for 1.26 semconv

* feat: adding support for 1.26 semconv

* feat: adding support for 1.26 semconv

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts
2025-08-29 09:50:16 +00:00
Abhi kumar
c838d7e2d4 chore: added api chagnes for logs retenetion v2 api (#8649)
* chore: added api chagnes for logs retenetion v2 api

* chore: added pr review fixes

* chore: minor fix

* feat: added api changes for setting retention period

* chore: pr review fixes

* chore: removed return statement

* fix: pr reviews

* fix: pr review
2025-08-29 15:10:15 +05:30
Nityananda Gohain
1a193fb1a9 fix: update email template for update role (#8610)
* fix: update email template for update role

* fix: remove space
2025-08-29 06:51:03 +00:00
Yunus M
88dff3f552 feat: minor ui updates (#8947) 2025-08-29 11:36:32 +05:30
Nityananda Gohain
5bb6d78c42 fix: remove isRoot and Entrypoint from selectfields (#8893)
* fix: remove isRoot and Entrypoint from selectfields

* fix: add comment

* fix: add comment

* fix: move logic to validation

* fix: remove requestType trace

* fix: update comment

* fix: update error message
2025-08-29 05:46:16 +00:00
Yunus M
369f77977d feat: use update props from data table component for better UX (#8950) 2025-08-29 10:06:43 +05:30
Vikrant Gupta
836605def5 feat(ingestion): add ingestion id to details (#8949) 2025-08-29 00:47:42 +05:30
Yunus M
cc80923265 feat: show timestamp in selected timezone format (#8948) 2025-08-29 00:18:05 +05:30
Yunus M
92e5986af2 feat: replace infinity list view component with data table component (#8904)
* feat: replace infinity list view component with data table component

* feat: remove duplicate hook calls

* chore: add @signozhq/table to transformIgnorePatterns

* feat: update test cases for frequency chart in logs explorer

* feat: address review comments , add sonner for notifications

* feat: address review comments
2025-08-28 19:03:29 +05:30
ahrefabhi
d0e5f6b478 test: added test for traceopertor util 2025-08-28 17:54:30 +05:30
ahrefabhi
8c75ba298a chore: moved traceoperator utils 2025-08-28 17:46:04 +05:30
ahrefabhi
bc217a2aa3 fix: added tsc fixes for trace operator 2025-08-28 16:49:05 +05:30
ahrefabhi
6c0b5abbc0 chore: minor ui issue fix 2025-08-28 15:36:22 +05:30
Abhi kumar
912a34da8d fix: added fix for context query not getting updated (#8941) 2025-08-28 15:14:18 +05:30
ahrefabhi
0234829492 chore: updated docs link 2025-08-28 14:31:01 +05:30
ahrefabhi
fba946bf78 chore: fixed logic to show trace operator 2025-08-28 13:13:04 +05:30
primus-bot[bot]
8b99ba0f9f chore(release): bump SigNoz to v0.93.0, OTel Collector to v0.129.2 (#8939)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-08-28 12:26:46 +05:30
ahrefabhi
55f96ca95f Merge branch 'main' of https://github.com/SigNoz/signoz into feature/trace-operators 2025-08-28 12:22:06 +05:30
ahrefabhi
20a87db5bc test: fixed breaking mapQueryDataFromApi test 2025-08-28 12:21:22 +05:30
ahrefabhi
ca774fe6a2 test: minor test fix 2025-08-28 11:59:42 +05:30
ahrefabhi
451c4bdeb7 chore: removed check for multiple queries in traceexplorer 2025-08-28 11:58:33 +05:30
Nityananda Gohain
841abf8c0b fix: update live tail api (#8807)
* fix: update live tail api

* fix: minor fixes

* fix: more minor fixes

* fix: remove zap

* fix: correct spelling

* fix: tests

* fix: lint issues

* fix: remove debug

* fix: address comments

* fix: update name

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-08-28 11:44:43 +05:30
ahrefabhi
dcd0de35a4 test: added test for traceoperatorcontext 2025-08-28 10:55:19 +05:30
ahrefabhi
00829423bd Merge branch 'main' of https://github.com/SigNoz/signoz into feature/trace-operators 2025-08-28 10:37:05 +05:30
Vishal Sharma
df54e6350d fix: trace explorer and home page keyboard shortcut (#8934) 2025-08-27 18:19:54 +00:00
ahrefabhi
585fadb867 Merge branch 'main' of https://github.com/SigNoz/signoz into feature/trace-operators 2025-08-27 11:43:15 +05:30
ahrefabhi
4a1e786f4e fix: updated grammer files 2025-08-27 11:39:59 +05:30
Srikanth Chekuri
f6bc30050b fix: exclusion operators use AND combinator on ambiguity (#8928) 2025-08-26 14:22:23 +00:00
Srikanth Chekuri
1e76046c7c chore: add value search for related values request (#8925) 2025-08-26 19:37:25 +05:30
SagarRajput-7
910751713d fix: removed transformstringwithprefix and removeprefix utilities (#8922)
* fix: removed transformstringwithprefix and removeprefix utilities

* fix: fixed testcase
2025-08-26 16:13:08 +05:30
Vikrant Gupta
85c671c8d5 fix(meter): meter regex for comment middleware (#8921) 2025-08-26 08:12:31 +00:00
Yunus M
4d2094b4ce feat: enable global actions (#8906)
* feat: enable global actions

* feat: separate actions for collapse and open sidebar

* fix: remove spaces from shortcuts

* chore: remove console log

* chore: yarn install to update yarn.lock
2025-08-26 13:28:28 +05:30
Shaheer Kochai
32410baa72 feat: display HTTP status badge in trace details v2 spans (#8699)
* feat: display http status badge in trace details v2 spans

* chore: change the fallback background for status code badge

* fix: align the status badge to the end of span details column

* chore: fix the failing tests
2025-08-26 06:38:23 +00:00
Yunus M
2a5fb9fd6f feat: date picker v2 (#8886)
* feat: date picker v2

* feat: custom date time range history

* feat: light mode updates and interaction fixes

* fix: improve usability

* chore: add calendar, input and popover to transformIgnorePatterns

* chore: add date-fns to transformIgnorePatterns

* chore: add @signozhq/button to transformIgnorePatterns

* feat: update css variables
2025-08-26 06:13:23 +00:00
Nityananda Gohain
514bceca34 feat: support for hasToken (#8891)
* feat: support for hasToken

* fix: address comments

* fix: address comments
2025-08-26 05:58:31 +00:00
Shaheer Kochai
ac7d8bcde2 chore: add tests for trace details actionables (#8840) 2025-08-26 05:48:25 +00:00
Shaheer Kochai
88312e971d fix: fix the trace explorer back navigation issue (#8760) 2025-08-26 04:47:28 +00:00
Shaheer Kochai
17533b2f1c fix: fix the issue of group by queries not switching to timeseries view in logs and traces explorer (#8870)
* fix: fix the issue of group by queries not switching to timeseries view in logs explorer

* fix: fix the issue of group by queries not switching to timeseries view in traces explorer

* chore: overall improvements
2025-08-25 13:05:50 +00:00
SagarRajput-7
c4044fa2c5 feat: fixed panel correlation and alert multiaggregation issues (#8733)
* feat: fixed panel coorelation not spreading the filter expression in explorer pages

* feat: fixed multiagregation not getting sent in create alert

* fix: fixed failing test cases

* Update frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* fix: fix lint error

* fix: stepInterval not updating in panel qb

* fix: added test cases for mapQueryDataFromApi and prepareQueryRangePayloadV5

* fix: added convertV5Response test cases - timeseries, pie and table

* fix: refactored handleRunQuery

* fix: code refactor

* fix: refactored the mapQueryDataFromApi function according to new sub_var api

* fix: updated test cases

* fix: removed isJSON and isColumn from everywhere

* fix: fixed code and test cases

* fix: fixed bar chart custom - step interval for qb v5 (#8806)

* fix: added querytype boolean check for v5 changes

* fix: fixed typechecks

* fix: fixed typechecks

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-08-25 11:15:17 +00:00
Abhi kumar
deddf47e84 fix: added fix for query builder filters (#8830)
* fix: added fix for query builder filters

* fix: added fix for multivalue operator without brackets

* test: added tests for querycontextUtils + querybuilderv2 utils

* fix: added fix for replacing filter with the new value

* fix: added fix for replacing filters + datetimepicker composite query

* test: fixed querybuilderv2 utils test

* chore: added changes for jest to use es6

* test: fixed tests for querycontextutils + querybuilderv2 utils

* test: fixed failing tests
2025-08-25 16:35:16 +05:30
Vishal Sharma
08323e4dfd chore: update dashboard template links to SigNoz website dashboard template landing page (#8897) 2025-08-25 13:29:55 +05:30
Vibhu Pandey
ee19f1749b fix(web): fix panic on nil file info (#8907)
fix panic on nil file info
2025-08-25 09:01:32 +05:30
eKuG
0454a92b80 Merge branch 'demo/trace-operators' of github.com:SigNoz/signoz into demo/trace_operators_backend 2025-08-24 20:35:14 +05:30
eKuG
2d6d342ef0 Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-08-24 20:35:03 +05:30
eKuG
fa06bac37b feat: resolved conflicts 2025-08-24 20:34:40 +05:30
Ekansh Gupta
deb821617b Merge branch 'main' into trace_operator_implementation 2025-08-24 20:03:40 +05:30
Ekansh Gupta
f8b2bda431 Merge branch 'main' into demo/trace-operators 2025-08-24 20:03:10 +05:30
eKuG
6d95095d2f Merge branch 'demo/trace-operators' of github.com:SigNoz/signoz into demo/trace_operators_backend 2025-08-24 20:02:17 +05:30
eKuG
28a2ed4273 Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-08-24 20:01:51 +05:30
eKuG
0fee724730 feat: resolved conflicts 2025-08-24 20:01:25 +05:30
Vikrant Gupta
b21db878e8 chore(meter): added product analytics for meter module (#8898)
* chore(meter): added product analytics for meter package

* chore(meter): added product analytics for meter package
2025-08-24 14:21:45 +05:30
ahrefabhi
6f99d54a50 Merge branch 'feature/trace-operators' into demo/trace-operators 2025-08-24 13:44:30 +05:30
ahrefabhi
8dd3130701 chore: minor ui fix 2025-08-24 13:43:43 +05:30
ahrefabhi
8eaa609076 fix: pr reviews 2025-08-24 13:39:01 +05:30
ahrefabhi
ac3c98b112 feat: added queryname boost + operator constants 2025-08-24 13:37:15 +05:30
ahrefabhi
c0b96ed103 feat: added traceoperator editor 2025-08-24 13:21:42 +05:30
ahrefabhi
608d1565c0 chore: added traceoperator validation function 2025-08-24 13:16:31 +05:30
ahrefabhi
e665d7c352 feat: added traceoperator context util 2025-08-24 13:15:48 +05:30
ahrefabhi
a5f9273743 feat: added trace operator grammer + antlr files 2025-08-24 13:11:00 +05:30
Srikanth Chekuri
a7ddd2ddf0 chore: do not send field context as tag for deprecated fields (#8902) 2025-08-24 11:14:12 +05:30
Srikanth Chekuri
4d72f47758 chore: parse into number alias for mat column from statement (#8900) 2025-08-24 09:44:58 +05:30
Vikrant Gupta
b5b513f1e0 chore(meter): add warnings and make meter live in sidenav (#8882)
* chore(meter): add warnings and make meter live in sidenav

* chore(meter): add warnings and make meter live in sidenav

* chore(meter): add warnings and make meter live in sidenav

* chore(meter): add warnings and make meter live in sidenav

* chore(meter): add warnings and make meter live in sidenav

* chore(meter): add warnings and make meter live in sidenav
2025-08-23 15:00:07 +05:30
Nityananda Gohain
4878f725ea fix: use lower and convert re2 to string in fulltext (#8887)
* fix: use lower and convert re2 to string in fulltext

* fix: minor error change

* fix: address comments

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-08-22 20:20:42 +05:30
Srikanth Chekuri
eca13075e9 fix: related links for rule history page (#8883) 2025-08-22 16:19:27 +05:30
eKuG
7a79a16300 Merge branch 'demo/trace-operators' of github.com:SigNoz/signoz into demo/trace_operators_backend 2025-08-22 12:24:01 +05:30
eKuG
c39f48a41e Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-08-22 12:23:44 +05:30
eKuG
d492d00976 feat: resolved conflicts 2025-08-22 12:23:24 +05:30
Ekansh Gupta
4c0d2f0e6f Merge branch 'main' into trace_operator_implementation 2025-08-21 23:50:33 +05:30
Ekansh Gupta
02126b65b1 Merge branch 'main' into demo/trace-operators 2025-08-21 23:50:05 +05:30
eKuG
5235f65d9a Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-08-21 23:49:35 +05:30
eKuG
43457eedc0 feat: resolved conflicts 2025-08-21 23:49:13 +05:30
Amlan Kumar Nandy
e5ab664483 fix: resolve sentry issues in alert list (#8878)
* fix: resolve sentry issues in alert list

* chore: update the key

---------

Co-authored-by: srikanthccv <srikanth.chekuri92@gmail.com>
2025-08-21 19:21:15 +05:30
eKuG
40c6458b31 Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-08-21 18:12:15 +05:30
eKuG
f70f238b84 feat: resolved conflicts 2025-08-21 18:11:47 +05:30
eKuG
43d0cee5b5 Merge branch 'demo/trace-operators' of github.com:SigNoz/signoz into demo/trace_operators_backend 2025-08-21 17:53:01 +05:30
eKuG
33e7d852df Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-08-21 17:52:49 +05:30
eKuG
5e968ec202 feat: resolved conflicts 2025-08-21 17:52:30 +05:30
ahrefabhi
bbad7dca3e Merge branch 'feature/trace-operators' of https://github.com/SigNoz/signoz into demo/trace-operators 2025-08-21 16:30:01 +05:30
ahrefabhi
7ce278778f feat: added changes for showing querynames in alerts 2025-08-21 16:28:36 +05:30
eKuG
f09b79e04f Merge branch 'demo/trace-operators' of github.com:SigNoz/signoz into demo/trace_operators_backend 2025-08-21 16:03:40 +05:30
eKuG
1c72861290 Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-08-21 16:03:21 +05:30
eKuG
9116c02e1c feat: resolved conflicts 2025-08-21 16:03:02 +05:30
ahrefabhi
5d3254eeeb Merge branch 'feature/trace-operators' into demo/trace-operators 2025-08-21 15:19:57 +05:30
eKuG
44a3fbfdd6 Merge branch 'demo/trace-operators' of github.com:SigNoz/signoz into demo/trace_operators_backend 2025-08-21 14:56:44 +05:30
eKuG
0dd41a07bd Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-08-21 14:56:04 +05:30
eKuG
6f8de8da4c feat: resolved conflicts 2025-08-21 14:55:43 +05:30
ahrefabhi
a5f57db0c7 chore: lint fixes 2025-08-21 14:55:04 +05:30
ahrefabhi
83f46aeff6 Merge branch 'feature/trace-operators' into demo/trace-operators 2025-08-21 14:12:11 +05:30
ahrefabhi
7372bf0291 chore: updated type 2025-08-21 14:11:23 +05:30
ahrefabhi
ee78805888 chore: linting fix + icon changes 2025-08-21 14:09:35 +05:30
eKuG
f6547210b2 Merge branch 'demo/trace-operators' of github.com:SigNoz/signoz into demo/trace_operators_backend 2025-08-21 13:50:53 +05:30
eKuG
7206bb82fe Merge branch 'trace_operator_implementation' into demo/trace_operators_backend 2025-08-21 13:44:01 +05:30
eKuG
a1ad2b7835 feat: resolved conflicts 2025-08-21 13:43:17 +05:30
ahrefabhi
4cb70ec07e test: fixed mapquerydatafromapi test 2025-08-21 13:25:53 +05:30
ahrefabhi
0469233063 Merge branch 'feature/trace-operators' into demo/trace-operators 2025-08-21 13:17:15 +05:30
ahrefabhi
f3621e14bf chore: minor fix in traceoperator styles 2025-08-21 13:16:50 +05:30
ahrefabhi
fd035d885e fix: added limit support in traceoperator 2025-08-21 13:12:39 +05:30
ahrefabhi
c516825e41 fix: fixed minor ts issues 2025-08-21 12:57:16 +05:30
ahrefabhi
188ff014d1 Merge branch 'trace_operator_implementation' of https://github.com/SigNoz/signoz into demo/trace-operators 2025-08-21 10:21:20 +05:30
eKuG
a2ab97a347 Merge branch 'demo/trace-operators' of github.com:SigNoz/signoz into demo/trace_operators_backend 2025-08-21 10:18:35 +05:30
ahrefabhi
da7cdec01f Merge branch 'main' of https://github.com/SigNoz/signoz into feature/trace-operators 2025-08-21 10:17:46 +05:30
ahrefabhi
7c1ca7544d Merge branch 'feature/trace-operators' into demo/trace-operators 2025-08-21 10:15:51 +05:30
ahrefabhi
1b0dcb86b5 chore: linter fix 2025-08-21 09:50:35 +05:30
ahrefabhi
cb49bc795b chore: minor pr review change 2025-08-21 01:33:10 +05:30
ahrefabhi
3f1aeb3077 chore: added traceoperators in alerts 2025-08-21 01:31:40 +05:30
ahrefabhi
cc2a905e0b chore: minor changes in queryaddon and aggregation for support 2025-08-21 01:30:37 +05:30
ahrefabhi
eba024fc5d chore: removed traceoperations and reused queryoperations 2025-08-21 01:29:30 +05:30
ahrefabhi
561ec8fd40 chore: added ui changes in the editor 2025-08-21 01:28:32 +05:30
ahrefabhi
aa1dfc6eb1 feat: added span selector 2025-08-21 01:27:58 +05:30
Vibhu Pandey
a3f32b3d85 fix(comment): add a dedicated comment parsing middleware (#8855)
## 📄 Summary

- add a dedicated comment parsing middleware. This removes duplication and double parsing of referrer.
2025-08-20 20:20:28 +05:30
eKuG
3248012716 feat: resolved conflicts 2025-08-20 18:59:21 +05:30
eKuG
4ce56ebab4 feat: resolved conflicts 2025-08-20 18:58:43 +05:30
eKuG
bb80d69819 feat: resolved conflicts 2025-08-20 17:32:15 +05:30
eKuG
49aaecd02c feat: resolved conflicts 2025-08-20 17:30:52 +05:30
eKuG
98f4e840cd feat: resolved conflicts 2025-08-20 17:20:44 +05:30
eKuG
74824e7853 feat: resolved conflicts 2025-08-20 16:59:01 +05:30
ahrefabhi
b574fee2d4 chore: fixed minor styles + minor ux fix 2025-08-20 15:18:11 +05:30
eKuG
675b66a7b9 feat: resolved conflicts 2025-08-20 12:18:37 +05:30
Ekansh Gupta
f55aeb5b5a Merge branch 'main' into trace_operator_implementation 2025-08-20 11:45:46 +05:30
eKuG
ae3806ce64 feat: resolved conflicts 2025-08-20 11:45:04 +05:30
ahrefabhi
9c489ebc84 chore: Added changes to prepare request payload 2025-08-20 11:24:19 +05:30
ahrefabhi
f6d432cfce chore: added initialvalue for trace operators 2025-08-20 11:23:42 +05:30
ahrefabhi
6ca6f615b0 chore: type changes 2025-08-20 11:22:40 +05:30
ahrefabhi
36e7820edd chore: minor UI fixes 2025-08-20 11:21:16 +05:30
ahrefabhi
f51cce844b feat: added conditions for traceoperator 2025-08-20 11:20:51 +05:30
ahrefabhi
b2d3d61b44 chore: minor style improvments 2025-08-20 11:20:06 +05:30
ahrefabhi
4e2c7c6309 feat: added traceoperator component and styles 2025-08-20 11:19:35 +05:30
Amlan Kumar Nandy
9c2f127282 chore: backend changes for y-axis management (#8730) 2025-08-20 04:04:50 +00:00
eKuG
885045d704 feat: resolved conflicts 2025-08-19 13:41:23 +05:30
Ekansh Gupta
9dc2e82ce1 Merge branch 'main' into trace_operator_implementation 2025-08-19 13:10:39 +05:30
Srikanth Chekuri
e30de5f13e chore: do not store query name in cache (#8838) 2025-08-19 13:01:55 +05:30
SagarRajput-7
019083983a fix: added sanity logic for explorer old urls (#8804)
* fix: added sanity logic for explorer old urls

* fix: added test for useSanitizeOrderBy

* fix: added sentry events for orderby validation

* fix: cleanup unused logic and renamed boolean state
2025-08-19 12:46:02 +05:30
eKuG
19e60ee688 feat: resolved conflicts 2025-08-19 12:26:51 +05:30
Shaheer Kochai
fdcad997f5 feat: trace detail page actionables (#8761)
* feat: add table view with actionables to span details drawer attributes

* feat: span actions functionality

* refactor: overall improvements

* feat: revert to key-value pair UI with hover actions

* feat: add support for copying trace attribute value on click

* refactor: prevent prop drilling and access return values from useTraceActions in AttributeActions

* refactor: integrate filter conversion logic into useTraceActions for improved query handling
2025-08-19 06:50:28 +00:00
Yunus M
03359a40a2 fix: set source on add new query (#8836)
Co-authored-by: Vikrant Gupta <vikrant@signoz.io>
2025-08-19 05:51:33 +00:00
eKuG
ea89714cb4 feat: resolved conflicts 2025-08-19 11:20:32 +05:30
Abhi kumar
4f45801729 fix: added fix for supporting older queries (#8834)
* fix: added fix for supporting older queries

* fix: added fix for exist operator

* chore: minor fix for quick filters

* chore: added tests for convertfilterstoexpression

* chore: added fix for regex to regexp conversion

* test: added test for regex to regexp

* fix: added fix for functions conversion and tests

* fix: added fix for negated non_value_operators
2025-08-19 10:55:45 +05:30
Amlan Kumar Nandy
674556d672 chore: add new y-axis unit selector (#8765) 2025-08-19 04:55:46 +00:00
Amlan Kumar Nandy
af987e53ce chore: add unit tests for k8s entity details (#8774) 2025-08-19 04:28:39 +00:00
Vikrant Gupta
59d5accd33 fix(meter): meter where clause keys fix (#8833) 2025-08-18 22:57:02 +05:30
manika-signoz
5a7ad670d8 feat: change copy /signup route (#8783)
* feat: change copy /signup route

* feat: remove firstName from payload
2025-08-18 16:48:09 +00:00
Abhi kumar
9d04b397ac fix: added fix for code block in light mode (#8831) 2025-08-18 22:00:58 +05:30
Vikrant Gupta
a4f3be5e46 feat(meter): add pre-defined panels for meter breakdown and improvements (#8827)
* feat(meter): add pre-defined panels for meter breakdown

* feat(meter): update the routes for future scope

* feat(meter): added graphs for total calculation

* feat(meter): added graphs for total calculation
2025-08-18 19:44:44 +05:30
eKuG
4be618bcde feat: resolved conflicts 2025-08-18 16:45:47 +05:30
eKuG
2bfecce3cb feat: resolved conflicts 2025-08-18 16:17:48 +05:30
eKuG
eefbcbd1eb feat: resolved conflicts 2025-08-18 15:43:49 +05:30
eKuG
a3f366ee36 feat: resolved conflicts 2025-08-18 15:35:45 +05:30
eKuG
cff547c303 feat: resolved conflicts 2025-08-18 15:28:53 +05:30
Ekansh Gupta
d6287cba52 Merge branch 'main' into trace_operator_implementation 2025-08-18 15:26:31 +05:30
eKuG
44b09fbef2 feat: resolved conflicts 2025-08-18 15:26:08 +05:30
Srikanth Chekuri
8f833fa62c fix: incorrect query prepared for group by body.{key} (#8823) 2025-08-18 15:11:53 +05:30
dependabot[bot]
7029233596 chore(deps): bump @babel/runtime from 7.21.0 to 7.28.2 in /frontend (#8726)
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.21.0 to 7.28.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.2/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-version: 7.28.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-18 07:09:52 +00:00
Vibhu Pandey
d26efd2833 feat: address bitnami migration (#8808) 2025-08-14 20:54:28 +05:30
Abhi kumar
0e3ac2a179 fix: added loading indicators in traces pages when running query (#8782) 2025-08-14 13:53:39 +05:30
Amlan Kumar Nandy
249f8be845 fix: resolve infinite loading issue in metric view in messaging queues (#8779) 2025-08-14 04:16:39 +00:00
primus-bot[bot]
9c952942ad chore(release): bump to v0.92.1 (#8780)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-08-13 15:10:08 +05:30
Nityananda Gohain
dac46d82ff fix: check ch version (#8778)
Check the clickhouse version, before the setting secondary_indices_enable_bulk_filtering is used.
2025-08-13 14:57:25 +05:30
primus-bot[bot]
802ce6de01 chore(release): bump to v0.92.0 (#8776)
#### Summary
 - Release SigNoz v0.92.0
 - Bump SigNoz OTel Collector to v0.129.0
2025-08-13 12:17:43 +05:30
dependabot[bot]
6853f0c99d chore(deps): bump urllib3 from 2.4.0 to 2.5.0 in /tests/integration (#8296) 2025-08-13 04:58:39 +00:00
Srikanth Chekuri
3f8a2870e4 fix: key CONTAINS value doesn't work for numeric values (#8768) 2025-08-13 09:59:28 +05:30
Srikanth Chekuri
5fa70ea802 chore: use *_keys tables instead of tag_attributes_v2 for suggestions (#8753) 2025-08-12 18:10:35 +05:30
Yunus M
3a952fa330 fix: pass metric name to get value suggestions api (#8671)
* fix: pass metric name to get value suggestions api

* feat: add source to get value suggestions
2025-08-11 08:10:31 +00:00
eKuG
081eb64893 feat: resolved conflicts 2025-08-11 13:03:23 +05:30
eKuG
6338af55dd feat: resolved conflicts 2025-08-11 12:44:17 +05:30
eKuG
5450b92650 feat: resolved conflicts 2025-08-11 11:52:33 +05:30
Ekansh Gupta
a9179321e1 Merge branch 'main' into trace_operator_implementation 2025-08-11 11:48:28 +05:30
eKuG
90366975d8 feat: resolved conflicts 2025-08-11 11:48:13 +05:30
eKuG
33f47993d3 feat: resolved conflicts 2025-08-11 11:46:47 +05:30
eKuG
9170846111 feat: resolved conflicts 2025-08-11 11:44:03 +05:30
Yunus M
6d97db1d9d fix: use localstorage value to avoid waiting for pref api to set the toggle state, add shortcut (#8751) 2025-08-11 10:26:27 +05:30
Shaheer Kochai
5412e7f70b feat: show count in span details drawer tabs (#8702)
* feat: show event count in Events tab of SpanDetailsDrawer

* feat: add count badges to all SpanDetailsDrawer tabs
2025-08-10 05:39:20 +00:00
aniketio-ctrl
8e5cb9046d fix(alert): added querier v5 in test notify (#8749) 2025-08-08 18:01:23 +05:30
Srikanth Chekuri
760eabb2dc chore: do not return err for meter source temporality (#8750) 2025-08-08 17:39:06 +05:30
Srikanth Chekuri
35ddaaa2fc chore: add env to override logs keys table name (#8748) 2025-08-08 11:34:09 +00:00
nikhilmantri0902
a51ee66c02 Improvement: Added Otel-collector setup for local dev environment (#8701)
* feat(devenv): add otel-collector support for local development

- Add .devenv/docker/otel-collector/ with compose.yaml and config
- Add devenv-otel-collector and devenv-up targets to Makefile
- Update development.md with otel-collector setup instructions
- Add README.md with usage documentation for otel-collector setup

This enables developers to run the complete SigNoz stack locally,
including the OpenTelemetry Collector for receiving telemetry data
on ports 4317 (gRPC) and 4318 (HTTP).

* docs: improve collector setup wordings

* chore: fixed comment and service name

* chore: docker service name updated otel-collector -> signoz-otel-collector
2025-08-08 16:54:05 +05:30
Yunus M
75d189162b feat: migrate old saved columns keys to name (#8747) 2025-08-08 14:41:34 +05:30
Yunus M
932918e3a4 feat: meter explorer (#8741)
* feat: meter explorer

* feat: meter explorer

* fix: remove meter as data source

* fix: change meter-explorer to meter - quick filter

* chore: delete test file

* fix: failing test cases
2025-08-08 12:03:26 +05:30
Vibhu Pandey
aa3bc16dcb test(integration): bump requests to 2.32.4 (#8743) 2025-08-08 00:25:38 +05:30
Yunus M
b5098e00a3 fix: logs explorer - should have atleast 1 column, discard empty key columns (#8740) 2025-08-07 20:17:34 +05:30
Abhi kumar
20dc561bfe fix: added fix for query becoming empty on time change (#8739) 2025-08-07 19:42:07 +05:30
Nityananda Gohain
99bbb87738 chore: add option to ignore data skipping indices (#8738)
* chore: add option to ignore data skipping indices

* fix: update example
2025-08-07 13:21:17 +00:00
Vikrant Gupta
f1ce93171c feat(telemetrymeter): add support for telemetry meter (#8667)
* feat(telemetry/meter): added base setup for telemetry meter signal

* feat(telemetry/meter): added metadata setup for meter

* feat(telemetry/meter): fix stmnt builder tests

* feat(telemetry/meter): test query range API fixes

* feat(telemetry/meter): improve error messages

* feat(telemetrymeter): step interval improvements

* feat(telemetrymeter): metadata changes and aggregate attribute changes

* feat(telemetrymeter): metadata changes and aggregate attribute changes

* feat(telemetrymeter): deprecate the signal and use aggregation instead

* feat(telemetrymeter): deprecate the signal and use aggregation instead

* feat(telemetrymeter): deprecate the signal and use aggregation instead

* feat(telemetrymeter): cleanup the types

* feat(telemetrymeter): introduce source for query

* feat(telemetrymeter): better naming for source in metadata

* feat(telemetrymeter): added quick filters for meter explorer

* feat(telemetrymeter): incorporate the new changes to stmnt builder

* feat(telemetrymeter): add the statement builder for the ranged cache queries

* feat(telemetrymeter): use meter aggregate keys

* feat(telemetrymeter): use meter aggregate keys

* feat(telemetrymeter): remove meter from complete bools

* feat(telemetrymeter): remove meter from complete bools

* feat(telemetrymeter): update the quick filters to use meter
2025-08-07 16:50:37 +05:30
Srikanth Chekuri
92794389d6 fix: limit keys for empty search key (#8728) 2025-08-07 00:34:44 +05:30
Srikanth Chekuri
bd02848623 chore: add sql migration for dashboards, alerts, and saved views (#8642)
## 📄 Summary

To reliably migrate the alerts and dashboards, we need access to the telemetrystore to fetch some metadata and while doing migration, I need to log some stuff to fix stuff later.

Key changes:
- Modified the migration to include telemetrystore and a logging provider (open to using a standard logger instead)
- To avoid the previous issues with imported dashboards failing during migration, I've ensured that imported JSON files are automatically transformed when migration is active
- Implemented detailed logic to handle dashboard migration cleanly and prevent unnecessary errors
- Separated the core migration logic from SQL migration code, as users from the dot metrics migration requested shareable code snippets for local migrations. This modular approach allows others to easily reuse the migration functionality.

Known: I didn't register the migration yet in this PR, and will not merge this yet, so please review with that in mid.
2025-08-06 23:05:39 +05:30
Abhi kumar
b5016b061b fix: added fix for key suggestions (#8727) 2025-08-06 11:48:43 +00:00
Abhi kumar
c308e8668c fix: added fix for query addon lightmode ui (#8725) 2025-08-06 16:21:35 +05:30
SagarRajput-7
41ee4176ad fix: fixed metric aggregation and value retention inconsistency in edit mode (#8718) 2025-08-06 13:55:16 +05:30
Abhi kumar
994663110d fix: added fix for query suggestions position (#8719)
* fix: added fix for query suggestions position

* chore: added box-shadows in the dropdowns
2025-08-06 12:48:07 +05:30
Abhi kumar
3a2eab2019 fixes: includes fixes required in the new QB (#8675)
* fix: removed unused code for querycontext (#8674)

* Update frontend/src/utils/queryValidationUtils.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* feat: added tooltips in metric aggregations

* feat: enabled legend enhancement for explorer pages and alert page

* feat: updated the error state in explorer pages with new APIError

* fix: cloned panel query shows previous query (#8681)

* fix: cloned panel query shows previous query

* chore: removed comments

* chore: added null check

* fix: added fix for auto run query in dashboard panel + trace view issue

---------

Co-authored-by: Abhi Kumar <abhikumar@Mac.lan>

* feat: added new SubstituteVars api and enable v5 for creating new alerts (#8683)

* feat: added new SubstituteVars api and enable v5 for creating new alerts

* feat: add warning notification for query response

* feat: fixed failing test case

* fix: metric histogram UI config state in edit mode

* fix: fixed table columns getting duplicate data (#8685)

* fix: added fix for conversion of QB function to filter expression. (#8684)

* fix: added fix for QB filters for functions

* chore: minor fix

---------

Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>

* feat: query builder fixes and enhancement (#8692)

* feat: legend format fixes around single and multiple aggregation

* feat: fixed table unit and metric units

* feat: add fallbacks to columnWidth and columnUnits for old-dashboards

* feat: fixed metric edit issue and having filter suggestion duplications

* feat: fix and cleanup functions across product for v5

* chore: add tooltips with links to documentation (#8676)

* fix: added fix for query validation and empty query error (#8694)

* fix: added fix for selected columns being empty in logs explorer (#8709)

* feat: added columnUnit changes for old dashboard migrations (#8706)

* fix: fixed keyfetching logic (#8712)

* chore: lint fix

* fix: fixed logs explorer test

* feat: fix type checks

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: SagarRajput-7 <sagar@signoz.io>
Co-authored-by: Abhi Kumar <abhikumar@Mac.lan>
Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-08-06 00:16:20 +05:30
SagarRajput-7
01202b5800 feat: created new error content plugin for QB v5 (#8700)
* feat: created new error content plugin for QB v5

* feat: added warning popover content for QB v5 feature

* feat: icon change for warning

* feat: added warning to QB v5 components

* feat: fixed type error

* feat: fix test cases

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-08-05 23:45:39 +05:30
Amlan Kumar Nandy
2901e052ae chore: improve metrics explorer empty state (#8711) 2025-08-05 20:45:21 +07:00
Ekansh Gupta
54baa9d76d Merge branch 'main' into trace_operator_implementation 2025-07-29 15:43:40 +05:30
eKuG
0ed6aac74e feat: refactored the consume function 2025-07-29 13:09:49 +05:30
Ekansh Gupta
b994fed409 Merge branch 'main' into trace_operator_implementation 2025-07-29 13:08:40 +05:30
eKuG
a9eb992f67 feat: refactored the consume function 2025-07-29 13:08:20 +05:30
eKuG
ed95815a6a feat: refactored the consume function 2025-07-29 13:06:32 +05:30
eKuG
2e2888346f feat: refactored the consume function 2025-07-29 12:24:44 +05:30
eKuG
525c5ac081 feat: refactored the consume function 2025-07-29 12:23:22 +05:30
eKuG
66cede4c03 feat: added postprocess 2025-07-28 23:29:27 +05:30
eKuG
33ea94991a feat: added postprocess 2025-07-28 23:28:10 +05:30
Ekansh Gupta
bae461d1f8 Merge branch 'main' into trace_operator_implementation 2025-07-28 21:24:02 +05:30
eKuG
9df82cc952 feat: added postprocess 2025-07-28 21:19:53 +05:30
Ekansh Gupta
d3d927c84d Merge branch 'main' into trace_operator_implementation 2025-07-28 14:24:46 +05:30
eKuG
36ab1ce8a2 feat: refactor trace operator 2025-07-25 17:55:13 +05:30
Ekansh Gupta
7bbf3ffba3 Merge branch 'main' into trace_operator_implementation 2025-07-25 13:56:43 +05:30
Ekansh Gupta
6ab5c3cf2e Merge branch 'main' into trace_operator_implementation 2025-07-23 15:35:13 +05:30
eKuG
c2384e387d feat: added implementation of trace operators 2025-07-07 21:18:46 +05:30
eKuG
a00f263bad feat: added implementation of trace operators 2025-06-29 13:35:49 +05:30
eKuG
9d648915cc feat: added implementation of trace operators 2025-06-23 16:24:01 +05:30
eKuG
e6bd7484fa feat: added implementation of trace operators 2025-06-23 16:13:02 +05:30
Ekansh Gupta
d780c7482e Merge branch 'main' into trace_operator_implementation 2025-06-23 16:00:33 +05:30
eKuG
ffa8d0267e feat: added implementation of trace operators 2025-06-23 15:59:53 +05:30
Ekansh Gupta
f0505a9c0e Merge branch 'main' into trace_operator_implementation 2025-06-22 15:44:55 +05:30
eKuG
09e212bd64 feat: added implementation of trace operators 2025-06-22 15:43:33 +05:30
eKuG
75f3131e65 feat: added implementation of trace operators 2025-06-22 15:39:43 +05:30
eKuG
b1b571ace9 feat: added implementation of trace operators 2025-06-22 15:38:42 +05:30
Ekansh Gupta
876f580f75 Merge branch 'main' into trace_operator_implementation 2025-06-20 15:45:15 +05:30
eKuG
7999f261ef feat: added implementation of trace operators 2025-06-20 14:41:12 +05:30
eKuG
66b8574f74 feat: added implementation of trace operators 2025-06-20 14:37:07 +05:30
eKuG
d7b8be11a4 feat: [draft] added implementation of trace operators 2025-06-20 00:18:27 +05:30
eKuG
aa3935cc31 feat: [draft] added implementation of trace operators 2025-06-20 00:08:52 +05:30
Ekansh Gupta
002c755ca5 Merge branch 'main' into trace_operator_implementation 2025-06-19 15:03:00 +05:30
eKuG
558739b4e7 feat: [draft] added implementation of trace operators 2025-06-19 00:08:41 +05:30
Ekansh Gupta
efdfa48ad0 Merge branch 'main' into trace_operator_implementation 2025-06-18 23:52:48 +05:30
eKuG
693c4451ee feat: [draft] added implementation of trace operators 2025-06-18 23:49:49 +05:30
738 changed files with 33938 additions and 11184 deletions

View File

@@ -24,7 +24,7 @@ services:
depends_on:
- zookeeper
zookeeper:
image: bitnami/zookeeper:3.7.1
image: signoz/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.128.2
image: signoz/signoz-schema-migrator:v0.129.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.128.2
image: signoz/signoz-schema-migrator:v0.129.2
container_name: schema-migrator-async
command:
- async

View File

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

@@ -0,0 +1,96 @@
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,3 +42,7 @@
/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.23
GO_VERSION: 1.24
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.23
GO_VERSION: 1.24
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.23
GO_VERSION: 1.24
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.23
GO_VERSION: 1.24
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.23
GO_VERSION: 1.24
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.23
GO_VERSION: 1.24
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.23
GO_VERSION: 1.24
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.23"
go-version: "1.24"
- 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.23"
go-version: "1.24"
- 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.23"
go-version: "1.24"
# 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.23"
go-version: "1.24"
- 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.23"
go-version: "1.24"
# copy the caches from build
- name: get-sha

2
.gitignore vendored
View File

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

View File

@@ -61,6 +61,17 @@ 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,10 +2,11 @@ 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.23-bullseye
FROM golang:1.24-bullseye
ARG OS="linux"
ARG TARGETARCH

View File

@@ -121,6 +121,8 @@ 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: bitnami/zookeeper:3.7.1
image: signoz/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.91.0
image: signoz/signoz:v0.93.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.128.2
image: signoz/signoz-otel-collector:v0.129.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.128.2
image: signoz/signoz-schema-migrator:v0.129.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: bitnami/zookeeper:3.7.1
image: signoz/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.91.0
image: signoz/signoz:v0.93.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.128.2
image: signoz/signoz-otel-collector:v0.129.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.128.2
image: signoz/signoz-schema-migrator:v0.129.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: bitnami/zookeeper:3.7.1
image: signoz/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.91.0}
image: signoz/signoz:${VERSION:-v0.93.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.128.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.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.128.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.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.128.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.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: bitnami/zookeeper:3.7.1
image: signoz/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.91.0}
image: signoz/signoz:${VERSION:-v0.93.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.128.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.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.128.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.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.128.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.2}
container_name: schema-migrator-async
command:
- async

View File

@@ -44,20 +44,35 @@ 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. Starting the Backend
### 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
1. Run the backend server:
```bash
@@ -73,7 +88,7 @@ This command:
> 💡 **Tip**: The API server runs at `http://localhost:8080/` by default
### 3. Setting up the Frontend
### 4. Setting up the Frontend
1. Navigate to the frontend directory:
```bash
@@ -98,3 +113,25 @@ 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, err := types.NewFactorPassword(uuid.NewString())
password := types.MustGenerateFactorPassword(newUser.ID.StringValue())
integrationUser, err := ah.Signoz.Modules.User.CreateUserWithPassword(ctx, newUser, password)
err = ah.Signoz.Modules.User.CreateUser(ctx, newUser, user.WithFactorPassword(password))
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
}
return integrationUser, nil
return newUser, nil
}
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (

View File

@@ -257,6 +257,7 @@ 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 (
"fmt"
"errors"
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: fmt.Errorf(s),
Err: errors.New(s),
}
}
@@ -73,7 +73,7 @@ func InternalError(err error) *ApiError {
func InternalErrorStr(s string) *ApiError {
return &ApiError{
Typ: basemodel.ErrorInternal,
Err: fmt.Errorf(s),
Err: errors.New(s),
}
}

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ const config: Config.InitialOptions = {
'ts-jest': {
useESM: true,
isolatedModules: true,
tsconfig: '<rootDir>/tsconfig.jest.json',
},
},
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
@@ -25,7 +26,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|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|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -43,11 +43,19 @@
"@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-github": "4.24.1",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/codemirror-theme-github": "4.24.1",
"@uiw/react-codemirror": "4.23.10",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
@@ -92,6 +100,7 @@
"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,5 +46,8 @@
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
"METER": "SigNoz | Meter"
}

View File

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

View File

@@ -4,6 +4,7 @@ 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';
@@ -25,6 +26,7 @@ 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';
@@ -368,39 +370,42 @@ function App(): JSX.Element {
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<CompatRouter>
<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>
<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>
</CompatRouter>
</Router>
</ConfigProvider>

View File

@@ -1,5 +1,6 @@
import ROUTES from 'constants/routes';
import MessagingQueues from 'pages/MessagingQueues';
import MeterExplorer from 'pages/MeterExplorer';
import { RouteProps } from 'react-router-dom';
import {
@@ -434,6 +435,28 @@ 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,637 @@
/* 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,4 +1,5 @@
/* 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';
@@ -7,7 +8,7 @@ import { isEmpty } from 'lodash-es';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
QueryFunctionProps,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
@@ -30,6 +31,7 @@ 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;
@@ -68,9 +70,46 @@ function getSignalType(dataSource: string): 'traces' | 'logs' | 'metrics' {
return 'metrics';
}
/**
* Creates base spec for builder queries
*/
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)
);
}
function createBaseSpec(
queryData: IBuilderQuery,
requestType: RequestType,
@@ -82,7 +121,7 @@ function createBaseSpec(
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
return {
stepInterval: queryData?.stepInterval || undefined,
stepInterval: queryData?.stepInterval || null,
disabled: queryData.disabled,
filter: queryData?.filter?.expression ? queryData.filter : undefined,
groupBy:
@@ -90,8 +129,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,
@@ -123,34 +162,52 @@ function createBaseSpec(
functions: isEmpty(queryData.functions)
? undefined
: queryData.functions.map(
(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,
})),
}),
(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,
})),
};
},
),
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,
}),
(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;
},
),
};
}
// 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'"
@@ -159,7 +216,7 @@ export function parseAggregations(
let match = regex.exec(expression);
while (match !== null) {
const expr = match[1];
let alias = match[2];
let alias = match[2] || availableAlias; // Use provided alias or availableAlias if not matched
if (alias) {
// Remove quotes if present
alias = alias.replace(/^['"]|['"]$/g, '');
@@ -210,9 +267,14 @@ export function createAggregation(
}
if (queryData.aggregations?.length > 0) {
return isEmpty(parseAggregations(queryData.aggregations?.[0].expression))
? [{ expression: 'count()' }]
: parseAggregations(queryData.aggregations?.[0].expression);
return queryData.aggregations.flatMap(
(agg: { expression: string; alias?: string }) => {
const parsedAggregations = parseAggregations(agg.expression, agg?.alias);
return isEmpty(parsedAggregations)
? [{ expression: 'count()' }]
: parsedAggregations;
},
);
}
return [{ expression: 'count()' }];
@@ -258,6 +320,7 @@ export function convertBuilderQueriesToV5(
spec = {
name: queryName,
signal: 'metrics' as const,
source: queryData.source || '',
...baseSpec,
aggregations: aggregations as MetricAggregation[],
// reduceTo: queryData.reduceTo,
@@ -273,6 +336,101 @@ 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
*/
@@ -354,14 +512,28 @@ export const prepareQueryRangePayloadV5 = ({
switch (query.queryType) {
case EQueryType.QUERY_BUILDER: {
const { queryData: data, queryFormulas } = query.builder;
const { queryData: data, queryFormulas, queryTraceOperator } = 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
@@ -394,8 +566,14 @@ export const prepareQueryRangePayloadV5 = ({
}),
);
// Combine both types
queries = [...builderQueries, ...formulaQueries];
const traceOperatorQueries = convertTraceOperatorToV5(
currentTraceOperator.data,
requestType,
graphType,
);
// Combine all query types
queries = [...builderQueries, ...formulaQueries, ...traceOperatorQueries];
break;
}
case EQueryType.PROM: {

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ export interface NavigateToExplorerProps {
endTime?: number;
sameTab?: boolean;
shouldResolveQuery?: boolean;
widgetQuery?: Query;
}
export function useNavigateToExplorer(): (
@@ -30,27 +31,34 @@ export function useNavigateToExplorer(): (
);
const prepareQuery = useCallback(
(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: [],
},
}),
(
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: [],
},
};
},
[currentQuery],
);
@@ -67,6 +75,7 @@ export function useNavigateToExplorer(): (
endTime,
sameTab,
shouldResolveQuery,
widgetQuery,
} = props;
const urlParams = new URLSearchParams();
if (startTime && endTime) {
@@ -77,7 +86,7 @@ export function useNavigateToExplorer(): (
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
}
let preparedQuery = prepareQuery(filters, dataSource);
let preparedQuery = prepareQuery(filters, dataSource, widgetQuery);
if (shouldResolveQuery) {
await getUpdatedQuery({

View File

@@ -137,5 +137,11 @@
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,8 +241,6 @@ function ClientSideQBSearch(
key: 'body',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
id: 'body--string----true',
},
op: OPERATORS.CONTAINS,

View File

@@ -0,0 +1,33 @@
.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

@@ -0,0 +1,59 @@
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,6 +1,16 @@
.custom-time-picker {
display: flex;
flex-direction: column;
.timeSelection-input {
&:hover {
border-color: #1d212d !important;
}
}
.time-input-suffix {
display: flex;
}
}
.time-options-container {
@@ -135,6 +145,7 @@
align-items: center;
color: var(--bg-vanilla-400);
gap: 6px;
.timezone {
display: flex;
align-items: center;
@@ -163,6 +174,52 @@
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);
@@ -180,8 +237,26 @@
}
}
}
.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,13 +5,12 @@ 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';
@@ -28,7 +27,10 @@ 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';
@@ -57,11 +59,9 @@ interface CustomTimePickerProps {
customDateTimeVisible?: boolean;
setCustomDTPickerVisible?: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler?: (dateTimeRange: DateTimeRangeType) => void;
handleGoLive?: () => void;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
showLiveLogs?: boolean;
onGoLive?: () => void;
onExitLiveLogs?: () => void;
}
function CustomTimePicker({
@@ -78,14 +78,19 @@ function CustomTimePicker({
customDateTimeVisible,
setCustomDTPickerVisible,
onCustomDateHandler,
handleGoLive,
onTimeChange,
onGoLive,
onExitLiveLogs,
showLiveLogs,
}: 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>(
@@ -164,9 +169,13 @@ function CustomTimePicker({
};
useEffect(() => {
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setSelectedTimePlaceholderValue(value);
}, [selectedTime, selectedValue]);
if (showLiveLogs) {
setSelectedTimePlaceholderValue('Live');
} else {
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setSelectedTimePlaceholderValue(value);
}
}, [selectedTime, selectedValue, showLiveLogs]);
const hide = (): void => {
setOpen(false);
@@ -256,6 +265,11 @@ function CustomTimePicker({
};
const handleSelect = (label: string, value: string): void => {
if (label === 'Custom') {
setCustomDTPickerVisible?.(true);
return;
}
onSelect(value);
setSelectedTimePlaceholderValue(label);
setInputStatus('');
@@ -318,84 +332,118 @@ 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">
<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" />
<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}
/>
) : (
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
<Clock size={14} />
</Tooltip>
content
)
}
suffix={
<>
{!!isTimezoneOverridden && activeTimezoneOffset && (
<div className="timezone-badge" onClick={handleTimezoneHintClick}>
<span>{activeTimezoneOffset}</span>
</div>
)}
<ChevronDown
size={14}
onClick={(): void => handleViewChange('datetime')}
/>
</>
}
/>
</Popover>
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>
{inputStatus === 'error' && inputErrorMessage && (
<Typography.Title level={5} className="valid-format-error">
{inputErrorMessage}
@@ -412,7 +460,8 @@ CustomTimePicker.defaultProps = {
customDateTimeVisible: false,
setCustomDTPickerVisible: noop,
onCustomDateHandler: noop,
handleGoLive: noop,
onGoLive: noop,
onCustomTimeStatusUpdate: noop,
onTimeChange: undefined,
onExitLiveLogs: noop,
showLiveLogs: false,
};

View File

@@ -4,21 +4,30 @@ 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, useMemo } from 'react';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { getCustomTimeRanges } from 'utils/customTimeRangeUtils';
import RangePickerModal from './RangePickerModal';
import TimezonePicker from './TimezonePicker';
interface CustomTimePickerPopoverContentProps {
@@ -31,16 +40,21 @@ interface CustomTimePickerPopoverContentProps {
lexicalContext?: LexicalContext,
) => void;
onSelectHandler: (label: string, value: string) => void;
handleGoLive: () => void;
onGoLive: () => void;
selectedTime: string;
activeView: 'datetime' | 'timezone';
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
isOpenedFromFooter: boolean;
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
onExitLiveLogs: () => void;
}
interface RecentlyUsedDateTimeRange {
label: string;
value: number;
timestamp: number;
from: string;
to: string;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -51,22 +65,68 @@ function CustomTimePickerPopoverContent({
setCustomDTPickerVisible,
onCustomDateHandler,
onSelectHandler,
handleGoLive,
onGoLive,
selectedTime,
activeView,
setActiveView,
isOpenedFromFooter,
setIsOpenedFromFooter,
onTimeChange,
onExitLiveLogs,
}: 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">
@@ -76,6 +136,7 @@ function CustomTimePickerPopoverContent({
className="time-btns"
key={option.label + option.value}
onClick={(): void => {
handleExitLiveLogs();
onSelectHandler(option.label, option.value);
}}
>
@@ -109,53 +170,87 @@ function CustomTimePickerPopoverContent({
);
}
const handleGoLive = (): void => {
onGoLive();
setIsOpen(false);
};
return (
<>
<div className="date-time-popover">
<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>
{!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={cx(
'relative-date-time',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
customDateTimeVisible ? 'date-picker' : 'relative-times',
)}
>
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
{customDateTimeVisible ? (
<DatePickerV2
onSetCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
onTimeChange={onTimeChange}
/>
) : (
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
<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>
)}
</div>
@@ -189,8 +284,4 @@ function CustomTimePickerPopoverContent({
);
}
CustomTimePickerPopoverContent.defaultProps = {
onTimeChange: undefined,
};
export default CustomTimePickerPopoverContent;

View File

@@ -0,0 +1,114 @@
.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

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

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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,50 @@
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;
initialValue?: string | number | null;
placeholder: string;
type?: string;
onClose?: () => void;

View File

@@ -0,0 +1,152 @@
.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

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

View File

@@ -23,6 +23,7 @@ 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';
@@ -39,7 +40,7 @@ import {
TextSelect,
X,
} from 'lucide-react';
import { useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useCopyToClipboard, useLocation } from 'react-use';
import { AppState } from 'store/reducers';
@@ -94,6 +95,8 @@ function LogDetailInner({
const { notifications } = useNotifications();
const { onLogCopy } = useCopyLogLink(log?.id);
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
const handleModeChange = (e: RadioChangeEvent): void => {
@@ -146,6 +149,34 @@ 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);
@@ -305,11 +336,19 @@ 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={(): void => {}}
onChange={(value): void => handleQueryExpressionChange(value, 0)}
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['='], undefined, dataType);
onAddToQuery(fieldKey, fieldValue, OPERATORS['='], dataType);
};
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
@@ -41,7 +41,6 @@ export interface AddToQueryHOCProps {
fieldKey: string,
fieldValue: string,
operator: string,
isJSON?: boolean,
dataType?: DataTypes,
) => void;
fontSize: FontSize;

View File

@@ -56,6 +56,8 @@ 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: {
@@ -83,7 +85,10 @@ 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)}>
@@ -101,6 +106,8 @@ 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,
@@ -135,6 +142,8 @@ 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

@@ -0,0 +1,19 @@
.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

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

View File

@@ -5,11 +5,13 @@ 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,
@@ -18,6 +20,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryComponents,
isListViewPanel = false,
showOnlyWhereClause = false,
showTraceOperator = false,
version,
}: QueryBuilderProps): JSX.Element {
const {
@@ -25,6 +28,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
addNewBuilderQuery,
addNewFormula,
handleSetConfig,
addTraceOperator,
panelType,
initialDataSource,
} = useQueryBuilder();
@@ -54,6 +58,11 @@ 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 },
@@ -97,11 +106,60 @@ 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">
{isListViewPanel && (
{!isMultiQueryAllowed ? (
<QueryV2
ref={containerRef}
key={currentQuery.builder.queryData[0].queryName}
@@ -109,15 +167,16 @@ 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}
@@ -127,12 +186,17 @@ 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">
@@ -157,15 +221,25 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
</div>
)}
{!showOnlyWhereClause && !isListViewPanel && (
{shouldShowFooter && (
<QueryFooter
showAddFormula={showFormula}
addNewBuilderQuery={addNewBuilderQuery}
addNewFormula={addNewFormula}
addTraceOperator={addTraceOperator}
showAddTraceOperator={showAddTraceOperator}
/>
)}
{hasTraceOperator && (
<TraceOperator
isListViewPanel={isListViewPanel}
traceOperator={traceOperator as IBuilderTraceOperator}
/>
)}
</div>
{!showOnlyWhereClause && !isListViewPanel && (
{showQueryList && (
<div className="query-names-section">
{currentQuery.builder.queryData.map((query) => (
<div key={query.queryName} className="query-name">

View File

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

View File

@@ -7,7 +7,6 @@ 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';
@@ -19,11 +18,13 @@ 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 {
@@ -50,17 +51,17 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
);
useEffect(() => {
setAggregationOptions([
setAggregationOptions(query.queryName, [
{
func: queryAggregation.spaceAggregation || 'count',
arg: queryAggregation.metricName || '',
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
queryAggregation.spaceAggregation,
queryAggregation.metricName,
setAggregationOptions,
query,
query.queryName,
]);
const handleChangeGroupByKeys = useCallback(
@@ -100,12 +101,22 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
<div className="metrics-time-aggregation-section">
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE BY TIME{' '}
<Tooltip title="AGGREGATE BY TIME">
<Info size={12} />
</Tooltip>
</div>
<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-value">
<OperatorsSelect
value={queryAggregation.timeAggregation || ''}
@@ -118,9 +129,30 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
{showAggregationInterval && (
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label">
every
</div>
<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-value">
<InputWithLabel
@@ -128,7 +160,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
label="Seconds"
placeholder="Auto"
labelAfter
initialValue={query?.stepInterval ?? undefined}
initialValue={query?.stepInterval ?? null}
/>
</div>
</div>
@@ -138,12 +170,22 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
<div className="metrics-space-aggregation-section">
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE LABELS
<Tooltip title="AGGREGATE LABELS">
<Info size={12} />
</Tooltip>
</div>
<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-value">
<SpaceAggregationOptions
panelType={panelType}
@@ -168,6 +210,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
disabled={!queryAggregation.metricName}
query={query}
onChange={handleChangeGroupByKeys}
signalSource={signalSource}
/>
</div>
</div>
@@ -204,13 +247,35 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
disabled={!queryAggregation.metricName}
query={query}
onChange={handleChangeGroupByKeys}
signalSource={signalSource}
/>
</div>
</div>
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label">
every
</div>
<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-value">
<InputWithLabel
@@ -218,7 +283,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
label="Seconds"
placeholder="Auto"
labelAfter
initialValue={query?.stepInterval ?? undefined}
initialValue={query?.stepInterval ?? null}
className="histogram-every-input"
/>
</div>

View File

@@ -44,13 +44,14 @@
.lightMode {
.metrics-select-container {
.ant-select-selector {
border: 1px solid var(--bg-slate-300) !important;
border: 1px solid var(--bg-vanilla-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,10 +9,12 @@ export const MetricsSelect = memo(function MetricsSelect({
query,
index,
version,
signalSource,
}: {
query: IBuilderQuery;
index: number;
version: string;
signalSource: 'meter' | '';
}): JSX.Element {
const { handleChangeAggregatorAttribute } = useQueryOperations({
index,
@@ -22,7 +24,12 @@ export const MetricsSelect = memo(function MetricsSelect({
return (
<div className="metrics-select-container">
<AggregatorFilter onChange={handleChangeAggregatorAttribute} query={query} />
<AggregatorFilter
onChange={handleChangeAggregatorAttribute}
query={query}
index={index}
signalSource={signalSource || ''}
/>
</div>
);
});

View File

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

View File

@@ -1,7 +1,11 @@
.query-add-ons {
width: 100%;
}
.add-ons-list {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
.add-ons-tabs {
display: flex;
@@ -111,17 +115,13 @@
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: -2px !important;
width: 100% !important;
position: absolute !important;
top: 38px !important;
top: calc(100% + 6px) !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,7 +129,9 @@
) !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;
@@ -165,7 +167,6 @@
overflow: hidden;
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
.cm-completionIcon {
display: none !important;
@@ -330,16 +331,18 @@
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,6 +1,7 @@
/* eslint-disable react/require-default-props */
import './QueryAddOns.styles.scss';
import { Button, Radio, RadioChangeEvent } from 'antd';
import { Button, Radio, RadioChangeEvent, Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
@@ -9,7 +10,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, ScrollText } from 'lucide-react';
import { BarChart2, ChevronUp, ExternalLink, ScrollText } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
@@ -21,6 +22,8 @@ interface AddOn {
icon: React.ReactNode;
label: string;
key: string;
description?: string;
docLink?: string;
}
const ADD_ONS_KEYS = {
@@ -36,26 +39,45 @@ 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',
},
];
@@ -63,8 +85,58 @@ 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,
@@ -72,6 +144,7 @@ function QueryAddOns({
showReduceTo,
panelType,
index,
isForTraceOperator = false,
}: {
query: IBuilderQuery;
version: string;
@@ -79,6 +152,7 @@ function QueryAddOns({
showReduceTo: boolean;
panelType: PANEL_TYPES | null;
index: number;
isForTraceOperator?: boolean;
}): JSX.Element {
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
@@ -88,6 +162,7 @@ function QueryAddOns({
index,
query,
entityVersion: '',
isForTraceOperator,
});
const { handleSetQueryData } = useQueryBuilder();
@@ -212,7 +287,21 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'group_by') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Group By</div>
<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="input">
<GroupByFilter
disabled={
@@ -234,7 +323,21 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'having') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Having</div>
<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="input">
<HavingFilter
onClose={(): void => {
@@ -266,7 +369,21 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'order_by') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Order By</div>
<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="input">
<OrderByFilter
entityVersion={version}
@@ -290,7 +407,21 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Reduce to</div>
<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="input">
<ReduceToFilter query={query} onChange={handleChangeReduceToV5} />
</div>
@@ -330,20 +461,32 @@ function QueryAddOns({
value={selectedViews}
>
{addOns.map((addOn) => (
<Radio.Button
key={addOn.label}
className={
selectedViews.find((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
<Tooltip
key={addOn.key}
title={
<TooltipContent
label={addOn.label}
description={addOn.description}
docLink={addOn.docLink}
/>
}
value={addOn}
placement="top"
mouseEnterDelay={0.5}
>
<div className="add-on-tab-title">
{addOn.icon}
{addOn.label}
</div>
</Radio.Button>
<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>
))}
</Radio.Group>
</div>

View File

@@ -63,17 +63,14 @@
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;
width: 100% !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%,
@@ -81,6 +78,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;
ul {
@@ -269,19 +267,17 @@
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 {
&:hover {
background-color: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600;
}
color: var(--bg-ink-300) !important;
&:hover,
&[aria-selected='true'] {
background: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600;
font-weight: 600 !important;
}
}
}

View File

@@ -1,9 +1,13 @@
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 } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import QueryAggregationSelect from './QueryAggregationSelect';
@@ -19,7 +23,7 @@ function QueryAggregationOptions({
panelType?: string;
onAggregationIntervalChange: (value: number) => void;
onChange?: (value: string) => void;
queryData: IBuilderQuery;
queryData: IBuilderQuery | IBuilderTraceOperator;
}): JSX.Element {
const showAggregationInterval = useMemo(() => {
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
@@ -53,12 +57,34 @@ function QueryAggregationOptions({
{showAggregationInterval && (
<div className="query-aggregation-interval">
<div className="query-aggregation-interval-label">every</div>
<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-input-container">
<InputWithLabel
initialValue={
queryData?.stepInterval ? queryData?.stepInterval : undefined
}
initialValue={queryData?.stepInterval ? queryData?.stepInterval : null}
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 } from 'antd';
import { Button, Popover, Tooltip } 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 { TriangleAlert } from 'lucide-react';
import { Info, 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,15 +154,23 @@ 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(
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
formatAggregations(queryData?.aggregations),
);
useEffect(() => {
setInput(
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
);
}, [queryData?.aggregations]);
setInput(formatAggregations(queryData?.aggregations));
}, [queryData?.aggregations, formatAggregations]);
const [cursorPos, setCursorPos] = useState(0);
const [functionArgPairs, setFunctionArgPairs] = useState<
@@ -263,7 +271,7 @@ function QueryAggregationSelect({
setValidationError(validateAggregations());
setFunctionArgPairs(pairs);
setAggregationOptions(pairs);
setAggregationOptions(queryData.queryName, pairs);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [input, maxAggregations, validFunctions]);
@@ -639,6 +647,50 @@ 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,33 +1,26 @@
/* eslint-disable react/require-default-props */
import { Button, Tooltip, Typography } from 'antd';
import { Plus, Sigma } from 'lucide-react';
import { DraftingCompass, Plus, Sigma } from 'lucide-react';
import BetaTag from 'periscope/components/BetaTag/BetaTag';
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
<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>
}
>
<Tooltip title={<div style={{ textAlign: 'center' }}>Add New Query</div>}>
<Button
className="add-new-query-button periscope-btn secondary"
type="text"
@@ -37,32 +30,65 @@ export default function QueryFooter({
</Tooltip>
</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}
{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>
}
>
Add Formula
</Button>
</Tooltip>
</div>
<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>
)}
</div>
</div>
);

View File

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

View File

@@ -16,12 +16,13 @@ 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 } from 'antd';
import { Button, Card, Collapse, Popover, Tag, Tooltip } 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,
@@ -30,7 +31,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebounce from 'hooks/useDebounce';
import { debounce, isNull } from 'lodash-es';
import { TriangleAlert } from 'lucide-react';
import { Info, TriangleAlert } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
IDetailedError,
@@ -40,11 +41,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';
@@ -80,18 +81,17 @@ 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[]>([
{ label: 'error', type: 'value' },
{ label: 'frontend', type: 'value' },
]);
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
const [activeKey, setActiveKey] = useState<string>('');
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
const [queryContext, setQueryContext] = useState<IQueryContext | null>(null);
@@ -114,9 +114,27 @@ 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(() => {
setQuery(queryData.filter?.expression || '');
}, [queryData.filter?.expression]);
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]);
const [keySuggestions, setKeySuggestions] = useState<
QueryKeyDataSuggestionsProps[] | null
@@ -127,7 +145,6 @@ function QuerySearch({
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
const [isFocused, setIsFocused] = useState(false);
const [isCompleteKeysList, setIsCompleteKeysList] = useState(false);
const [
isFetchingCompleteValuesList,
setIsFetchingCompleteValuesList,
@@ -138,6 +155,7 @@ 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);
@@ -170,36 +188,78 @@ function QuerySearch({
500,
);
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,
});
const toggleSuggestions = useCallback(
(timeout?: number) => {
const timeoutId = setTimeout(() => {
if (!editorRef.current) return;
if (isFocused) {
startCompletion(editorRef.current);
} else {
closeCompletion(editorRef.current);
}
}, timeout);
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);
});
return (): void => clearTimeout(timeoutId);
},
[isFocused],
);
const fetchKeySuggestions = useCallback(
async (searchText?: string): Promise<void> => {
if (
dataSource === DataSource.METRICS &&
!queryData.aggregateAttribute?.key
) {
setKeySuggestions([]);
return;
}
setKeySuggestions(Array.from(merged.values()));
setIsCompleteKeysList(complete);
}
};
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],
);
useEffect(() => {
setKeySuggestions([]);
fetchKeySuggestions();
debouncedFetchKeySuggestions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataSource, debouncedMetricName]);
@@ -310,6 +370,11 @@ function QuerySearch({
},
]);
// Force reopen the completion if editor is available and focused
if (editorRef.current) {
toggleSuggestions(10);
}
const sanitizedSearchText = searchText ? searchText?.trim() : '';
try {
@@ -317,6 +382,8 @@ function QuerySearch({
key,
searchText: sanitizedSearchText,
signal: dataSource,
signalSource: signalSource as 'meter' | '',
metricName: debouncedMetricName ?? undefined,
});
// Skip updates if component unmounted or key changed
@@ -382,13 +449,9 @@ function QuerySearch({
]);
}
// Force reopen the completion if editor is available
// Force reopen the completion if editor is available and focused
if (editorRef.current) {
setTimeout(() => {
if (isMountedRef.current && editorRef.current) {
startCompletion(editorRef.current);
}
}, 10);
toggleSuggestions(10);
}
}
} catch (error) {
@@ -408,7 +471,14 @@ function QuerySearch({
setIsFetchingCompleteValuesList(false);
}
},
[activeKey, dataSource, isLoadingSuggestions],
[
activeKey,
dataSource,
isLoadingSuggestions,
debouncedMetricName,
signalSource,
toggleSuggestions,
],
);
const debouncedFetchValueSuggestions = useMemo(
@@ -468,14 +538,13 @@ 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 => {
@@ -483,24 +552,27 @@ function QuerySearch({
setIsFocused(false);
};
useEffect(() => {
if (query) {
handleQueryValidation(query);
}
return (): void => {
useEffect(
() => (): 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);
handleQueryChange(newQuery);
// Mark as internal change to avoid triggering external validation
setIsExternalQueryChange(false);
// Update lastExternalQuery to prevent external validation trigger
setLastExternalQuery(newQuery);
};
// Helper function to render a badge for the current context mode
@@ -743,16 +815,14 @@ function QuerySearch({
}
if (queryContext.isInKey) {
const searchText = word?.text.toLowerCase() ?? '';
const searchText = word?.text.toLowerCase().trim() ?? '';
options = (keySuggestions || []).filter((option) =>
option.label.toLowerCase().includes(searchText),
);
if (!isCompleteKeysList && options.length === 0) {
setTimeout(() => {
fetchKeySuggestions(searchText);
}, 300);
if (options.length === 0 && lastFetchedKeyRef.current !== searchText) {
debouncedFetchKeySuggestions(searchText);
}
// If we have previous pairs, we can prioritize keys that haven't been used yet
@@ -827,12 +897,32 @@ function QuerySearch({
QUERY_BUILDER_KEY_TYPES.STRING
].includes(op.label),
)
.map((op) => ({
...op,
boost: ['=', '!=', 'LIKE', 'ILIKE', 'CONTAINS', 'IN'].includes(op.label)
? 100
: 0,
}));
.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,
};
});
} else if (keyType === QUERY_BUILDER_KEY_TYPES.BOOLEAN) {
// Prioritize boolean operators
options = options
@@ -841,10 +931,24 @@ function QuerySearch({
QUERY_BUILDER_KEY_TYPES.BOOLEAN
].includes(op.label),
)
.map((op) => ({
...op,
boost: ['=', '!='].includes(op.label) ? 100 : 0,
}));
.map((op) => {
if (op.label === OPERATORS['=']) {
return {
...op,
boost: 200,
};
}
if (op.label === OPERATORS['!=']) {
return {
...op,
boost: 100,
};
}
return {
...op,
boost: 0,
};
});
}
}
}
@@ -1034,26 +1138,15 @@ function QuerySearch({
// Effect to handle focus state and trigger suggestions
useEffect(() => {
if (editorRef.current) {
if (!isFocused) {
closeCompletion(editorRef.current);
} else {
startCompletion(editorRef.current);
}
}
}, [isFocused]);
const clearTimeout = toggleSuggestions(10);
return (): void => clearTimeout();
}, [isFocused, toggleSuggestions]);
useEffect(() => {
if (!queryContext) return;
// Trigger suggestions based on context
if (editorRef.current) {
// Small delay to ensure the context is fully updated
setTimeout(() => {
if (editorRef.current) {
startCompletion(editorRef.current);
}
}, 50);
toggleSuggestions(10);
}
// Handle value suggestions for value context
@@ -1066,7 +1159,28 @@ function QuerySearch({
fetchValueSuggestions({ key });
}
}
}, [queryContext, activeKey, isLoadingSuggestions, fetchValueSuggestions]);
}, [
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>
);
return (
<div className="code-mirror-where-clause">
@@ -1109,6 +1223,31 @@ 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}
@@ -1153,7 +1292,7 @@ function QuerySearch({
if (onRun && typeof onRun === 'function') {
onRun(query);
} else {
handleRunQuery(true, true);
handleRunQuery();
}
return true;
},
@@ -1167,7 +1306,7 @@ function QuerySearch({
]),
),
]}
placeholder="Enter your filter query (e.g., status = 'error' AND service = 'frontend')"
placeholder="Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')"
basicSetup={{
lineNumbers: false,
}}
@@ -1313,6 +1452,7 @@ function QuerySearch({
QuerySearch.defaultProps = {
onRun: undefined,
signalSource: '',
};
export default QuerySearch;

View File

@@ -1,3 +1,4 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { Dropdown } from 'antd';
import cx from 'classnames';
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
@@ -26,8 +27,12 @@ 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();
@@ -74,6 +79,15 @@ 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);
@@ -107,11 +121,12 @@ export const QueryV2 = memo(function QueryV2({
ref={ref}
>
<div className="qb-content-section">
{!showOnlyWhereClause && (
{(!showOnlyWhereClause || showTraceOperator) && (
<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) ||
@@ -121,6 +136,7 @@ export const QueryV2 = memo(function QueryV2({
false
}
isCollapsed={isCollapsed}
showTraceOperator={showTraceOperator}
entityType="query"
entityData={query}
onToggleVisibility={handleToggleDisableQuery}
@@ -138,7 +154,28 @@ export const QueryV2 = memo(function QueryV2({
/>
</div>
{!isListViewPanel && (
{!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 && (
<Dropdown
className="query-actions-dropdown"
menu={{
@@ -175,31 +212,36 @@ export const QueryV2 = memo(function QueryV2({
query={query}
index={index}
version={ENTITY_VERSION_V5}
signalSource={signalSource as 'meter' | ''}
/>
</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} />
{!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>
)}
</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}
@@ -218,19 +260,21 @@ export const QueryV2 = memo(function QueryV2({
index={index}
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
version="v4"
signalSource={signalSource as 'meter' | ''}
/>
)}
{!showOnlyWhereClause && (
<QueryAddOns
index={index}
query={query}
version="v3"
isListViewPanel={isListViewPanel}
showReduceTo={showReduceTo}
panelType={panelType}
/>
)}
{!showOnlyWhereClause &&
!(hasTraceOperator && query.dataSource === DataSource.TRACES) && (
<QueryAddOns
index={index}
query={query}
version="v3"
isListViewPanel={isListViewPanel}
showReduceTo={showReduceTo}
panelType={panelType}
/>
)}
</div>
)}
</div>

View File

@@ -0,0 +1,185 @@
.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

@@ -0,0 +1,119 @@
/* 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

@@ -0,0 +1,491 @@
/* 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

@@ -0,0 +1,425 @@
/* 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

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

@@ -0,0 +1,562 @@
/* 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

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

@@ -0,0 +1,974 @@
/* 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,8 +1,12 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
import { OPERATORS } from 'constants/antlrQueryConstants';
import {
DEPRECATED_OPERATORS_MAP,
OPERATORS,
QUERY_BUILDER_FUNCTIONS,
} from 'constants/antlrQueryConstants';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { cloneDeep } from 'lodash-es';
import { cloneDeep, isEqual, sortBy } from 'lodash-es';
import { IQueryPair } from 'types/antlrQueryTypes';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@@ -18,9 +22,10 @@ import {
TraceAggregation,
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { DataSource, ReduceOperators } 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';
/**
@@ -86,8 +91,32 @@ export const convertFiltersToExpression = (
return '';
}
const formattedValue = formatValueForExpression(value, op);
return `${key.key} ${op} ${formattedValue}`;
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}`;
})
.filter((expression) => expression !== ''); // Remove empty expressions
@@ -112,7 +141,6 @@ export const convertExpressionToFilters = (
if (!expression) return [];
const queryPairs = extractQueryPairs(expression);
const filters: TagFilterItem[] = [];
queryPairs.forEach((pair) => {
@@ -135,39 +163,57 @@ 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,
filter: convertFiltersToExpression(filters),
filters: updatedFilters,
filter: convertFiltersToExpression(updatedFilters),
};
}
// 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
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];
}),
);
}
let queryPairsMap = getQueryPairsMap(existingQuery.trim());
filters?.items?.forEach((filter) => {
const { key, op, value } = filter;
@@ -196,10 +242,37 @@ 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;
}
@@ -225,6 +298,7 @@ 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 (
@@ -241,6 +315,7 @@ 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 (
@@ -257,6 +332,7 @@ 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
}
@@ -278,6 +354,7 @@ export const convertFiltersToExpressionWithExistingQuery = (
} ${formattedValue} ${modifiedQuery.slice(
notEqualsPair.position.valueEnd + 1,
)}`;
queryPairsMap = getQueryPairsMap(modifiedQuery);
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
}
@@ -290,6 +367,23 @@ 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());
}
@@ -486,14 +580,25 @@ export const convertHavingToExpression = (
* @returns New aggregation format based on data source
*
*/
export const convertAggregationToExpression = (
aggregateOperator: string,
aggregateAttribute: BaseAutocompleteData,
dataSource: DataSource,
timeAggregation?: string,
spaceAggregation?: string,
alias?: string,
): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
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 => {
// Skip if no operator or attribute key
if (!aggregateOperator) {
return undefined;
@@ -511,7 +616,9 @@ export const convertAggregationToExpression = (
if (dataSource === DataSource.METRICS) {
return [
{
metricName: aggregateAttribute.key,
metricName: aggregateAttribute?.key || '',
reduceTo,
temporality,
timeAggregation: (normalizedTimeAggregation || normalizedOperator) as any,
spaceAggregation: (normalizedSpaceAggregation || normalizedOperator) as any,
} as MetricAggregation,
@@ -519,7 +626,9 @@ export const convertAggregationToExpression = (
}
// For traces and logs, use expression format
const expression = `${normalizedOperator}(${aggregateAttribute.key})`;
const expression = aggregateAttribute?.key
? `${normalizedOperator}(${aggregateAttribute?.key})`
: `${normalizedOperator}()`;
if (dataSource === DataSource.TRACES) {
return [
@@ -539,43 +648,16 @@ 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 {
return `${queryName}.${aggregation.expression}`;
if (isMultipleAggregations && aggregation.expression) {
return `${queryName}.${aggregation.expression}`;
}
return queryName;
}
// function to give you label value for query name taking multiaggregation into account
@@ -599,7 +681,7 @@ export function getQueryLabelWithAggregation(
const isMultipleAggregations = aggregations.length > 1;
aggregations.forEach((agg: any, index: number) => {
const columnId = getColId(queryName, agg);
const columnId = getColId(queryName, agg, isMultipleAggregations);
// For display purposes, show the aggregation index for multiple aggregations
const displayLabel = isMultipleAggregations

View File

@@ -17,6 +17,7 @@ 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';
@@ -73,18 +74,59 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
searchText: searchText ?? '',
},
{
enabled: isOpen,
enabled: isOpen && source !== QuickFiltersSource.METER_EXPLORER,
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]);
}, [data?.payload, filter.attributeKey.dataType, keyValueSuggestions, source]);
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
@@ -478,12 +520,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
)}
</section>
</section>
{isOpen && isLoading && !attributeValues.length && (
<section className="loading">
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && !isLoading && (
{isOpen &&
(isLoading || isLoadingKeyValueSuggestions) &&
!attributeValues.length && (
<section className="loading">
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && !isLoading && !isLoadingKeyValueSuggestions && (
<>
{!isEmptyStateWithDocsEnabled && (
<section className="search">

View File

@@ -1,6 +1,8 @@
.quick-filters-container {
display: flex;
height: 100%;
position: relative;
.quick-filters-settings-container {
position: relative;
}
@@ -102,6 +104,37 @@
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