Compare commits

...

293 Commits

Author SHA1 Message Date
Aditya Singh
a8b30749da Merge branch 'main' into feat/trace-details-css-modules 2026-05-13 16:56:10 +05:30
Aditya Singh
2fc2955f7b Merge branch 'main' into feat/trace-details-css-modules 2026-05-13 16:37:54 +05:30
aks07
c8548ea27d feat: fix test 2026-05-13 16:34:40 +05:30
aks07
ede67c877a Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-details-css-modules 2026-05-13 16:04:49 +05:30
aks07
074d3b8c85 feat: fix minor style 2026-05-13 16:04:18 +05:30
aks07
07b0f8e6cb feat: migrate to css modules 2026-05-13 15:47:03 +05:30
aks07
24660642cb Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-details-pending-2 2026-05-13 12:38:22 +05:30
aks07
f379a01095 feat: hide v3 2026-05-13 09:48:06 +05:30
aks07
d5a07d10bb Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-details-pending-2 2026-05-13 09:45:49 +05:30
aks07
537d183d34 feat: update tests 2026-05-13 09:45:35 +05:30
aks07
10ad886981 feat: use pinned attributes accross views 2026-05-12 23:17:23 +05:30
aks07
6cf8ccbfb8 Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-details-pending-2 2026-05-12 22:31:19 +05:30
aks07
0162d3c7e2 Merge branch 'feat/trace-details-pending' of github.com:SigNoz/signoz into feat/trace-details-pending-2 2026-05-12 20:41:11 +05:30
aks07
7124896d37 feat: minor fix 2026-05-12 20:33:49 +05:30
aks07
9edd1e88bd Merge branch 'feat/trace-details-pending' of github.com:SigNoz/signoz into feat/trace-details-pending-2 2026-05-12 19:16:23 +05:30
aks07
c2ba08fd31 Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-details-pending 2026-05-12 19:15:57 +05:30
aks07
2d26411705 feat: add links to non filterable keys 2026-05-12 19:12:42 +05:30
aks07
181c81ec71 feat: support filter on parent keys 2026-05-12 18:49:27 +05:30
aks07
192cbb15f8 feat: add icons 2026-05-12 17:43:28 +05:30
aks07
c0b6ef28cd feat: update border color 2026-05-12 16:33:41 +05:30
aks07
ad6813fbe7 feat: update test cases 2026-05-12 16:30:09 +05:30
aks07
8be39cb4c5 feat: move to zustand 2026-05-12 15:49:17 +05:30
aks07
c1905361d2 Merge branch 'feat/trace-details-pending' of github.com:SigNoz/signoz into feat/trace-details-pending-2 2026-05-12 15:08:33 +05:30
aks07
d27d8443fc feat: minor change 2026-05-12 15:07:15 +05:30
aks07
90d09a7a37 feat: minor change 2026-05-12 15:03:14 +05:30
aks07
6a784088f2 Merge branch 'feat/trace-details-pending' of github.com:SigNoz/signoz into feat/trace-details-pending-2 2026-05-12 14:59:30 +05:30
aks07
08b08b85ca Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-details-pending 2026-05-12 14:57:44 +05:30
aks07
d1298b7b91 feat: update icons 2026-05-12 14:51:46 +05:30
aks07
2ae7ff394c feat: dont use antd button and tooltip 2026-05-12 14:40:27 +05:30
aks07
9b79d86436 feat: dont use antd button and tooltip 2026-05-12 14:36:01 +05:30
aks07
8d4df49bb4 feat: minor fix 2026-05-12 13:59:20 +05:30
aks07
4ba3c32ca4 feat: minor fix 2026-05-12 13:40:19 +05:30
aks07
593997caa2 feat: additional check 2026-05-12 13:27:24 +05:30
aks07
0ce20e963c Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-details-pending 2026-05-12 13:16:54 +05:30
aks07
5c58e8c2a4 feat: move analytics panel 2026-05-12 13:16:12 +05:30
aks07
7336557e79 feat: auto expand on highlight errors 2026-05-12 10:41:07 +05:30
aks07
ec392e6e4a feat: only scroll when span is not in view 2026-05-12 01:01:54 +05:30
aks07
b5e3ac7179 feat: send selected span when flamegraph is sampled 2026-05-11 20:58:37 +05:30
aks07
77a4eaeebb Merge branch 'feat/trace-details-pending' of github.com:SigNoz/signoz into feat/trace-details-pending-2 2026-05-11 18:45:05 +05:30
aks07
8f3ed3b725 feat: allow removing all fields in preview 2026-05-11 17:57:16 +05:30
aks07
43ccc88440 feat: replace antd btns 2026-05-11 17:53:10 +05:30
aks07
f9b23dbe29 Merge branch 'feat/trace-details-pending' of github.com:SigNoz/signoz into feat/trace-details-pending-2 2026-05-11 17:38:18 +05:30
aks07
1e07156cd0 Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-details-pending 2026-05-11 17:37:56 +05:30
aks07
74e5e4d2ac feat: revert route change 2026-05-11 17:37:00 +05:30
aks07
0a35a09f1e feat: revert route change 2026-05-11 17:34:42 +05:30
aks07
b88e9e52be feat: update test cases 2026-05-11 17:23:46 +05:30
aks07
cf58d49de4 feat: show api error correctly 2026-05-11 16:58:15 +05:30
aks07
8e95128414 feat: add llm as fast filter 2026-05-11 15:19:44 +05:30
aks07
aaff6d8bdd feat: add more fields in color by 2026-05-11 15:05:29 +05:30
aks07
4b22ac05b2 feat: persist user choosing old view 2026-05-11 15:05:08 +05:30
aks07
8e0ecb2666 feat: add status message to highlight section 2026-05-11 13:43:39 +05:30
aks07
83ed560236 feat: make trace id copyable 2026-05-11 13:37:14 +05:30
aks07
e46510fa02 Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-details-pending 2026-05-11 12:25:55 +05:30
aks07
07f0bf3e8b feat: minor fixes 2026-05-11 12:25:31 +05:30
aks07
3b6fd6a5e8 Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-details-pending 2026-05-11 12:12:15 +05:30
aks07
c312a54e63 feat: fix logs in span details styles 2026-05-11 10:48:55 +05:30
aks07
35ecfc5e37 feat: change how we show span hover data in waterfall 2026-05-11 10:48:07 +05:30
aks07
7b9caf14b8 feat: fix waterfall child sibling indent 2026-05-10 21:33:40 +05:30
aks07
e3db42b7ce feat: show error count 2026-05-10 20:27:58 +05:30
aks07
401f253090 feat: fix failing test 2026-05-10 19:46:17 +05:30
aks07
2d930c0e4b fix: pagination calls in waterfall 2026-05-10 19:43:35 +05:30
aks07
ce8d1837ef feat: correct status code 2026-05-10 18:57:26 +05:30
aks07
c38bfa1027 feat: trace details always visible 2026-05-08 18:28:38 +05:30
aks07
473b91f41f Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-details-pending 2026-05-07 16:46:51 +05:30
aks07
4f32c23f63 Merge branch 'feat/dropdown-items' of github.com:SigNoz/signoz into feat/trace-details-pending 2026-05-07 16:46:39 +05:30
aks07
8708fa0627 feat: fix hidden filters 2026-05-07 16:22:10 +05:30
Aditya Singh
cabd7b6641 Merge branch 'main' into feat/dropdown-items 2026-05-07 16:08:38 +05:30
aks07
bc91476bce feat: fixed the waterfall span hover card 2026-05-07 00:43:40 +05:30
aks07
b5789e1e36 feat: remove feature flag 2026-05-07 00:31:00 +05:30
aks07
52f2b40e18 feat: update signoz ui 2026-05-06 23:49:06 +05:30
aks07
9fe4ca02da feat: fix mouse wheel in json view 2026-05-06 23:42:56 +05:30
aks07
41fd155fd3 feat: leaf node indent fix 2026-05-06 23:33:05 +05:30
aks07
88575f3ea1 Merge branch 'feat/dropdown-items' of github.com:SigNoz/signoz into feat/trace-details-pending 2026-05-06 21:01:47 +05:30
aks07
48f1a4cbf3 Merge branch 'main' of github.com:SigNoz/signoz into feat/dropdown-items 2026-05-06 20:57:43 +05:30
aks07
bb499973bf feat: remove antd component usage 2026-05-06 20:57:04 +05:30
aks07
13c087d34d feat: remove common component 2026-05-06 19:54:09 +05:30
aks07
f5e772f8a0 Merge branch 'feat/dropdown-items' of github.com:SigNoz/signoz into feat/trace-details-pending 2026-05-06 19:19:52 +05:30
aks07
feb9031bcd feat: packages remove 2026-05-06 19:17:51 +05:30
aks07
bc4a6b7ded feat: packages remove 2026-05-06 18:01:18 +05:30
aks07
9d83c9b43d Merge branch 'feat/trace-details-pending' of github.com:SigNoz/signoz into feat/trace-details-pending 2026-05-06 17:31:53 +05:30
aks07
0144bb78df Merge branch 'feat/dropdown-items' of github.com:SigNoz/signoz into feat/trace-details-pending 2026-05-06 17:30:51 +05:30
aks07
9216bb5f34 Merge branch 'main' of github.com:SigNoz/signoz into feat/dropdown-items 2026-05-06 17:26:48 +05:30
aks07
18d2806f95 feat: minor refactors 2026-05-06 17:03:36 +05:30
aks07
8d666471e1 feat: v3 behind feature flag 2026-05-06 17:00:16 +05:30
aks07
9d022772b7 feat: minor refactors 2026-05-06 16:28:47 +05:30
aks07
648a48cbaa feat: minor refactors 2026-05-06 15:39:44 +05:30
aks07
6e35cee1e7 feat: preview fields 2026-05-06 12:22:31 +05:30
aks07
1298074cb2 feat: migrate v2 pinned attr 2026-05-05 22:59:41 +05:30
aks07
16c8db5fd9 feat: save color by pref in user pref 2026-05-05 21:20:54 +05:30
aks07
6855be7859 feat: color by field 2026-05-05 18:24:11 +05:30
aks07
2c398396dd feat: analytics spans 2026-05-04 19:29:52 +05:30
Aditya Singh
a58539e25c Merge branch 'main' into feat/trace-details-pending 2026-05-04 18:04:23 +05:30
aks07
eeb7fa3aa5 feat: lint fix 2026-05-04 18:03:49 +05:30
aks07
d11234531d Merge branch 'feat/dropdown-items' of github.com:SigNoz/signoz into feat/trace-details-pending 2026-05-04 17:56:01 +05:30
aks07
eb22c57a67 feat: minor change 2026-05-04 17:43:31 +05:30
aks07
896379b680 feat: minor change 2026-05-04 17:34:32 +05:30
aks07
f041b16e4b feat: delete waterfall go 2026-05-04 17:04:39 +05:30
aks07
0bd591458d Merge branch 'feat/dropdown-items' of github.com:SigNoz/signoz into feat/trace-details-pending 2026-05-04 16:59:26 +05:30
aks07
9ca3a7fd3e Merge branch 'main' of github.com:SigNoz/signoz into feat/dropdown-items 2026-05-04 16:57:14 +05:30
aks07
43e122367c Merge branch 'main' of github.com:SigNoz/signoz into feat/dropdown-items
# Conflicts:
#	docs/api/openapi.yml
#	frontend/package.json
#	frontend/src/api/generated/services/sigNoz.schemas.ts
#	frontend/src/api/generated/services/tracedetail/index.ts
#	frontend/src/auto-import-registry.d.ts
#	frontend/src/components/LogDetail/index.tsx
#	frontend/src/hooks/trace/useCopySpanLink.ts
#	frontend/src/styles.scss
#	frontend/yarn.lock
#	pkg/apiserver/signozapiserver/provider.go
#	pkg/apiserver/signozapiserver/tracedetail.go
#	pkg/modules/tracedetail/impltracedetail/handler.go
#	pkg/modules/tracedetail/impltracedetail/module.go
#	pkg/modules/tracedetail/impltracedetail/store.go
#	pkg/modules/tracedetail/impltracedetail/waterfall_test.go
#	pkg/signoz/module.go
#	pkg/signoz/openapi.go
#	pkg/signoz/provider.go
#	pkg/types/tracedetailtypes/store.go
#	pkg/types/tracedetailtypes/waterfall.go
2026-05-04 16:56:32 +05:30
aks07
33520c41c8 fix: scroll to span in frontend mode 2026-05-04 11:32:13 +05:30
aks07
b994d6dd8e feat: fix color 2026-04-25 16:22:48 +05:30
aks07
5e231e799e feat: filter toggle added 2026-04-25 11:26:59 +05:30
aks07
5f4a79c201 feat: filters init 2026-04-23 18:58:51 +05:30
aks07
8edf375019 feat: floating fields set 2026-04-22 21:11:33 +05:30
aks07
0d1fd6d0bd feat: minor changes 2026-04-22 18:47:47 +05:30
aks07
fefd0effef feat: add trace details header styles 2026-04-22 17:59:48 +05:30
aks07
36a137be4d feat: add trace details header styles 2026-04-22 15:49:17 +05:30
aks07
68dc7e426a feat: add trace details header 2026-04-22 15:32:01 +05:30
aks07
603077c575 feat: open in new tab 2026-04-21 10:01:34 +05:30
aks07
7e5c4476f7 feat: add service name 2026-04-21 09:22:17 +05:30
aks07
da648ed3f3 feat: style fix 2026-04-21 01:43:12 +05:30
aks07
9fa56aacd1 feat: uncollapse spans on select from flamegraph 2026-04-21 01:37:22 +05:30
aks07
5acd79419c Merge branch 'main' of github.com:SigNoz/signoz into feat/dropdown-items 2026-04-21 01:30:25 +05:30
aks07
9b7b0f8862 feat: load all spans in waterfall under limit 2026-04-21 01:23:33 +05:30
aks07
c29e8a0136 feat: add status codes 2026-04-20 21:25:49 +05:30
aks07
ebac945ac2 feat: minor fix 2026-04-20 14:04:05 +05:30
aks07
e787497695 feat: fix colors 2026-04-20 13:46:25 +05:30
aks07
eba6bd5f5b feat: disable crosshair in waterfall 2026-04-20 11:26:35 +05:30
aks07
1aeab2718d feat: fix test 2026-04-20 10:44:21 +05:30
aks07
d879af4fb3 Merge branch 'ns/waterfall-v3-2' of github.com:SigNoz/signoz into feat/dropdown-items 2026-04-20 10:19:56 +05:30
Nikhil Soni
ac10be2eb2 Merge remote-tracking branch 'origin/main' into ns/waterfall-v3-2 2026-04-20 10:04:09 +05:30
aks07
113d1544ba feat: add crosshair 2026-04-20 09:46:14 +05:30
aks07
df02da664c feat: use semantic tokens 2026-04-17 21:03:41 +05:30
aks07
d0a491ed8e feat: use semantic tokens 2026-04-17 20:59:46 +05:30
aks07
77c39a9f05 feat: delete unused file 2026-04-17 20:26:50 +05:30
aks07
309a76e5fd feat: rename copy to copy value 2026-04-17 19:13:28 +05:30
aks07
43e80caf09 Merge branch 'feat/filter-search' of github.com:SigNoz/signoz into feat/dropdown-items 2026-04-17 10:31:48 +05:30
aks07
a2d853daf5 Merge branch 'main' of github.com:SigNoz/signoz into feat/filter-search 2026-04-17 10:23:53 +05:30
aks07
3970619afa Merge remote-tracking branch 'origin/ns/waterfall-v3-2' into feat/filter-search 2026-04-17 10:11:31 +05:30
aks07
9dc87761c1 feat: minor fix 2026-04-16 20:22:17 +05:30
aks07
86a44fad42 Merge branch 'feat/filter-search' of github.com:SigNoz/signoz into feat/dropdown-items 2026-04-16 20:20:41 +05:30
aks07
91f74144cb feat: old trace btn added 2026-04-16 20:19:27 +05:30
aks07
0863c5170b feat: no data screen 2026-04-16 18:14:18 +05:30
aks07
837cd2a463 feat: fix color duplications 2026-04-16 16:46:37 +05:30
aks07
c88a2d5d90 feat: dropdown added to span details 2026-04-16 15:42:09 +05:30
Nikhil Soni
c9abc2cb30 chore: use sqlbuilder in clickhouse store where possible 2026-04-16 09:39:36 +05:30
Nikhil Soni
01824b0b62 chore: remove unused duration nano field 2026-04-16 09:39:36 +05:30
Nikhil Soni
d1b378992d refactor: use same method for cache key creation 2026-04-16 09:39:36 +05:30
Nikhil Soni
52ca921d2a refactor: return 404 on missing trace and other cleanup 2026-04-16 09:39:36 +05:30
Nikhil Soni
42f12dfef3 refactor: breakdown GetSelectedSpans for readability 2026-04-16 09:39:36 +05:30
Nikhil Soni
f2a694447e refactor: separate out store calls and computations 2026-04-16 09:39:36 +05:30
Nikhil Soni
2e7dfa739f refactor: move error to types as well 2026-04-16 09:39:36 +05:30
Nikhil Soni
0aa73580a3 refactor: extract ClickHouse queries into a store abstraction
Move GetTraceSummary and GetTraceSpans out of module.go into a
traceStore interface backed by clickhouseTraceStore in store.go.
The module struct now holds a traceStore instead of a raw
telemetrystore.TelemetryStore, keeping DB access separate from
business logic.
2026-04-16 09:39:36 +05:30
Nikhil Soni
2ff1a43bf8 refactor: move type creation and constants to types package
- Move DB/table/cache/windowing constants to tracedetailtypes package
- Add NewWaterfallTrace and NewWaterfallResponse constructors in types
- Use constructors in module.go instead of inline struct literals
- Reorder waterfall.go so public functions precede private ones
2026-04-16 09:39:36 +05:30
Nikhil Soni
c1477c78be chore: avoid returning nil, nil 2026-04-16 09:39:36 +05:30
Nikhil Soni
9807dd5295 refactor: break down GetWaterfall method for readability 2026-04-16 09:39:36 +05:30
Nikhil Soni
2c59eeff26 fix: update openapi specs 2026-04-16 09:39:36 +05:30
Nikhil Soni
8ccfb4efef fix: use int16 for status code as per db schema 2026-04-16 09:39:36 +05:30
Nikhil Soni
87d18160e8 fix: remove timeout since waterfall take longer 2026-04-16 09:39:36 +05:30
Nikhil Soni
bfa7ee96da chore: generate openapi spec for v3 waterfall 2026-04-16 09:39:33 +05:30
Nikhil Soni
5e3eb66d3a fix: use typed paramter field in logs 2026-04-16 09:38:12 +05:30
Nikhil Soni
3d8cdf18bd fix: add timeout to module context 2026-04-16 09:38:12 +05:30
Nikhil Soni
cb4e501047 fix: rename timestamp to milli for readability 2026-04-16 09:38:12 +05:30
Nikhil Soni
cb8b2137ba fix: remove unused fields and rename span type
To avoid confusing with otel span
2026-04-16 09:38:12 +05:30
Nikhil Soni
998315a255 chore: avoid sorting on every traversal 2026-04-16 09:38:12 +05:30
Nikhil Soni
250657e46b chore: add same test cases as for old waterfall api 2026-04-16 09:38:12 +05:30
Nikhil Soni
795ae9ab18 refactor: convert waterfall api to modules format 2026-04-16 09:38:05 +05:30
Nikhil Soni
6a9ea8d9f8 fix: remove unused fields and rename span type
To avoid confusing with otel span
2026-04-16 09:31:48 +05:30
Nikhil Soni
2723e18023 fix: update span.attributes to map of string to any
To support otel format of diffrent types of attributes
2026-04-16 09:31:48 +05:30
Nikhil Soni
6e89d5f6eb chore: add reason for using snake case in response 2026-04-16 09:31:48 +05:30
Nikhil Soni
4c2a815236 refactor: move type conversion logic to types pkg 2026-04-16 09:31:48 +05:30
Nikhil Soni
b1d66b2e5f feat: setup types and interface for waterfall v3
v3 is required for udpating the response json of
the waterfall api. There wont' be any logical change.
Using this requirement as an opportunity to move
waterfall api to provider codebase architecture from
older query-service
2026-04-16 09:31:48 +05:30
aks07
ae88edbb5e feat: disable scroll to view for collapse and uncollapse 2026-04-15 22:36:58 +05:30
aks07
7c9484d47b feat: api integration v3 2026-04-15 21:50:02 +05:30
aks07
24128bd394 feat: show caution on sampled flamegraph 2026-04-15 13:35:55 +05:30
aks07
2118916a23 feat: fix flamegraph and waterfall bg color 2026-04-15 00:51:39 +05:30
aks07
52220412a1 fix: align timeline and span length in flamegraph and waterfall 2026-04-15 00:32:35 +05:30
aks07
85abee8476 feat: show child count in waterfall 2026-04-14 21:27:15 +05:30
aks07
650a29d184 feat: set limit to 100k for flamegraph 2026-04-14 21:17:26 +05:30
aks07
d9c7101d22 feat: reduce time 2026-04-14 18:45:14 +05:30
aks07
b1e7c25189 Merge branch 'main' of github.com:SigNoz/signoz into feat/filter-search 2026-04-14 15:33:19 +05:30
aks07
e9904a0558 Merge branch 'feat/filter-search' of github.com:SigNoz/signoz into feat/filter-search 2026-04-14 15:33:04 +05:30
aks07
5cd199f535 feat: automatically scroll left on vertical scroll 2026-04-14 15:32:23 +05:30
aks07
f6f48ca0bc feat: api integration 2026-04-14 14:46:49 +05:30
Aditya Singh
847f91e22e Merge branch 'main' into feat/filter-search 2026-04-14 13:14:46 +05:30
aks07
29d0abe5a8 Merge branch 'ns/waterfall-v3-2' of github.com:SigNoz/signoz into feat/filter-search 2026-04-14 13:11:26 +05:30
Nikhil Soni
c08840a827 fix: update openapi specs 2026-04-14 10:50:36 +05:30
Nikhil Soni
a3e7bb90b0 fix: use int16 for status code as per db schema 2026-04-14 10:50:36 +05:30
Nikhil Soni
8515d2f37c fix: remove timeout since waterfall take longer 2026-04-14 10:50:36 +05:30
Nikhil Soni
07c05ac3a6 chore: generate openapi spec for v3 waterfall 2026-04-14 10:50:36 +05:30
Nikhil Soni
6289f59ba3 fix: use typed paramter field in logs 2026-04-14 10:50:36 +05:30
Nikhil Soni
76371c9fa2 fix: add timeout to module context 2026-04-14 10:50:36 +05:30
Nikhil Soni
f082e396eb fix: rename timestamp to milli for readability 2026-04-14 10:50:36 +05:30
Nikhil Soni
840eb8f228 fix: remove unused fields and rename span type
To avoid confusing with otel span
2026-04-14 10:50:36 +05:30
Nikhil Soni
2911baf6bb chore: avoid sorting on every traversal 2026-04-14 10:50:36 +05:30
Nikhil Soni
fc5be4eeb5 chore: add same test cases as for old waterfall api 2026-04-14 10:50:36 +05:30
Nikhil Soni
a1b92c79a4 refactor: convert waterfall api to modules format 2026-04-14 10:50:31 +05:30
Nikhil Soni
7a0acd5c8b fix: remove unused fields and rename span type
To avoid confusing with otel span
2026-04-14 10:49:58 +05:30
Nikhil Soni
069cbe2c6f fix: update span.attributes to map of string to any
To support otel format of diffrent types of attributes
2026-04-14 10:49:58 +05:30
Nikhil Soni
4c821f9721 chore: add reason for using snake case in response 2026-04-14 10:49:58 +05:30
Nikhil Soni
4eccea92db refactor: move type conversion logic to types pkg 2026-04-14 10:49:58 +05:30
Nikhil Soni
c8d8966a5d feat: setup types and interface for waterfall v3
v3 is required for udpating the response json of
the waterfall api. There wont' be any logical change.
Using this requirement as an opportunity to move
waterfall api to provider codebase architecture from
older query-service
2026-04-14 10:49:58 +05:30
aks07
1e52a5603e Merge branch 'ns/waterfall-v3-2' of github.com:SigNoz/signoz into feat/filter-search 2026-04-14 10:33:46 +05:30
aks07
780ba1a359 feat: new filter ui 2026-04-14 10:27:21 +05:30
aks07
3b71abe820 feat: remove trace header 2026-04-13 18:49:38 +05:30
aks07
70b9d0ff02 feat: filter ui fix 2026-04-13 15:49:43 +05:30
aks07
f4657861e1 feat: make filter and search work with flamegraph 2026-04-13 15:49:09 +05:30
Nikhil Soni
66fe5b5240 fix: update openapi specs 2026-04-13 14:04:45 +05:30
Nikhil Soni
c333cecf43 fix: use int16 for status code as per db schema 2026-04-13 13:23:39 +05:30
Nikhil Soni
276e09853e fix: remove timeout since waterfall take longer 2026-04-13 12:31:08 +05:30
aks07
4defd41504 feat: prevent api call on closing span detail 2026-04-13 12:07:28 +05:30
aks07
ab53b29a14 feat: add span details loader 2026-04-13 10:25:49 +05:30
aks07
b58e82efbf feat: disable anaytics span tab for now 2026-04-12 21:33:07 +05:30
aks07
0a1a676877 feat: show total span count 2026-04-12 21:14:19 +05:30
aks07
bb2aa9f77c feat: auto scroll horizontally to span 2026-04-12 20:08:55 +05:30
aks07
04bef4ac06 feat: sync error and loading state for flamegraph for n/w and computation logic 2026-04-11 23:48:47 +05:30
aks07
3bcb2c2c41 feat: added loading to flamegraph and timeout to webworker 2026-04-11 22:37:10 +05:30
aks07
9e77b76122 feat: add icons 2026-04-11 21:28:00 +05:30
aks07
ff4a41d842 feat: analytics 2026-04-11 14:34:57 +05:30
aks07
387deb779d feat: span details ux 2026-04-10 21:31:40 +05:30
aks07
1ec2663d51 feat: lint fix 2026-04-10 13:40:43 +05:30
aks07
1b17370da0 feat: fix test 2026-04-10 13:10:28 +05:30
aks07
c6484a79e2 feat: fix test 2026-04-10 13:09:08 +05:30
Nikhil Soni
16a2c7a1af chore: generate openapi spec for v3 waterfall 2026-04-10 10:54:36 +05:30
aks07
3c4ac0e85e feat: supress click 2026-04-10 09:08:45 +05:30
aks07
87ba729a00 feat: minor change 2026-04-10 02:46:55 +05:30
aks07
f1ed7145e4 feat: add limit 2026-04-10 02:41:22 +05:30
aks07
bc15495e17 Merge branch 'main' of github.com:SigNoz/signoz into feat/span-details 2026-04-10 02:01:59 +05:30
aks07
f7d3012daf feat: api integration 2026-04-10 00:53:25 +05:30
Nikhil Soni
6ec9a2ec41 fix: use typed paramter field in logs 2026-04-09 21:39:29 +05:30
Nikhil Soni
9c056f809a fix: add timeout to module context 2026-04-09 20:03:47 +05:30
Nikhil Soni
c1d4273416 fix: rename timestamp to milli for readability 2026-04-09 16:54:44 +05:30
Nikhil Soni
618fe891d5 fix: remove unused fields and rename span type
To avoid confusing with otel span
2026-04-09 14:02:44 +05:30
Nikhil Soni
549c7e7034 chore: avoid sorting on every traversal 2026-04-09 11:38:55 +05:30
Nikhil Soni
dd65f83c3d chore: add same test cases as for old waterfall api 2026-04-09 11:38:55 +05:30
Nikhil Soni
8463a131fc refactor: convert waterfall api to modules format 2026-04-09 11:38:55 +05:30
Nikhil Soni
2d42518440 fix: remove unused fields and rename span type
To avoid confusing with otel span
2026-04-09 11:38:55 +05:30
Nikhil Soni
43d75a3853 fix: update span.attributes to map of string to any
To support otel format of diffrent types of attributes
2026-04-09 11:38:55 +05:30
Nikhil Soni
c5bb34e385 chore: add reason for using snake case in response 2026-04-09 11:38:54 +05:30
Nikhil Soni
6fd129991d refactor: move type conversion logic to types pkg 2026-04-09 11:38:54 +05:30
Nikhil Soni
9c5cca426a feat: setup types and interface for waterfall v3
v3 is required for udpating the response json of
the waterfall api. There wont' be any logical change.
Using this requirement as an opportunity to move
waterfall api to provider codebase architecture from
older query-service
2026-04-09 11:38:54 +05:30
aks07
a467efb97d feat: style fixes 2026-04-09 08:31:12 +05:30
aks07
58e2718090 feat: linked spans 2026-04-08 22:04:42 +05:30
aks07
65fee725c9 feat: key value label style fixes 2026-04-08 14:21:54 +05:30
aks07
ea87174088 feat: fix span details headr 2026-04-08 11:39:47 +05:30
aks07
627c483d86 feat: copy link change and url clear on span close 2026-04-07 20:06:53 +05:30
aks07
2533137db4 feat: use resizable for waterfall table as well 2026-04-07 15:55:06 +05:30
aks07
a774f8a4fe feat: add collapsible sections in trace details 2026-04-07 13:28:59 +05:30
aks07
8487f6cf66 feat: add bound to drags while floating 2026-04-01 19:33:53 +05:30
aks07
6ebe51126e feat: fix pinning. fix drag on top 2026-04-01 16:48:59 +05:30
aks07
ed64d5cd9f feat: replace draggable package 2026-03-31 21:33:42 +05:30
aks07
c04076e664 feat: span details folder rename 2026-03-31 17:50:02 +05:30
aks07
3c129e2c7d feat: span details floating drawer added 2026-03-31 17:44:25 +05:30
aks07
0ba51e2058 feat: json viewer with select dropdown added 2026-03-31 15:17:15 +05:30
aks07
cdc2ab134c feat: style fix 2026-03-26 20:27:41 +05:30
aks07
fb0c05b553 feat: refactor 2026-03-26 20:25:25 +05:30
aks07
68e9707e3b feat: search in pretty view 2026-03-26 20:08:25 +05:30
aks07
17ffaf9ccf feat: minor change 2026-03-26 19:42:48 +05:30
aks07
efec669b76 feat: update yarn lock 2026-03-26 19:31:10 +05:30
aks07
17b9e14d34 feat: added pretty view 2026-03-26 19:25:06 +05:30
aks07
2db9f969c3 feat: key attr section added 2026-03-26 15:01:16 +05:30
aks07
9fa466b124 feat: added span percentile 2026-03-26 14:33:06 +05:30
aks07
0c7768ebff feat: details field component 2026-03-25 03:10:50 +05:30
aks07
58dd51e92f feat: span details header 2026-03-25 02:22:11 +05:30
aks07
870c9bf6dc Merge branch 'main' of github.com:SigNoz/signoz into feat/span-details 2026-03-25 01:08:22 +05:30
aks07
7604956bf0 feat: span details init 2026-03-25 00:34:56 +05:30
aks07
66510e4919 feat: waterfall resizable 2026-03-18 20:58:01 +05:30
aks07
a1bf0e67db feat: event dots in trace details 2026-03-18 18:54:19 +05:30
aks07
a06046612a feat: connector line ux 2026-03-17 18:45:11 +05:30
aks07
31c9d4309b feat: move to service worker 2026-03-17 17:06:05 +05:30
aks07
7bef8b86c4 feat: subtree segregated tree 2026-03-17 14:39:57 +05:30
aks07
d26acd36a3 feat: subtree segregated tree 2026-03-17 13:55:58 +05:30
aks07
1cee595135 feat: subtree segregated tree 2026-03-17 13:29:07 +05:30
aks07
dd1868fcbc feat: row based flamegraph 2026-03-16 23:27:31 +05:30
aks07
a20beb8ba2 feat: span hover popover sync 2026-03-16 14:18:12 +05:30
aks07
998d652feb feat: fix hover option overflow 2026-03-16 10:19:42 +05:30
aks07
3695d3c180 feat: match span style 2026-03-13 15:53:46 +05:30
aks07
da175bafbc feat: add TimelineV3 ruler to waterfall header with padding fix
Add the TimelineV3 component to the sticky header of the waterfall's
right panel so timeline tick marks are visible. Add horizontal padding
to both the timeline header and span duration bars to prevent label
overflow/clipping at the edges.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:12:49 +05:30
aks07
021b187cbc feat: decouple waterfall left (span tree) and right (timeline bars) panels
Split the waterfall into two independent panels with a shared virtualizer
so deeply nested span names are visible via horizontal scroll in the left
panel. Left panel uses useReactTable + <table> for future column
extensibility; right panel uses plain divs for timeline bars. A draggable
resize handle separates the two panels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:04:32 +05:30
aks07
f42b468597 feat: waterfall init 2026-03-11 17:15:27 +05:30
aks07
7e2cf57819 Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-flamegraph 2026-03-11 12:24:06 +05:30
aks07
dc9ebc5b26 feat: add test utils 2026-03-11 03:31:10 +05:30
aks07
398ab6e9d9 feat: add test cases for flamegraph 2026-03-11 03:28:35 +05:30
aks07
fec60671d8 feat: minor comment added 2026-03-11 03:03:49 +05:30
aks07
99259cc4e8 feat: remove unnecessary props 2026-03-10 16:01:24 +05:30
aks07
ca311717c2 feat: bg color for selected and hover spans 2026-03-06 23:16:35 +05:30
aks07
a614da2c65 fix: update color 2026-03-06 20:22:23 +05:30
aks07
ce18709002 fix: style fix 2026-03-06 20:03:42 +05:30
aks07
2b6977e891 feat: reduce timeline intervals 2026-03-06 20:01:50 +05:30
aks07
3e6eedbcab feat: fix style 2026-03-06 19:57:19 +05:30
aks07
fd9e3f0411 feat: fix style 2026-03-06 19:26:23 +05:30
aks07
e99465e030 Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-flamegraph 2026-03-06 18:45:18 +05:30
aks07
9ad2db4b99 feat: scroll to selected span 2026-03-06 16:00:48 +05:30
aks07
07fd5f70ef feat: fix timerange unit selection when zoomed 2026-03-06 12:55:34 +05:30
aks07
ba79121795 feat: temp change 2026-03-06 12:49:06 +05:30
aks07
6e4e419b5e Merge branch 'main' of github.com:SigNoz/signoz into feat/trace-flamegraph 2026-03-06 10:30:41 +05:30
aks07
2f06afaf27 feat: handle click and hover with tooltip 2026-03-05 23:25:10 +05:30
aks07
f77c3cb23c feat: update span colors 2026-03-05 22:40:06 +05:30
aks07
9e3a8efcfc feat: zoom and drag added 2026-03-05 18:22:55 +05:30
aks07
8e325ba8b3 feat: added timeline v3 2026-03-05 12:31:17 +05:30
aks07
884f516766 feat: add text to spans 2026-03-03 12:20:06 +05:30
aks07
4bcbb4ffc3 feat: flamegraph canvas init 2026-03-02 19:21:23 +05:30
55 changed files with 2400 additions and 2464 deletions

View File

@@ -0,0 +1,89 @@
.body {
padding: 12px 6px;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
background: var(--l1-background);
// TabsRoot — last direct child div
> div:last-child {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
[role='tablist'] {
flex-shrink: 0;
}
// Tabs library wrapper — scoped by `.body` so the global match is contained.
:global([class*='tabs__list-wrapper']) {
padding-left: 0 !important;
}
}
.tabsScroll {
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
}
.list {
display: grid;
grid-template-columns: auto auto 1fr;
gap: 4px 8px;
padding: 8px 0;
align-items: center;
}
.dot {
width: 8px;
height: 8px;
border-radius: 2px;
}
.serviceName {
font-size: 13px;
color: var(--l1-foreground);
word-break: break-word;
}
.barCell {
display: flex;
align-items: center;
gap: 8px;
}
.bar {
flex: 1;
height: 6px;
background: var(--l3-background);
border-radius: 3px;
min-width: 40px;
}
.barFill {
height: 100%;
border-radius: 3px;
}
.value {
flex-shrink: 0;
text-align: right;
font-size: 13px;
font-weight: 500;
color: var(--l1-foreground);
}
.valueWide {
min-width: 55px;
}
.valueNarrow {
min-width: 25px;
}

View File

@@ -1,96 +0,0 @@
.analytics-panel {
&__body {
padding: 12px 6px;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
background: var(--l1-background);
// TabsRoot — last direct child div
> div:last-child {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
[role='tablist'] {
flex-shrink: 0;
}
}
&__tabs-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
}
&__list {
display: grid;
grid-template-columns: auto auto 1fr;
gap: 4px 8px;
padding: 8px 0;
align-items: center;
}
&__dot {
width: 8px;
height: 8px;
border-radius: 2px;
}
&__service-name {
font-size: 13px;
color: var(--l1-foreground);
word-break: break-word;
}
&__bar-cell {
display: flex;
align-items: center;
gap: 8px;
}
&__bar {
flex: 1;
height: 6px;
background: var(--l3-background);
border-radius: 3px;
min-width: 40px;
&--small {
max-width: 80px;
flex: 0 0 80px;
}
}
&__bar-fill {
height: 100%;
border-radius: 3px;
}
&__value {
flex-shrink: 0;
text-align: right;
font-size: 13px;
font-weight: 500;
color: var(--l1-foreground);
&--wide {
min-width: 55px;
}
&--narrow {
min-width: 25px;
}
}
// Tabs root
[class*='tabs__list-wrapper'] {
padding-left: 0 !important;
}
}

View File

@@ -5,6 +5,7 @@ import {
TabsRoot,
TabsTrigger,
} from '@signozhq/ui/tabs';
import cx from 'classnames';
import { DetailsHeader } from 'components/DetailsPanel';
import { themeColors } from 'constants/theme';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
@@ -16,7 +17,7 @@ import {
getAggregationMap as findAggregationMap,
} from '../../utils/aggregations';
import './AnalyticsPanel.styles.scss';
import styles from './AnalyticsPanel.module.scss';
interface AnalyticsPanelProps {
isOpen: boolean;
@@ -86,7 +87,6 @@ function AnalyticsPanel({
return (
<FloatingPanel
isOpen
className="analytics-panel"
width={PANEL_WIDTH}
height={window.innerHeight - PANEL_MARGIN_TOP - PANEL_MARGIN_BOTTOM}
defaultPosition={{
@@ -110,7 +110,7 @@ function AnalyticsPanel({
className="floating-panel__drag-handle"
/>
<div className="analytics-panel__body">
<div className={styles.body}>
<TabsRoot defaultValue="exec-time">
<TabsList variant="secondary">
<TabsTrigger value="exec-time" variant="secondary">
@@ -121,33 +121,30 @@ function AnalyticsPanel({
</TabsTrigger>
</TabsList>
<div className="analytics-panel__tabs-scroll">
<div className={styles.tabsScroll}>
<TabsContent value="exec-time">
<div className="analytics-panel__list">
<div className={styles.list}>
{execTimeRows.map((row) => (
<>
<div
key={`${row.group}-dot`}
className="analytics-panel__dot"
className={styles.dot}
style={{ backgroundColor: row.color }}
/>
<span
key={`${row.group}-name`}
className="analytics-panel__service-name"
>
<span key={`${row.group}-name`} className={styles.serviceName}>
{row.group}
</span>
<div key={`${row.group}-bar`} className="analytics-panel__bar-cell">
<div className="analytics-panel__bar">
<div key={`${row.group}-bar`} className={styles.barCell}>
<div className={styles.bar}>
<div
className="analytics-panel__bar-fill"
className={styles.barFill}
style={{
width: `${Math.min(row.percentage, 100)}%`,
backgroundColor: row.color,
}}
/>
</div>
<span className="analytics-panel__value analytics-panel__value--wide">
<span className={cx(styles.value, styles.valueWide)}>
{row.percentage.toFixed(2)}%
</span>
</div>
@@ -157,31 +154,28 @@ function AnalyticsPanel({
</TabsContent>
<TabsContent value="spans">
<div className="analytics-panel__list">
<div className={styles.list}>
{spanCountRows.map((row) => (
<>
<div
key={`${row.group}-dot`}
className="analytics-panel__dot"
className={styles.dot}
style={{ backgroundColor: row.color }}
/>
<span
key={`${row.group}-name`}
className="analytics-panel__service-name"
>
<span key={`${row.group}-name`} className={styles.serviceName}>
{row.group}
</span>
<div key={`${row.group}-bar`} className="analytics-panel__bar-cell">
<div className="analytics-panel__bar">
<div key={`${row.group}-bar`} className={styles.barCell}>
<div className={styles.bar}>
<div
className="analytics-panel__bar-fill"
className={styles.barFill}
style={{
width: `${(row.count / row.max) * 100}%`,
backgroundColor: row.color,
}}
/>
</div>
<span className="analytics-panel__value analytics-panel__value--narrow">
<span className={cx(styles.value, styles.valueNarrow)}>
{row.count}
</span>
</div>

View File

@@ -0,0 +1,26 @@
.root {
position: relative;
}
.toggle {
display: inline-flex;
align-items: center;
gap: 4px;
background: none;
border: none;
padding: 0;
cursor: pointer;
color: inherit;
font: inherit;
}
.label {
white-space: nowrap;
}
.list {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px 0;
}

View File

@@ -1,34 +0,0 @@
.linked-spans {
position: relative;
&__toggle {
display: inline-flex;
align-items: center;
gap: 4px;
background: none;
border: none;
padding: 0;
cursor: pointer;
color: inherit;
font: inherit;
}
&__label {
white-space: nowrap;
}
&__chevron {
transition: transform 0.15s ease;
&--open {
transform: rotate(90deg);
}
}
&__list {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px 0;
}
}

View File

@@ -5,7 +5,7 @@ import { Badge } from '@signozhq/ui/badge';
import ROUTES from 'constants/routes';
import KeyValueLabel from 'periscope/components/KeyValueLabel';
import './LinkedSpans.styles.scss';
import styles from './LinkedSpans.module.scss';
interface SpanReference {
traceId: string;
@@ -56,12 +56,12 @@ export function LinkedSpansToggle({
toggleOpen: () => void;
}): JSX.Element {
if (count === 0) {
return <span className="linked-spans__label">0 linked spans</span>;
return <span className={styles.label}>0 linked spans</span>;
}
return (
<button type="button" className="linked-spans__toggle" onClick={toggleOpen}>
<span className="linked-spans__label">
<button type="button" className={styles.toggle} onClick={toggleOpen}>
<span className={styles.label}>
{count} linked span{count !== 1 ? 's' : ''}
</span>
{isOpen ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
@@ -87,7 +87,7 @@ export function LinkedSpansPanel({
}
return (
<div className="linked-spans__list">
<div className={styles.list}>
{linkedSpans.map((item) => (
<KeyValueLabel
key={item.spanId}
@@ -108,7 +108,7 @@ function LinkedSpans({ references }: LinkedSpansProps): JSX.Element {
const { linkedSpans, count, isOpen, toggleOpen } = useLinkedSpans(references);
return (
<div className="linked-spans">
<div className={styles.root}>
<LinkedSpansToggle count={count} isOpen={isOpen} toggleOpen={toggleOpen} />
<LinkedSpansPanel linkedSpans={linkedSpans} isOpen={isOpen} />
</div>

View File

@@ -0,0 +1,146 @@
.root {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.body {
padding: 12px;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
background: var(--l1-background);
font-size: 14px;
gap: 16px;
// DataViewer keeps its global `.data-viewer` class — give it a min-height
// so the tab area doesn't collapse on short content.
:global(.data-viewer) {
min-height: 500px;
}
}
.detailsSection {
flex-shrink: 0;
min-width: 0;
}
.tabsSection {
flex: 1;
min-width: 0;
max-height: 100%;
min-height: 400px;
display: flex;
flex-direction: column;
overflow: hidden;
// TabsRoot — direct child of tabs-section
> div {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
[role='tablist'] {
flex-shrink: 0;
}
[class*='tabs__list-wrapper'] {
padding-left: 0 !important;
}
}
.tabsScroll {
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
display: flex;
flex-direction: column;
[role='tabpanel'] {
padding: 0;
flex: 1;
min-height: 0;
}
}
.spanRow {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
justify-content: space-between;
}
.spanInfo {
display: flex;
flex-wrap: wrap;
gap: 4px 16px;
padding: 8px 0;
}
.spanInfoItem {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--l2-foreground);
white-space: nowrap;
}
.highlightedOptions {
padding: 8px 0;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0;
// KeyValueLabel uses a global `.key-value-label` root; constrain it
// inside the two-column grid so values can ellipsize cleanly.
:global(.key-value-label) {
width: auto;
min-width: 0;
overflow: hidden;
}
}
.serviceDot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent-forest);
flex-shrink: 0;
}
.traceId {
color: var(--accent-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
.traceIdCopy {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0;
height: auto;
justify-content: flex-start;
}
// Tooltip is rendered in a portal but the SpanDetailsPanel can be docked as a
// FloatingPanel (z-index 999), which would otherwise sit on top of the default
// tooltip (z-index 50). Bump the tooltip above the panel.
.dockToggleTooltip {
--tooltip-z-index: 1000;
}

View File

@@ -1,170 +0,0 @@
.span-details-panel {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
&__header-nav {
display: flex;
align-items: center;
gap: 2px;
}
&__body {
padding: 12px;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
background: var(--l1-background);
font-size: 14px;
gap: 16px;
.data-viewer {
min-height: 500px;
}
}
&__details-section {
flex-shrink: 0;
min-width: 0;
}
&__tabs-section {
flex: 1;
min-width: 0;
max-height: 100%;
min-height: 400px;
display: flex;
flex-direction: column;
overflow: hidden;
// TabsRoot — direct child of tabs-section
> div {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
[role='tablist'] {
flex-shrink: 0;
}
[class*='tabs__list-wrapper'] {
padding-left: 0 !important;
}
}
&__tabs-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
display: flex;
flex-direction: column;
[role='tabpanel'] {
padding: 0;
flex: 1;
min-height: 0;
}
}
&__span-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
justify-content: space-between;
}
&__span-info {
display: flex;
flex-wrap: wrap;
gap: 4px 16px;
padding: 8px 0;
}
&__span-info-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--l2-foreground);
white-space: nowrap;
}
&__highlighted-options {
padding: 8px 0;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0;
.key-value-label {
width: auto;
min-width: 0;
overflow: hidden;
}
}
&__service-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent-forest);
flex-shrink: 0;
}
&__trace-id {
color: var(--accent-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
&__trace-id-copy {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0;
height: auto;
justify-content: flex-start;
}
&__key-attributes {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 0;
&-label {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
color: var(--l2-foreground);
text-transform: uppercase;
letter-spacing: 0.48px;
line-height: var(--line-height-20);
}
&-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
}
}
// Tooltip is rendered in a portal but the SpanDetailsPanel can be docked as a
// FloatingPanel (z-index 999), which would otherwise sit on top of the default
// tooltip (z-index 50). Bump the tooltip above the panel.
.dock-toggle-tooltip {
--tooltip-z-index: 1000;
}

View File

@@ -72,7 +72,7 @@ import SpanPercentileBadge from './SpanPercentile/SpanPercentileBadge';
import SpanPercentilePanel from './SpanPercentile/SpanPercentilePanel';
import useSpanPercentile from './SpanPercentile/useSpanPercentile';
import './SpanDetailsPanel.styles.scss';
import styles from './SpanDetailsPanel.module.scss';
interface SpanDetailsPanelProps {
panelState: DetailsPanelState;
@@ -275,9 +275,9 @@ function SpanDetailsContent({
// }, [selectedSpan]);
return (
<div className="span-details-panel__body">
<div className="span-details-panel__details-section">
<div className="span-details-panel__span-row">
<div className={styles.body}>
<div className={styles.detailsSection}>
<div className={styles.spanRow}>
<KeyValueLabel
badgeKey="Span name"
badgeValue={selectedSpan.name}
@@ -296,8 +296,8 @@ function SpanDetailsContent({
<SpanPercentilePanel selectedSpan={selectedSpan} percentile={percentile} />
{/* Span info: exec time + start time */}
<div className="span-details-panel__span-info">
<div className="span-details-panel__span-info-item">
<div className={styles.spanInfo}>
<div className={styles.spanInfoItem}>
<Timer size={14} />
<span>
{getYAxisFormattedValue(`${selectedSpan.duration_nano / 1000000}`, 'ms')}
@@ -316,13 +316,13 @@ function SpanDetailsContent({
)}
</span>
</div>
<div className="span-details-panel__span-info-item">
<div className={styles.spanInfoItem}>
<CalendarClock size={14} />
<span>
{dayjs(selectedSpan.timestamp).format('HH:mm:ss — MMM D, YYYY')}
</span>
</div>
<div className="span-details-panel__span-info-item">
<div className={styles.spanInfoItem}>
<Link2 size={14} />
<LinkedSpansToggle
count={linkedSpans.count}
@@ -338,7 +338,7 @@ function SpanDetailsContent({
/>
{/* Step 6: HighlightedOptions */}
<div className="span-details-panel__highlighted-options">
<div className={styles.highlightedOptions}>
{HIGHLIGHTED_OPTIONS.map((option) => {
const rendered = option.render(selectedSpan);
if (!rendered) {
@@ -382,7 +382,7 @@ function SpanDetailsContent({
{/* Step 8: MiniTraceContext */}
</div>
<div className="span-details-panel__tabs-section">
<div className={styles.tabsSection}>
{/* Step 9: ContentTabs */}
<TabsRoot defaultValue="overview">
<TabsList variant="secondary">
@@ -402,7 +402,7 @@ function SpanDetailsContent({
)}
</TabsList>
<div className="span-details-panel__tabs-scroll">
<div className={styles.tabsScroll}>
<TabsContent value="overview">
<DataViewer
data={spanDisplayData}
@@ -512,7 +512,7 @@ function SpanDetailsPanel({
{isDocked ? <Dock size={14} /> : <PanelBottom size={14} />}
</Button>
</TooltipTrigger>
<TooltipContent className="dock-toggle-tooltip">
<TooltipContent className={styles.dockToggleTooltip}>
{isDocked ? 'Open as floating panel' : 'Dock at the bottom'}
</TooltipContent>
</Tooltip>
@@ -546,7 +546,7 @@ function SpanDetailsPanel({
traceEndTime={traceEndTime}
/>
) : (
<div className="span-details-panel__body">
<div className={styles.body}>
<Skeleton active paragraph={{ rows: 6 }} title={{ width: '60%' }} />
</div>
)}
@@ -554,7 +554,7 @@ function SpanDetailsPanel({
);
if (variant === SpanDetailVariant.DOCKED) {
return <div className="span-details-panel">{content}</div>;
return <div className={styles.root}>{content}</div>;
}
if (variant === SpanDetailVariant.DRAWER) {
@@ -562,7 +562,7 @@ function SpanDetailsPanel({
<DetailsPanelDrawer
isOpen={panelState.isOpen}
onClose={panelState.close}
className="span-details-panel"
className={styles.root}
>
{content}
</DetailsPanelDrawer>
@@ -572,7 +572,7 @@ function SpanDetailsPanel({
return (
<FloatingPanel
isOpen={panelState.isOpen}
className="span-details-panel"
className={styles.root}
width={PANEL_WIDTH}
minWidth={480}
height={window.innerHeight - PANEL_MARGIN_TOP - PANEL_MARGIN_BOTTOM}

View File

@@ -1,258 +0,0 @@
// Badge — wraps a KeyValueLabel, clickable to toggle panel
.span-percentile-badge {
cursor: pointer;
// Override key color for the percentile value (p99)
.key-value-label__key {
color: var(--destructive);
}
&__loader {
display: inline-flex;
align-items: center;
padding: 2px 4px;
color: var(--foreground);
}
&__value {
display: inline-flex;
align-items: center;
gap: 4px;
}
&__icon {
flex-shrink: 0;
color: var(--l2-foreground);
}
}
// Panel — collapsible, renders below the row
.span-percentile-panel {
display: flex;
flex-direction: column;
position: relative;
border: 1px solid var(--l1-border);
border-radius: 4px;
filter: drop-shadow(2px 4px 16px rgba(0, 0, 0, 0.2));
backdrop-filter: blur(20px);
margin: 8px 16px;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid var(--l1-border);
&-text {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
&-icon {
color: var(--l2-foreground);
}
}
&__content {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
&-title {
font-size: var(--font-size-sm);
line-height: var(--line-height-20);
}
&-highlight {
color: var(--destructive);
}
&-loader {
display: inline-flex;
align-items: flex-end;
margin: 0 4px;
line-height: 18px;
}
}
&__timerange {
width: 100%;
&-select {
width: 100%;
margin-top: 8px;
margin-bottom: 16px;
.ant-select-selector {
border-radius: 50px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
font-size: 12px;
height: 32px;
}
}
}
&__table {
&-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
&-text {
color: var(--l1-foreground);
font-size: 11px;
font-weight: 500;
line-height: 20px;
text-transform: uppercase;
}
}
&-rows {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
&-skeleton {
.ant-skeleton-title {
width: 100% !important;
margin-top: 0 !important;
}
.ant-skeleton-paragraph {
margin-top: 8px;
& > li + li {
margin-top: 10px;
width: 100% !important;
}
}
}
&-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 0 4px;
&-key {
flex: 0 0 auto;
color: var(--l1-foreground);
font-size: 12px;
font-weight: 500;
line-height: 20px;
}
&-value {
color: var(--l2-foreground);
font-size: 12px;
line-height: 20px;
}
&-dash {
flex: 1;
height: 0;
margin: 0 8px;
border-top: 1px solid transparent;
border-image: repeating-linear-gradient(
to right,
var(--l1-border) 0,
var(--l1-border) 10px,
transparent 10px,
transparent 20px
)
1 stretch;
}
&--current {
border-radius: 2px;
background: rgba(78, 116, 248, 0.2);
.span-percentile-panel__table-row-key {
color: var(--text-robin-300);
}
.span-percentile-panel__table-row-dash {
border-image: repeating-linear-gradient(
to right,
#abbdff 0,
#abbdff 10px,
transparent 10px,
transparent 20px
)
1 stretch;
}
.span-percentile-panel__table-row-value {
color: var(--text-robin-400);
}
}
}
}
&__resource-selector {
overflow: hidden;
width: calc(100% + 16px);
position: absolute;
top: 32px;
left: -8px;
z-index: 1000;
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
&-header {
border-bottom: 1px solid var(--l1-border);
}
&-input {
border-radius: 0;
border: none !important;
box-shadow: none !important;
height: 36px;
}
&-items {
height: 200px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--l3-background);
}
}
&-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
&-value {
color: var(--l1-foreground);
font-size: 13px;
line-height: 20px;
}
}
}
}

View File

@@ -0,0 +1,28 @@
// Badge — wraps a KeyValueLabel, clickable to toggle panel
.root {
cursor: pointer;
// KeyValueLabel renders its key with a global class; recolor only the badge
// instance inside this badge wrapper.
:global(.key-value-label__key) {
color: var(--destructive);
}
}
.loader {
display: inline-flex;
align-items: center;
padding: 2px 4px;
color: var(--foreground);
}
.value {
display: inline-flex;
align-items: center;
gap: 4px;
}
.icon {
flex-shrink: 0;
color: var(--l2-foreground);
}

View File

@@ -3,7 +3,7 @@ import KeyValueLabel from 'periscope/components/KeyValueLabel';
import { UseSpanPercentileReturn } from './useSpanPercentile';
import './SpanPercentile.styles.scss';
import styles from './SpanPercentileBadge.module.scss';
type SpanPercentileBadgeProps = Pick<
UseSpanPercentileReturn,
@@ -25,7 +25,7 @@ function SpanPercentileBadge({
}: SpanPercentileBadgeProps): JSX.Element | null {
if (loading) {
return (
<div className="span-percentile-badge__loader">
<div className={styles.loader}>
<Loader size={14} className="animate-spin" />
</div>
);
@@ -37,7 +37,7 @@ function SpanPercentileBadge({
return (
<div
className="span-percentile-badge"
className={styles.root}
onClick={toggleOpen}
role="button"
tabIndex={0}
@@ -50,12 +50,12 @@ function SpanPercentileBadge({
<KeyValueLabel
badgeKey={`p${percentileValue}`}
badgeValue={
<span className="span-percentile-badge__value">
<span className={styles.value}>
{duration}
{isOpen ? (
<ChevronUp size={14} className="span-percentile-badge__icon" />
<ChevronUp size={14} className={styles.icon} />
) : (
<ChevronDown size={14} className="span-percentile-badge__icon" />
<ChevronDown size={14} className={styles.icon} />
)}
</span>
}

View File

@@ -0,0 +1,217 @@
// Panel — collapsible, renders below the row
.root {
display: flex;
flex-direction: column;
position: relative;
border: 1px solid var(--l1-border);
border-radius: 4px;
filter: drop-shadow(2px 4px 16px rgba(0, 0, 0, 0.2));
backdrop-filter: blur(20px);
margin: 8px 16px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px;
border-bottom: 1px solid var(--l1-border);
}
.content {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
}
.contentTitle {
font-size: var(--font-size-sm);
line-height: var(--line-height-20);
}
.contentHighlight {
color: var(--destructive);
}
.contentLoader {
display: inline-flex;
align-items: flex-end;
margin: 0 4px;
line-height: 18px;
}
.timerange {
width: 100%;
}
.timerangeSelect {
width: 100%;
margin-top: 8px;
margin-bottom: 16px;
:global(.ant-select-selector) {
border-radius: 50px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
font-size: 12px;
height: 32px;
}
}
.tableHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.tableHeaderText {
color: var(--l1-foreground);
font-size: 11px;
font-weight: 500;
line-height: 20px;
text-transform: uppercase;
}
.tableRows {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
.tableSkeleton {
:global(.ant-skeleton-title) {
width: 100% !important;
margin-top: 0 !important;
}
:global(.ant-skeleton-paragraph) {
margin-top: 8px;
& > li + li {
margin-top: 10px;
width: 100% !important;
}
}
}
.tableRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 0 4px;
}
.tableRowKey {
flex: 0 0 auto;
color: var(--l1-foreground);
font-size: 12px;
font-weight: 500;
line-height: 20px;
}
.tableRowValue {
color: var(--l2-foreground);
font-size: 12px;
line-height: 20px;
}
.tableRowDash {
flex: 1;
height: 0;
margin: 0 8px;
border-top: 1px solid transparent;
border-image: repeating-linear-gradient(
to right,
var(--l1-border) 0,
var(--l1-border) 10px,
transparent 10px,
transparent 20px
)
1 stretch;
}
.isCurrent {
border-radius: 2px;
background: rgba(78, 116, 248, 0.2);
.tableRowKey {
color: var(--text-robin-300);
}
.tableRowDash {
border-image: repeating-linear-gradient(
to right,
#abbdff 0,
#abbdff 10px,
transparent 10px,
transparent 20px
)
1 stretch;
}
.tableRowValue {
color: var(--text-robin-400);
}
}
.resourceSelector {
overflow: hidden;
width: calc(100% + 16px);
position: absolute;
top: 32px;
left: -8px;
z-index: 1000;
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
}
.resourceSelectorHeader {
border-bottom: 1px solid var(--l1-border);
}
.resourceSelectorInput {
border-radius: 0;
border: none !important;
box-shadow: none !important;
height: 36px;
}
.resourceSelectorItems {
height: 200px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--l3-background);
}
}
.resourceSelectorItem {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
}
.resourceSelectorItemValue {
color: var(--l1-foreground);
font-size: 13px;
line-height: 20px;
}

View File

@@ -1,5 +1,7 @@
import { Checkbox, Input, Select, Skeleton } from 'antd';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import dayjs from 'dayjs';
@@ -8,7 +10,7 @@ import { SpanV3 } from 'types/api/trace/getTraceV3';
import { UseSpanPercentileReturn } from './useSpanPercentile';
import './SpanPercentile.styles.scss';
import styles from './SpanPercentilePanel.module.scss';
const DEFAULT_RESOURCE_ATTRIBUTES = {
serviceName: 'service.name',
@@ -53,46 +55,46 @@ function SpanPercentilePanel({
}
return (
<div className="span-percentile-panel">
<div className="span-percentile-panel__header">
<Typography.Text
className="span-percentile-panel__header-text"
<div className={styles.root}>
<div className={styles.header}>
<Button
variant="link"
color="secondary"
onClick={toggleOpen}
prefix={<ChevronDown size={16} />}
>
<ChevronDown size={16} /> Span Percentile
</Typography.Text>
Span Percentile
</Button>
{showResourceAttributesSelector ? (
<Check
size={16}
className="cursor-pointer span-percentile-panel__header-icon"
onClick={(): void => setShowResourceAttributesSelector(false)}
/>
) : (
<Plus
size={16}
className="cursor-pointer span-percentile-panel__header-icon"
onClick={(): void => setShowResourceAttributesSelector(true)}
/>
)}
<Button
variant="link"
color="secondary"
size="icon"
onClick={(): void =>
setShowResourceAttributesSelector(!showResourceAttributesSelector)
}
prefix={
showResourceAttributesSelector ? <Check size={16} /> : <Plus size={16} />
}
/>
</div>
{showResourceAttributesSelector && (
<div
className="span-percentile-panel__resource-selector"
className={styles.resourceSelector}
ref={resourceAttributesSelectorRef}
>
<div className="span-percentile-panel__resource-selector-header">
<div className={styles.resourceSelectorHeader}>
<Input
placeholder="Search resource attributes"
className="span-percentile-panel__resource-selector-input"
className={styles.resourceSelectorInput}
value={resourceAttributesSearchQuery}
onChange={(e): void =>
setResourceAttributesSearchQuery(e.target.value as string)
}
/>
</div>
<div className="span-percentile-panel__resource-selector-items">
<div className={styles.resourceSelectorItems}>
{spanResourceAttributes
.filter((attr) =>
attr.key
@@ -100,10 +102,7 @@ function SpanPercentilePanel({
.includes(resourceAttributesSearchQuery.toLowerCase()),
)
.map((attr) => (
<div
className="span-percentile-panel__resource-selector-item"
key={attr.key}
>
<div className={styles.resourceSelectorItem} key={attr.key}>
<Checkbox
checked={attr.isSelected}
onChange={(e): void => {
@@ -118,9 +117,7 @@ function SpanPercentilePanel({
attr.key === DEFAULT_RESOURCE_ATTRIBUTES.name
}
>
<div className="span-percentile-panel__resource-selector-item-value">
{attr.key}
</div>
<div className={styles.resourceSelectorItemValue}>{attr.key}</div>
</Checkbox>
</div>
))}
@@ -128,15 +125,15 @@ function SpanPercentilePanel({
</div>
)}
<div className="span-percentile-panel__content">
<Typography.Text className="span-percentile-panel__content-title">
<div className={styles.content}>
<Typography.Text className={styles.contentTitle}>
This span duration is{' '}
{!loading && spanPercentileData ? (
<span className="span-percentile-panel__content-highlight">
<span className={styles.contentHighlight}>
p{Math.floor(spanPercentileData.percentile || 0)}
</span>
) : (
<span className="span-percentile-panel__content-loader">
<span className={styles.contentLoader}>
<Loader size={12} className="animate-spin" />
</span>
)}{' '}
@@ -144,11 +141,11 @@ function SpanPercentilePanel({
hour(s) since the span start time.
</Typography.Text>
<div className="span-percentile-panel__timerange">
<div className={styles.timerange}>
<Select
labelInValue
placeholder="Select timerange"
className="span-percentile-panel__timerange-select"
className={styles.timerangeSelect}
getPopupContainer={(trigger): HTMLElement =>
trigger.parentElement || document.body
}
@@ -167,45 +164,45 @@ function SpanPercentilePanel({
/>
</div>
<div className="span-percentile-panel__table">
<div className="span-percentile-panel__table-header">
<Typography.Text className="span-percentile-panel__table-header-text">
<div>
<div className={styles.tableHeader}>
<Typography.Text className={styles.tableHeaderText}>
Percentile
</Typography.Text>
<Typography.Text className="span-percentile-panel__table-header-text">
<Typography.Text className={styles.tableHeaderText}>
Duration
</Typography.Text>
</div>
<div className="span-percentile-panel__table-rows">
<div className={styles.tableRows}>
{isLoadingData || isFetchingData ? (
<Skeleton
active
paragraph={{ rows: 3 }}
className="span-percentile-panel__table-skeleton"
className={styles.tableSkeleton}
/>
) : (
<>
{Object.entries(spanPercentileData?.percentiles || {}).map(
([pKey, pDuration]) => (
<div className="span-percentile-panel__table-row" key={pKey}>
<Typography.Text className="span-percentile-panel__table-row-key">
<div className={styles.tableRow} key={pKey}>
<Typography.Text className={styles.tableRowKey}>
{pKey}
</Typography.Text>
<div className="span-percentile-panel__table-row-dash" />
<Typography.Text className="span-percentile-panel__table-row-value">
<div className={styles.tableRowDash} />
<Typography.Text className={styles.tableRowValue}>
{getYAxisFormattedValue(`${pDuration / 1000000}`, 'ms')}
</Typography.Text>
</div>
),
)}
<div className="span-percentile-panel__table-row span-percentile-panel__table-row--current">
<Typography.Text className="span-percentile-panel__table-row-key">
<div className={cx(styles.tableRow, styles.isCurrent)}>
<Typography.Text className={styles.tableRowKey}>
p{Math.floor(spanPercentileData?.percentile || 0)}
</Typography.Text>
<div className="span-percentile-panel__table-row-dash" />
<Typography.Text className="span-percentile-panel__table-row-value">
<div className={styles.tableRowDash} />
<Typography.Text className={styles.tableRowValue}>
(this span){' '}
{getYAxisFormattedValue(
`${selectedSpan.duration_nano / 1000000}`,

View File

@@ -5,6 +5,8 @@ import { toast } from '@signozhq/ui/sonner';
import ROUTES from 'constants/routes';
import { SpanV3 } from 'types/api/trace/getTraceV3';
import styles from './SpanDetailsPanel.module.scss';
interface TraceIdFieldProps {
span: SpanV3;
}
@@ -36,7 +38,7 @@ export function TraceIdField({ span }: TraceIdFieldProps): JSX.Element {
<Button
variant="link"
color="secondary"
className="span-details-panel__trace-id-copy"
className={styles.traceIdCopy}
onClick={handleCopy}
title="Click to copy trace ID"
>
@@ -51,7 +53,7 @@ export function TraceIdField({ span }: TraceIdFieldProps): JSX.Element {
pathname: `/trace/${span.trace_id}`,
search: window.location.search,
}}
className="span-details-panel__trace-id"
className={styles.traceId}
>
{span.trace_id}
</Link>

View File

@@ -2,6 +2,7 @@ import { ReactNode } from 'react';
import { Badge } from '@signozhq/ui/badge';
import { SpanV3 } from 'types/api/trace/getTraceV3';
import styles from './SpanDetailsPanel.module.scss';
import { TraceIdField } from './TraceIdField';
interface HighlightedOption {
@@ -17,7 +18,7 @@ export const HIGHLIGHTED_OPTIONS: HighlightedOption[] = [
render: (span): ReactNode | null =>
span['service.name'] ? (
<Badge color="vanilla">
<span className="span-details-panel__service-dot" />
<span className={styles.serviceDot} />
{span['service.name']}
</Badge>
) : null,

View File

@@ -0,0 +1,60 @@
.root {
font-size: 12px;
color: var(--l1-foreground);
max-width: 300px;
}
.header {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--l2-background);
border: 1px solid var(--l2-border);
border-radius: 3px;
padding: 2px 6px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--l2-foreground);
margin-bottom: 6px;
}
.name {
font-weight: 600;
margin-bottom: 2px;
color: var(--text-robin-400);
}
.hasError {
color: var(--destructive);
}
.time {
font-size: 11px;
color: var(--l2-foreground);
margin-bottom: 4px;
}
.divider {
border-top: 1px solid var(--l2-border);
margin: 6px 0;
}
.attributes {
font-size: 11px;
}
.kv {
margin-bottom: 2px;
line-height: 1.4;
word-break: break-all;
}
.key {
color: var(--l2-foreground);
}
.value {
color: var(--l1-foreground);
}

View File

@@ -1,60 +0,0 @@
.event-tooltip-content {
font-size: 12px;
color: var(--l1-foreground);
max-width: 300px;
&__header {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--l2-background);
border: 1px solid var(--l2-border);
border-radius: 3px;
padding: 2px 6px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--l2-foreground);
margin-bottom: 6px;
}
&__name {
font-weight: 600;
margin-bottom: 2px;
color: var(--text-robin-400);
&.error {
color: var(--destructive);
}
}
&__time {
font-size: 11px;
color: var(--l2-foreground);
margin-bottom: 4px;
}
&__divider {
border-top: 1px solid var(--l2-border);
margin: 6px 0;
}
&__attributes {
font-size: 11px;
}
&__kv {
margin-bottom: 2px;
line-height: 1.4;
word-break: break-all;
}
&__key {
color: var(--l2-foreground);
}
&__value {
color: var(--l1-foreground);
}
}

View File

@@ -1,8 +1,9 @@
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import { Diamond } from '@signozhq/icons';
import cx from 'classnames';
import { toFixed } from 'utils/toFixed';
import './EventTooltipContent.styles.scss';
import styles from './EventTooltipContent.module.scss';
export interface EventTooltipContentProps {
eventName: string;
@@ -20,25 +21,25 @@ export function EventTooltipContent({
const { time, timeUnitName } = convertTimeToRelevantUnit(timeOffsetMs);
return (
<div className="event-tooltip-content">
<div className="event-tooltip-content__header">
<div className={styles.root}>
<div className={styles.header}>
<Diamond size={10} />
<span>EVENT DETAILS</span>
</div>
<div className={`event-tooltip-content__name ${isError ? 'error' : ''}`}>
<div className={cx(styles.name, isError && styles.hasError)}>
{eventName}
</div>
<div className="event-tooltip-content__time">
<div className={styles.time}>
{toFixed(time, 2)} {timeUnitName} since span start
</div>
{Object.keys(attributeMap).length > 0 && (
<>
<div className="event-tooltip-content__divider" />
<div className="event-tooltip-content__attributes">
<div className={styles.divider} />
<div className={styles.attributes}>
{Object.entries(attributeMap).map(([key, value]) => (
<div key={key} className="event-tooltip-content__kv">
<span className="event-tooltip-content__key">{key}:</span>{' '}
<span className="event-tooltip-content__value">{value}</span>
<div key={key} className={styles.kv}>
<span className={styles.key}>{key}:</span>{' '}
<span className={styles.value}>{value}</span>
</div>
))}
</div>

View File

@@ -2,13 +2,13 @@
// `top` is updated by the parent to track the hovered row's Y; its `left`
// is the sidebar/timeline boundary so the popover always opens at the same
// X regardless of which row is hovered.
.span-hover-card-anchor {
.anchor {
position: absolute;
width: 1px;
pointer-events: none;
}
.span-hover-card-popover {
.popover {
// Hover card may be rendered while the SpanDetailsPanel is docked as
// a FloatingPanel (z-index 999); bump above the default tooltip z-index.
--tooltip-z-index: 1000;
@@ -20,31 +20,29 @@
color: var(--l1-foreground);
}
// Flamegraph tooltip rendered as a portal, uses same semantic tokens.
// Position is set inline on the element (left/top track the cursor); the
// static layout/decoration lives here.
.flamegraph-tooltip {
position: fixed;
z-index: 1000;
pointer-events: none;
background-color: var(--l1-background);
padding: 8px 12px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid var(--l2-border);
}
.span-hover-card-content {
.content {
font-size: 12px;
color: var(--l1-foreground);
&__name {
font-weight: 600;
margin-bottom: 4px;
}
&__row {
line-height: 1.5;
color: var(--l2-foreground);
}
}
.name {
font-weight: 600;
margin-bottom: 4px;
}
.row {
line-height: 1.5;
color: var(--l2-foreground);
}
.preview {
// container for additional preview rows
}
.previewKey {
color: var(--l2-foreground);
}
.previewValue {
color: var(--l1-foreground);
}

View File

@@ -11,7 +11,7 @@ import { useMemo } from 'react';
import { SpanV3 } from 'types/api/trace/getTraceV3';
import { toFixed } from 'utils/toFixed';
import './SpanHoverCard.styles.scss';
import styles from './SpanHoverCard.module.scss';
/**
* Span-level fields that the tooltip always shows (as the colored title or
@@ -51,27 +51,21 @@ export function SpanTooltipContent({
convertTimeToRelevantUnit(durationMs);
return (
<div className="span-hover-card-content">
<div className="span-hover-card-content__name" style={{ color }}>
<div className={styles.content}>
<div className={styles.name} style={{ color }}>
{spanName}
</div>
<div className="span-hover-card-content__row">
status: {hasError ? 'error' : 'ok'}
</div>
<div className="span-hover-card-content__row">
start: {toFixed(relativeStartMs, 2)} ms
</div>
<div className="span-hover-card-content__row">
<div className={styles.row}>status: {hasError ? 'error' : 'ok'}</div>
<div className={styles.row}>start: {toFixed(relativeStartMs, 2)} ms</div>
<div className={styles.row}>
duration: {toFixed(formattedDuration, 2)} {timeUnitName}
</div>
{previewRows && previewRows.length > 0 && (
<div className="span-hover-card-content__preview">
<div className={styles.preview}>
{previewRows.map((row) => (
<div key={row.key} className="span-hover-card-content__row">
<span className="span-hover-card-content__preview-key">{row.key}:</span>{' '}
<span className="span-hover-card-content__preview-value">
{row.value}
</span>
<div key={row.key} className={styles.row}>
<span className={styles.previewKey}>{row.key}:</span>{' '}
<span className={styles.previewValue}>{row.value}</span>
</div>
))}
</div>
@@ -152,7 +146,7 @@ export function SpanHoverCard({
<Tooltip open={hoverCardData !== null} onOpenChange={onOpenChange}>
<TooltipTrigger asChild>
<div
className="span-hover-card-anchor"
className={styles.anchor}
style={{
top: hoverCardData?.anchorTop ?? 0,
left: anchorLeft,
@@ -164,7 +158,7 @@ export function SpanHoverCard({
side="right"
align="start"
sideOffset={8}
className="span-hover-card-popover"
className={styles.popover}
>
{hoverCardData && <SpanTooltipContent {...hoverCardData.tooltip} />}
</TooltipContent>

View File

@@ -0,0 +1,75 @@
.wrapper {
flex-shrink: 0;
position: relative;
}
.header {
display: flex;
align-items: center;
padding: 8px 16px;
gap: 8px;
// KeyValueLabel renders with a global `.key-value-label` root; keep it from
// shrinking on the trace details header.
:global(.key-value-label) {
flex-shrink: 0;
}
}
.backBtn {
flex-shrink: 0;
}
.filter {
min-width: 0;
}
.isExpanded {
max-width: none;
flex: 1;
}
.oldViewBtn {
flex-shrink: 0;
}
.analyticsBtn {
flex-shrink: 0;
margin-left: auto;
}
.subHeader {
display: flex;
align-items: center;
gap: 16px;
padding: 4px 16px 8px;
font-size: 13px;
color: var(--l2-foreground);
}
.subItem {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.separator {
color: var(--l2-foreground);
opacity: 0.5;
}
.entryPointBadge {
padding: 2px 8px;
border: 1px solid var(--l2-border);
border-radius: 4px;
font-size: 12px;
}
.skeleton {
:global(.ant-skeleton-input) {
width: 160px !important;
min-height: 20px !important;
height: 20px !important;
}
}

View File

@@ -1,74 +0,0 @@
.trace-details-header-wrapper {
flex-shrink: 0;
position: relative;
}
.trace-details-header {
display: flex;
align-items: center;
padding: 8px 16px;
gap: 8px;
&__back-btn {
flex-shrink: 0;
}
.key-value-label {
flex-shrink: 0;
}
&__filter {
&.trace-v3-filter-row {
padding: 0;
}
min-width: 0;
&--expanded {
max-width: none;
flex: 1;
}
}
&__old-view-btn {
flex-shrink: 0;
}
&__analytics-btn {
flex-shrink: 0;
margin-left: auto;
}
&__sub-header {
display: flex;
align-items: center;
gap: 16px;
padding: 4px 16px 8px;
font-size: 13px;
color: var(--l2-foreground);
}
&__sub-item {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
&__separator {
color: var(--l2-foreground);
opacity: 0.5;
}
&__entry-point-badge {
padding: 2px 8px;
border: 1px solid var(--l2-border);
border-radius: 4px;
font-size: 12px;
}
&__skeleton .ant-skeleton-input {
width: 160px !important;
min-height: 20px !important;
height: 20px !important;
}
}

View File

@@ -9,6 +9,7 @@ import {
} from '@signozhq/ui/tooltip';
import { Skeleton } from 'antd';
import setLocalStorageKey from 'api/browser/localstorage/set';
import cx from 'classnames';
import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
@@ -33,7 +34,7 @@ import AnalyticsPanel from '../SpanDetailsPanel/AnalyticsPanel/AnalyticsPanel';
import Filters from '../TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters';
import TraceOptionsMenu from './TraceOptionsMenu';
import './TraceDetailsHeader.styles.scss';
import styles from './TraceDetailsHeader.module.scss';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
interface FilterMetadata {
@@ -68,7 +69,7 @@ function DetailsLoader(): JSX.Element {
key={i}
active
size="small"
className="trace-details-header__skeleton"
className={styles.skeleton}
/>
))}
</>
@@ -120,15 +121,15 @@ function TraceDetailsHeader({
convertTimeToRelevantUnit(durationMs);
return (
<div className="trace-details-header-wrapper">
<div className="trace-details-header">
<div className={styles.wrapper}>
<div className={styles.header}>
{!isFilterExpanded && (
<>
<Button
variant="solid"
color="secondary"
size="md"
className="trace-details-header__back-btn"
className={styles.backBtn}
onClick={handlePreviousBtnClick}
>
<ArrowLeft size={14} />
@@ -151,7 +152,7 @@ function TraceDetailsHeader({
variant="ghost"
size="icon"
color="secondary"
className="trace-details-header__analytics-btn"
className={styles.analyticsBtn}
onClick={(): void => setIsAnalyticsOpen((prev) => !prev)}
>
<ChartPie size={14} />
@@ -167,11 +168,7 @@ function TraceDetailsHeader({
/>
</>
)}
<div
className={`trace-details-header__filter${
isFilterExpanded ? ' trace-details-header__filter--expanded' : ''
}`}
>
<div className={cx(styles.filter, isFilterExpanded && styles.isExpanded)}>
<Filters
startTime={filterMetadata.startTime}
endTime={filterMetadata.endTime}
@@ -187,7 +184,7 @@ function TraceDetailsHeader({
variant="solid"
color="secondary"
size="sm"
className="trace-details-header__old-view-btn"
className={styles.oldViewBtn}
onClick={handleSwitchToOldView}
>
Legacy View
@@ -198,22 +195,22 @@ function TraceDetailsHeader({
</div>
{showTraceDetails && (
<div className="trace-details-header__sub-header">
<div className={styles.subHeader}>
{traceMetadata ? (
<>
<span className="trace-details-header__sub-item">
<span className={styles.subItem}>
<Server size={13} />
{traceMetadata.rootServiceName}
<span className="trace-details-header__separator"></span>
<span className="trace-details-header__entry-point-badge">
<span className={styles.separator}></span>
<span className={styles.entryPointBadge}>
{traceMetadata.rootServiceEntryPoint}
</span>
</span>
<span className="trace-details-header__sub-item">
<span className={styles.subItem}>
<Timer size={13} />
{parseFloat(formattedDuration.toFixed(2))} {timeUnitName}
</span>
<span className="trace-details-header__sub-item">
<span className={styles.subItem}>
<CalendarClock size={13} />
{dayjs(traceMetadata.startTimestampMillis).format(
DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS,

View File

@@ -0,0 +1,127 @@
.root {
height: calc(100vh);
display: flex;
flex-direction: column;
}
.content {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
// Shared Ant Collapse chrome reset for both the flamegraph and waterfall
// collapse panels.
.flameCollapse,
.waterfallCollapse {
border: none;
border-radius: 0;
background: transparent;
:global(.ant-collapse-item) {
border: none;
}
:global(.ant-collapse-header) {
border-top: 1px solid var(--l2-border);
border-bottom: 1px solid var(--l2-border);
}
:global(.ant-collapse-content) {
background: transparent;
border-top: none;
// Disable collapse animation — virtualizer and canvas flicker during
// height transitions.
transition: none !important;
}
}
.collapseLabel {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.collapseTitle {
display: inline-flex;
align-items: center;
gap: 6px;
}
.collapseCount {
display: flex;
gap: 12px;
align-items: center;
font-size: 12px;
font-weight: 400;
color: var(--l2-foreground);
}
.collapseCountItem {
display: inline-flex;
align-items: center;
gap: 4px;
}
.hasErrors {
color: var(--destructive);
}
.flameCollapse {
flex-shrink: 0;
:global(.ant-collapse-content-box) {
padding: 0 !important;
}
}
.waterfallCollapse {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
:global(.ant-collapse-item) {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
:global(.ant-collapse-content.ant-collapse-content-active) {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
:global(.ant-collapse-content-box) {
padding: 0 !important;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
&.isDocked {
flex: none;
:global(.ant-collapse-item) {
flex: none;
display: block;
}
:global(.ant-collapse-content-box) {
flex: none;
display: block;
}
}
}
.dockedSpanDetails {
flex: 1;
overflow: auto;
min-height: 0;
}

View File

@@ -1,124 +0,0 @@
.trace-details-v3 {
height: calc(100vh);
display: flex;
flex-direction: column;
&__content {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
&__flame-collapse,
&__waterfall-collapse {
border: none;
border-radius: 0;
background: transparent;
.ant-collapse-item {
border: none;
}
.ant-collapse-header {
border-top: 1px solid var(--l2-border);
border-bottom: 1px solid var(--l2-border);
}
.ant-collapse-content {
background: transparent;
border-top: none;
// Disable collapse animation — virtualizer and canvas flicker during height transitions
transition: none !important;
}
}
&__collapse-label {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
&__collapse-title {
display: inline-flex;
align-items: center;
gap: 6px;
}
&__collapse-count {
display: flex;
gap: 12px;
align-items: center;
font-size: 12px;
font-weight: 400;
color: var(--l2-foreground);
}
&__collapse-count-item {
display: inline-flex;
align-items: center;
gap: 4px;
}
&__collapse-count-errors {
color: var(--destructive);
}
&__flame-collapse {
flex-shrink: 0;
.ant-collapse-content-box {
padding: 0 !important;
}
}
&__waterfall-collapse {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-collapse-item {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.ant-collapse-content.ant-collapse-content-active {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.ant-collapse-content-box {
padding: 0 !important;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
&--docked {
flex: none;
.ant-collapse-item {
flex: none;
display: block;
}
.ant-collapse-content-box {
flex: none;
display: block;
}
}
}
&__docked-span-details {
flex: 1;
overflow: auto;
min-height: 0;
}
}

View File

@@ -0,0 +1,42 @@
.root {
display: flex;
flex-direction: column;
height: 100%;
padding: 0 15px;
}
.viewport {
flex: 1;
overflow: hidden;
position: relative;
}
.main {
display: block;
width: 100%;
cursor: grab;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
pointer-events: none;
}
// Flamegraph tooltip — rendered as a portal, follows the cursor.
// Position is set inline on the element (left/top); the layout/decoration
// lives here. Shares visual treatment with the waterfall hover popover
// but is positioned `fixed` instead of anchored.
.tooltip {
position: fixed;
z-index: 1000;
pointer-events: none;
background-color: var(--l1-background);
padding: 8px 12px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid var(--l2-border);
color: var(--l1-foreground);
}

View File

@@ -1,26 +0,0 @@
.flamegraph-canvas {
display: flex;
flex-direction: column;
height: 100%;
padding: 0 15px;
}
.flamegraph-canvas__viewport {
flex: 1;
overflow: hidden;
position: relative;
}
.flamegraph-canvas__main {
display: block;
width: 100%;
cursor: grab;
}
.flamegraph-canvas__overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
pointer-events: none;
}

View File

@@ -16,7 +16,7 @@ import { useFlamegraphZoom } from './hooks/useFlamegraphZoom';
import { useScrollToSpan } from './hooks/useScrollToSpan';
import { EventRect, FlamegraphCanvasProps, SpanRect } from './types';
import './FlamegraphCanvas.styles.scss';
import styles from './FlamegraphCanvas.module.scss';
function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
const {
@@ -194,7 +194,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
const tooltipElement = tooltipContent
? createPortal(
<div
className="span-hover-card-popover flamegraph-tooltip"
className={styles.tooltip}
style={{
left: Math.min(tooltipContent.clientX + 15, window.innerWidth - 220),
top: Math.min(tooltipContent.clientY + 15, window.innerHeight - 100),
@@ -223,7 +223,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
: null;
return (
<div className="flamegraph-canvas">
<div className={styles.root}>
{tooltipElement}
<TimelineV3
startTimestamp={viewStartTs}
@@ -234,7 +234,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
/>
<div
ref={containerRef}
className="flamegraph-canvas__viewport"
className={styles.viewport}
onMouseEnter={(): void => {
isOverFlamegraphRef.current = true;
}}
@@ -242,7 +242,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
>
<canvas
ref={canvasRef}
className="flamegraph-canvas__main"
className={styles.main}
onMouseDown={(e): void => {
handleMouseDown(e);
handleMouseDownForClick(e);
@@ -251,7 +251,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
onMouseUp={handleMouseUp}
onClick={handleClick}
/>
<canvas ref={overlayCanvasRef} className="flamegraph-canvas__overlay" />
<canvas ref={overlayCanvasRef} className={styles.overlay} />
</div>
</div>
);

View File

@@ -0,0 +1,230 @@
// Container — applied to the `.ant-modal` element via SignozModal's className
// prop. Ant Modal portals into document.body, but the className still lives on
// the modal root, so descendant overrides work via `:global` nesting.
.container {
:global(.ant-modal-content),
:global(.ant-modal-header) {
background: var(--l1-background);
}
:global(.ant-modal-header) {
border-bottom: none;
:global(.ant-modal-title) {
color: var(--l1-foreground);
}
}
:global(.ant-modal-body) {
padding: 14px 16px !important;
padding-bottom: 0 !important;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
:global(.ant-modal-footer) {
margin-top: 0;
background: var(--l2-background);
border-top: 1px solid var(--l2-border);
padding: 16px !important;
.saveButton {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
color: var(--l1-foreground);
font-size: 12px;
font-weight: 500;
line-height: 24px;
width: 135px;
:global(.ant-btn-icon) {
display: flex;
}
&:disabled {
color: var(--l2-foreground);
:global(.ant-btn-icon svg) {
stroke: var(--l2-foreground);
}
}
}
.discardButton {
background: var(--l3-background);
}
:global(.ant-btn) {
border-radius: 2px;
padding: 4px 8px;
margin: 0 !important;
border: none;
box-shadow: none;
}
}
}
// Inner content wrapper (sibling of modal chrome)
.root {
// Funnel-detail view overrides — only when the inner wrapper has
// `.isDetails` applied alongside `.root`.
&.isDetails {
:global(.traces-funnel-details) {
height: unset;
:global(.traces-funnel-details__steps-config) {
width: unset;
border: none;
}
:global(.funnel-step-wrapper) {
gap: 15px;
}
:global(.steps-content) {
max-height: 500px;
}
}
}
:global(.funnel-item) {
padding: 8px 8px 12px 16px;
&,
&:first-child {
border-radius: 6px;
}
:global(.funnel-item__header) {
line-height: 20px;
}
:global(.funnel-item__details) {
line-height: 18px;
}
}
}
.loadingSpinner {
display: flex;
align-items: center;
justify-content: center;
height: 400px;
}
.search {
display: flex;
gap: 12px;
margin-bottom: 14px;
align-items: center;
}
.searchInput {
flex: 1;
padding: 6px 8px;
background: var(--l3-background);
:global(.ant-input-prefix) {
height: 18px;
margin-inline-end: 6px;
svg {
opacity: 0.4;
}
}
&,
input {
font-size: 14px;
line-height: 18px;
letter-spacing: -0.07px;
font-weight: 400;
background: var(--l3-background);
}
input::placeholder {
color: var(--l2-foreground);
opacity: 0.6;
}
}
.createButton {
width: 153px;
padding: 4px 8px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
flex-shrink: 0;
border-radius: 2px;
background: var(--l3-background);
border: none;
box-shadow: none;
color: var(--l2-foreground);
font-size: 12px;
font-weight: 500;
line-height: 24px;
}
.list {
max-height: 400px;
overflow-y: scroll;
:global(.funnels-empty__content) {
padding: 0;
}
:global(.funnels-list) {
gap: 8px;
:global(.funnel-item) {
padding: 8px 16px 12px;
:global(.funnel-item__details) {
margin-top: 8px;
}
}
}
}
.spinner {
height: 400px;
}
.backButton {
display: flex;
align-items: center;
gap: 6px;
color: var(--l2-foreground);
font-size: 14px;
line-height: 20px;
margin-bottom: 14px;
}
.detailsView {
display: flex;
flex-direction: column;
gap: 24px;
:global(.funnel-configuration__steps) {
padding: 0;
:global(.funnel-step__content .filters__service-and-span .ant-select) {
width: 170px;
}
:global(.funnel-step__footer .error) {
width: 25%;
}
:global(.inter-step-config) {
width: calc(100% - 104px);
}
}
:global(.funnel-item__actions-popover) {
display: none;
}
}

View File

@@ -1,236 +0,0 @@
// Modal base styles
.add-span-to-funnel-modal {
&__loading-spinner {
display: flex;
align-items: center;
justify-content: center;
height: 400px;
}
&-container {
.ant-modal {
&-content,
&-header {
background: var(--l1-background);
}
&-header {
border-bottom: none;
.ant-modal-title {
color: var(--l1-foreground);
}
}
&-body {
padding: 14px 16px !important;
padding-bottom: 0 !important;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
&-footer {
margin-top: 0;
background: var(--l2-background);
border-top: 1px solid var(--l2-border);
padding: 16px !important;
.add-span-to-funnel-modal {
&__save-button {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
color: var(--l1-foreground);
font-size: 12px;
font-weight: 500;
line-height: 24px;
width: 135px;
.ant-btn-icon {
display: flex;
}
&:disabled {
color: var(--l2-foreground);
.ant-btn-icon {
svg {
stroke: var(--l2-foreground);
}
}
}
}
&__discard-button {
background: var(--l3-background);
}
}
.ant-btn {
border-radius: 2px;
padding: 4px 8px;
margin: 0 !important;
border: none;
box-shadow: none;
}
}
}
}
}
// Main modal styles
.add-span-to-funnel-modal {
// Common button styles
%button-base {
display: flex;
align-items: center;
}
// Details view styles
&--details {
.traces-funnel-details {
height: unset;
&__steps-config {
width: unset;
border: none;
}
.funnel-step-wrapper {
gap: 15px;
}
.steps-content {
max-height: 500px;
}
}
}
// Search section
&__search {
display: flex;
gap: 12px;
margin-bottom: 14px;
align-items: center;
&-input {
flex: 1;
padding: 6px 8px;
background: var(--l3-background);
.ant-input-prefix {
height: 18px;
margin-inline-end: 6px;
svg {
opacity: 0.4;
}
}
&,
input {
font-size: 14px;
line-height: 18px;
letter-spacing: -0.07px;
font-weight: 400;
background: var(--l3-background);
}
input::placeholder {
color: var(--l2-foreground);
opacity: 0.6;
}
}
}
// Create button
&__create-button {
@extend %button-base;
width: 153px;
padding: 4px 8px;
justify-content: center;
gap: 4px;
flex-shrink: 0;
border-radius: 2px;
background: var(--l3-background);
border: none;
box-shadow: none;
color: var(--l2-foreground);
font-size: 12px;
font-weight: 500;
line-height: 24px;
}
.funnel-item {
padding: 8px 8px 12px 16px;
&,
&:first-child {
border-radius: 6px;
}
&__header {
line-height: 20px;
}
&__details {
line-height: 18px;
}
}
// List section
&__list {
max-height: 400px;
overflow-y: scroll;
.funnels-empty {
&__content {
padding: 0;
}
}
.funnels-list {
gap: 8px;
.funnel-item {
padding: 8px 16px 12px;
&__details {
margin-top: 8px;
}
}
}
}
&__spinner {
height: 400px;
}
// Back button
&__back-button {
@extend %button-base;
gap: 6px;
color: var(--l2-foreground);
font-size: 14px;
line-height: 20px;
margin-bottom: 14px;
}
// Details section
&__details {
display: flex;
flex-direction: column;
gap: 24px;
.funnel-configuration__steps {
padding: 0;
.funnel-step {
&__content .filters__service-and-span .ant-select {
width: 170px;
}
&__footer .error {
width: 25%;
}
}
.inter-step-config {
width: calc(100% - 104px);
}
}
.funnel-item__actions-popover {
display: none;
}
}
}

View File

@@ -22,7 +22,7 @@ import { filterFunnelsByQuery } from 'pages/TracesFunnels/utils';
import { SpanV3 } from 'types/api/trace/getTraceV3';
import { FunnelData } from 'types/api/traceFunnels';
import './AddSpanToFunnelModal.styles.scss';
import styles from './AddSpanToFunnelModal.module.scss';
enum ModalView {
LIST = 'list',
@@ -62,7 +62,7 @@ function FunnelDetailsView({
}, [triggerDiscard, funnel.steps, handleRestoreSteps]);
return (
<div className="add-span-to-funnel-modal__details">
<div className={styles.detailsView}>
<FunnelListItem
funnel={funnel}
shouldRedirectToTracesListOnDeleteSuccess={false}
@@ -161,11 +161,11 @@ function AddSpanToFunnelModal({
};
const renderListView = (): JSX.Element => (
<div className="add-span-to-funnel-modal">
<div className={styles.root}>
{!!filteredData?.length && (
<div className="add-span-to-funnel-modal__search">
<div className={styles.search}>
<Input
className="add-span-to-funnel-modal__search-input"
className={styles.searchInput}
placeholder="Search by name, description, or tags..."
prefix={<Search size={12} />}
value={searchQuery}
@@ -173,7 +173,7 @@ function AddSpanToFunnelModal({
/>
</div>
)}
<div className="add-span-to-funnel-modal__list">
<div className={styles.list}>
<OverlayScrollbar>
<TracesFunnelsContentRenderer
isError={isError}
@@ -201,11 +201,11 @@ function AddSpanToFunnelModal({
);
const renderDetailsView = ({ span }: { span: SpanV3 }): JSX.Element => (
<div className="add-span-to-funnel-modal add-span-to-funnel-modal--details">
<div className={cx(styles.root, styles.isDetails)}>
<Button
variant="ghost"
color="secondary"
className="add-span-to-funnel-modal__back-button"
className={styles.backButton}
onClick={handleBack}
prefix={<ArrowLeft size={14} />}
>
@@ -214,7 +214,7 @@ function AddSpanToFunnelModal({
<div className="traces-funnel-details">
<div className="traces-funnel-details__steps-config">
<Spin
className="add-span-to-funnel-modal__loading-spinner"
className={styles.loadingSpinner}
spinning={isFunnelDetailsLoading || isFunnelDetailsFetching}
indicator={<LoaderCircle size={14} className="animate-spin" />}
>
@@ -245,10 +245,7 @@ function AddSpanToFunnelModal({
onCancel={onClose}
width={570}
title="Add span to funnel"
className={cx('add-span-to-funnel-modal-container', {
'add-span-to-funnel-modal-container--details':
activeView === ModalView.DETAILS,
})}
className={styles.container}
footer={
activeView === ModalView.DETAILS
? [
@@ -257,7 +254,7 @@ function AddSpanToFunnelModal({
color="secondary"
key="discard"
onClick={handleDiscard}
className="add-span-to-funnel-modal__discard-button"
className={styles.discardButton}
disabled={!isUnsavedChanges}
>
Discard
@@ -266,7 +263,7 @@ function AddSpanToFunnelModal({
key="save"
variant="solid"
color="primary"
className="add-span-to-funnel-modal__save-button"
className={styles.saveButton}
onClick={handleSaveFunnel}
disabled={!isUnsavedChanges}
prefix={<Check size={14} />}
@@ -279,7 +276,7 @@ function AddSpanToFunnelModal({
key="create"
variant="outlined"
color="secondary"
className="add-span-to-funnel-modal__create-button"
className={styles.createButton}
onClick={handleCreateNewClick}
prefix={<Plus size={14} />}
>

View File

@@ -1,4 +1,4 @@
.span-line-action-buttons {
.root {
display: flex;
position: absolute;
transform: translate(-50%, -50%);
@@ -8,20 +8,20 @@
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
}
.copy-span-btn {
border: none;
box-shadow: none;
padding: 9px;
justify-content: center;
align-items: center;
display: flex;
border-color: var(--l1-border) !important;
}
.copyBtn {
border: none;
box-shadow: none;
padding: 9px;
justify-content: center;
align-items: center;
display: flex;
border-color: var(--l1-border) !important;
}
// Tooltip rendered in a portal; bump above FloatingPanel (z-index 999) so it
// stays visible when the SpanDetailsPanel is docked as a floating panel.
.span-line-action-tooltip {
.tooltip {
--tooltip-z-index: 1000;
}

View File

@@ -70,24 +70,6 @@ describe('SpanLineActionButtons', () => {
expect(mockOnSpanCopy).toHaveBeenCalledTimes(1);
});
it('applies correct styling classes', () => {
(useCopySpanLink as jest.Mock).mockReturnValue({
onSpanCopy: jest.fn(),
});
render(<SpanLineActionButtons span={mockSpan} />);
// Check if the main container has the correct class
const container = screen
.getByRole('button')
.closest('.span-line-action-buttons');
expect(container).toHaveClass('span-line-action-buttons');
// Check if the button has the correct class
const copyButton = screen.getByRole('button');
expect(copyButton).toHaveClass('copy-span-btn');
});
it('copies span link to clipboard when copy button is clicked', () => {
const mockSetCopy = jest.fn();
const mockUrlQuery = {

View File

@@ -9,7 +9,7 @@ import { useCopySpanLink } from 'hooks/trace/useCopySpanLink';
import { Link } from '@signozhq/icons';
import { Span } from 'types/api/trace/getTraceV2';
import './SpanLineActionButtons.styles.scss';
import styles from './SpanLineActionButtons.module.scss';
export interface SpanLineActionButtonsProps {
span: Span;
@@ -20,7 +20,7 @@ export default function SpanLineActionButtons({
const { onSpanCopy } = useCopySpanLink(span);
return (
<div className="span-line-action-buttons">
<div className={styles.root}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@@ -29,14 +29,12 @@ export default function SpanLineActionButtons({
size="icon"
color="secondary"
onClick={onSpanCopy}
className="copy-span-btn"
className={styles.copyBtn}
>
<Link size={14} />
</Button>
</TooltipTrigger>
<TooltipContent className="span-line-action-tooltip">
Copy Span Link
</TooltipContent>
<TooltipContent className={styles.tooltip}>Copy Span Link</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>

View File

@@ -0,0 +1,11 @@
.root {
height: 100%;
display: flex;
flex-direction: column;
}
.loadingSkeleton {
justify-content: center;
align-items: center;
padding: 20px;
}

View File

@@ -1,11 +0,0 @@
.trace-waterfall {
height: 100%;
display: flex;
flex-direction: column;
.loading-skeleton {
justify-content: center;
align-items: center;
padding: 20px;
}
}

View File

@@ -13,7 +13,7 @@ import { getVisibleSpans } from './utils';
import { IInterestedSpan } from './types';
import './TraceWaterfall.styles.scss';
import styles from './TraceWaterfall.module.scss';
interface ITraceWaterfallProps {
traceId: string;
@@ -100,7 +100,7 @@ function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
switch (traceWaterfallState) {
case TraceWaterfallStates.LOADING:
return (
<div className="loading-skeleton">
<div className={styles.loadingSkeleton}>
<Skeleton active paragraph={{ rows: 6 }} />
</div>
);
@@ -158,7 +158,7 @@ function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
uncollapsedNodes,
]);
return <div className="trace-waterfall">{getContent}</div>;
return <div className={styles.root}>{getContent}</div>;
}
export default TraceWaterfall;

View File

@@ -0,0 +1,24 @@
.root {
display: flex;
padding: 12px;
margin: 20px;
gap: 12px;
align-items: flex-start;
border-radius: 4px;
background: var(--destructive);
}
.text,
.value {
color: var(--destructive-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
.text {
flex-shrink: 0;
}

View File

@@ -1,30 +0,0 @@
.error-waterfall {
display: flex;
padding: 12px;
margin: 20px;
gap: 12px;
align-items: flex-start;
border-radius: 4px;
background: var(--destructive);
.text {
color: var(--destructive-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
flex-shrink: 0;
}
.value {
color: var(--destructive-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}

View File

@@ -2,7 +2,7 @@ import { Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { AxiosError } from 'axios';
import './Error.styles.scss';
import styles from './Error.module.scss';
interface IErrorProps {
error: AxiosError;
@@ -12,10 +12,16 @@ function Error(props: IErrorProps): JSX.Element {
const { error } = props;
return (
<div className="error-waterfall">
<Typography.Text className="text">Something went wrong!</Typography.Text>
<div className={styles.root}>
<Typography.Text className={styles.text}>
Something went wrong!
</Typography.Text>
<Tooltip title={error?.message}>
<Typography.Text className="value" title={error?.message} truncate={1}>
<Typography.Text
className={styles.value}
title={error?.message}
truncate={1}
>
{error?.message}
</Typography.Text>
</Tooltip>

View File

@@ -0,0 +1,136 @@
.root {
display: flex;
align-items: center;
gap: 12px;
// QuerySearch child sets `query-builder-search-v2` globally; size it to the
// search container by reaching into the descendant.
:global(.query-builder-search-v2) {
width: 100%;
}
// ToggleGroup children use generated class names; nest the global selectors
// under the local row so they only apply inside this filter row.
:global([class*='toggle-group']) {
flex-shrink: 0;
:global([class*='toggle-group-item']) {
flex: 0 0 auto;
}
}
}
.isExpanded {
flex: 1;
}
.searchContainer {
flex: 1;
min-width: 0;
}
.pill {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border: 1px solid var(--l1-border);
border-radius: 4px;
cursor: pointer;
max-width: 220px;
min-width: 120px;
height: 32px;
background: var(--l1-background);
&:hover {
border-color: var(--l3-border);
}
}
.pillText {
font-size: 12px;
color: var(--l2-foreground);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.pillIndicator {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--primary-background);
flex-shrink: 0;
}
.pillPopover {
max-width: 400px;
}
.pillPopoverHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.pillPopoverExpression {
font-family: 'Geist Mono', 'Fira Code', monospace;
font-size: 12px;
color: var(--l1-foreground);
word-break: break-all;
padding: 6px 8px;
background: var(--l2-background);
border-radius: 4px;
}
.collapseBtn {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: none;
}
.highlightErrorsToggle {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
white-space: nowrap;
}
.preNextToggle {
display: flex;
flex-shrink: 0;
gap: 12px;
}
.preNextCount {
display: flex;
align-items: center;
margin: auto;
color: var(--l2-foreground);
font-family: 'Geist Mono';
font-size: 12px;
font-weight: 400;
line-height: 18px;
}
.filterStatus {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
color: var(--l2-foreground);
font-family: 'Geist Mono';
font-size: 12px;
font-weight: 400;
line-height: 18px;
}
.hasError {
color: var(--destructive);
cursor: help;
}

View File

@@ -1,145 +0,0 @@
.trace-v3-filter-row {
display: flex;
align-items: center;
gap: 12px;
&.expanded {
flex: 1;
}
.filter-search-container {
flex: 1;
min-width: 0;
}
.query-builder-search-v2 {
width: 100%;
}
// --- Collapsed pill ---
.filter-pill {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border: 1px solid var(--l1-border);
border-radius: 4px;
cursor: pointer;
max-width: 220px;
min-width: 120px;
height: 32px;
background: var(--l1-background);
&:hover {
border-color: var(--l3-border);
}
&__text {
font-size: 12px;
color: var(--l2-foreground);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
&__indicator {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--primary-background);
flex-shrink: 0;
}
}
// --- Collapsed pill popover ---
.filter-pill-popover {
max-width: 400px;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
&__expression {
font-family: 'Geist Mono', 'Fira Code', monospace;
font-size: 12px;
color: var(--l1-foreground);
word-break: break-all;
padding: 6px 8px;
background: var(--l2-background);
border-radius: 4px;
}
}
// --- ToggleGroup override: size to content, don't stretch items ---
[class*='toggle-group'] {
flex-shrink: 0;
[class*='toggle-group-item'] {
flex: 0 0 auto;
}
}
// --- Collapse button ---
.filter-collapse-btn {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: none;
}
// --- Highlight errors toggle ---
.highlight-errors-toggle {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
white-space: nowrap;
}
// --- Prev/next navigation ---
.pre-next-toggle {
display: flex;
flex-shrink: 0;
gap: 12px;
&__count {
display: flex;
align-items: center;
margin: auto;
color: var(--l2-foreground);
font-family: 'Geist Mono';
font-size: 12px;
font-weight: 400;
line-height: 18px;
}
.ant-btn {
display: flex;
align-items: center;
justify-content: center;
box-shadow: none;
}
}
.filter-status {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
color: var(--l2-foreground);
font-family: 'Geist Mono';
font-size: 12px;
font-weight: 400;
line-height: 18px;
&--error {
color: var(--destructive);
cursor: help;
}
}
}

View File

@@ -22,6 +22,7 @@ import {
} from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { AxiosError } from 'axios';
import cx from 'classnames';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
@@ -42,7 +43,7 @@ import {
useSpanCategoryFilter,
} from './hooks/useSpanCategoryFilter';
import './Filters.styles.scss';
import styles from './Filters.module.scss';
function prepareQuery(filters: TagFilter, traceID: string): Query {
return {
@@ -255,7 +256,7 @@ function Filters({
);
const highlightErrorsToggle = (
<div className="highlight-errors-toggle">
<div className={styles.highlightErrorsToggle}>
<Typography.Text>Highlight errors</Typography.Text>
<Switch
color="cherry"
@@ -271,7 +272,7 @@ function Filters({
{error && (
<Tooltip>
<TooltipTrigger asChild>
<span className="filter-status filter-status--error">
<span className={cx(styles.filterStatus, styles.hasError)}>
<Info />
API error
</span>
@@ -282,7 +283,7 @@ function Filters({
</Tooltip>
)}
{!error && noData && (
<Typography.Text className="filter-status">
<Typography.Text className={styles.filterStatus}>
No results found
</Typography.Text>
)}
@@ -293,22 +294,22 @@ function Filters({
if (!isExpanded) {
const pill = (
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
<div className="filter-pill" onClick={onExpand}>
<div className={styles.pill} onClick={onExpand}>
<Search size={12} />
<span className="filter-pill__text">{expression || 'Search...'}</span>
{expression && <span className="filter-pill__indicator" />}
<span className={styles.pillText}>{expression || 'Search...'}</span>
{expression && <span className={styles.pillIndicator} />}
</div>
);
return (
<TooltipProvider>
<div className="trace-v3-filter-row collapsed">
<div className={styles.root}>
{expression ? (
<Tooltip>
<TooltipTrigger asChild>{pill}</TooltipTrigger>
<TooltipContent side="bottom" align="start">
<div className="filter-pill-popover">
<div className="filter-pill-popover__header">
<div className={styles.pillPopover}>
<div className={styles.pillPopoverHeader}>
<Typography.Text>Search query</Typography.Text>
<Button
variant="ghost"
@@ -325,7 +326,7 @@ function Filters({
<Copy size={12} />
</Button>
</div>
<div className="filter-pill-popover__expression">{expression}</div>
<div className={styles.pillPopoverExpression}>{expression}</div>
</div>
</TooltipContent>
</Tooltip>
@@ -342,7 +343,7 @@ function Filters({
// --- EXPANDED VIEW ---
return (
<TooltipProvider>
<div className="trace-v3-filter-row expanded">
<div className={cx(styles.root, styles.isExpanded)}>
<ToggleGroup
type="single"
value={selectedCategory}
@@ -361,7 +362,7 @@ function Filters({
</ToggleGroup>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div
className="filter-search-container"
className={styles.searchContainer}
ref={containerRef}
onBlur={(e): void => {
if (!containerRef.current?.contains(e.relatedTarget as Node)) {
@@ -382,8 +383,8 @@ function Filters({
/>
</div>
{filteredSpanIds.length > 0 && (
<div className="pre-next-toggle">
<Typography.Text className="pre-next-toggle__count">
<div className={styles.preNextToggle}>
<Typography.Text className={styles.preNextCount}>
{currentSearchedIndex + 1} / {filteredSpanIds.length}
</Typography.Text>
<Button
@@ -416,7 +417,7 @@ function Filters({
variant="ghost"
size="icon"
color="secondary"
className="filter-collapse-btn"
className={styles.collapseBtn}
onClick={onCollapse}
>
<X size={14} />

View File

@@ -0,0 +1,634 @@
// Inside a .module.scss, postcss-modules auto-scopes `@keyframes` identifiers
// and rewrites `animation`/`animation-name` references to match.
@keyframes waterfallLoading {
0% {
opacity: 0.3;
transform: scaleX(0.3);
transform-origin: left;
}
50% {
opacity: 1;
transform: scaleX(1);
transform-origin: left;
}
100% {
opacity: 0.3;
transform: scaleX(0.3);
transform-origin: right;
}
}
.loadingBar {
height: 2px;
background: var(--primary);
animation: waterfallLoading 1.5s ease-in-out infinite;
flex-shrink: 0;
}
// Event-dot tooltip. Visually matches SpanHoverCard's popover; styles are
// inlined rather than `compose`d from another module because postcss-modules
// loads cross-module composes through the plain CSS parser, which chokes on
// SCSS `//` comments in the target file.
.popover {
--tooltip-z-index: 1000;
background-color: var(--l1-background);
padding: 8px 12px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid var(--l2-border);
color: var(--l1-foreground);
}
.root {
overflow-y: hidden;
overflow-x: hidden;
max-width: 100%;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.missingSpans {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
margin: 16px;
padding: 12px;
border-radius: 4px;
background: rgba(69, 104, 220, 0.1);
}
.leftInfo {
display: flex;
align-items: center;
gap: 8px;
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
.text {
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
.rightInfo {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
&:hover {
background-color: unset;
color: var(--bg-robin-200);
}
}
.splitPanel {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 0px 20px 0px 20px;
&::-webkit-scrollbar {
width: 0.1rem;
}
}
.splitHeader {
position: sticky;
top: 0;
z-index: 2;
display: flex;
height: 25px;
background-color: var(--l1-background);
}
.sidebarHeader {
flex-shrink: 0;
}
.resizeHandleHeader {
width: 4px;
flex-shrink: 0;
}
.statusHeader {
width: 50px;
flex-shrink: 0;
}
.timelineHeader {
flex: 1;
overflow: hidden;
padding: 0 15px;
}
.splitBody {
display: flex;
position: relative;
}
// Invisible IntersectionObserver targets pinned at the top and bottom of the
// virtualized content. See `useBoundaryPagination`.
.loadMoreSentinel {
position: absolute;
left: 0;
right: 0;
height: 1px;
pointer-events: none;
}
.loadMoreSentinelTop {
top: 0;
}
.loadMoreSentinelBottom {
bottom: 0;
}
.sidebar {
overflow-x: auto;
overflow-y: hidden;
flex-shrink: 0;
border-right: 1px solid var(--l1-border);
// ResizableBox child renders with a global `.resizable-box__content` class
// — give it independent horizontal scrolling.
:global(.resizable-box__content) {
overflow-x: auto;
overflow-y: hidden;
&::-webkit-scrollbar {
height: 0;
}
scrollbar-width: none;
}
&::-webkit-scrollbar {
height: 0.3rem;
}
&::-webkit-scrollbar-thumb {
background: var(--l3-background);
border-radius: 4px;
}
}
.treeTable {
position: relative;
border-collapse: collapse;
}
.treeRow {
display: flex;
align-items: center;
}
.treeCell {
display: flex;
width: 100%;
height: 28px;
align-items: center;
overflow: visible;
padding: 0;
}
.treeRow:hover,
.treeRow.hoveredSpan {
border-radius: 4px;
background: color-mix(
in srgb,
var(--l3-background) 20%,
transparent
) !important;
.spanOverview {
background: unset !important;
}
}
.sidebarResizeHandle {
width: 4px;
flex-shrink: 0;
cursor: col-resize;
user-select: none;
touch-action: none;
background: transparent;
transition: background 0.15s ease;
z-index: 1;
&:hover,
&:active {
background: rgba(35, 196, 248, 0.2);
}
}
.statusCol {
width: 50px;
flex-shrink: 0;
position: relative;
}
.statusCell {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
--badge-border-radius: 2px;
--badge-padding: 3px 6px;
--badge-line-height: 12px;
--badge-border-width: 0px;
&.hoveredSpan {
border-radius: 4px;
background: color-mix(
in srgb,
var(--l3-background) 20%,
transparent
) !important;
}
&.isInterested,
&.isSelectedNonMatching {
border-radius: 4px;
background: color-mix(
in srgb,
var(--l3-background) 40%,
transparent
) !important;
}
&.isDimmed {
opacity: 0.15;
}
}
.timeline {
flex: 1;
position: relative;
overflow: hidden;
}
.crosshair {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
background: var(--l3-background);
pointer-events: none;
z-index: 1;
}
.timelineRow {
display: flex;
align-items: center;
// Match timelineHeader's 15px padding so bars align with ticks
padding: 0 15px;
&:hover,
&.hoveredSpan {
border-radius: 4px;
background: color-mix(
in srgb,
var(--l3-background) 20%,
transparent
) !important;
}
&:has(.isInterested),
&:has(.isSelectedNonMatching) {
border-radius: 4px;
background: color-mix(
in srgb,
var(--l3-background) 40%,
transparent
) !important;
}
}
.spanOverview {
display: flex;
align-items: center;
flex-shrink: 0;
height: 100%;
width: 100%;
cursor: pointer;
position: relative;
white-space: nowrap;
&:hover .rowActions {
opacity: 1;
pointer-events: auto;
}
&.isInterested,
&.isSelectedNonMatching {
border-radius: 4px;
background: color-mix(
in srgb,
var(--l3-background) 40%,
transparent
) !important;
}
}
.treeIndent {
display: inline-block;
flex-shrink: 0;
}
.treeLine {
position: absolute;
background-color: var(--l2-border);
pointer-events: none;
}
.treeConnector {
position: absolute;
width: 14px;
height: 50%;
border-left: 1px solid var(--l2-border);
border-bottom: 1px solid var(--l2-border);
border-bottom-left-radius: 6px;
pointer-events: none;
}
// Reserved horizontal space for the chevron — present on every row, filled
// only when the span has children. Keeps sibling icons aligned.
.treeArrowSlot {
width: 18px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
.treeArrow {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
flex-shrink: 0;
cursor: pointer;
color: var(--l1-foreground);
border-radius: 4px;
&:hover {
background: var(--l3-background);
}
}
// Reserved horizontal space for the subtree-count badge — same reason.
// Right-aligns the badge inside so single-digit counts don't push the icon
// left of where multi-digit counts would put it.
.subtreeCountSlot {
min-width: 34px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: flex-end;
padding-right: 4px;
}
.subtreeCount {
display: inline-flex;
align-items: center;
flex-shrink: 0;
:global(.badge) {
font-size: 10px;
line-height: 14px;
padding: 0 4px;
height: 14px;
}
}
.treeIcon {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
margin: 0 6px;
&.hasError {
box-shadow: 0 0 0 2px rgba(255, 70, 70, 0.3);
}
}
.treeLabel {
color: var(--l1-foreground);
font-family: 'Inter';
font-size: 13px;
font-weight: 400;
line-height: 28px;
letter-spacing: 0.01em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.treeServiceName {
margin-left: 18px;
color: var(--l3-foreground);
font-weight: 400;
}
.rowActions {
position: sticky;
right: 0;
display: flex;
align-items: center;
gap: 2px;
margin-left: auto;
padding-right: 4px;
padding-left: 8px;
flex-shrink: 0;
height: 100%;
background: linear-gradient(to left, var(--l1-background) 60%, transparent);
z-index: 2;
opacity: 0;
pointer-events: none;
}
.actionBtn {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 2px;
cursor: pointer;
color: var(--l1-foreground);
background: transparent;
border: none;
padding: 0;
&:hover {
background: var(--l3-background);
}
}
// Also reveal the actions when the parent tree row is hovered.
.treeRow:hover .rowActions,
.treeRow.hoveredSpan .rowActions {
opacity: 1;
pointer-events: auto;
}
.spanDuration {
display: flex;
align-items: center;
height: 28px;
position: relative;
width: 100%;
padding: 0 15px;
cursor: pointer;
}
.spanBar {
position: absolute;
height: 18px;
top: 5px;
border-radius: 2px;
display: flex;
align-items: center;
padding: 0 8px;
overflow: hidden;
cursor: pointer;
white-space: nowrap;
color: rgba(0, 0, 0, 0.9);
background-color: var(--span-color);
border: 1px solid transparent;
}
.spanInfo {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
overflow: hidden;
z-index: 1;
}
.spanName {
font-weight: 500;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
min-width: 0;
}
.spanDurationText {
color: inherit;
opacity: 0.8;
font-size: 10px;
margin-left: 8px;
flex-shrink: 0;
}
.eventDot {
position: absolute;
top: 50%;
transform: translate(-50%, -50%) rotate(45deg);
width: 5px;
height: 5px;
background-color: var(--event-dot-bg, var(--bg-robin-500));
border: 1px solid var(--event-dot-border, var(--bg-robin-600));
cursor: pointer;
z-index: 1;
&.hasError {
background-color: var(--destructive);
border-color: var(--destructive);
}
&:hover {
transform: translate(-50%, -50%) rotate(45deg) scale(1.5);
}
}
// Hover state: solid l2-background fill + border (matches flamegraph)
.timelineRow:hover .spanBar,
.timelineRow.hoveredSpan .spanBar {
color: var(--span-color);
background-color: var(--l2-background);
background-image: none;
border: 1px solid var(--span-color);
}
// Selected state: solid l3-background fill + dashed border (matches flamegraph)
.isInterested .spanBar,
.isSelectedNonMatching .spanBar {
color: var(--span-color);
background-color: var(--l2-background);
background-image: none;
border: 1px dashed var(--span-color);
}
.isDimmed {
opacity: 0.15;
}
.isHighlighted {
opacity: 1;
}
.isSelectedNonMatching {
.treeLabel {
opacity: 0.5;
}
}
// `.spanBar` text color is the one place where semantic tokens don't fit
// cleanly: in dark mode the bar's bright `--span-color` background needs dark
// text; in light mode `generateColor` produces darker bar fills, so the text
// must flip to white.
:global(.lightMode) {
.root {
.spanDuration .spanBar {
color: rgba(255, 255, 255, 0.9);
}
.timelineRow:hover .spanBar,
.timelineRow.hoveredSpan .spanBar,
.isInterested .spanBar,
.isSelectedNonMatching .spanBar {
color: var(--span-color);
}
}
}
// Tooltips for the row's hover-revealed action buttons (Copy / Add to Funnel).
// Bumped above FloatingPanel (z-index 999) so they stay visible when the
// SpanDetailsPanel is docked as a floating panel.
.actionTooltip {
--tooltip-z-index: 1000;
}

View File

@@ -1,636 +0,0 @@
.waterfall-loading-bar {
height: 2px;
background: var(--primary);
animation: waterfall-loading 1.5s ease-in-out infinite;
flex-shrink: 0;
}
@keyframes waterfall-loading {
0% {
opacity: 0.3;
transform: scaleX(0.3);
transform-origin: left;
}
50% {
opacity: 1;
transform: scaleX(1);
transform-origin: left;
}
100% {
opacity: 0.3;
transform: scaleX(0.3);
transform-origin: right;
}
}
.success-content {
overflow-y: hidden;
overflow-x: hidden;
max-width: 100%;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
.missing-spans {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
margin: 16px;
padding: 12px;
border-radius: 4px;
background: rgba(69, 104, 220, 0.1);
.left-info {
display: flex;
align-items: center;
gap: 8px;
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.text {
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.right-info {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.right-info:hover {
background-color: unset;
color: var(--bg-robin-200);
}
}
.waterfall-split-panel {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 0px 20px 0px 20px;
&::-webkit-scrollbar {
width: 0.1rem;
}
}
.waterfall-split-header {
position: sticky;
top: 0;
z-index: 2;
display: flex;
height: 25px;
background-color: var(--l1-background);
.sidebar-header {
flex-shrink: 0;
}
.resize-handle-header {
width: 4px;
flex-shrink: 0;
}
.status-header {
width: 50px;
flex-shrink: 0;
}
.timeline-header {
flex: 1;
overflow: hidden;
padding: 0 15px;
}
}
.waterfall-split-body {
display: flex;
position: relative;
}
// Invisible IntersectionObserver targets pinned at the top and bottom of
// the virtualized content. See `useBoundaryPagination`.
.waterfall-load-more-sentinel {
position: absolute;
left: 0;
right: 0;
height: 1px;
pointer-events: none;
&--top {
top: 0;
}
&--bottom {
bottom: 0;
}
}
.waterfall-sidebar {
overflow-x: auto;
overflow-y: hidden;
flex-shrink: 0;
border-right: 1px solid var(--l1-border);
.resizable-box__content {
overflow-x: auto;
overflow-y: hidden;
&::-webkit-scrollbar {
height: 0;
}
scrollbar-width: none;
}
&::-webkit-scrollbar {
height: 0.3rem;
}
&::-webkit-scrollbar-thumb {
background: var(--l3-background);
border-radius: 4px;
}
}
.span-tree-table {
position: relative;
border-collapse: collapse;
.span-tree-row {
display: flex;
align-items: center;
}
.span-tree-cell {
display: flex;
width: 100%;
height: 28px;
align-items: center;
overflow: visible;
padding: 0;
}
.span-tree-row:hover,
.span-tree-row.hovered-span {
border-radius: 4px;
background: color-mix(
in srgb,
var(--l3-background) 20%,
transparent
) !important;
.span-overview {
background: unset !important;
}
}
}
.sidebar-resize-handle {
width: 4px;
flex-shrink: 0;
cursor: col-resize;
user-select: none;
touch-action: none;
background: transparent;
transition: background 0.15s ease;
z-index: 1;
&:hover,
&:active {
background: rgba(35, 196, 248, 0.2);
}
}
.waterfall-status-col {
width: 50px;
flex-shrink: 0;
position: relative;
.status-cell {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
--badge-border-radius: 2px;
--badge-padding: 3px 6px;
--badge-line-height: 12px;
--badge-border-width: 0px;
&.hovered-span {
border-radius: 4px;
background: color-mix(
in srgb,
var(--l3-background) 20%,
transparent
) !important;
}
&.interested-span,
&.selected-non-matching-span {
border-radius: 4px;
background: color-mix(
in srgb,
var(--l3-background) 40%,
transparent
) !important;
}
&.dimmed-span {
opacity: 0.15;
}
}
}
.waterfall-timeline {
flex: 1;
position: relative;
overflow: hidden;
.waterfall-crosshair {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
background: var(--l3-background);
pointer-events: none;
z-index: 1;
}
.timeline-row {
display: flex;
align-items: center;
// Match timeline-header's 15px padding so bars align with ticks
padding: 0 15px;
}
.timeline-row:hover,
.timeline-row.hovered-span {
border-radius: 4px;
background: color-mix(
in srgb,
var(--l3-background) 20%,
transparent
) !important;
}
.timeline-row:has(.interested-span),
.timeline-row:has(.selected-non-matching-span) {
border-radius: 4px;
background: color-mix(
in srgb,
var(--l3-background) 40%,
transparent
) !important;
}
}
// Shared span component styles (used in both panels)
.span-overview {
display: flex;
align-items: center;
flex-shrink: 0;
height: 100%;
width: 100%;
cursor: pointer;
position: relative;
white-space: nowrap;
.tree-indent {
display: inline-block;
flex-shrink: 0;
}
.tree-line {
position: absolute;
background-color: var(--l2-border);
pointer-events: none;
}
.tree-connector {
position: absolute;
width: 14px;
height: 50%;
border-left: 1px solid var(--l2-border);
border-bottom: 1px solid var(--l2-border);
border-bottom-left-radius: 6px;
pointer-events: none;
}
// Reserved horizontal space for the chevron — present on every row,
// filled only when the span has children. Keeps sibling icons aligned.
.tree-arrow-slot {
width: 18px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
.tree-arrow {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
flex-shrink: 0;
cursor: pointer;
color: var(--l1-foreground);
border-radius: 4px;
&:hover {
background: var(--l3-background);
}
}
// Reserved horizontal space for the subtree-count badge — same reason.
// Right-aligns the badge inside so single-digit counts don't push the
// icon left of where multi-digit counts would put it.
.subtree-count-slot {
min-width: 34px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: flex-end;
padding-right: 4px;
}
.subtree-count {
display: inline-flex;
align-items: center;
flex-shrink: 0;
.badge {
font-size: 10px;
line-height: 14px;
padding: 0 4px;
height: 14px;
}
}
.tree-icon {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
margin: 0 6px;
&.is-error {
box-shadow: 0 0 0 2px rgba(255, 70, 70, 0.3);
}
}
.tree-label {
color: var(--l1-foreground);
font-family: 'Inter';
font-size: 13px;
font-weight: 400;
line-height: 28px;
letter-spacing: 0.01em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
.tree-service-name {
margin-left: 18px;
color: var(--l3-foreground);
font-weight: 400;
}
}
.span-row-actions {
position: sticky;
right: 0;
display: flex;
align-items: center;
gap: 2px;
margin-left: auto;
padding-right: 4px;
padding-left: 8px;
flex-shrink: 0;
height: 100%;
background: linear-gradient(to left, var(--l1-background) 60%, transparent);
z-index: 2;
opacity: 0;
pointer-events: none;
.span-action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 2px;
cursor: pointer;
color: var(--l1-foreground);
background: transparent;
border: none;
padding: 0;
&:hover {
background: var(--l3-background);
}
}
}
&:hover .span-row-actions {
opacity: 1;
pointer-events: auto;
}
}
// Also show action buttons when hovering the tree row (parent of span-overview)
.span-tree-row:hover .span-row-actions,
.span-tree-row.hovered-span .span-row-actions {
opacity: 1;
pointer-events: auto;
}
.span-duration {
display: flex;
align-items: center;
height: 28px;
position: relative;
width: 100%;
padding: 0 15px;
cursor: pointer;
.span-bar {
position: absolute;
height: 18px;
top: 5px;
border-radius: 2px;
display: flex;
align-items: center;
padding: 0 8px;
overflow: hidden;
cursor: pointer;
white-space: nowrap;
color: rgba(0, 0, 0, 0.9);
background-color: var(--span-color);
border: 1px solid transparent;
.span-info {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
overflow: hidden;
z-index: 1;
.span-name {
font-weight: 500;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
min-width: 0;
}
.span-duration-text {
color: inherit;
opacity: 0.8;
font-size: 10px;
margin-left: 8px;
flex-shrink: 0;
}
}
}
.event-dot {
position: absolute;
top: 50%;
transform: translate(-50%, -50%) rotate(45deg);
width: 5px;
height: 5px;
background-color: var(--event-dot-bg, var(--bg-robin-500));
border: 1px solid var(--event-dot-border, var(--bg-robin-600));
cursor: pointer;
z-index: 1;
&.error {
background-color: var(--destructive);
border-color: var(--destructive);
}
&:hover {
transform: translate(-50%, -50%) rotate(45deg) scale(1.5);
}
}
}
// Hover state: solid l2-background fill + border (matches flamegraph)
.timeline-row:hover .span-bar,
.timeline-row.hovered-span .span-bar {
color: var(--span-color);
background-color: var(--l2-background);
background-image: none;
border: 1px solid var(--span-color);
}
// Selected state: solid l3-background fill + dashed border (matches flamegraph)
.interested-span .span-bar,
.selected-non-matching-span .span-bar {
color: var(--span-color);
background-color: var(--l2-background);
background-image: none;
border: 1px dashed var(--span-color);
}
// Shared state classes for both panels
// Background highlight for selection is on .timeline-row via :has() — see .waterfall-timeline
// Only apply on .span-overview (left panel) where there's no parent row with :has()
.span-overview.interested-span,
.span-overview.selected-non-matching-span {
border-radius: 4px;
background: color-mix(
in srgb,
var(--l3-background) 40%,
transparent
) !important;
}
.dimmed-span {
opacity: 0.15;
}
.highlighted-span {
opacity: 1;
}
.selected-non-matching-span {
.tree-label {
opacity: 0.5;
}
}
}
.span-dets {
.related-logs {
display: flex;
width: 160px;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 8px;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: none;
}
}
// `.span-bar` text color is the one place where semantic tokens don't fit
// cleanly: in dark mode the bar's bright `--span-color` background needs dark
// text; in light mode `generateColor` produces darker bar fills, so the text
// must flip to white. This is the only `.lightMode` carve-out left after the
// migration to semantic tokens — the hover/selected rules are repeated here
// to beat the default-state rule's specificity inside `.lightMode`.
.lightMode {
.success-content {
.span-duration .span-bar {
color: rgba(255, 255, 255, 0.9);
}
.timeline-row:hover .span-bar,
.timeline-row.hovered-span .span-bar,
.interested-span .span-bar,
.selected-non-matching-span .span-bar {
color: var(--span-color);
}
}
}
// Tooltips for the row's hover-revealed action buttons (Copy / Add to Funnel).
// Bumped above FloatingPanel (z-index 999) so they stay visible when the
// SpanDetailsPanel is docked as a floating panel.
.span-action-tooltip {
--tooltip-z-index: 1000;
}

View File

@@ -53,7 +53,7 @@ import { SpanHoverCard } from '../../../SpanHoverCard/SpanHoverCard';
import AddSpanToFunnelModal from '../../AddSpanToFunnelModal/AddSpanToFunnelModal';
import { IInterestedSpan } from '../../types';
import './Success.styles.scss';
import styles from './Success.module.scss';
/**
* Lazy event dot — only mounts the tooltip when the user hovers.
@@ -91,7 +91,7 @@ const LazyEventDotPopover = memo(function LazyEventDotPopover({
const dot = (
<div
className={`event-dot ${isError ? 'error' : ''}`}
className={cx(styles.eventDot, isError && styles.hasError)}
style={
{
left: `${dotLeft}%`,
@@ -121,7 +121,7 @@ const LazyEventDotPopover = memo(function LazyEventDotPopover({
}}
>
<TooltipTrigger asChild>{dot}</TooltipTrigger>
<TooltipContent className="span-hover-card-popover">
<TooltipContent className={styles.popover}>
<EventTooltipContent
eventName={event.name}
timeOffsetMs={eventTimeMs - spanTimestamp}
@@ -243,11 +243,11 @@ const SpanOverview = memo(function SpanOverview({
return (
<div
className={cx('span-overview', {
'interested-span': isSelected && (!isFilterActive || isMatching),
'highlighted-span': isHighlighted,
'selected-non-matching-span': isSelectedNonMatching,
'dimmed-span': isDimmed,
className={cx(styles.spanOverview, {
[styles.isInterested]: isSelected && (!isFilterActive || isMatching),
[styles.isHighlighted]: isHighlighted,
[styles.isSelectedNonMatching]: isSelectedNonMatching,
[styles.isDimmed]: isDimmed,
})}
onClick={(): void => handleSpanClick(span)}
onMouseEnter={(): void => onHoverEnter(span.span_id)}
@@ -264,7 +264,7 @@ const SpanOverview = memo(function SpanOverview({
return (
<div
key={lvl}
className="tree-line"
className={styles.treeLine}
style={{
left: xPos,
top: 0,
@@ -277,25 +277,25 @@ const SpanOverview = memo(function SpanOverview({
return (
<div key={lvl}>
<div
className="tree-line"
className={styles.treeLine}
style={{ left: xPos, top: 0, width: 1, height: '50%' }}
/>
<div className="tree-connector" style={{ left: xPos, top: 0 }} />
<div className={styles.treeConnector} style={{ left: xPos, top: 0 }} />
</div>
);
})}
{/* Indent spacer */}
<span className="tree-indent" style={{ width: `${indentWidth}px` }} />
<span className={styles.treeIndent} style={{ width: `${indentWidth}px` }} />
{/* Expand/collapse arrow + child count slots — always render the
slots, fill them only when the span has children. Reserving the
horizontal space on leaf rows aligns sibling icons regardless
of whether each sibling is a parent or a leaf. */}
<span className="tree-arrow-slot">
<span className={styles.treeArrowSlot}>
{span.has_children && (
<span
className={cx('tree-arrow', { expanded: !isSpanCollapsed })}
className={styles.treeArrow}
onClick={(event): void => {
event.stopPropagation();
event.preventDefault();
@@ -306,9 +306,9 @@ const SpanOverview = memo(function SpanOverview({
</span>
)}
</span>
<span className="subtree-count-slot">
<span className={styles.subtreeCountSlot}>
{span.has_children && (
<span className="subtree-count">
<span className={styles.subtreeCount}>
<Badge color="vanilla">{span.sub_tree_node_count}</Badge>
</span>
)}
@@ -316,18 +316,18 @@ const SpanOverview = memo(function SpanOverview({
{/* Colored service dot */}
<span
className={cx('tree-icon', { 'is-error': span.has_error })}
className={cx(styles.treeIcon, { [styles.hasError]: span.has_error })}
style={{ backgroundColor: color }}
/>
{/* Span name + service name */}
<span className="tree-label">
<span className={styles.treeLabel}>
{span.name}
<span className="tree-service-name">{span['service.name']}</span>
<span className={styles.treeServiceName}>{span['service.name']}</span>
</span>
{/* Action buttons — shown on hover via CSS, right-aligned */}
<span className="span-row-actions">
<span className={styles.rowActions}>
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
@@ -335,13 +335,13 @@ const SpanOverview = memo(function SpanOverview({
variant="ghost"
size="icon"
color="secondary"
className="span-action-btn"
className={styles.actionBtn}
onClick={onSpanCopy}
>
<Link size={12} />
</Button>
</TooltipTrigger>
<TooltipContent className="span-action-tooltip">
<TooltipContent className={styles.actionTooltip}>
Copy Span Link
</TooltipContent>
</Tooltip>
@@ -351,13 +351,13 @@ const SpanOverview = memo(function SpanOverview({
variant="ghost"
size="icon"
color="secondary"
className="span-action-btn"
className={styles.actionBtn}
onClick={handleFunnelClick}
>
<ListPlus size={12} />
</Button>
</TooltipTrigger>
<TooltipContent className="span-action-tooltip">
<TooltipContent className={styles.actionTooltip}>
Add to Trace Funnel
</TooltipContent>
</Tooltip>
@@ -410,16 +410,16 @@ export const SpanDuration = memo(function SpanDuration({
return (
<div
className={cx('span-duration', {
'interested-span': isSelected && (!isFilterActive || isMatching),
'highlighted-span': isHighlighted,
'selected-non-matching-span': isSelectedNonMatching,
'dimmed-span': isDimmed,
className={cx(styles.spanDuration, {
[styles.isInterested]: isSelected && (!isFilterActive || isMatching),
[styles.isHighlighted]: isHighlighted,
[styles.isSelectedNonMatching]: isSelectedNonMatching,
[styles.isDimmed]: isDimmed,
})}
onClick={(): void => handleSpanClick(span)}
>
<div
className="span-bar"
className={styles.spanBar}
style={
{
left: `${leftOffset}%`,
@@ -429,9 +429,9 @@ export const SpanDuration = memo(function SpanDuration({
} as React.CSSProperties
}
>
<span className="span-info">
<span className="span-name">{span.name}</span>
<span className="span-duration-text">{`${toFixed(
<span className={styles.spanInfo}>
<span className={styles.spanName}>{span.name}</span>
<span className={styles.spanDurationText}>{`${toFixed(
time,
2,
)} ${timeUnitName}`}</span>
@@ -529,11 +529,11 @@ function Success(props: ISuccessProps): JSX.Element {
if (prev) {
const prevElements = document.querySelectorAll(`[data-span-id="${prev}"]`);
prevElements.forEach((el) => el.classList.remove('hovered-span'));
prevElements.forEach((el) => el.classList.remove(styles.hoveredSpan));
}
if (spanId) {
const nextElements = document.querySelectorAll(`[data-span-id="${spanId}"]`);
nextElements.forEach((el) => el.classList.add('hovered-span'));
nextElements.forEach((el) => el.classList.add(styles.hoveredSpan));
}
prevHoveredSpanIdRef.current = spanId;
}, []);
@@ -798,17 +798,17 @@ function Success(props: ISuccessProps): JSX.Element {
}, []);
return (
<div className="success-content">
<div className={styles.root}>
{traceMetadata.hasMissingSpans && (
<div className="missing-spans">
<section className="left-info">
<div className={styles.missingSpans}>
<section className={styles.leftInfo}>
<CircleAlert size={14} />
<span className="text">This trace has missing spans</span>
<span className={styles.text}>This trace has missing spans</span>
</section>
<Button
variant="ghost"
color="secondary"
className="right-info"
className={styles.rightInfo}
suffix={<ArrowUpRight size={14} />}
onClick={(): WindowProxy | null =>
window.open(
@@ -821,17 +821,17 @@ function Success(props: ISuccessProps): JSX.Element {
</Button>
</div>
)}
{isFetching && <div className="waterfall-loading-bar" />}
<div className="waterfall-split-panel" ref={scrollContainerRef}>
{isFetching && <div className={styles.loadingBar} />}
<div className={styles.splitPanel} ref={scrollContainerRef}>
{/* Sticky header row */}
<div className="waterfall-split-header">
<div className={styles.splitHeader}>
<div
className="sidebar-header"
className={styles.sidebarHeader}
style={{ width: sidebarWidth, flexShrink: 0 }}
/>
<div className="resize-handle-header" />
<div className="status-header" />
<div className="timeline-header">
<div className={styles.resizeHandleHeader} />
<div className={styles.statusHeader} />
<div className={styles.timelineHeader}>
<TimelineV3
startTimestamp={traceMetadata.startTime}
endTimestamp={traceMetadata.endTime}
@@ -844,7 +844,7 @@ function Success(props: ISuccessProps): JSX.Element {
{/* Split body */}
<div
className="waterfall-split-body"
className={styles.splitBody}
style={{
minHeight: virtualizer.getTotalSize(),
height: '100%',
@@ -854,11 +854,11 @@ function Success(props: ISuccessProps): JSX.Element {
fires a load-more via useBoundaryPagination. */}
<div
ref={loadMoreTopSentinelRef}
className="waterfall-load-more-sentinel waterfall-load-more-sentinel--top"
className={cx(styles.loadMoreSentinel, styles.loadMoreSentinelTop)}
/>
<div
ref={loadMoreBottomSentinelRef}
className="waterfall-load-more-sentinel waterfall-load-more-sentinel--bottom"
className={cx(styles.loadMoreSentinel, styles.loadMoreSentinelBottom)}
/>
<SpanHoverCard
hoveredSpanId={hoveredSpanId}
@@ -875,9 +875,9 @@ function Success(props: ISuccessProps): JSX.Element {
minWidth={MIN_SIDEBAR_WIDTH}
maxWidth={MAX_SIDEBAR_WIDTH}
onResize={setSidebarWidth}
className="waterfall-sidebar"
className={styles.sidebar}
>
<table className="span-tree-table" style={{ width: maxContentWidth }}>
<table className={styles.treeTable} style={{ width: maxContentWidth }}>
<tbody>
{virtualItems.map((virtualRow) => {
const row = leftRows[virtualRow.index];
@@ -887,7 +887,7 @@ function Success(props: ISuccessProps): JSX.Element {
key={String(virtualRow.key)}
data-testid={`cell-0-${span.span_id}`}
data-span-id={span.span_id}
className="span-tree-row"
className={styles.treeRow}
style={{
position: 'absolute',
top: 0,
@@ -900,7 +900,7 @@ function Success(props: ISuccessProps): JSX.Element {
onMouseLeave={handleRowMouseLeave}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="span-tree-cell">
<td key={cell.id} className={styles.treeCell}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
@@ -912,7 +912,7 @@ function Success(props: ISuccessProps): JSX.Element {
</ResizableBox>
{/* Status code column */}
<div className="waterfall-status-col">
<div className={styles.statusCol}>
{virtualItems.map((virtualRow) => {
const span = spans[virtualRow.index];
const { isSelected, isDimmed, isSelectedNonMatching, isMatching } =
@@ -925,10 +925,10 @@ function Success(props: ISuccessProps): JSX.Element {
return (
<div
key={`status-${String(virtualRow.key)}`}
className={cx('status-cell', {
'interested-span': isSelected && (!isFilterActive || isMatching),
'dimmed-span': isDimmed,
'selected-non-matching-span': isSelectedNonMatching,
className={cx(styles.statusCell, {
[styles.isInterested]: isSelected && (!isFilterActive || isMatching),
[styles.isDimmed]: isDimmed,
[styles.isSelectedNonMatching]: isSelectedNonMatching,
})}
style={{
position: 'absolute',
@@ -953,13 +953,13 @@ function Success(props: ISuccessProps): JSX.Element {
{/* Right panel - timeline bars */}
<div
className="waterfall-timeline"
className={styles.timeline}
ref={timelineAreaRef}
onMouseMove={onCrosshairMove}
onMouseLeave={onCrosshairLeave}
>
{cursorX !== null && (
<div className="waterfall-crosshair" style={{ left: cursorX }} />
<div className={styles.crosshair} style={{ left: cursorX }} />
)}
{virtualItems.map((virtualRow) => {
const span = spans[virtualRow.index];
@@ -968,7 +968,7 @@ function Success(props: ISuccessProps): JSX.Element {
key={String(virtualRow.key)}
data-testid={`cell-1-${span.span_id}`}
data-span-id={span.span_id}
className="timeline-row"
className={styles.timelineRow}
style={{
position: 'absolute',
top: 0,

View File

@@ -2,18 +2,33 @@ import useUrlQuery from 'hooks/useUrlQuery';
import { fireEvent, render, screen } from 'tests/test-utils';
import { SpanV3 } from 'types/api/trace/getTraceV3';
// Local identity-proxy mock for this module so `styles.foo` resolves to
// `'foo'` in test assertions. The global `__mocks__/cssMock.ts` stays as
// `export default {}`; we override resolution for this specific file only.
jest.mock(
'../Success.module.scss',
() =>
new Proxy(
{},
{
get: (_target, prop): string | undefined =>
typeof prop === 'string' && prop !== '__esModule' ? prop : undefined,
},
),
);
import { SpanDuration } from '../Success';
import successStyles from '../Success.module.scss';
const renderWithTraceProvider: typeof render = (ui, options) =>
render(ui, options);
// Constants to avoid string duplication
const SPAN_DURATION_TEXT = '1.16 ms';
const SPAN_DURATION_CLASS = '.span-duration';
const INTERESTED_SPAN_CLASS = 'interested-span';
const HIGHLIGHTED_SPAN_CLASS = 'highlighted-span';
const DIMMED_SPAN_CLASS = 'dimmed-span';
const SELECTED_NON_MATCHING_SPAN_CLASS = 'selected-non-matching-span';
const SPAN_DURATION_CLASS = `.${successStyles.spanDuration}`;
const INTERESTED_SPAN_CLASS = successStyles.isInterested;
const HIGHLIGHTED_SPAN_CLASS = successStyles.isHighlighted;
const DIMMED_SPAN_CLASS = successStyles.isDimmed;
const SELECTED_NON_MATCHING_SPAN_CLASS = successStyles.isSelectedNonMatching;
jest.mock('components/TimelineV3/TimelineV3', () => ({
__esModule: true,
@@ -127,10 +142,7 @@ describe('SpanDuration', () => {
const spanElement = screen.getByText(SPAN_DURATION_TEXT);
// Hover over the span should add hovered-span class
fireEvent.mouseEnter(spanElement);
// Mouse leave should remove hovered-span class
fireEvent.mouseLeave(spanElement);
});
@@ -259,8 +271,8 @@ describe('SpanDuration', () => {
traceMetadata={mockTraceMetadata}
selectedSpan={undefined}
handleSpanClick={mockSetSelectedSpan}
filteredSpanIds={[]} // Empty array but filter is active
isFilterActive // This is the key difference
filteredSpanIds={[]}
isFilterActive
/>,
);

View File

@@ -2,7 +2,23 @@ import React from 'react';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { SpanV3 } from 'types/api/trace/getTraceV3';
// Local identity-proxy mock for this module so `styles.foo` resolves to
// `'foo'` in test assertions. The global `__mocks__/cssMock.ts` stays as
// `export default {}`; we override resolution for this specific file only.
jest.mock(
'../Success.module.scss',
() =>
new Proxy(
{},
{
get: (_target, prop): string | undefined =>
typeof prop === 'string' && prop !== '__esModule' ? prop : undefined,
},
),
);
import Success from '../Success';
import successStyles from '../Success.module.scss';
const renderWithTraceProvider: typeof render = (ui, options, customOptions) =>
render(ui, options, customOptions);
@@ -211,10 +227,10 @@ describe('Span Click User Flows', () => {
const FIRST_SPAN_TEST_ID = 'cell-0-span-1';
const FIRST_SPAN_DURATION_TEST_ID = 'cell-1-span-1';
const SECOND_SPAN_TEST_ID = 'cell-0-span-2';
const SPAN_OVERVIEW_CLASS = '.span-overview';
const SPAN_DURATION_CLASS = '.span-duration';
const INTERESTED_SPAN_CLASS = 'interested-span';
const SECOND_SPAN_DURATION_TEST_ID = 'cell-1-span-2';
const SPAN_OVERVIEW_CLASS = `.${successStyles.spanOverview}`;
const SPAN_DURATION_CLASS = `.${successStyles.spanDuration}`;
const INTERESTED_SPAN_CLASS = successStyles.isInterested;
beforeEach(() => {
jest.clearAllMocks();
@@ -268,7 +284,6 @@ describe('Span Click User Flows', () => {
initialRoute: '/trace',
});
// Wait for initial render and selection
await waitFor(() => {
const spanDuration = screen.getByTestId(FIRST_SPAN_DURATION_TEST_ID);
const spanDurationElement = spanDuration.querySelector(
@@ -277,14 +292,12 @@ describe('Span Click User Flows', () => {
expect(spanDurationElement).toHaveClass(INTERESTED_SPAN_CLASS);
});
// Click on span-2 to test selection change
const span2Duration = screen.getByTestId(SECOND_SPAN_DURATION_TEST_ID);
const span2DurationElement = span2Duration.querySelector(
SPAN_DURATION_CLASS,
) as HTMLElement;
await user.click(span2DurationElement);
// Wait for the state update and re-render
await waitFor(() => {
const spanDuration = screen.getByTestId(FIRST_SPAN_DURATION_TEST_ID);
const spanDurationElement = spanDuration.querySelector(
@@ -307,7 +320,6 @@ describe('Span Click User Flows', () => {
initialRoute: '/trace',
});
// Wait for initial render and selection
await waitFor(() => {
const spanOverview = screen.getByTestId(FIRST_SPAN_TEST_ID);
const spanDuration = screen.getByTestId(FIRST_SPAN_DURATION_TEST_ID);
@@ -318,19 +330,16 @@ describe('Span Click User Flows', () => {
SPAN_DURATION_CLASS,
) as HTMLElement;
// Initially both areas should show the same visual selection (first span is auto-selected)
expect(spanOverviewElement).toHaveClass(INTERESTED_SPAN_CLASS);
expect(spanDurationElement).toHaveClass(INTERESTED_SPAN_CLASS);
});
// Click span-2 to test selection change
const span2Overview = screen.getByTestId(SECOND_SPAN_TEST_ID);
const span2Element = span2Overview.querySelector(
SPAN_OVERVIEW_CLASS,
) as HTMLElement;
await user.click(span2Element);
// Wait for the state update and re-render
await waitFor(() => {
const spanOverview = screen.getByTestId(FIRST_SPAN_TEST_ID);
const spanDuration = screen.getByTestId(FIRST_SPAN_DURATION_TEST_ID);
@@ -341,17 +350,15 @@ describe('Span Click User Flows', () => {
SPAN_DURATION_CLASS,
) as HTMLElement;
// Now span-2 should be selected, span-1 should not
expect(spanOverviewElement).not.toHaveClass(INTERESTED_SPAN_CLASS);
expect(spanDurationElement).not.toHaveClass(INTERESTED_SPAN_CLASS);
// Check that span-2 is selected
const span2Overview = screen.getByTestId(SECOND_SPAN_TEST_ID);
const span2Duration = screen.getByTestId(SECOND_SPAN_DURATION_TEST_ID);
const span2OverviewElement = span2Overview.querySelector(
const span2OverviewSub = screen.getByTestId(SECOND_SPAN_TEST_ID);
const span2DurationSub = screen.getByTestId(SECOND_SPAN_DURATION_TEST_ID);
const span2OverviewElement = span2OverviewSub.querySelector(
SPAN_OVERVIEW_CLASS,
) as HTMLElement;
const span2DurationElement = span2Duration.querySelector(
const span2DurationElement = span2DurationSub.querySelector(
SPAN_DURATION_CLASS,
) as HTMLElement;
@@ -367,7 +374,6 @@ describe('Span Click User Flows', () => {
initialRoute: '/trace',
});
// Wait for initial render and selection
await waitFor(() => {
const span1Overview = screen.getByTestId(FIRST_SPAN_TEST_ID);
const span1Element = span1Overview.querySelector(
@@ -376,27 +382,24 @@ describe('Span Click User Flows', () => {
expect(span1Element).toHaveClass(INTERESTED_SPAN_CLASS);
});
// Click second span
const span2Overview = screen.getByTestId(SECOND_SPAN_TEST_ID);
const span2Element = span2Overview.querySelector(
SPAN_OVERVIEW_CLASS,
) as HTMLElement;
await user.click(span2Element);
// Wait for the state update and re-render
await waitFor(() => {
const span1Overview = screen.getByTestId(FIRST_SPAN_TEST_ID);
const span1Element = span1Overview.querySelector(
SPAN_OVERVIEW_CLASS,
) as HTMLElement;
const span2Overview = screen.getByTestId(SECOND_SPAN_TEST_ID);
const span2Element = span2Overview.querySelector(
const span2OverviewSub = screen.getByTestId(SECOND_SPAN_TEST_ID);
const span2ElementSub = span2OverviewSub.querySelector(
SPAN_OVERVIEW_CLASS,
) as HTMLElement;
// Second span should be selected, first should not
expect(span1Element).not.toHaveClass(INTERESTED_SPAN_CLASS);
expect(span2Element).toHaveClass(INTERESTED_SPAN_CLASS);
expect(span2ElementSub).toHaveClass(INTERESTED_SPAN_CLASS);
});
});
@@ -426,7 +429,6 @@ describe('Span Click User Flows', () => {
{ initialRoute: '/trace' },
);
// Click on the actual span element (not the wrapper)
const spanOverview = screen.getByTestId(FIRST_SPAN_TEST_ID);
const spanElement = spanOverview.querySelector(
SPAN_OVERVIEW_CLASS,
@@ -438,7 +440,6 @@ describe('Span Click User Flows', () => {
expect(mockUrlQuery.get('anotherParam')).toBe('anotherValue');
expect(mockUrlQuery.get('spanId')).toBe('span-1');
// Verify navigation was called with all parameters
expect(mockSafeNavigate).toHaveBeenCalledWith({
search: expect.stringMatching(
/existingParam=existingValue.*anotherParam=anotherValue.*spanId=span-1/,

View File

@@ -15,10 +15,13 @@ import {
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button } from '@signozhq/ui/button';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { GripVertical } from '@signozhq/icons';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import styles from './FieldsSettings.module.scss';
function SortableField({
field,
onRemove,
@@ -40,14 +43,17 @@ function SortableField({
<div
ref={setNodeRef}
style={style}
className={`fs-field-item ${allowDrag ? 'drag-enabled' : 'drag-disabled'}`}
className={cx(
styles.fieldItem,
allowDrag ? styles.isDragEnabled : styles.isDragDisabled,
)}
>
<div {...attributes} {...listeners} className="drag-handle">
<div {...attributes} {...listeners} className={styles.dragHandle}>
{allowDrag && <GripVertical size={14} />}
<span className="fs-field-key">{field.key}</span>
<span className={styles.fieldKey}>{field.key}</span>
</div>
<Button
className="remove-field-btn periscope-btn"
className={cx(styles.removeBtn, 'periscope-btn')}
variant="outlined"
color="destructive"
size="sm"
@@ -94,9 +100,9 @@ function AddedFields({
const allowDrag = inputValue.length === 0;
return (
<div className="fs-section fs-added">
<div className="fs-section-header">ADDED FIELDS</div>
<div className="fs-added-list">
<div className={cx(styles.section, styles.sectionAdded)}>
<div className={styles.sectionHeader}>ADDED FIELDS</div>
<div className={styles.addedList}>
<OverlayScrollbar>
<DndContext
sensors={sensors}
@@ -104,7 +110,7 @@ function AddedFields({
onDragEnd={handleDragEnd}
>
{filteredFields.length === 0 ? (
<div className="fs-no-values">No values found</div>
<div className={styles.noValues}>No values found</div>
) : (
<SortableContext
items={fields.map((f) => f.key)}

View File

@@ -0,0 +1,161 @@
.root {
display: flex;
flex-direction: column;
height: 100%;
background: var(--l1-background);
color: var(--l1-foreground);
overflow: hidden;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-bottom: 1px solid var(--l1-border);
}
.title {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
font-weight: 500;
}
.closeIcon {
cursor: pointer;
color: var(--l2-foreground);
&:hover {
color: var(--l1-foreground);
}
}
.searchInput {
background-color: var(--l1-background);
height: 40px;
border-radius: 0;
border-left: none;
border-right: none;
}
.section {
display: flex;
flex-direction: column;
}
.sectionAdded {
max-height: 40%;
border-bottom: 1px solid var(--l1-border);
}
.sectionOther {
flex: 1;
min-height: 0;
}
.sectionHeader {
color: var(--muted-foreground);
font-size: 11px;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.88px;
text-transform: uppercase;
padding: 8px 12px;
}
.addedList {
overflow: hidden;
}
.otherList {
flex: 1;
min-height: 0;
overflow: hidden;
// Ant Skeleton.Input rendered inside the loading state — override its
// hard-coded width.
:global(.ant-skeleton-input) {
width: 300px;
margin: 8px 12px;
}
}
.noValues {
padding: 8px;
text-align: center;
color: var(--l2-foreground);
font-size: 12px;
}
.limitHint {
padding: 8px 12px;
text-align: center;
color: var(--muted-foreground);
font-size: 11px;
}
.fieldItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border-radius: 4px;
user-select: none;
font-size: 13px;
&:hover {
background-color: var(--l2-background);
.removeBtn,
.addBtn {
opacity: 1;
}
}
}
.dragHandle {
display: flex;
align-items: center;
gap: 8px;
flex-grow: 1;
}
.fieldKey {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.isDragEnabled {
cursor: grab;
&:active {
cursor: grabbing;
}
}
.isDragDisabled {
padding: 6px 12px;
}
.otherFieldItem {
height: 32px;
}
.removeBtn,
.addBtn {
padding: 4px 10px;
opacity: 0;
transition: opacity 0.15s ease-in-out;
flex-shrink: 0;
}
.footer {
display: flex;
gap: 12px;
padding: 12px;
border-top: 1px solid var(--l1-border);
justify-content: space-between;
}

View File

@@ -1,161 +0,0 @@
.fields-settings {
display: flex;
flex-direction: column;
height: 100%;
background: var(--l1-background);
color: var(--l1-foreground);
overflow: hidden;
.fs-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-bottom: 1px solid var(--l1-border);
.fs-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
font-weight: 500;
}
.fs-close-icon {
cursor: pointer;
color: var(--l2-foreground);
&:hover {
color: var(--l1-foreground);
}
}
}
.fs-search {
.fs-search-input {
background-color: var(--l1-background);
height: 40px;
border-radius: 0;
border-left: none;
border-right: none;
}
}
.fs-section {
display: flex;
flex-direction: column;
&.fs-added {
max-height: 40%;
border-bottom: 1px solid var(--l1-border);
}
&.fs-other {
flex: 1;
min-height: 0;
}
}
.fs-section-header {
color: var(--muted-foreground);
font-size: 11px;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.88px;
text-transform: uppercase;
padding: 8px 12px;
}
.fs-added-list {
overflow: hidden;
}
.fs-other-list {
flex: 1;
min-height: 0;
overflow: hidden;
.ant-skeleton-input {
width: 300px;
margin: 8px 12px;
}
}
.fs-no-values {
padding: 8px;
text-align: center;
color: var(--l2-foreground);
font-size: 12px;
}
.fs-limit-hint {
padding: 8px 12px;
text-align: center;
color: var(--muted-foreground);
font-size: 11px;
}
}
.fs-field-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border-radius: 4px;
user-select: none;
font-size: 13px;
.drag-handle {
display: flex;
align-items: center;
gap: 8px;
flex-grow: 1;
}
.fs-field-key {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&.drag-enabled {
cursor: grab;
&:active {
cursor: grabbing;
}
}
&.drag-disabled {
padding: 6px 12px;
}
&.other-field-item {
height: 32px;
}
.remove-field-btn,
.add-field-btn {
padding: 4px 10px;
opacity: 0;
transition: opacity 0.15s ease-in-out;
flex-shrink: 0;
}
&:hover {
background-color: var(--l2-background);
.remove-field-btn,
.add-field-btn {
opacity: 1;
}
}
}
.fs-footer {
display: flex;
gap: 12px;
padding: 12px;
border-top: 1px solid var(--l1-border);
justify-content: space-between;
}

View File

@@ -10,7 +10,7 @@ import { DataSource } from 'types/common/queryBuilder';
import AddedFields from './AddedFields';
import OtherFields from './OtherFields';
import './FieldsSettings.styles.scss';
import styles from './FieldsSettings.module.scss';
const MAX_FIELDS_DEFAULT = 10;
@@ -89,18 +89,18 @@ function FieldsSettings({
const isAtLimit = draftFields.length >= maxFields;
return (
<div className="fields-settings">
<div className="fs-header">
<div className="fs-title">
<div className={styles.root}>
<div className={styles.header}>
<div className={styles.title}>
<TableColumnsSplit size={16} />
{title}
</div>
<X className="fs-close-icon" size={16} onClick={onClose} />
<X className={styles.closeIcon} size={16} onClick={onClose} />
</div>
<section className="fs-search">
<section>
<Input
className="fs-search-input"
className={styles.searchInput}
type="text"
value={inputValue}
placeholder="Search for a field..."
@@ -123,7 +123,7 @@ function FieldsSettings({
/>
{hasUnsavedChanges && (
<div className="fs-footer">
<div className={styles.footer}>
<Button
variant="outlined"
color="secondary"

View File

@@ -1,12 +1,15 @@
import { useMemo } from 'react';
import { Button } from '@signozhq/ui/button';
import { Skeleton } from 'antd';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import styles from './FieldsSettings.module.scss';
interface OtherFieldsProps {
dataSource: DataSource;
debouncedInputValue: string;
@@ -50,9 +53,9 @@ function OtherFields({
if (isFetching) {
return (
<div className="fs-section fs-other">
<div className="fs-section-header">OTHER FIELDS</div>
<div className="fs-other-list">
<div className={cx(styles.section, styles.sectionOther)}>
<div className={styles.sectionHeader}>OTHER FIELDS</div>
<div className={styles.otherList}>
{Array.from({ length: 5 }).map((_, i) => (
// eslint-disable-next-line react/no-array-index-key
<Skeleton.Input active size="small" key={i} />
@@ -63,20 +66,23 @@ function OtherFields({
}
return (
<div className="fs-section fs-other">
<div className="fs-section-header">OTHER FIELDS</div>
<div className="fs-other-list">
<div className={cx(styles.section, styles.sectionOther)}>
<div className={styles.sectionHeader}>OTHER FIELDS</div>
<div className={styles.otherList}>
<OverlayScrollbar>
<>
{otherFields.length === 0 ? (
<div className="fs-no-values">No values found</div>
<div className={styles.noValues}>No values found</div>
) : (
otherFields.map((attr) => (
<div key={attr.key} className="fs-field-item other-field-item">
<span className="fs-field-key">{attr.key}</span>
<div
key={attr.key}
className={cx(styles.fieldItem, styles.otherFieldItem)}
>
<span className={styles.fieldKey}>{attr.key}</span>
{!isAtLimit && (
<Button
className="add-field-btn periscope-btn"
className={cx(styles.addBtn, 'periscope-btn')}
variant="outlined"
color="secondary"
size="sm"
@@ -88,7 +94,7 @@ function OtherFields({
</div>
))
)}
{isAtLimit && <div className="fs-limit-hint">Maximum 10 fields</div>}
{isAtLimit && <div className={styles.limitHint}>Maximum 10 fields</div>}
</>
</OverlayScrollbar>
</div>

View File

@@ -32,7 +32,9 @@ import TraceWaterfall from './TraceWaterfall/TraceWaterfall';
import { IInterestedSpan } from './TraceWaterfall/types';
import { getAncestorSpanIds } from './TraceWaterfall/utils';
import './TraceDetailsV3.styles.scss';
import cx from 'classnames';
import styles from './TraceDetailsV3.module.scss';
function TraceDetailsV3(): JSX.Element {
const { id: traceId } = useParams<TraceDetailV3URLProps>();
@@ -285,7 +287,7 @@ function TraceDetailsV3(): JSX.Element {
return (
<TraceStoreSync aggregations={traceData?.payload?.aggregations}>
<div className="trace-details-v3">
<div className={styles.root}>
<TraceDetailsHeader
filterMetadata={filterMetadata}
onFilteredSpansChange={handleFilteredSpansChange}
@@ -297,20 +299,20 @@ function TraceDetailsV3(): JSX.Element {
<NoData />
) : (
<>
<div className="trace-details-v3__content">
<div className={styles.content}>
<Collapse
// @ts-expect-error motion is passed through to rc-collapse to disable animation
motion={false}
activeKey={activeKeys.filter((k) => k === 'flame')}
onChange={(): void => handleCollapseChange('flame')}
size="small"
className="trace-details-v3__flame-collapse"
className={styles.flameCollapse}
items={[
{
key: 'flame',
label: (
<div className="trace-details-v3__collapse-label">
<span className="trace-details-v3__collapse-title">
<div className={styles.collapseLabel}>
<span className={styles.collapseTitle}>
Flame Graph
{traceData?.payload?.totalSpansCount &&
traceData.payload.totalSpansCount > FLAMEGRAPH_SPAN_LIMIT && (
@@ -321,17 +323,15 @@ function TraceDetailsV3(): JSX.Element {
)}
</span>
{traceData?.payload?.totalSpansCount ? (
<span className="trace-details-v3__collapse-count">
<span className="trace-details-v3__collapse-count-item">
<span className={styles.collapseCount}>
<span className={styles.collapseCountItem}>
<ChartNoAxesGantt size={13} />
Spans: {traceData.payload.totalSpansCount}
</span>
<span
className={`trace-details-v3__collapse-count-item${
traceData.payload.totalErrorSpansCount > 0
? ' trace-details-v3__collapse-count-errors'
: ''
}`}
className={cx(styles.collapseCountItem, {
[styles.hasErrors]: traceData.payload.totalErrorSpansCount > 0,
})}
>
<TriangleAlert size={13} />
Errors: {traceData.payload.totalErrorSpansCount ?? 0}
@@ -360,11 +360,9 @@ function TraceDetailsV3(): JSX.Element {
activeKey={activeKeys.filter((k) => k === 'waterfall')}
onChange={(): void => handleCollapseChange('waterfall')}
size="small"
className={`trace-details-v3__waterfall-collapse${
isWaterfallDocked
? ' trace-details-v3__waterfall-collapse--docked'
: ''
}`}
className={cx(styles.waterfallCollapse, {
[styles.isDocked]: isWaterfallDocked,
})}
items={[
{
key: 'waterfall',
@@ -375,7 +373,7 @@ function TraceDetailsV3(): JSX.Element {
/>
{panelState.isOpen && isDocked && (
<div className="trace-details-v3__docked-span-details">
<div className={styles.dockedSpanDetails}>
<SpanDetailsPanel
panelState={panelState}
selectedSpan={selectedSpan}