Compare commits

..

303 Commits

Author SHA1 Message Date
aks07
879f8a2e16 Merge branch 'feat/add-clear-filter' of github.com:SigNoz/signoz into feat/add-clear-filter 2026-05-19 13:13:31 +05:30
aks07
1802480f1f feat: realign trace details filters 2026-05-19 13:13:05 +05:30
Aditya Singh
80a78a5426 Merge branch 'main' into feat/add-clear-filter 2026-05-18 19:42:55 +05:30
aks07
e7911999d7 feat: add clear filter 2026-05-18 02:23:12 +05:30
aks07
9efe2aacab feat: minor change 2026-05-18 01:51:32 +05:30
aks07
f85da6d8d4 Merge branch 'main' of github.com:SigNoz/signoz into feat/enable-trace-details-v3 2026-05-18 01:42:19 +05:30
aks07
0e9d7bf537 feat: remove unnecessary waterfall api calls if span already in the list 2026-05-18 01:41:30 +05:30
aks07
9c50930f00 Merge branch 'feat/enable-trace-details-v3' of github.com:SigNoz/signoz into feat/enable-trace-details-v3 2026-05-15 01:09:53 +05:30
aks07
3eab3e1556 Merge branch 'main' of github.com:SigNoz/signoz into feat/enable-trace-details-v3 2026-05-15 01:09:05 +05:30
Aditya Singh
b1a81c09ce Merge branch 'main' into feat/enable-trace-details-v3 2026-05-14 17:07:53 +05:30
Aditya Singh
227b098067 Merge branch 'main' into feat/enable-trace-details-v3 2026-05-14 16:53:23 +05:30
aks07
3882c06054 feat: enable new trace details 2026-05-13 20:21:50 +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
178 changed files with 2242 additions and 4630 deletions

View File

@@ -8,14 +8,6 @@ packages:
filename: "alertmanager.go"
structname: 'Mock{{.InterfaceName}}'
pkgname: '{{.SrcPackageName}}test'
github.com/SigNoz/signoz/pkg/types/alertmanagertypes:
interfaces:
MaintenanceStore:
config:
dir: '{{.InterfaceDir}}/alertmanagertypestest'
filename: "maintenance.go"
structname: 'Mock{{.InterfaceName}}'
pkgname: '{{.SrcPackageName}}test'
github.com/SigNoz/signoz/pkg/tokenizer:
config:
all: true

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.125.0
image: signoz/signoz:v0.124.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.125.0
image: signoz/signoz:v0.124.0
ports:
- "8080:8080" # signoz port
volumes:

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.125.0}
image: signoz/signoz:${VERSION:-v0.124.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.125.0}
image: signoz/signoz:${VERSION:-v0.124.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -96,53 +96,6 @@ components:
- createdAt
- updatedAt
type: object
AlertmanagertypesMaintenanceKind:
enum:
- fixed
- recurring
type: string
AlertmanagertypesMaintenanceStatus:
enum:
- active
- upcoming
- expired
type: string
AlertmanagertypesPlannedMaintenance:
properties:
alertIds:
items:
type: string
nullable: true
type: array
createdAt:
format: date-time
type: string
createdBy:
type: string
description:
type: string
id:
type: string
kind:
$ref: '#/components/schemas/AlertmanagertypesMaintenanceKind'
name:
type: string
schedule:
$ref: '#/components/schemas/AlertmanagertypesSchedule'
status:
$ref: '#/components/schemas/AlertmanagertypesMaintenanceStatus'
updatedAt:
format: date-time
type: string
updatedBy:
type: string
required:
- id
- name
- schedule
- status
- kind
type: object
AlertmanagertypesPostableChannel:
oneOf:
- required:
@@ -259,23 +212,6 @@ components:
required:
- name
type: object
AlertmanagertypesPostablePlannedMaintenance:
properties:
alertIds:
items:
type: string
nullable: true
type: array
description:
type: string
name:
type: string
schedule:
$ref: '#/components/schemas/AlertmanagertypesSchedule'
required:
- name
- schedule
type: object
AlertmanagertypesPostableRoutePolicy:
properties:
channels:
@@ -301,60 +237,6 @@ components:
- channels
- name
type: object
AlertmanagertypesRecurrence:
properties:
duration:
type: string
endTime:
format: date-time
nullable: true
type: string
repeatOn:
items:
$ref: '#/components/schemas/AlertmanagertypesRepeatOn'
nullable: true
type: array
repeatType:
$ref: '#/components/schemas/AlertmanagertypesRepeatType'
startTime:
format: date-time
type: string
required:
- startTime
- duration
- repeatType
type: object
AlertmanagertypesRepeatOn:
enum:
- sunday
- monday
- tuesday
- wednesday
- thursday
- friday
- saturday
type: string
AlertmanagertypesRepeatType:
enum:
- daily
- weekly
- monthly
type: string
AlertmanagertypesSchedule:
properties:
endTime:
format: date-time
type: string
recurrence:
$ref: '#/components/schemas/AlertmanagertypesRecurrence'
startTime:
format: date-time
type: string
timezone:
type: string
required:
- timezone
type: object
AuthtypesAttributeMapping:
properties:
email:
@@ -5255,6 +5137,17 @@ components:
message:
type: string
type: object
RuletypesMaintenanceKind:
enum:
- fixed
- recurring
type: string
RuletypesMaintenanceStatus:
enum:
- active
- upcoming
- expired
type: string
RuletypesMatchType:
enum:
- at_least_once
@@ -5282,6 +5175,59 @@ components:
- table
- graph
type: string
RuletypesPlannedMaintenance:
properties:
alertIds:
items:
type: string
nullable: true
type: array
createdAt:
format: date-time
type: string
createdBy:
type: string
description:
type: string
id:
type: string
kind:
$ref: '#/components/schemas/RuletypesMaintenanceKind'
name:
type: string
schedule:
$ref: '#/components/schemas/RuletypesSchedule'
status:
$ref: '#/components/schemas/RuletypesMaintenanceStatus'
updatedAt:
format: date-time
type: string
updatedBy:
type: string
required:
- id
- name
- schedule
- status
- kind
type: object
RuletypesPostablePlannedMaintenance:
properties:
alertIds:
items:
type: string
nullable: true
type: array
description:
type: string
name:
type: string
schedule:
$ref: '#/components/schemas/RuletypesSchedule'
required:
- name
- schedule
type: object
RuletypesPostableRule:
properties:
alert:
@@ -5334,6 +5280,29 @@ components:
- clickhouse_sql
- promql
type: string
RuletypesRecurrence:
properties:
duration:
type: string
endTime:
format: date-time
nullable: true
type: string
repeatOn:
items:
$ref: '#/components/schemas/RuletypesRepeatOn'
nullable: true
type: array
repeatType:
$ref: '#/components/schemas/RuletypesRepeatType'
startTime:
format: date-time
type: string
required:
- startTime
- duration
- repeatType
type: object
RuletypesRenotify:
properties:
alertStates:
@@ -5345,6 +5314,22 @@ components:
interval:
type: string
type: object
RuletypesRepeatOn:
enum:
- sunday
- monday
- tuesday
- wednesday
- thursday
- friday
- saturday
type: string
RuletypesRepeatType:
enum:
- daily
- weekly
- monthly
type: string
RuletypesRollingWindow:
properties:
evalWindow:
@@ -5464,6 +5449,21 @@ components:
- promql_rule
- anomaly_rule
type: string
RuletypesSchedule:
properties:
endTime:
format: date-time
type: string
recurrence:
$ref: '#/components/schemas/RuletypesRecurrence'
startTime:
format: date-time
type: string
timezone:
type: string
required:
- timezone
type: object
RuletypesScheduleType:
enum:
- hourly
@@ -8024,7 +8024,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
type: array
status:
type: string
@@ -8067,7 +8067,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/AlertmanagertypesPostablePlannedMaintenance'
$ref: '#/components/schemas/RuletypesPostablePlannedMaintenance'
responses:
"201":
content:
@@ -8075,7 +8075,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
status:
type: string
required:
@@ -8178,7 +8178,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/AlertmanagertypesPlannedMaintenance'
$ref: '#/components/schemas/RuletypesPlannedMaintenance'
status:
type: string
required:
@@ -8232,7 +8232,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/AlertmanagertypesPostablePlannedMaintenance'
$ref: '#/components/schemas/RuletypesPostablePlannedMaintenance'
responses:
"204":
description: No Content

View File

@@ -80,15 +80,6 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
fineGrainedAuthz := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseFineGrainedAuthz, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureUseFineGrainedAuthz.String()),
Active: fineGrainedAuthz,
Usage: 0,
UsageLimit: -1,
Route: "",
})
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {

View File

@@ -13,6 +13,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
@@ -48,7 +49,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, tr)
// create ch rule task for evaluation
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
@@ -72,7 +73,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, pr)
// create promql rule task for evaluation
task = newTask(baserules.TaskTypeProm, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
task = newTask(baserules.TaskTypeProm, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
// create anomaly rule
@@ -95,7 +96,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules = append(rules, ar)
// create anomaly rule task for evaluation
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
@@ -209,9 +210,9 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
}
// newTask returns an appropriate group for the rule type
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc) baserules.Task {
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, maintenanceStore ruletypes.MaintenanceStore, orgID valuer.UUID) baserules.Task {
if taskType == baserules.TaskTypeCh {
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify)
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
}
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify)
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
}

View File

@@ -10,13 +10,6 @@ export default defineConfig({
signoz: {
input: {
target: '../docs/api/openapi.yml',
// Perses' `common.JSONRef` (used by `DashboardGridItem.content`) has a
// field tagged `json:"$ref"`, so our spec contains a property literally
// named `$ref`.
// Orval v8's validator (`@scalar/openapi-parser`) treats every `$ref` key
// as a JSON Reference and aborts with `INVALID_REFERENCE` when the value isn't a URI string.
// Safe to disable: yes, the spec is generated by `cmd/openapi.go` and gated by backend CI, not hand-edited.
unsafeDisableValidation: true,
},
output: {
target: './src/api/generated/services',
@@ -34,7 +27,7 @@ export default defineConfig({
signal: true,
useOperationIdAsQueryKey: false,
},
useDates: false,
useDates: true,
useNamedParameters: true,
enumGenerationType: 'enum',
mutator: {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {
@@ -18,7 +19,6 @@ import type {
} from 'react-query';
import type {
AlertmanagertypesPostablePlannedMaintenanceDTO,
CreateDowntimeSchedule201,
DeleteDowntimeScheduleByIDPathParameters,
GetDowntimeScheduleByID200,
@@ -26,6 +26,7 @@ import type {
ListDowntimeSchedules200,
ListDowntimeSchedulesParams,
RenderErrorResponseDTO,
RuletypesPostablePlannedMaintenanceDTO,
UpdateDowntimeScheduleByIDPathParameters,
} from '../sigNoz.schemas';
@@ -135,14 +136,14 @@ export const invalidateListDowntimeSchedules = async (
* @summary Create downtime schedule
*/
export const createDowntimeSchedule = (
alertmanagertypesPostablePlannedMaintenanceDTO?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>,
ruletypesPostablePlannedMaintenanceDTO?: BodyType<RuletypesPostablePlannedMaintenanceDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateDowntimeSchedule201>({
url: `/api/v1/downtime_schedules`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: alertmanagertypesPostablePlannedMaintenanceDTO,
data: ruletypesPostablePlannedMaintenanceDTO,
signal,
});
};
@@ -154,13 +155,13 @@ export const getCreateDowntimeScheduleMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
TError,
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
TError,
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
TContext
> => {
const mutationKey = ['createDowntimeSchedule'];
@@ -174,7 +175,7 @@ export const getCreateDowntimeScheduleMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> }
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -188,7 +189,7 @@ export type CreateDowntimeScheduleMutationResult = NonNullable<
Awaited<ReturnType<typeof createDowntimeSchedule>>
>;
export type CreateDowntimeScheduleMutationBody =
| BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>
| BodyType<RuletypesPostablePlannedMaintenanceDTO>
| undefined;
export type CreateDowntimeScheduleMutationError =
ErrorType<RenderErrorResponseDTO>;
@@ -203,13 +204,13 @@ export const useCreateDowntimeSchedule = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
TError,
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
TError,
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
TContext
> => {
return useMutation(getCreateDowntimeScheduleMutationOptions(options));
@@ -403,14 +404,14 @@ export const invalidateGetDowntimeScheduleByID = async (
*/
export const updateDowntimeScheduleByID = (
{ id }: UpdateDowntimeScheduleByIDPathParameters,
alertmanagertypesPostablePlannedMaintenanceDTO?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>,
ruletypesPostablePlannedMaintenanceDTO?: BodyType<RuletypesPostablePlannedMaintenanceDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/downtime_schedules/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: alertmanagertypesPostablePlannedMaintenanceDTO,
data: ruletypesPostablePlannedMaintenanceDTO,
signal,
});
};
@@ -424,7 +425,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
},
TContext
>;
@@ -433,7 +434,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
},
TContext
> => {
@@ -450,7 +451,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -465,7 +466,7 @@ export type UpdateDowntimeScheduleByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>
>;
export type UpdateDowntimeScheduleByIDMutationBody =
| BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>
| BodyType<RuletypesPostablePlannedMaintenanceDTO>
| undefined;
export type UpdateDowntimeScheduleByIDMutationError =
ErrorType<RenderErrorResponseDTO>;
@@ -482,7 +483,7 @@ export const useUpdateDowntimeScheduleByID = <
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
},
TContext
>;
@@ -491,7 +492,7 @@ export const useUpdateDowntimeScheduleByID = <
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
},
TContext
> => {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,13 +3,14 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
export interface AlertmanagertypesChannelDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -34,7 +35,7 @@ export interface AlertmanagertypesChannelDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface ModelLabelSetDTO {
@@ -62,7 +63,7 @@ export interface AlertmanagertypesDeprecatedGettableAlertDTO {
* @type string
* @format date-time
*/
endsAt?: string;
endsAt?: Date;
/**
* @type string
*/
@@ -80,7 +81,7 @@ export interface AlertmanagertypesDeprecatedGettableAlertDTO {
* @type string
* @format date-time
*/
startsAt?: string;
startsAt?: Date;
status?: TypesAlertStatusDTO;
}
@@ -97,7 +98,7 @@ export interface AlertmanagertypesGettableRoutePolicyDTO {
* @type string
* @format date-time
*/
createdAt: string;
createdAt: Date;
/**
* @type string,null
*/
@@ -127,116 +128,13 @@ export interface AlertmanagertypesGettableRoutePolicyDTO {
* @type string
* @format date-time
*/
updatedAt: string;
updatedAt: Date;
/**
* @type string,null
*/
updatedBy?: string | null;
}
export enum AlertmanagertypesMaintenanceKindDTO {
fixed = 'fixed',
recurring = 'recurring',
}
export enum AlertmanagertypesMaintenanceStatusDTO {
active = 'active',
upcoming = 'upcoming',
expired = 'expired',
}
export enum AlertmanagertypesRepeatOnDTO {
sunday = 'sunday',
monday = 'monday',
tuesday = 'tuesday',
wednesday = 'wednesday',
thursday = 'thursday',
friday = 'friday',
saturday = 'saturday',
}
export enum AlertmanagertypesRepeatTypeDTO {
daily = 'daily',
weekly = 'weekly',
monthly = 'monthly',
}
export interface AlertmanagertypesRecurrenceDTO {
/**
* @type string
*/
duration: string;
/**
* @type string,null
* @format date-time
*/
endTime?: string | null;
/**
* @type array,null
*/
repeatOn?: AlertmanagertypesRepeatOnDTO[] | null;
repeatType: AlertmanagertypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: string;
}
export interface AlertmanagertypesScheduleDTO {
/**
* @type string
* @format date-time
*/
endTime?: string;
recurrence?: AlertmanagertypesRecurrenceDTO;
/**
* @type string
* @format date-time
*/
startTime?: string;
/**
* @type string
*/
timezone: string;
}
export interface AlertmanagertypesPlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id: string;
kind: AlertmanagertypesMaintenanceKindDTO;
/**
* @type string
*/
name: string;
schedule: AlertmanagertypesScheduleDTO;
status: AlertmanagertypesMaintenanceStatusDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface ConfigAuthorizationDTO {
/**
* @type string
@@ -1700,22 +1598,6 @@ export type AlertmanagertypesPostableChannelDTO = unknown & {
wechat_configs?: ConfigWechatConfigDTO[];
};
export interface AlertmanagertypesPostablePlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name: string;
schedule: AlertmanagertypesScheduleDTO;
}
export interface AlertmanagertypesPostableRoutePolicyDTO {
/**
* @type array,null
@@ -1953,7 +1835,7 @@ export interface AuthtypesGettableAuthDomainDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -1970,7 +1852,7 @@ export interface AuthtypesGettableAuthDomainDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface AuthtypesGettableTokenDTO {
@@ -2128,7 +2010,7 @@ export interface AuthtypesRoleDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -2153,7 +2035,7 @@ export interface AuthtypesRoleDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface AuthtypesSessionContextDTO {
@@ -2181,7 +2063,7 @@ export interface AuthtypesUserRoleDTO {
* @type string
* @format date-time
*/
createdAt: string;
createdAt: Date;
/**
* @type string
*/
@@ -2195,7 +2077,7 @@ export interface AuthtypesUserRoleDTO {
* @type string
* @format date-time
*/
updatedAt: string;
updatedAt: Date;
/**
* @type string
*/
@@ -2207,7 +2089,7 @@ export interface AuthtypesUserWithRolesDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -2236,7 +2118,7 @@ export interface AuthtypesUserWithRolesDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
/**
* @type array,null
*/
@@ -2403,7 +2285,7 @@ export interface CloudintegrationtypesAccountDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -2424,12 +2306,12 @@ export interface CloudintegrationtypesAccountDTO {
* @type string,null
* @format date-time
*/
removedAt: string | null;
removedAt: Date | null;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface DashboardtypesStorableDashboardDataDTO {
@@ -2560,7 +2442,7 @@ export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -2570,7 +2452,7 @@ export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
};
/**
@@ -2764,12 +2646,12 @@ export interface CloudintegrationtypesGettableAgentCheckInDTO {
* @type string,null
* @format date-time
*/
removed_at: string | null;
removed_at: Date | null;
/**
* @type string,null
* @format date-time
*/
removedAt: string | null;
removedAt: Date | null;
}
export interface CloudintegrationtypesServiceMetadataDTO {
@@ -3004,7 +2886,7 @@ export interface DashboardtypesDashboardDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -3026,7 +2908,7 @@ export interface DashboardtypesDashboardDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
/**
* @type string
*/
@@ -3208,7 +3090,7 @@ export interface GatewaytypesLimitDTO {
* @type string
* @format date-time
*/
created_at?: string;
created_at?: Date;
/**
* @type string
*/
@@ -3230,7 +3112,7 @@ export interface GatewaytypesLimitDTO {
* @type string
* @format date-time
*/
updated_at?: string;
updated_at?: Date;
}
export interface GatewaytypesIngestionKeyDTO {
@@ -3238,12 +3120,12 @@ export interface GatewaytypesIngestionKeyDTO {
* @type string
* @format date-time
*/
created_at?: string;
created_at?: Date;
/**
* @type string
* @format date-time
*/
expires_at?: string;
expires_at?: Date;
/**
* @type string
*/
@@ -3264,7 +3146,7 @@ export interface GatewaytypesIngestionKeyDTO {
* @type string
* @format date-time
*/
updated_at?: string;
updated_at?: Date;
/**
* @type string
*/
@@ -3288,7 +3170,7 @@ export interface GatewaytypesPostableIngestionKeyDTO {
* @type string
* @format date-time
*/
expires_at?: string;
expires_at?: Date;
/**
* @type string
*/
@@ -4558,7 +4440,7 @@ export interface LlmpricingruletypesLLMPricingRuleDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -4597,13 +4479,13 @@ export interface LlmpricingruletypesLLMPricingRuleDTO {
* @type string,null
* @format date-time
*/
syncedAt?: string | null;
syncedAt?: Date | null;
unit: LlmpricingruletypesLLMPricingRuleUnitDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
/**
* @type string
*/
@@ -5829,7 +5711,7 @@ export interface Querybuildertypesv5RawRowDTO {
* @type string
* @format date-time
*/
timestamp?: string;
timestamp?: Date;
}
export interface Querybuildertypesv5RawDataDTO {
@@ -6235,6 +6117,15 @@ export interface RuletypesGettableTestRuleDTO {
message?: string;
}
export enum RuletypesMaintenanceKindDTO {
fixed = 'fixed',
recurring = 'recurring',
}
export enum RuletypesMaintenanceStatusDTO {
active = 'active',
upcoming = 'upcoming',
expired = 'expired',
}
export interface RuletypesRenotifyDTO {
/**
* @type array
@@ -6266,6 +6157,116 @@ export interface RuletypesNotificationSettingsDTO {
usePolicy?: boolean;
}
export enum RuletypesRepeatOnDTO {
sunday = 'sunday',
monday = 'monday',
tuesday = 'tuesday',
wednesday = 'wednesday',
thursday = 'thursday',
friday = 'friday',
saturday = 'saturday',
}
export enum RuletypesRepeatTypeDTO {
daily = 'daily',
weekly = 'weekly',
monthly = 'monthly',
}
export interface RuletypesRecurrenceDTO {
/**
* @type string
*/
duration: string;
/**
* @type string,null
* @format date-time
*/
endTime?: Date | null;
/**
* @type array,null
*/
repeatOn?: RuletypesRepeatOnDTO[] | null;
repeatType: RuletypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: Date;
}
export interface RuletypesScheduleDTO {
/**
* @type string
* @format date-time
*/
endTime?: Date;
recurrence?: RuletypesRecurrenceDTO;
/**
* @type string
* @format date-time
*/
startTime?: Date;
/**
* @type string
*/
timezone: string;
}
export interface RuletypesPlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id: string;
kind: RuletypesMaintenanceKindDTO;
/**
* @type string
*/
name: string;
schedule: RuletypesScheduleDTO;
status: RuletypesMaintenanceStatusDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
/**
* @type string
*/
updatedBy?: string;
}
export interface RuletypesPostablePlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name: string;
schedule: RuletypesScheduleDTO;
}
export type RuletypesPostableRuleDTOAnnotations = { [key: string]: string };
export type RuletypesPostableRuleDTOLabels = { [key: string]: string };
@@ -6406,7 +6407,7 @@ export interface RuletypesRuleDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -6455,7 +6456,7 @@ export interface RuletypesRuleDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
/**
* @type string
*/
@@ -6474,7 +6475,7 @@ export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type integer
* @minimum 0
@@ -6488,7 +6489,7 @@ export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
* @type string
* @format date-time
*/
lastObservedAt: string;
lastObservedAt: Date;
/**
* @type string
*/
@@ -6501,7 +6502,7 @@ export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface ServiceaccounttypesGettableFactorAPIKeyWithKeyDTO {
@@ -6546,7 +6547,7 @@ export interface ServiceaccounttypesServiceAccountDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -6571,7 +6572,7 @@ export interface ServiceaccounttypesServiceAccountDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface ServiceaccounttypesServiceAccountRoleDTO {
@@ -6579,7 +6580,7 @@ export interface ServiceaccounttypesServiceAccountRoleDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -6597,7 +6598,7 @@ export interface ServiceaccounttypesServiceAccountRoleDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface ServiceaccounttypesServiceAccountWithRolesDTO {
@@ -6605,7 +6606,7 @@ export interface ServiceaccounttypesServiceAccountWithRolesDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -6634,7 +6635,7 @@ export interface ServiceaccounttypesServiceAccountWithRolesDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface ServiceaccounttypesUpdatableFactorAPIKeyDTO {
@@ -6676,7 +6677,7 @@ export interface SpantypesSpanMapperGroupDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -6701,7 +6702,7 @@ export interface SpantypesSpanMapperGroupDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
/**
* @type string
*/
@@ -6770,7 +6771,7 @@ export interface SpantypesSpanMapperDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -6796,7 +6797,7 @@ export interface SpantypesSpanMapperDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
/**
* @type string
*/
@@ -7163,7 +7164,7 @@ export interface TypesDeprecatedUserDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -7196,7 +7197,7 @@ export interface TypesDeprecatedUserDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface TypesIdentifiableDTO {
@@ -7211,7 +7212,7 @@ export interface TypesInviteDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -7244,7 +7245,7 @@ export interface TypesInviteDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface TypesOrganizationDTO {
@@ -7256,7 +7257,7 @@ export interface TypesOrganizationDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -7278,7 +7279,7 @@ export interface TypesOrganizationDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface TypesPostableInviteDTO {
@@ -7345,7 +7346,7 @@ export interface TypesResetPasswordTokenDTO {
* @type string
* @format date-time
*/
expiresAt?: string;
expiresAt?: Date;
/**
* @type string
*/
@@ -7372,7 +7373,7 @@ export interface TypesUserDTO {
* @type string
* @format date-time
*/
createdAt?: string;
createdAt?: Date;
/**
* @type string
*/
@@ -7401,7 +7402,7 @@ export interface TypesUserDTO {
* @type string
* @format date-time
*/
updatedAt?: string;
updatedAt?: Date;
}
export interface ZeustypesHostDTO {
@@ -7793,7 +7794,7 @@ export type ListDowntimeSchedules200 = {
/**
* @type array
*/
data: AlertmanagertypesPlannedMaintenanceDTO[];
data: RuletypesPlannedMaintenanceDTO[];
/**
* @type string
*/
@@ -7801,7 +7802,7 @@ export type ListDowntimeSchedules200 = {
};
export type CreateDowntimeSchedule201 = {
data: AlertmanagertypesPlannedMaintenanceDTO;
data: RuletypesPlannedMaintenanceDTO;
/**
* @type string
*/
@@ -7815,7 +7816,7 @@ export type GetDowntimeScheduleByIDPathParameters = {
id: string;
};
export type GetDowntimeScheduleByID200 = {
data: AlertmanagertypesPlannedMaintenanceDTO;
data: RuletypesPlannedMaintenanceDTO;
/**
* @type string
*/

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -59,7 +59,7 @@ function getDeleteTooltip(
function getInviteButtonLabel(
isLoading: boolean,
existingToken: { expiresAt?: string } | undefined,
existingToken: { expiresAt?: Date } | undefined,
isExpired: boolean,
notFound: boolean,
): string {

View File

@@ -1,176 +0,0 @@
import { useCallback, useMemo, useState } from 'react';
import { toast } from '@signozhq/ui/sonner';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
import { FloatingPanel } from 'periscope/components/FloatingPanel';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import AddedFields from './AddedFields';
import OtherFields from './OtherFields';
import styles from './FieldsSelector.module.scss';
const DEFAULT_PANEL_WIDTH = 350;
const DEFAULT_PANEL_HEIGHT_OFFSET = 100;
const DEFAULT_PANEL_RIGHT_INSET = 100;
const DEFAULT_PANEL_TOP_INSET = 50;
interface FieldsSelectorProps {
isOpen: boolean;
title: string;
fields: TelemetryFieldKey[];
onFieldsChange: (fields: TelemetryFieldKey[]) => void;
onClose: () => void;
signal: DataSource;
maxFields?: number;
width?: number;
height?: number;
defaultPosition?: { x: number; y: number };
}
function FieldsSelector({
isOpen,
title,
fields,
onFieldsChange,
onClose,
signal,
maxFields,
width = DEFAULT_PANEL_WIDTH,
height,
defaultPosition,
}: FieldsSelectorProps): JSX.Element | null {
if (!isOpen) {
return null;
}
const resolvedHeight =
height ?? window.innerHeight - DEFAULT_PANEL_HEIGHT_OFFSET;
const resolvedPosition = defaultPosition ?? {
x: window.innerWidth - width - DEFAULT_PANEL_RIGHT_INSET,
y: DEFAULT_PANEL_TOP_INSET,
};
const [draftFields, setDraftFields] = useState<TelemetryFieldKey[]>(fields);
const [inputValue, setInputValue] = useState('');
const [debouncedInputValue, setDebouncedInputValue] = useState('');
const debouncedUpdate = useDebouncedFn((value) => {
setDebouncedInputValue(value as string);
}, 400);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value.trim().toLowerCase();
setInputValue(value);
debouncedUpdate(value);
},
[debouncedUpdate],
);
const handleAdd = useCallback(
(field: TelemetryFieldKey): void => {
if (maxFields !== undefined && draftFields.length >= maxFields) {
return;
}
if (draftFields.some((f) => f.name === field.name)) {
return;
}
setDraftFields((prev) => [...prev, field]);
},
[draftFields, maxFields],
);
const handleSave = useCallback((): void => {
onFieldsChange(draftFields);
toast.success('Saved successfully', {
position: 'top-right',
});
onClose();
}, [draftFields, onFieldsChange, onClose]);
const handleDiscard = useCallback((): void => {
setDraftFields(fields);
}, [fields]);
const hasUnsavedChanges = useMemo(
() =>
!(
draftFields.length === fields.length &&
draftFields.every((f, i) => f.name === fields[i]?.name)
),
[draftFields, fields],
);
const isAtLimit = maxFields !== undefined && draftFields.length >= maxFields;
return (
<FloatingPanel
isOpen
width={width}
height={resolvedHeight}
defaultPosition={resolvedPosition}
enableResizing={false}
>
<div className={styles.root}>
<div className={styles.header}>
<div className={styles.title}>
<TableColumnsSplit size={16} />
{title}
</div>
<X className={styles.closeIcon} size={16} onClick={onClose} />
</div>
<section>
<Input
className={styles.searchInput}
type="text"
value={inputValue}
placeholder="Search for a field..."
onChange={handleInputChange}
/>
</section>
<AddedFields
inputValue={inputValue}
fields={draftFields}
onFieldsChange={setDraftFields}
maxFields={maxFields}
/>
<OtherFields
signal={signal}
debouncedInputValue={debouncedInputValue}
addedFields={draftFields}
onAdd={handleAdd}
isAtLimit={isAtLimit}
/>
{hasUnsavedChanges && (
<div className={styles.footer}>
<Button
variant="outlined"
color="secondary"
onClick={handleDiscard}
prefix={<X width={14} height={14} />}
>
Discard
</Button>
<Button
variant="solid"
color="primary"
onClick={handleSave}
prefix={<Check width={14} height={14} />}
>
Save changes
</Button>
</div>
)}
</div>
</FloatingPanel>
);
}
export default FieldsSelector;

View File

@@ -1 +0,0 @@
export { default } from './FieldsSelector';

View File

@@ -5,8 +5,6 @@ import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents } from 'container/AIAssistant/events';
import { normalizePage } from 'container/AIAssistant/hooks/useAIAssistantAnalyticsContext';
import {
openAIAssistant,
useAIAssistantStore,
@@ -52,14 +50,6 @@ function HeaderRightSection({
setOpenAnnouncementsModal(false);
}, [location.pathname]);
const handleOpenAIAssistant = useCallback((): void => {
void logEvent(AIAssistantEvents.Opened, {
source: 'header',
currentPage: normalizePage(location.pathname),
});
openAIAssistant();
}, [location.pathname]);
const handleOpenShareURLModal = useCallback((): void => {
logEvent('Share: Clicked', {
page: location.pathname,
@@ -111,7 +101,7 @@ function HeaderRightSection({
<Button
variant="solid"
color="secondary"
onClick={handleOpenAIAssistant}
onClick={openAIAssistant}
aria-label={
showHeaderPendingBadge
? pendingUserInputCount === 1

View File

@@ -1,12 +0,0 @@
/* Hide native browser spinners for our number input, matching antd defaults. */
.signoz-input-number {
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
&[type='number'] {
-moz-appearance: textfield;
}
}

View File

@@ -1,130 +0,0 @@
import './InputNumber.styles.scss';
import {
ChangeEvent,
CSSProperties,
FocusEventHandler,
forwardRef,
KeyboardEventHandler,
ReactNode,
} from 'react';
import { Input } from '@signozhq/ui/input';
import cx from 'classnames';
export type InputNumberProps = {
value?: number | null;
defaultValue?: number | null;
onChange?: (value: number | null) => void;
min?: number;
max?: number;
step?: number;
/** When set, values emitted via onChange are rounded to this many decimals. */
precision?: number;
placeholder?: string;
disabled?: boolean;
prefix?: ReactNode;
suffix?: ReactNode;
className?: string;
rootClassName?: string;
style?: CSSProperties;
id?: string;
name?: string;
testId?: string;
autoFocus?: boolean;
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>;
onFocus?: FocusEventHandler<HTMLInputElement>;
'aria-label'?: string;
'data-testid'?: string;
};
const toInputValue = (value: number | null | undefined): string | undefined => {
if (value === null || value === undefined || Number.isNaN(value)) {
return '';
}
return String(value);
};
const parseValue = (raw: string, precision?: number): number | null => {
if (raw === '' || raw === '-') {
return null;
}
const parsed = Number(raw);
if (Number.isNaN(parsed)) {
return null;
}
if (precision === undefined) {
return parsed;
}
const factor = 10 ** precision;
return Math.round(parsed * factor) / factor;
};
const InputNumber = forwardRef<HTMLInputElement, InputNumberProps>(
(
{
value,
defaultValue,
onChange,
min,
max,
step,
precision,
placeholder,
disabled,
prefix,
suffix,
className,
rootClassName,
style,
id,
name,
testId,
autoFocus,
onKeyDown,
onBlur,
onFocus,
'aria-label': ariaLabel,
'data-testid': dataTestId,
},
ref,
): JSX.Element => {
const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
onChange?.(parseValue(event.target.value, precision));
};
return (
<Input
ref={ref}
type="number"
value={value === undefined ? undefined : toInputValue(value)}
defaultValue={
defaultValue === undefined ? undefined : toInputValue(defaultValue)
}
onChange={handleChange}
min={min}
max={max}
step={step}
placeholder={placeholder}
disabled={disabled}
prefix={prefix}
suffix={suffix}
className={cx('signoz-input-number', className)}
containerClassName={cx('signoz-input-number-container', rootClassName)}
style={style}
id={id}
name={name}
testId={testId ?? dataTestId}
autoFocus={autoFocus}
onKeyDown={onKeyDown}
onBlur={onBlur}
onFocus={onFocus}
aria-label={ariaLabel}
/>
);
},
);
InputNumber.displayName = 'InputNumber';
export default InputNumber;

View File

@@ -1,2 +0,0 @@
export { default } from './InputNumber';
export type { InputNumberProps } from './InputNumber';

View File

@@ -1,9 +1,8 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Input, Popover, Tooltip } from 'antd';
import { Button, Input, InputNumber, Popover, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type { DefaultOptionType } from 'antd/es/select';
import cx from 'classnames';
import InputNumber from 'components/InputNumber';
import { LogViewMode } from 'container/LogsTable';
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
import useDebouncedFn from 'hooks/useDebouncedFunction';

View File

@@ -144,7 +144,6 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
loading={loading}
notFoundContent={notFoundContent}
options={options}
optionFilterProp="label"
optionRender={(option): JSX.Element => (
<Checkbox
checked={value.includes(option.value as string)}
@@ -163,7 +162,6 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
return (
<Select
id={id}
showSearch
value={value || undefined}
onChange={onChange}
placeholder={placeholder}
@@ -172,7 +170,6 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
loading={loading}
notFoundContent={notFoundContent}
options={options}
optionFilterProp="label"
getPopupContainer={getPopupContainer}
disabled={disabled}
/>

View File

@@ -28,7 +28,7 @@ const mockKey: ServiceaccounttypesGettableFactorAPIKeyDTO = {
id: 'key-1',
name: 'Original Key Name',
expiresAt: 0,
lastObservedAt: null as unknown as string,
lastObservedAt: null as unknown as Date,
serviceAccountId: 'sa-1',
};

View File

@@ -29,14 +29,14 @@ const keys: ServiceaccounttypesGettableFactorAPIKeyDTO[] = [
id: 'key-1',
name: 'Production Key',
expiresAt: 0,
lastObservedAt: null as unknown as string,
lastObservedAt: null as unknown as Date,
serviceAccountId: 'sa-1',
},
{
id: 'key-2',
name: 'Staging Key',
expiresAt: 1924905600, // 2030-12-31
lastObservedAt: '2026-03-10T10:00:00Z',
lastObservedAt: new Date('2026-03-10T10:00:00Z'),
serviceAccountId: 'sa-1',
},
];

View File

@@ -10,5 +10,4 @@ export enum FeatureKeys {
ONBOARDING_V3 = 'onboarding_v3',
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
USE_JSON_BODY = 'use_json_body',
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
}

View File

@@ -108,7 +108,4 @@ export const REACT_QUERY_KEY = {
// Dashboard Grid Card Query Keys
DASHBOARD_GRID_CARD_QUERY_RANGE: 'DASHBOARD_GRID_CARD_QUERY_RANGE',
// Fields Selector Query Keys
GET_FIELDS_SELECTOR_SUGGESTIONS: 'GET_FIELDS_SELECTOR_SUGGESTIONS',
} as const;

View File

@@ -1,20 +1,13 @@
import { useCallback, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { useHistory, useLocation } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import ROUTES from 'constants/routes';
import { History, Maximize2, Minus, Plus, Sparkles, X } from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import HistorySidebar from '../components/ConversationsList';
import ConversationView from '../ConversationView';
import { AIAssistantEvents } from '../events';
import {
normalizePage,
useAIAssistantAnalyticsContext,
} from '../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../store/useAIAssistantStore';
import { VariantContext } from '../VariantContext';
@@ -31,7 +24,6 @@ import styles from './AIAssistantModal.module.scss';
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function AIAssistantModal(): JSX.Element | null {
const history = useHistory();
const { pathname } = useLocation();
const [showHistory, setShowHistory] = useState(false);
const isOpen = useAIAssistantStore((s) => s.isModalOpen);
@@ -44,7 +36,6 @@ export default function AIAssistantModal(): JSX.Element | null {
const startNewConversation = useAIAssistantStore(
(s) => s.startNewConversation,
);
const analyticsCtx = useAIAssistantAnalyticsContext();
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent): void => {
@@ -64,10 +55,6 @@ export default function AIAssistantModal(): JSX.Element | null {
} else {
startNewConversation();
setShowHistory(false);
void logEvent(AIAssistantEvents.Opened, {
source: 'shortcut',
currentPage: normalizePage(pathname),
});
openModal();
}
return;
@@ -81,7 +68,7 @@ export default function AIAssistantModal(): JSX.Element | null {
window.addEventListener('keydown', handleKeyDown);
return (): void => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, openModal, closeModal, startNewConversation, pathname]);
}, [isOpen, openModal, closeModal, startNewConversation]);
// ── Handlers ────────────────────────────────────────────────────────────────
@@ -90,28 +77,15 @@ export default function AIAssistantModal(): JSX.Element | null {
return;
}
closeModal();
// Router state tells AIAssistantPage to skip its mount-time Opened fire:
// the assistant was already open in the modal, so this is a surface
// switch, not a new open.
history.push(
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
{ fromInApp: true },
);
}, [activeConversationId, closeModal, history]);
const handleNew = useCallback(() => {
void logEvent(AIAssistantEvents.NewChatClicked, {
...analyticsCtx,
// useAIAssistantAnalyticsContext() runs above this component's
// VariantContext.Provider, so the hook reports the default 'page'
// mode. Override here: the modal collapses to 'sidepane' in our
// taxonomy alongside the drawer.
mode: 'sidepane',
source: 'header',
});
startNewConversation();
setShowHistory(false);
}, [startNewConversation, analyticsCtx]);
}, [startNewConversation]);
const handleHistorySelect = useCallback(() => {
setShowHistory(false);

View File

@@ -5,12 +5,8 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
import ROUTES from 'constants/routes';
import { History, Maximize2, Plus, Sparkles, X } from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import ConversationsList from '../components/ConversationsList';
import ConversationView from '../ConversationView';
import { AIAssistantEvents } from '../events';
import { useAIAssistantAnalyticsContext } from '../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../store/useAIAssistantStore';
import { VariantContext } from '../VariantContext';
@@ -36,35 +32,21 @@ export default function AIAssistantPanel(): JSX.Element | null {
const startNewConversation = useAIAssistantStore(
(s) => s.startNewConversation,
);
const analyticsCtx = useAIAssistantAnalyticsContext();
const handleExpand = useCallback(() => {
if (!activeConversationId) {
return;
}
closeDrawer();
// Router state tells AIAssistantPage to skip its mount-time Opened fire:
// the assistant was already open in the drawer, so this is a surface
// switch, not a new open.
history.push(
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
{ fromInApp: true },
);
}, [activeConversationId, closeDrawer, history]);
const handleNew = useCallback(() => {
void logEvent(AIAssistantEvents.NewChatClicked, {
...analyticsCtx,
// useAIAssistantAnalyticsContext() runs above this component's
// VariantContext.Provider, so the hook reports the default 'page'
// mode. Override here: this handler only runs when the drawer
// itself is mounted, which is unambiguously the sidepane surface.
mode: 'sidepane',
source: 'header',
});
startNewConversation();
setShowHistory(false);
}, [startNewConversation, analyticsCtx]);
}, [startNewConversation]);
// When user picks a conversation from the list, close the sidebar
const handleHistorySelect = useCallback(() => {

View File

@@ -1,13 +1,9 @@
import { useCallback } from 'react';
import { matchPath, useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import { Bot } from '@signozhq/icons';
import { AIAssistantEvents } from '../events';
import { normalizePage } from '../hooks/useAIAssistantAnalyticsContext';
import {
openAIAssistant,
useAIAssistantStore,
@@ -29,14 +25,6 @@ export default function AIAssistantTrigger(): JSX.Element | null {
exact: true,
});
const handleOpen = useCallback((): void => {
void logEvent(AIAssistantEvents.Opened, {
source: 'icon',
currentPage: normalizePage(pathname),
});
openAIAssistant();
}, [pathname]);
if (isDrawerOpen || isModalOpen || isFullScreenPage) {
return null;
}
@@ -47,7 +35,7 @@ export default function AIAssistantTrigger(): JSX.Element | null {
variant="solid"
color="primary"
className={styles.trigger}
onClick={handleOpen}
onClick={openAIAssistant}
aria-label="Open AI Assistant"
>
<Bot size={20} />

View File

@@ -1,15 +1,11 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import cx from 'classnames';
import logEvent from 'api/common/logEvent';
import ChatInput, { autoContextKey } from '../components/ChatInput';
import ConversationSkeleton from '../components/ConversationSkeleton';
import VirtualizedMessages from '../components/VirtualizedMessages';
import { AIAssistantEvents } from '../events';
import { getAutoContexts } from '../getAutoContexts';
import { useAIAssistantAnalyticsContext } from '../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../store/useAIAssistantStore';
import { MessageAttachment } from '../types';
import { MessageContext } from '../../../api/ai-assistant/chat';
@@ -43,7 +39,6 @@ export default function ConversationView({
);
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
const cancelStream = useAIAssistantStore((s) => s.cancelStream);
const analyticsCtx = useAIAssistantAnalyticsContext(conversationId);
// Auto-derived contexts come from the route the user is currently looking
// at (dashboard detail, service metrics, an explorer, …). Skip when the
@@ -87,50 +82,14 @@ export default function ConversationView({
attachments?: MessageAttachment[],
contexts?: MessageContext[],
) => {
const hasAuto = contexts?.some((c) => c.source === 'auto') ?? false;
const hasManual = contexts?.some((c) => c.source === 'mention') ?? false;
let contextType: 'manual' | 'auto' | 'both' | undefined;
if (hasAuto && hasManual) {
contextType = 'both';
} else if (hasAuto) {
contextType = 'auto';
} else if (hasManual) {
contextType = 'manual';
}
void logEvent(AIAssistantEvents.MessageSent, {
...analyticsCtx,
queryLength: text.length,
hasContext: hasAuto || hasManual,
contextType,
respondingToClarification: Boolean(pendingClarificationHere),
});
void sendMessage(text, attachments, contexts);
},
[sendMessage, analyticsCtx, pendingClarificationHere],
[sendMessage],
);
// Wall-clock timestamp of the current streaming start, used to compute
// `secondsSinceStart` on Cancel clicked. Cleared whenever streaming ends.
const streamStartedAtRef = useRef<number | null>(null);
useEffect(() => {
if (!isStreamingHere) {
streamStartedAtRef.current = null;
return;
}
if (streamStartedAtRef.current === null) {
streamStartedAtRef.current = Date.now();
}
}, [isStreamingHere]);
const handleCancel = useCallback(() => {
const startedAt = streamStartedAtRef.current;
void logEvent(AIAssistantEvents.CancelClicked, {
threadId: analyticsCtx.threadId,
secondsSinceStart:
startedAt !== null ? Math.round((Date.now() - startedAt) / 1000) : null,
});
cancelStream(conversationId);
}, [cancelStream, conversationId, analyticsCtx.threadId]);
}, [cancelStream, conversationId]);
const messages = conversation?.messages ?? [];
const showDisclaimer = messages.length > 0;
@@ -175,7 +134,6 @@ export default function ConversationView({
conversationId={conversationId}
messages={messages}
isStreaming={isStreamingHere}
onSendSuggestedPrompt={(text): void => handleSend(text)}
/>
{showDisclaimer && (
<div className={disclaimerClass} role="note" aria-live="polite">

View File

@@ -41,68 +41,12 @@ import {
Undo,
} from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents, SuggestedPromptCategory } from '../../events';
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
import styles from './ActionsSection.module.scss';
interface ActionsSectionProps {
actions: MessageActionDTO[];
/** ID of the assistant message these actions belong to — used in analytics. */
messageId: string;
}
/**
* Resource-type strings the backend uses for `open_resource` and rollback
* actions. Centralized here so the route/module lookups below stay in sync.
*/
const ResourceType = {
dashboard: 'dashboard',
alert: 'alert',
service: 'service',
saved_view: 'saved_view',
logs_explorer: 'logs_explorer',
traces_explorer: 'traces_explorer',
metrics_explorer: 'metrics_explorer',
} as const;
/** Maps an open_resource action's resourceType to its product module name. */
function targetModuleForResource(resourceType: string): string | null {
switch (resourceType) {
case ResourceType.dashboard:
return 'dashboards';
case ResourceType.alert:
return 'alerts';
case ResourceType.service:
return 'apm';
case ResourceType.saved_view:
return 'savedViews';
case ResourceType.logs_explorer:
return 'logs';
case ResourceType.traces_explorer:
return 'traces';
case ResourceType.metrics_explorer:
return 'metrics';
default:
return null;
}
}
/** Maps an apply_filter signal to its product module name. */
function targetModuleForSignal(signal: ApplyFilterSignalDTO): string | null {
switch (signal) {
case ApplyFilterSignalDTO.logs:
return 'logs';
case ApplyFilterSignalDTO.traces:
return 'traces';
case ApplyFilterSignalDTO.metrics:
return 'metrics';
default:
return null;
}
}
type ChipState = 'idle' | 'loading' | 'success' | 'error';
@@ -150,23 +94,23 @@ function resourceRoute(
resourceId: string,
): string | null {
switch (resourceType) {
case ResourceType.dashboard:
case 'dashboard':
return ROUTES.DASHBOARD.replace(':dashboardId', resourceId);
case ResourceType.alert: {
case 'alert': {
const params = new URLSearchParams({ [QueryParams.ruleId]: resourceId });
return `${ROUTES.EDIT_ALERTS}?${params.toString()}`;
}
case ResourceType.service:
case 'service':
return ROUTES.SERVICE_METRICS.replace(':servicename', resourceId);
case ResourceType.saved_view:
case 'saved_view':
// No detail route — saved views land on the list page.
// Caller may provide signal-aware metadata in future; default to logs.
return ROUTES.LOGS_SAVE_VIEWS;
case ResourceType.logs_explorer:
case 'logs_explorer':
return ROUTES.LOGS_EXPLORER;
case ResourceType.traces_explorer:
case 'traces_explorer':
return ROUTES.TRACES_EXPLORER;
case ResourceType.metrics_explorer:
case 'metrics_explorer':
return ROUTES.METRICS_EXPLORER_EXPLORER;
default:
return null;
@@ -280,24 +224,6 @@ function actionKey(action: MessageActionDTO, index: number): string {
: `${action.kind}:${action.label}:${index}`;
}
/**
* Resolves the prompt to send for a follow_up action. The chip's `label` is
* the short display text (e.g. "Python setup"); the real prompt lives in
* `input.intent` per the schema doc. Falls back to label defensively so a
* malformed server payload doesn't drop the click silently. Both branches
* are trimmed so whitespace-only payloads don't become whitespace messages.
*/
function followUpIntent(action: MessageActionDTO): string {
const intent = action.input?.intent;
if (typeof intent === 'string') {
const trimmed = intent.trim();
if (trimmed.length > 0) {
return trimmed;
}
}
return action.label.trim();
}
/** Maps a signal to its target explorer route. */
function explorerRouteForSignal(signal: ApplyFilterSignalDTO): string | null {
switch (signal) {
@@ -427,12 +353,10 @@ function rollbackCall(
*/
export default function ActionsSection({
actions,
messageId,
}: ActionsSectionProps): JSX.Element | null {
const history = useHistory();
const { pathname } = useLocation();
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
const { threadId, page, mode } = useAIAssistantAnalyticsContext();
const { redirectWithQueryBuilderData, handleSetQueryData } = useQueryBuilder();
// Per-chip click state, keyed by chip key (see `key` below). Persists
@@ -506,39 +430,13 @@ export default function ActionsSection({
switch (action.kind) {
case MessageActionKindDTO.open_docs: {
if (action.url) {
void logEvent(AIAssistantEvents.DocOpened, {
threadId,
messageId,
docPath: action.url,
});
openInNewTab(action.url);
}
break;
}
case MessageActionKindDTO.follow_up: {
const intent = followUpIntent(action);
if (intent) {
// Fire SuggestedPromptClicked + MessageSent so analytics can compute
// both the click-through rate against follow-ups offered *and* keep
// the unified send funnel intact. `category` distinguishes server-
// emitted follow-ups from the empty-state grid. `promptId` stays the
// label so dashboards group identical chip texts together regardless
// of the dynamic intent payload.
void logEvent(AIAssistantEvents.SuggestedPromptClicked, {
threadId,
messageId,
promptId: action.label,
category: SuggestedPromptCategory.FollowUp,
});
void logEvent(AIAssistantEvents.MessageSent, {
threadId,
page,
mode,
queryLength: intent.length,
hasContext: false,
respondingToClarification: false,
});
void sendMessage(intent);
if (action.label) {
void sendMessage(action.label);
}
break;
}
@@ -546,12 +444,6 @@ export default function ActionsSection({
if (action.resourceType && action.resourceId) {
const path = resourceRoute(action.resourceType, action.resourceId);
if (path) {
void logEvent(AIAssistantEvents.ResourceOpened, {
threadId,
messageId,
targetModule: targetModuleForResource(action.resourceType),
resourceId: action.resourceId,
});
history.push(path);
}
}
@@ -564,13 +456,6 @@ export default function ActionsSection({
break;
}
case MessageActionKindDTO.apply_filter: {
if (action.signal) {
void logEvent(AIAssistantEvents.ApplyFilterClicked, {
threadId,
messageId,
targetModule: targetModuleForSignal(action.signal),
});
}
applyFilter(action, {
history,
pathname,

View File

@@ -5,17 +5,13 @@ import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
import { toast } from '@signozhq/ui/sonner';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import type { UploadFile } from 'antd';
import getSessionStorage from 'api/browser/sessionstorage/get';
import setSessionStorage from 'api/browser/sessionstorage/set';
import {
getListRulesQueryKey,
useListRules,
} from 'api/generated/services/rules';
import type { ListRules200 } from 'api/generated/services/sigNoz.schemas';
import logEvent from 'api/common/logEvent';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import { useQueryService } from 'hooks/useQueryService';
@@ -26,8 +22,6 @@ import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { AIAssistantEvents, getBrowserInfo } from '../../events';
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
import { useSpeechRecognition } from '../../hooks/useSpeechRecognition';
import { MessageAttachment } from '../../types';
import { MessageContext } from '../../../../api/ai-assistant/chat';
@@ -143,8 +137,6 @@ function autoContextCategory(ctx: MessageContext): string {
const MAX_INPUT_LENGTH = 20000;
const WARNING_THRESHOLD = 15000;
const HOME_SERVICES_INTERVAL = 30 * 60 * 1000;
/** sessionStorage key for the "voice input failed this tab" flag. */
const VOICE_UNAVAILABLE_KEY = 'ai-assistant-voice-unavailable';
const CONTEXT_CATEGORIES = ['Dashboards', 'Alerts', 'Services'] as const;
@@ -376,28 +368,6 @@ export default function ChatInput({
// ── Voice input ────────────────────────────────────────────────────────────
const analyticsCtx = useAIAssistantAnalyticsContext();
// Captured at the start of a voice session, consumed when it ends.
// Tracks both the trigger (button vs. PTT shortcut) and the wall-clock
// start time so we can attribute `durationMs` on the Voice input used
// event regardless of which control ended the session.
const voiceStartedAtRef = useRef<number | null>(null);
const voiceSourceRef = useRef<'button' | 'shortcut' | null>(null);
// Set to true after a `network`, `not-allowed`, or `not-supported` failure
// so we hide the mic button for the rest of the tab session — silent
// retries don't help, and Chromium derivatives without the Google Speech
// API key always fail with `network` no matter how many times the user
// clicks. Persisted to sessionStorage so a page reload doesn't surface the
// button again (closing the tab still resets, in case the user fixed
// permissions or switched browsers).
const [voiceUnavailable, setVoiceUnavailable] = useState(
() => getSessionStorage(VOICE_UNAVAILABLE_KEY) === 'true',
);
const markVoiceUnavailable = useCallback((): void => {
setVoiceUnavailable(true);
setSessionStorage(VOICE_UNAVAILABLE_KEY, 'true');
}, []);
const {
isListening,
isSupported,
@@ -418,81 +388,9 @@ export default function ChatInput({
setText(capText(committedTextRef.current + separator + transcriptText));
}
},
onError: (error) => {
// Guard against double-fire: Chrome can fire `onerror` more than
// once per session when `continuous = true` (it retries internally
// before giving up). Only fire the analytics event for the first
// error in a given session — voiceSourceRef being null means we've
// already handled it.
const source = voiceSourceRef.current;
if (source === null) {
return;
}
voiceStartedAtRef.current = null;
voiceSourceRef.current = null;
void logEvent(AIAssistantEvents.VoiceInputFailed, {
...analyticsCtx,
...getBrowserInfo(),
source,
errorType: error,
});
if (error === 'network') {
markVoiceUnavailable();
toast.error('Voice input unavailable in this browser', {
description:
'This browser cannot reach the speech recognition service. Try Google Chrome or Microsoft Edge.',
});
} else if (error === 'not-allowed') {
markVoiceUnavailable();
toast.error('Microphone access denied', {
description:
'Grant microphone permission in your browser settings to use voice input.',
});
} else if (error === 'not-supported') {
markVoiceUnavailable();
toast.error('Voice input is not supported in this browser.');
}
// `no-speech` is benign (just silence) — don't toast or hide.
},
});
const showMic = isSupported && micPermission !== 'denied' && !voiceUnavailable;
const startVoiceInput = useCallback(
(source: 'button' | 'shortcut') => {
// Defense in depth: the button is hidden when `voiceUnavailable` is
// true, but the PTT shortcut listener can still call us. Bailing here
// keeps a single source of truth and prevents repeat `Voice input
// failed` events in the same session.
if (voiceUnavailable) {
return;
}
voiceStartedAtRef.current = Date.now();
voiceSourceRef.current = source;
start();
},
[start, voiceUnavailable],
);
const fireVoiceInputEvent = useCallback(
(outcome: 'sent' | 'discarded') => {
const startedAt = voiceStartedAtRef.current;
const source = voiceSourceRef.current;
voiceStartedAtRef.current = null;
voiceSourceRef.current = null;
if (startedAt === null || source === null) {
return;
}
void logEvent(AIAssistantEvents.VoiceInputUsed, {
...analyticsCtx,
...getBrowserInfo(),
source,
outcome,
durationMs: Date.now() - startedAt,
});
},
[analyticsCtx],
);
const showMic = isSupported && micPermission !== 'denied';
// Stop recording and immediately send whatever is in the textarea.
const handleStopAndSend = useCallback(async () => {
@@ -500,17 +398,15 @@ export default function ChatInput({
committedTextRef.current = capText(text);
// Stop recognition without triggering onTranscript again (would double-append).
discard();
fireVoiceInputEvent('sent');
await handleSend();
}, [text, discard, handleSend, capText, fireVoiceInputEvent]);
}, [text, discard, handleSend, capText]);
// Stop recording and revert the textarea to what it was before voice started.
const handleDiscard = useCallback(() => {
discard();
fireVoiceInputEvent('discarded');
setText(committedTextRef.current);
textareaRef.current?.focus();
}, [discard, fireVoiceInputEvent]);
}, [discard]);
// ── Push-to-talk (Cmd/Ctrl + Shift + Space) ────────────────────────────────
// Hold the combo to record; release Space to submit. We track which key
@@ -519,7 +415,7 @@ export default function ChatInput({
// "session active" ref so a held key only calls `start()` once.
const pttActiveRef = useRef(false);
useEffect(() => {
if (!isSupported || micPermission === 'denied' || voiceUnavailable) {
if (!isSupported || micPermission === 'denied') {
return undefined;
}
@@ -536,7 +432,7 @@ export default function ChatInput({
return; // ignore auto-repeat
}
pttActiveRef.current = true;
startVoiceInput('shortcut');
start();
};
const handleKeyUp = (e: KeyboardEvent): void => {
@@ -570,10 +466,9 @@ export default function ChatInput({
}, [
isSupported,
micPermission,
voiceUnavailable,
disabled,
isStreaming,
startVoiceInput,
start,
handleStopAndSend,
]);
@@ -1008,7 +903,7 @@ export default function ChatInput({
<Button
variant="ghost"
size="icon"
onClick={(): void => startVoiceInput('button')}
onClick={start}
disabled={disabled}
aria-label="Start voice input"
className={styles.micBtn}

View File

@@ -9,7 +9,6 @@ import {
SelectItem,
SelectTrigger,
} from '@signozhq/ui/select';
import logEvent from 'api/common/logEvent';
import { ClarificationFieldTypeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import type {
ClarificationEventDTO,
@@ -17,8 +16,6 @@ import type {
} from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { CircleHelp, Send, X } from '@signozhq/icons';
import { AIAssistantEvents } from '../../events';
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
import styles from './ClarificationForm.module.scss';
@@ -47,8 +44,6 @@ export default function ClarificationForm({
const isStreaming = useAIAssistantStore(
(s) => s.streams[conversationId]?.isStreaming ?? false,
);
const { threadId, page, mode } =
useAIAssistantAnalyticsContext(conversationId);
const fields = clarification.fields ?? [];
const initialAnswers = Object.fromEntries(
@@ -65,18 +60,6 @@ export default function ClarificationForm({
const handleSubmit = async (): Promise<void> => {
setSubmitted(true);
// Approximate queryLength as the JSON encoding of the form answers — the
// clarification API doesn't render a single user-visible string, but the
// JSON size is a reasonable stand-in for "how much did the user provide".
const queryLength = JSON.stringify(answers).length;
void logEvent(AIAssistantEvents.MessageSent, {
threadId,
page,
mode,
queryLength,
hasContext: false,
respondingToClarification: true,
});
await submitClarification(
conversationId,
clarification.clarificationId,
@@ -86,10 +69,6 @@ export default function ClarificationForm({
const handleCancel = (): void => {
setCancelled(true);
void logEvent(AIAssistantEvents.CancelClicked, {
threadId,
secondsSinceStart: null,
});
cancelStream(conversationId);
};

View File

@@ -5,9 +5,6 @@ import { Input } from '@signozhq/ui/input';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Plus, Search } from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents } from '../../events';
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
import { Conversation } from '../../types';
import { useVariant } from '../../VariantContext';
@@ -139,17 +136,6 @@ export default function ConversationsList({
const handleSelect = (id: string): void => {
const conv = conversations[id];
// Skip re-selecting the currently active thread — Notion-style click on
// the highlighted row in the history list shouldn't inflate the funnel.
const isReselectingActive = id === activeConversationId;
if (conv?.threadId && !isReselectingActive) {
void logEvent(AIAssistantEvents.ThreadOpenedFromHistory, {
threadId: conv.threadId,
threadAgeDays: Math.floor(
(Date.now() - conv.createdAt) / (24 * 60 * 60 * 1000),
),
});
}
if (conv?.threadId) {
// Always load from backend — refreshes messages and reconnects
// to active execution if the thread is still busy.

View File

@@ -144,7 +144,7 @@ export default function MessageBubble({
)}
{!isUser && message.actions && message.actions.length > 0 && (
<ActionsSection actions={message.actions} messageId={message.id} />
<ActionsSection actions={message.actions} />
)}
</div>
</div>

View File

@@ -8,10 +8,6 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { Check, Copy, RefreshCw, ThumbsDown, ThumbsUp } from '@signozhq/icons';
import { useTimezone } from 'providers/Timezone';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents } from '../../events';
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
import { FeedbackRating, Message } from '../../types';
@@ -58,7 +54,6 @@ export default function MessageFeedback({
const submitMessageFeedback = useAIAssistantStore(
(s) => s.submitMessageFeedback,
);
const { threadId } = useAIAssistantAnalyticsContext();
const { formatTimezoneAdjustedTimestamp } = useTimezone();
@@ -96,21 +91,10 @@ export default function MessageFeedback({
}, [message.createdAt]);
const handleCopy = useCallback((): void => {
void logEvent(AIAssistantEvents.MessageCopied, {
role: message.role,
messageId: message.id,
hadToolCalls: Boolean(message.blocks?.some((b) => b.type === 'tool_call')),
});
copyToClipboard(message.content);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}, [
copyToClipboard,
message.content,
message.id,
message.role,
message.blocks,
]);
}, [copyToClipboard, message.content]);
const handleVote = useCallback(
(rating: FeedbackRating): void => {
@@ -123,31 +107,20 @@ export default function MessageFeedback({
return;
}
setVote(rating);
void logEvent(AIAssistantEvents.FeedbackSubmitted, {
messageId: message.id,
threadId,
rating: 'up',
hasComment: false,
commentLength: 0,
});
submitMessageFeedback(message.id, rating);
},
[vote, message.id, submitMessageFeedback, threadId],
[vote, message.id, submitMessageFeedback],
);
const handleSubmitNegative = useCallback((): void => {
setVote('negative');
setIsNegativeDialogOpen(false);
const trimmed = negativeComment.trim();
void logEvent(AIAssistantEvents.FeedbackSubmitted, {
messageId: message.id,
threadId,
rating: 'down',
hasComment: trimmed.length > 0,
commentLength: trimmed.length,
});
submitMessageFeedback(message.id, 'negative', trimmed || undefined);
}, [message.id, negativeComment, submitMessageFeedback, threadId]);
submitMessageFeedback(
message.id,
'negative',
negativeComment.trim() || undefined,
);
}, [message.id, negativeComment, submitMessageFeedback]);
return (
<>

View File

@@ -4,9 +4,6 @@ import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Check, Copy } from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents } from '../../events';
import { Message } from '../../types';
import styles from './UserMessageActions.module.scss';
@@ -28,15 +25,10 @@ export default function UserMessageActions({
const [, copyToClipboard] = useCopyToClipboard();
const handleCopy = useCallback((): void => {
void logEvent(AIAssistantEvents.MessageCopied, {
role: message.role,
messageId: message.id,
hadToolCalls: false,
});
copyToClipboard(message.content);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}, [copyToClipboard, message.content, message.id, message.role]);
}, [copyToClipboard, message.content]);
return (
<div className={styles.actions}>

View File

@@ -10,10 +10,6 @@ import {
Sparkles,
} from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents, SuggestedPromptCategory } from '../../events';
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
import { Message, StreamingEventItem } from '../../types';
import MessageBubble from '../MessageBubble';
@@ -50,24 +46,17 @@ interface VirtualizedMessagesProps {
conversationId: string;
messages: Message[];
isStreaming: boolean;
/**
* Called when a user clicks an empty-state suggested prompt. Routed
* through the parent so analytics (Message sent) fire with the same
* page/mode/context attribution as a normal send.
*/
onSendSuggestedPrompt: (text: string) => void;
}
export default function VirtualizedMessages({
conversationId,
messages,
isStreaming,
onSendSuggestedPrompt,
}: VirtualizedMessagesProps): JSX.Element {
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
const regenerateAssistantMessage = useAIAssistantStore(
(s) => s.regenerateAssistantMessage,
);
const { threadId } = useAIAssistantAnalyticsContext(conversationId);
const streamingStatus = useAIAssistantStore(
(s) => s.streams[conversationId]?.streamingStatus ?? '',
);
@@ -96,13 +85,9 @@ export default function VirtualizedMessages({
if (isStreaming) {
return;
}
void logEvent(AIAssistantEvents.RegenerateClicked, {
messageId,
threadId,
});
void regenerateAssistantMessage(conversationId, messageId);
},
[conversationId, isStreaming, regenerateAssistantMessage, threadId],
[conversationId, isStreaming, regenerateAssistantMessage],
);
// Scroll all the way to the actual bottom — including the 64px of bottom
@@ -161,11 +146,7 @@ export default function VirtualizedMessages({
color="secondary"
className={styles.emptyChip}
onClick={(): void => {
void logEvent(AIAssistantEvents.SuggestedPromptClicked, {
promptId: s.text,
category: SuggestedPromptCategory.EmptyState,
});
onSendSuggestedPrompt(s.text);
sendMessage(s.text);
}}
prefix={<s.icon size={14} />}
>

View File

@@ -1,10 +1,7 @@
import cx from 'classnames';
import { Button } from '@signozhq/ui/button';
import logEvent from 'api/common/logEvent';
import { Check, X } from '@signozhq/icons';
import { AIAssistantEvents } from '../../../events';
import { useAIAssistantAnalyticsContext } from '../../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../../store/useAIAssistantStore';
import { useMessageContext } from '../../MessageContext';
@@ -40,7 +37,6 @@ export default function ConfirmBlock({
const answeredBlocks = useAIAssistantStore((s) => s.answeredBlocks);
const markBlockAnswered = useAIAssistantStore((s) => s.markBlockAnswered);
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
const { threadId, page, mode } = useAIAssistantAnalyticsContext();
// Durable answered state — survives re-renders/remounts
const answeredChoice = messageId ? answeredBlocks[messageId] : undefined;
@@ -51,14 +47,6 @@ export default function ConfirmBlock({
if (messageId) {
markBlockAnswered(messageId, choice);
}
void logEvent(AIAssistantEvents.MessageSent, {
threadId,
page,
mode,
queryLength: responseText.length,
hasContext: false,
respondingToClarification: false,
});
sendMessage(responseText);
};

View File

@@ -1,11 +1,8 @@
import { useState } from 'react';
import cx from 'classnames';
import { Button } from '@signozhq/ui/button';
import logEvent from 'api/common/logEvent';
import { Checkbox, Radio } from 'antd';
import { AIAssistantEvents } from '../../../events';
import { useAIAssistantAnalyticsContext } from '../../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../../store/useAIAssistantStore';
import { useMessageContext } from '../../MessageContext';
@@ -39,7 +36,6 @@ export default function InteractiveQuestion({
const answeredBlocks = useAIAssistantStore((s) => s.answeredBlocks);
const markBlockAnswered = useAIAssistantStore((s) => s.markBlockAnswered);
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
const { threadId, page, mode } = useAIAssistantAnalyticsContext();
// Persist selected state locally only for the pending (not-yet-submitted) case
const [selected, setSelected] = useState<string[]>([]);
@@ -56,14 +52,6 @@ export default function InteractiveQuestion({
if (messageId) {
markBlockAnswered(messageId, answer);
}
void logEvent(AIAssistantEvents.MessageSent, {
threadId,
page,
mode,
queryLength: answer.length,
hasContext: false,
respondingToClarification: false,
});
sendMessage(answer);
};

View File

@@ -1,81 +0,0 @@
/**
* Analytics event names for the AI Assistant feature. Backend-emitted events
* (Execution finished, Approval resolved, Resource mutated, Clarification
* requested, Limit hit) are not declared here — they fire from the AI service.
*/
export interface BrowserInfo {
browserName: string;
browserVersion: string;
}
type NavigatorWithBrandHints = Navigator & {
userAgentData?: { brands: { brand: string; version: string }[] };
brave?: { isBrave: () => Promise<boolean> };
};
/**
* We mainly need to distinguish Chrome / Edge (Speech API works) from Chromium
* derivatives (no Google API key → voice fails with `network`). UA sniffing is
* the source of truth for derivative identification; `userAgentData` is used
* only as a fast happy path for Chrome / Edge. Brave needs its own probe — it
* advertises Chrome in both UA and brand hints.
*/
export function getBrowserInfo(): BrowserInfo {
if (typeof navigator === 'undefined') {
return { browserName: 'unknown', browserVersion: 'unknown' };
}
const nav = navigator as NavigatorWithBrandHints;
const ua = nav.userAgent;
// Order matters: derivatives put "Chrome" in their UA; Chrome puts "Safari".
const matchers: { name: string; re: RegExp }[] = [
{ name: 'Edge', re: /Edg(?:e|A|iOS)?\/([\d.]+)/ },
{ name: 'Opera', re: /OPR\/([\d.]+)/ },
{ name: 'Vivaldi', re: /Vivaldi\/([\d.]+)/ },
{ name: 'Chrome', re: /Chrome\/([\d.]+)/ },
{ name: 'Firefox', re: /Firefox\/([\d.]+)/ },
{ name: 'Safari', re: /Version\/([\d.]+).*Safari/ },
];
let browserName = 'unknown';
let browserVersion = 'unknown';
for (const { name, re } of matchers) {
const m = ua.match(re);
if (m) {
browserName = name;
browserVersion = m[1];
break;
}
}
// Brave hides as Chrome in UA + brand hints; its probe is the only tell.
if (nav.brave?.isBrave) {
browserName = 'Brave';
}
return { browserName, browserVersion };
}
export const SuggestedPromptCategory = {
FollowUp: 'follow_up',
EmptyState: 'empty_state',
} as const;
export type SuggestedPromptCategory =
(typeof SuggestedPromptCategory)[keyof typeof SuggestedPromptCategory];
export enum AIAssistantEvents {
Opened = 'AI Assistant: Opened',
MessageSent = 'AI Assistant: Message sent',
SuggestedPromptClicked = 'AI Assistant: Suggested prompt clicked',
CancelClicked = 'AI Assistant: Cancel clicked',
RegenerateClicked = 'AI Assistant: Regenerate clicked',
MessageCopied = 'AI Assistant: Message copied',
FeedbackSubmitted = 'AI Assistant: Feedback submitted',
ResourceOpened = 'AI Assistant: Resource opened',
DocOpened = 'AI Assistant: Doc opened',
ApplyFilterClicked = 'AI Assistant: Apply filter clicked',
ThreadOpenedFromHistory = 'AI Assistant: Thread opened from history',
VoiceInputUsed = 'AI Assistant: Voice input used',
VoiceInputFailed = 'AI Assistant: Voice input failed',
NewChatClicked = 'AI Assistant: New chat clicked',
}

View File

@@ -1,60 +0,0 @@
import { matchPath, useLocation } from 'react-router-dom';
import ROUTES from 'constants/routes';
import { useAIAssistantStore } from '../store/useAIAssistantStore';
import { useVariant } from '../VariantContext';
export interface AIAssistantAnalyticsContext {
/** Backend thread ID for the resolved conversation; undefined before the first send. */
threadId: string | undefined;
/**
* Normalised route template for the current page (e.g. `/dashboard/:dashboardId`).
* Falls back to the raw pathname for routes not in ROUTES. We normalise to keep
* analytics cardinality bounded and avoid leaking customer identifiers
* (dashboard IDs, service names, trace IDs, conversation IDs) into the event.
*/
page: string;
/** Surface the assistant is rendered on. `panel` / `modal` collapse to `sidepane`. */
mode: 'sidepane' | 'full_screen';
}
// Pre-sorted longest-first so more specific templates match before their
// less specific siblings (e.g. `/services/:s/top-level-operations` wins
// over `/services/:s`). Module-level — ROUTES is static.
const ROUTE_TEMPLATES = Object.values(ROUTES).sort(
(a, b) => b.length - a.length,
);
export function normalizePage(pathname: string): string {
for (const template of ROUTE_TEMPLATES) {
if (matchPath(pathname, { path: template, exact: true })) {
return template;
}
}
return pathname;
}
/**
* Shared base attributes for AI Assistant analytics events (Message sent,
* Cancel clicked, Feedback submitted, Resource/Doc/Apply filter, …).
*
* Pass `conversationId` when the caller is scoped to a specific
* conversation (e.g. `ClarificationForm`, `VirtualizedMessages`); omit
* to fall back to the store's active conversation.
*/
export function useAIAssistantAnalyticsContext(
conversationId?: string,
): AIAssistantAnalyticsContext {
const { pathname } = useLocation();
const variant = useVariant();
const threadId = useAIAssistantStore((s) => {
const id = conversationId ?? s.activeConversationId;
return id ? s.conversations[id]?.threadId : undefined;
});
return {
threadId,
page: normalizePage(pathname),
mode: variant === 'page' ? 'full_screen' : 'sidepane',
};
}

View File

@@ -186,40 +186,77 @@
display: flex;
flex-direction: column;
section {
.section-1 {
display: flex;
flex-direction: column;
align-items: start;
border-bottom: 1px solid var(--l1-border);
.ant-btn {
display: flex;
width: 100%;
height: unset;
padding: 8px;
height: 20px;
padding: 16px 18px 18px 14px;
align-items: center;
gap: 12px;
gap: 6px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
.ant-btn-icon {
margin-inline-end: 0px;
}
}
}
.section-2 {
display: flex;
flex-direction: column;
align-items: start;
border-bottom: 1px solid var(--l1-border);
.ant-btn {
display: flex;
width: 100%;
height: 20px;
padding: 16px 18px 18px 14px;
align-items: center;
gap: 6px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
border-top: none;
.ant-btn-icon {
margin-inline-end: 0px;
}
}
}
.delete-dashboard {
display: flex;
flex-direction: column;
align-items: start;
.section-1,
.section-2 {
border-bottom: 1px solid var(--l1-border);
}
.delete-dashboard .ant-btn {
color: var(--bg-cherry-400) !important;
.ant-typography {
display: flex;
width: 100%;
height: 20px;
padding: 16px 18px 18px 14px;
align-items: center;
gap: 6px;
color: var(--bg-cherry-400) !important;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
}
}
}
}

View File

@@ -211,12 +211,7 @@
display: grid;
grid-template-columns: max-content 1fr;
.typography-variables {
display: block;
}
.default-value-description {
display: block;
color: var(--l2-foreground);
font-family: Inter;
font-size: 11px;

View File

@@ -11,7 +11,7 @@ import {
SyncTooltipFilterMode,
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import { isEqual } from 'lodash-es';
import { Check, ExternalLink, SolidInfoCircle, X } from '@signozhq/icons';
import { Check, ExternalLink, Info, X } from '@signozhq/icons';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import styles from './GeneralSettings.module.scss';
@@ -201,7 +201,7 @@ function GeneralDashboardSettings(): JSX.Element {
placement="top"
mouseEnterDelay={0.5}
>
<SolidInfoCircle size="md" className={styles.crossPanelSyncInfoIcon} />
<Info size={14} className={styles.crossPanelSyncInfoIcon} />
</Tooltip>
</div>
<div className={styles.crossPanelSyncRow}>

View File

@@ -3,13 +3,14 @@ import {
Checkbox,
Collapse,
Form,
InputNumber,
InputNumberProps,
Select,
SelectProps,
Space,
} from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type { DefaultOptionType } from 'antd/es/select';
import InputNumber from 'components/InputNumber';
import {
getCategoryByOptionId,
getCategorySelectOptionByName,
@@ -288,7 +289,7 @@ function RuleOptions({
</Form.Item>
);
const onChange = (value: number | null): void => {
const onChange: InputNumberProps['onChange'] = (value): void => {
setAlertDef({
...alertDef,
condition: {
@@ -390,9 +391,11 @@ function RuleOptions({
<Space direction="horizontal" align="center">
<Form.Item noStyle>
<InputNumber
prefix={t('field_threshold')}
addonBefore={t('field_threshold')}
value={alertDef?.condition?.target}
onChange={onChange}
type="number"
onWheel={(e): void => e.currentTarget.blur()}
/>
</Form.Item>
@@ -452,6 +455,8 @@ function RuleOptions({
},
});
}}
type="number"
onWheel={(e): void => e.currentTarget.blur()}
/>
</Form.Item>
<Typography.Text>{t('text_for')}</Typography.Text>
@@ -489,6 +494,8 @@ function RuleOptions({
},
});
}}
type="number"
onWheel={(e): void => e.currentTarget.blur()}
/>
</Form.Item>
<Typography.Text>{t('text_num_points')}</Typography.Text>

View File

@@ -5,8 +5,7 @@ import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { BellDot, CircleAlert, ExternalLink, Save } from '@signozhq/icons';
import { Button, FormInstance, SelectProps } from 'antd';
import { ConfirmDialog } from '@signozhq/ui/dialog';
import { Button, FormInstance, Modal, SelectProps } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
@@ -163,7 +162,6 @@ function FormAlertRules({
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
const [detectionMethod, setDetectionMethod] = useState<string | null>(null);
const [isConfirmSaveOpen, setIsConfirmSaveOpen] = useState(false);
useEffect(() => {
if (!isEqual(currentQuery.unit, yAxisUnit)) {
@@ -579,16 +577,19 @@ function FormAlertRules({
});
// invalidate rule in cache
await ruleCache.invalidateQueries([
ruleCache.invalidateQueries([
REACT_QUERY_KEY.ALERT_RULE_DETAILS,
`${ruleId}`,
]);
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
// eslint-disable-next-line sonarjs/no-identical-functions
setTimeout(() => {
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, 2000);
} catch (e) {
const apiError = convertToApiError(e as AxiosError<RenderErrorResponseDTO>);
logData = {
@@ -624,9 +625,24 @@ function FormAlertRules({
urlQuery,
]);
const onSaveHandler = useCallback(() => {
setIsConfirmSaveOpen(true);
}, []);
const onSaveHandler = useCallback(async () => {
const content = (
<Typography.Text>
{' '}
{t('confirm_save_content_part1')}{' '}
<QueryTypeTag queryType={currentQuery.queryType} />{' '}
{t('confirm_save_content_part2')}
</Typography.Text>
);
Modal.confirm({
icon: <CircleAlert size="md" />,
title: t('confirm_save_title'),
centered: true,
content,
onOk: saveRule,
className: 'create-alert-modal',
});
}, [t, saveRule, currentQuery]);
const onTestRuleHandler = useCallback(async () => {
if (!isFormValid()) {
@@ -972,27 +988,6 @@ function FormAlertRules({
</ButtonContainer>
</MainFormContainer>
</div>
<ConfirmDialog
open={isConfirmSaveOpen}
onOpenChange={setIsConfirmSaveOpen}
title={t('confirm_save_title')}
titleIcon={<CircleAlert size={14} />}
confirmText="OK"
confirmColor="primary"
onConfirm={async (): Promise<boolean> => {
await saveRule();
return true;
}}
onCancel={() => setIsConfirmSaveOpen(false)}
width="narrow"
>
<Typography.Text>
{t('confirm_save_content_part1')}{' '}
<QueryTypeTag queryType={currentQuery.queryType} />{' '}
{t('confirm_save_content_part2')}
</Typography.Text>
</ConfirmDialog>
</>
);
}

View File

@@ -26,13 +26,10 @@
gap: 8px;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.widget-header-title {
max-width: 80%;
min-width: 0;
}
.widget-header-actions {

View File

@@ -11,6 +11,7 @@ import {
DatePicker,
Form,
Input,
InputNumber,
Modal,
Row,
Select,
@@ -22,7 +23,6 @@ import {
Tooltip,
} from 'antd';
import { Typography } from '@signozhq/ui/typography';
import InputNumber from 'components/InputNumber';
import type { NotificationInstance } from 'antd/es/notification/interface';
import type { CollapseProps } from 'antd/lib';
import {
@@ -438,7 +438,9 @@ function MultiIngestionSettings(): JSX.Element {
data: {
name: values.name,
tags: updatedTags,
expires_at: dayjs(values.expires_at).endOf('day').toISOString(),
expires_at: new Date(
dayjs(values.expires_at).endOf('day').toISOString(),
),
},
},
{
@@ -469,11 +471,13 @@ function MultiIngestionSettings(): JSX.Element {
const requestPayload = {
name: values.name,
tags: updatedTags,
expires_at: dayjs(values.expires_at).endOf('day').toISOString(),
expires_at: new Date(dayjs(values.expires_at).endOf('day').toISOString()),
};
createIngestionKey(
{ data: requestPayload },
{
data: requestPayload,
},
{
onSuccess: (_data) => {
notifications.success({
@@ -1212,7 +1216,7 @@ function MultiIngestionSettings(): JSX.Element {
<Form.Item name="dailyLimit" key="dailyLimit">
<InputNumber
disabled={!activeSignal?.config?.day?.enabled}
suffix={
addonAfter={
<Select defaultValue="GiB" disabled>
<Option value="TiB">TiB</Option>
<Option value="GiB">GiB</Option>
@@ -1235,7 +1239,7 @@ function MultiIngestionSettings(): JSX.Element {
<Form.Item name="dailyCount" key="dailyCount">
<InputNumber
placeholder="Enter max # of samples/day"
suffix={
addonAfter={
<Form.Item
name="dailyCountUnit"
noStyle
@@ -1302,7 +1306,7 @@ function MultiIngestionSettings(): JSX.Element {
<Form.Item name="secondsLimit" key="secondsLimit">
<InputNumber
disabled={!activeSignal?.config?.second?.enabled}
suffix={
addonAfter={
<Select defaultValue="GiB" disabled>
<Option value="TiB">TiB</Option>
<Option value="GiB">GiB</Option>
@@ -1325,7 +1329,7 @@ function MultiIngestionSettings(): JSX.Element {
<Form.Item name="secondsCount" key="secondsCount">
<InputNumber
placeholder="Enter max # of samples/s"
suffix={
addonAfter={
<Form.Item
name="secondsCountUnit"
noStyle

View File

@@ -79,12 +79,12 @@ describe('MultiIngestionSettings Page', () => {
keys: [
{
name: 'Key One',
expires_at: TEST_EXPIRES_AT,
expires_at: new Date(TEST_EXPIRES_AT),
value: 'secret',
workspace_id: TEST_WORKSPACE_ID,
id: 'k1',
created_at: TEST_CREATED_UPDATED,
updated_at: TEST_CREATED_UPDATED,
created_at: new Date(TEST_CREATED_UPDATED),
updated_at: new Date(TEST_CREATED_UPDATED),
tags: [],
limits: [
{
@@ -160,12 +160,12 @@ describe('MultiIngestionSettings Page', () => {
keys: [
{
name: 'Key Logs',
expires_at: TEST_EXPIRES_AT,
expires_at: new Date(TEST_EXPIRES_AT),
value: 'secret',
workspace_id: TEST_WORKSPACE_ID,
id: 'k2',
created_at: TEST_CREATED_UPDATED,
updated_at: TEST_CREATED_UPDATED,
created_at: new Date(TEST_CREATED_UPDATED),
updated_at: new Date(TEST_CREATED_UPDATED),
tags: [],
limits: [
{
@@ -238,12 +238,12 @@ describe('MultiIngestionSettings Page', () => {
keys: [
{
name: KEY_NAME,
expires_at: TEST_EXPIRES_AT,
expires_at: new Date(TEST_EXPIRES_AT),
value: 'secret',
workspace_id: TEST_WORKSPACE_ID,
id: 'k1',
created_at: TEST_CREATED_UPDATED,
updated_at: TEST_CREATED_UPDATED,
created_at: new Date(TEST_CREATED_UPDATED),
updated_at: new Date(TEST_CREATED_UPDATED),
tags: [],
limits: [
{
@@ -299,12 +299,12 @@ describe('MultiIngestionSettings Page', () => {
keys: [
{
name: 'Key Regular',
expires_at: TEST_EXPIRES_AT,
expires_at: new Date(TEST_EXPIRES_AT),
value: 'secret1',
workspace_id: TEST_WORKSPACE_ID,
id: 'k1',
created_at: TEST_CREATED_UPDATED,
updated_at: TEST_CREATED_UPDATED,
created_at: new Date(TEST_CREATED_UPDATED),
updated_at: new Date(TEST_CREATED_UPDATED),
tags: [],
limits: [],
},
@@ -319,12 +319,12 @@ describe('MultiIngestionSettings Page', () => {
keys: [
{
name: 'Key Search Result',
expires_at: TEST_EXPIRES_AT,
expires_at: new Date(TEST_EXPIRES_AT),
value: 'secret2',
workspace_id: TEST_WORKSPACE_ID,
id: 'k2',
created_at: TEST_CREATED_UPDATED,
updated_at: TEST_CREATED_UPDATED,
created_at: new Date(TEST_CREATED_UPDATED),
updated_at: new Date(TEST_CREATED_UPDATED),
tags: [],
limits: [],
},

View File

@@ -13,9 +13,9 @@ describe('filterAlerts', () => {
const mockAlertBase: Partial<RuletypesRuleDTO> = {
state: 'active' as RuletypesAlertStateDTO,
disabled: false,
createdAt: '2024-01-01T00:00:00Z',
createdAt: new Date('2024-01-01T00:00:00Z'),
createdBy: 'test-user',
updatedAt: '2024-01-01T00:00:00Z',
updatedAt: new Date('2024-01-01T00:00:00Z'),
updatedBy: 'test-user',
version: '1',
condition: {

View File

@@ -1,36 +0,0 @@
.actionContent {
display: flex;
flex-direction: column;
}
.actionBtn {
display: flex;
padding: 8px;
height: unset;
align-items: center;
gap: 6px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
:global(.ant-icon-btn) {
margin-inline-end: 0px;
}
}
.deleteBtn {
composes: actionBtn;
color: var(--danger-background) !important;
border-top: 1px solid var(--l1-border);
}
.deleteBtn:hover {
background-color: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
}
.deleteModal :global(.ant-modal-confirm-body) {
align-items: center;
}

View File

@@ -745,6 +745,52 @@
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
padding: 0px;
.dashboard-action-content {
.section-1 {
display: flex;
flex-direction: column;
.action-btn {
display: flex;
padding: 8px;
height: unset;
align-items: center;
gap: 6px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
.ant-icon-btn {
margin-inline-end: 0px;
}
}
}
.section-2 {
display: flex;
flex-direction: column;
border-top: 1px solid var(--l1-border);
.ant-typography {
display: flex;
padding: 12px 8px;
align-items: center;
gap: 6px;
color: var(--bg-cherry-400) !important;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
}
}
}
}
}

View File

@@ -102,7 +102,6 @@ import {
filterDashboards,
} from './utils';
import styles from './DashboardActions.module.scss';
import './DashboardList.styles.scss';
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -437,53 +436,57 @@ function DashboardsList(): JSX.Element {
{action && (
<Popover
content={
<div className={styles.actionContent}>
<Button
type="text"
className={styles.actionBtn}
icon={<Expand size={12} />}
onClick={onClickHandler}
>
View
</Button>
<Button
type="text"
className={styles.actionBtn}
icon={<SquareArrowOutUpRight size={12} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
openInNewTab(getLink());
}}
>
Open in New Tab
</Button>
<Button
type="text"
className={styles.actionBtn}
icon={<Link2 size={12} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setCopy(getAbsoluteUrl(getLink()));
}}
>
Copy Link
</Button>
<Button
type="text"
className={styles.actionBtn}
icon={<FileJson size={12} />}
onClick={handleJsonExport}
>
Export JSON
</Button>
<DeleteButton
name={dashboard.name}
id={dashboard.id}
isLocked={dashboard.isLocked}
createdBy={dashboard.createdBy}
/>
<div className="dashboard-action-content">
<section className="section-1">
<Button
type="text"
className="action-btn"
icon={<Expand size={12} />}
onClick={onClickHandler}
>
View
</Button>
<Button
type="text"
className="action-btn"
icon={<SquareArrowOutUpRight size={12} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
openInNewTab(getLink());
}}
>
Open in New Tab
</Button>
<Button
type="text"
className="action-btn"
icon={<Link2 size={12} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setCopy(getAbsoluteUrl(getLink()));
}}
>
Copy Link
</Button>
<Button
type="text"
className="action-btn"
icon={<FileJson size={12} />}
onClick={handleJsonExport}
>
Export JSON
</Button>
</section>
<section className="section-2">
<DeleteButton
name={dashboard.name}
id={dashboard.id}
isLocked={dashboard.isLocked}
createdBy={dashboard.createdBy}
/>
</section>
</div>
}
placement="bottomRight"

View File

@@ -0,0 +1,9 @@
.delete-modal {
.ant-modal-confirm-body {
align-items: center;
}
}
.delete-btn:hover {
background-color: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
}

View File

@@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import { CircleAlert, Trash2 } from '@signozhq/icons';
import { Button, Modal, Tooltip } from 'antd';
import { Flex, Modal, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
@@ -12,8 +12,10 @@ import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
import styles from '../DashboardActions.module.scss';
import { Data } from '../DashboardsList';
import { TableLinkText } from './styles';
import './DeleteButton.styles.scss';
interface DeleteButtonProps {
createdBy: string;
@@ -83,7 +85,7 @@ export function DeleteButton({
},
},
centered: true,
className: styles.deleteModal,
className: 'delete-modal',
});
}, [
modal,
@@ -107,16 +109,10 @@ export function DeleteButton({
return '';
};
const isDisabled = isLocked || (user.role === USER_ROLES.VIEWER && !isAuthor);
return (
<>
<Tooltip placement="left" title={getDeleteTooltipContent()}>
<Button
type="text"
className={styles.deleteBtn}
icon={<Trash2 size={12} />}
disabled={isDisabled}
<TableLinkText
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
@@ -124,9 +120,13 @@ export function DeleteButton({
openConfirmationDialog();
}
}}
className="delete-btn"
disabled={isLocked || (user.role === USER_ROLES.VIEWER && !isAuthor)}
>
Delete Dashboard
</Button>
<Flex align="center" justify="center" gap={4}>
<Trash2 size={14} /> Delete dashboard
</Flex>
</TableLinkText>
</Tooltip>
{contextHolder}

View File

@@ -0,0 +1,8 @@
import styled from 'styled-components';
export const TableLinkText = styled.span<{ disabled: boolean }>`
color: var(--destructive);
cursor: ${({ disabled }): string => (disabled ? 'not-allowed' : 'pointer')};
${({ disabled }): string => (disabled ? 'opacity: 0.5;' : '')}
padding: var(--spacing-3) var(--spacing-4);
`;

View File

@@ -22,9 +22,3 @@
gap: 8px;
}
}
// FieldRenderer is used inside log/trace/metric detail drawers (z-index 1000).
// The design-system tooltip defaults to z-index 50 and would render behind them.
.field-renderer-tooltip-content {
--tooltip-z-index: 1000;
}

View File

@@ -1,5 +1,4 @@
import { Divider } from 'antd';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Divider, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { TagContainer, TagLabel, TagValue } from './FieldRenderer.styles';
@@ -8,10 +7,6 @@ import { getFieldAttributes } from './utils';
import './FieldRenderer.styles.scss';
const TOOLTIP_CONTENT_PROPS = {
className: 'field-renderer-tooltip-content',
};
function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
const { dataType, newField, logType } = getFieldAttributes(field);
@@ -19,16 +14,11 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
<span className="field-renderer-container">
{dataType && newField && logType ? (
<>
<TooltipSimple
title={newField}
side="left"
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
arrow
>
<Tooltip placement="left" title={newField} mouseLeaveDelay={0}>
<Typography.Text truncate={1} className="label">
{newField}{' '}
</Typography.Text>
</TooltipSimple>
</Tooltip>
<div className="tags">
<TagContainer>

View File

@@ -20,7 +20,7 @@ const mockUsers: TypesUserDTO[] = [
displayName: 'Alice Smith',
email: 'alice@signoz.io',
status: 'active',
createdAt: '2024-01-01T00:00:00.000Z',
createdAt: new Date('2024-01-01T00:00:00.000Z'),
orgId: 'org-1',
},
{
@@ -28,7 +28,7 @@ const mockUsers: TypesUserDTO[] = [
displayName: 'Bob Jones',
email: 'bob@signoz.io',
status: 'active',
createdAt: '2024-01-02T00:00:00.000Z',
createdAt: new Date('2024-01-02T00:00:00.000Z'),
orgId: 'org-1',
},
{
@@ -36,7 +36,7 @@ const mockUsers: TypesUserDTO[] = [
displayName: '',
email: 'charlie@signoz.io',
status: 'pending_invite',
createdAt: '2024-01-03T00:00:00.000Z',
createdAt: new Date('2024-01-03T00:00:00.000Z'),
orgId: 'org-1',
},
{
@@ -44,7 +44,7 @@ const mockUsers: TypesUserDTO[] = [
displayName: 'Dave Deleted',
email: 'dave@signoz.io',
status: 'deleted',
createdAt: '2024-01-04T00:00:00.000Z',
createdAt: new Date('2024-01-04T00:00:00.000Z'),
orgId: 'org-1',
},
];

View File

@@ -1,11 +1,8 @@
import { Color } from '@signozhq/design-tokens';
import { Button, Spin } from 'antd';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Button, Spin, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { useGetMetricHighlights } from 'api/generated/services/metrics';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { Info } from '@signozhq/icons';
import { useTimezone } from 'providers/Timezone';
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
import { HighlightsProps } from './types';
@@ -14,10 +11,6 @@ import {
formatTimestampToReadableDate,
} from './utils';
const TOOLTIP_CONTENT_PROPS = {
className: 'metric-highlights-tooltip-content',
};
function Highlights({ metricName }: HighlightsProps): JSX.Element {
const {
data: metricHighlightsData,
@@ -46,13 +39,6 @@ function Highlights({ metricName }: HighlightsProps): JSX.Element {
const lastReceivedText = formatTimestampToReadableDate(
metricHighlights?.lastReceived,
);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const lastReceivedTooltipText = metricHighlights?.lastReceived
? `Last received on ${formatTimezoneAdjustedTimestamp(
metricHighlights.lastReceived,
DATE_TIME_FORMATS.DASH_DATETIME_UTC,
)}`
: 'No data received yet';
if (isErrorMetricHighlights) {
return (
@@ -104,42 +90,27 @@ function Highlights({ metricName }: HighlightsProps): JSX.Element {
className="metric-details-grid-value"
data-testid="metric-highlights-data-points"
>
<TooltipSimple
title={metricHighlights?.dataPoints?.toLocaleString()}
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
arrow
>
<span>
{formatNumberIntoHumanReadableFormat(
metricHighlights?.dataPoints ?? 0,
)}
</span>
</TooltipSimple>
<Tooltip title={metricHighlights?.dataPoints?.toLocaleString()}>
{formatNumberIntoHumanReadableFormat(metricHighlights?.dataPoints ?? 0)}
</Tooltip>
</Typography.Text>
<Typography.Text
className="metric-details-grid-value"
data-testid="metric-highlights-time-series-total"
>
<TooltipSimple
title="Active time series are those that have received data points in the last 1 hour."
side="top"
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
arrow
<Tooltip
title="Active time series are those that have received data points in the last 1
hour."
placement="top"
>
<span>{`${timeSeriesTotal} total ⎯ ${timeSeriesActive} active`}</span>
</TooltipSimple>
</Tooltip>
</Typography.Text>
<Typography.Text
className="metric-details-grid-value"
data-testid="metric-highlights-last-received"
>
<TooltipSimple
title={lastReceivedTooltipText}
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
arrow
>
<span>{lastReceivedText}</span>
</TooltipSimple>
<Tooltip title={lastReceivedText}>{lastReceivedText}</Tooltip>
</Typography.Text>
</>
)}

View File

@@ -510,12 +510,6 @@
color: var(--bg-robin-400) !important;
}
// The MetricDetails Drawer sits at z-index 1000; the design-system tooltip
// defaults to z-index 50 and would otherwise render behind the drawer.
.metric-highlights-tooltip-content {
--tooltip-z-index: 1000;
}
@keyframes fade-in-out {
0% {
opacity: 0;

View File

@@ -1,22 +1,10 @@
import { render, screen } from '@testing-library/react';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import * as metricsExplorerHooks from 'api/generated/services/metrics';
import TimezoneProvider from 'providers/Timezone';
import Highlights from '../Highlights';
import { formatTimestampToReadableDate } from '../utils';
import { getMockMetricHighlightsData, MOCK_METRIC_NAME } from './testUtlls';
function renderHighlights(metricName: string): ReturnType<typeof render> {
return render(
<TimezoneProvider>
<TooltipProvider>
<Highlights metricName={metricName} />
</TooltipProvider>
</TimezoneProvider>,
);
}
const useGetMetricHighlightsMock = jest.spyOn(
metricsExplorerHooks,
'useGetMetricHighlights',
@@ -28,7 +16,7 @@ describe('Highlights', () => {
});
it('should render all highlights data correctly', () => {
renderHighlights(MOCK_METRIC_NAME);
render(<Highlights metricName={MOCK_METRIC_NAME} />);
const dataPoints = screen.getByTestId('metric-highlights-data-points');
const timeSeriesTotal = screen.getByTestId(
@@ -53,7 +41,7 @@ describe('Highlights', () => {
),
);
renderHighlights(MOCK_METRIC_NAME);
render(<Highlights metricName={MOCK_METRIC_NAME} />);
expect(
screen.getByTestId('metric-highlights-error-state'),
@@ -70,7 +58,7 @@ describe('Highlights', () => {
),
);
renderHighlights(MOCK_METRIC_NAME);
render(<Highlights metricName={MOCK_METRIC_NAME} />);
expect(screen.getByText('SAMPLES')).toBeInTheDocument();
expect(screen.getByText('TIME SERIES')).toBeInTheDocument();

View File

@@ -1,8 +1,7 @@
import { Dispatch, SetStateAction } from 'react';
import { Select } from 'antd';
import { InputNumber, Select } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Axis3D, ChartLine, Spline } from '@signozhq/icons';
import InputNumber from 'components/InputNumber';
import SettingsSection from '../../components/SettingsSection/SettingsSection';
@@ -49,6 +48,7 @@ export default function AxesSection({
<section className="container">
<Typography.Text className="text">Soft Min</Typography.Text>
<InputNumber
type="number"
value={softMin}
onChange={softMinHandler}
rootClassName="input"
@@ -58,6 +58,7 @@ export default function AxesSection({
<Typography.Text className="text">Soft Max</Typography.Text>
<InputNumber
value={softMax}
type="number"
rootClassName="input"
onChange={softMaxHandler}
/>

View File

@@ -1,7 +1,6 @@
import { Dispatch, SetStateAction } from 'react';
import { Switch } from 'antd';
import { InputNumber, Switch } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import InputNumber from 'components/InputNumber';
import SettingsSection from '../../components/SettingsSection/SettingsSection';
@@ -32,6 +31,7 @@ export default function HistogramBucketsSection({
</Typography.Text>
<InputNumber
value={bucketCount || null}
type="number"
min={0}
rootClassName="bucket-input"
placeholder="Default: 30"
@@ -44,6 +44,7 @@ export default function HistogramBucketsSection({
</Typography.Text>
<InputNumber
value={bucketWidth || null}
type="number"
precision={2}
placeholder="Default: Auto"
step={0.1}

View File

@@ -1,8 +1,7 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useMemo, useRef, useState } from 'react';
import { useDrag, useDrop, XYCoord } from 'react-dnd';
import { Button, Input, Select, Space } from 'antd';
import InputNumber from 'components/InputNumber';
import { Button, Input, InputNumber, Select, Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import YAxisUnitSelector from 'components/YAxisUnitSelector';
import { Y_AXIS_UNIT_NAMES } from 'components/YAxisUnitSelector/constants';

View File

@@ -1,53 +0,0 @@
import { SolidAlertTriangle } from '@signozhq/icons';
import { ConfirmDialog } from '@signozhq/ui/dialog';
import { Typography } from '@signozhq/ui/typography';
export interface DiscardChangesModalProps {
open: boolean;
isNewPanel: boolean;
panelTitle?: string;
dashboardTitle?: string;
onDiscard: () => void;
onClose: () => void;
}
export default function DiscardChangesModal({
open,
isNewPanel,
panelTitle,
dashboardTitle,
onDiscard,
onClose,
}: DiscardChangesModalProps): JSX.Element {
const dashboardName = dashboardTitle ? (
<>
{' '}
to <strong>{dashboardTitle}</strong>
</>
) : null;
const panelLabel = panelTitle ? <strong>{panelTitle}</strong> : 'this panel';
return (
<ConfirmDialog
open={open}
onOpenChange={(next): void => {
if (!next) {
onClose();
}
}}
title="Discard changes?"
titleIcon={<SolidAlertTriangle size={14} color="#fdd600" />}
confirmText="Discard"
confirmColor="destructive"
cancelText="Keep editing"
onConfirm={onDiscard}
onCancel={onClose}
>
{isNewPanel ? (
<Typography>This new panel won&apos;t be added{dashboardName}.</Typography>
) : (
<Typography>Your unsaved edits to {panelLabel} will be lost.</Typography>
)}
</ConfirmDialog>
);
}

View File

@@ -1,17 +1,13 @@
import {
initialAutocompleteData,
initialQueryBuilderFormValuesMap,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { cloneDeep } from 'lodash-es';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import type { PartialPanelTypes } from '../utils';
import { getIsQueryModified, handleQueryChange } from '../utils';
import { handleQueryChange } from '../utils';
const buildSupersetQuery = (extras?: Record<string, unknown>): Query => ({
queryType: EQueryType.QUERY_BUILDER,
@@ -41,128 +37,6 @@ const buildSupersetQuery = (extras?: Record<string, unknown>): Query => ({
},
});
const buildMetricsQuery = (
overrides?: Partial<{
metricName: string;
aggregateAttributeKey: string;
legend: string;
groupByKey: string;
}>,
): Query => ({
queryType: EQueryType.QUERY_BUILDER,
promql: [],
clickhouse_sql: [],
id: 'query-id',
unit: '',
builder: {
queryFormulas: [],
queryData: [
{
...initialQueryBuilderFormValuesMap[DataSource.METRICS],
queryName: 'A',
aggregateAttribute: overrides?.aggregateAttributeKey
? {
...initialAutocompleteData,
key: overrides.aggregateAttributeKey,
type: 'tag',
dataType: DataTypes.Float64,
}
: cloneDeep(initialAutocompleteData),
aggregations: [
{
metricName: overrides?.metricName ?? 'system.cpu.load',
temporality: '',
timeAggregation: 'rate',
spaceAggregation: 'sum',
reduceTo: 'avg',
} as MetricAggregation,
],
legend: overrides?.legend ?? '',
groupBy: overrides?.groupByKey
? [
{
...initialAutocompleteData,
key: overrides.groupByKey,
type: 'tag',
dataType: DataTypes.String,
},
]
: [],
},
],
queryTraceOperator: [],
},
});
describe('getIsQueryModified', () => {
it('returns false when baseline is null (new unsaved panel with no edits anchor)', () => {
const current = buildMetricsQuery();
expect(getIsQueryModified(current, null)).toBe(false);
});
it('returns false when baseline is undefined', () => {
const current = buildMetricsQuery();
expect(getIsQueryModified(current, undefined)).toBe(false);
});
it('returns false when current only differs by auto-backfilled aggregateAttribute', () => {
// saved widget query: aggregateAttribute is the v5-style empty initial value
// (stripped from persisted spec; spread back in as initialAutocompleteData on load)
const savedQuery = buildMetricsQuery({ metricName: 'system.cpu.load' });
// after MetricNameSelector edit-mode backfill, currentQuery has the populated
// aggregateAttribute while the rest of the query is identical
const currentQuery = buildMetricsQuery({
metricName: 'system.cpu.load',
aggregateAttributeKey: 'system.cpu.load',
});
expect(getIsQueryModified(currentQuery, savedQuery)).toBe(false);
});
it('returns true when the user edits the legend', () => {
const baseline = buildMetricsQuery({ metricName: 'system.cpu.load' });
const edited = buildMetricsQuery({
metricName: 'system.cpu.load',
legend: 'cpu-load',
});
expect(getIsQueryModified(edited, baseline)).toBe(true);
});
it('returns true when the user picks a different metric (aggregations diverges)', () => {
const baseline = buildMetricsQuery({ metricName: 'system.cpu.load' });
const edited = buildMetricsQuery({ metricName: 'system.memory.usage' });
expect(getIsQueryModified(edited, baseline)).toBe(true);
});
it('returns true when the user adds a groupBy', () => {
const baseline = buildMetricsQuery({ metricName: 'system.cpu.load' });
const edited = buildMetricsQuery({
metricName: 'system.cpu.load',
groupByKey: 'host.name',
});
expect(getIsQueryModified(edited, baseline)).toBe(true);
});
it('returns true on existing widget when current diverges from saved (Stage-and-Run silent-loss flow)', () => {
// After Edit → Stage and Run, stagedQuery is reset to match currentQuery.
// The dirty check must compare against the SAVED widget query, not stagedQuery.
const savedQuery = buildMetricsQuery({ metricName: 'system.cpu.load' });
const currentQuery = buildMetricsQuery({ metricName: 'system.memory.usage' });
expect(getIsQueryModified(currentQuery, savedQuery)).toBe(true);
});
it('returns false for a new panel where currentQuery still matches stagedQuery baseline', () => {
const stagedQuery = buildMetricsQuery();
const currentQuery = buildMetricsQuery();
expect(getIsQueryModified(currentQuery, stagedQuery)).toBe(false);
});
it('returns true for a new panel where currentQuery has been edited away from stagedQuery', () => {
const stagedQuery = buildMetricsQuery();
const currentQuery = buildMetricsQuery({ legend: 'custom' });
expect(getIsQueryModified(currentQuery, stagedQuery)).toBe(true);
});
});
describe('handleQueryChange', () => {
it('sets list-specific fields when switching to LIST', () => {
const superset = buildSupersetQuery();

View File

@@ -1,17 +1,18 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { generatePath } from 'react-router-dom';
import { Check, X } from '@signozhq/icons';
import { Check, SolidAlertTriangle, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@signozhq/ui/resizable';
import { Flex } from 'antd';
import { Flex, Modal, Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
@@ -68,6 +69,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { getGraphType, getGraphTypeForFormat } from 'utils/getGraphType';
import LeftContainer from './LeftContainer';
import QueryTypeTag from './LeftContainer/QueryTypeTag';
import RightContainer from './RightContainer';
import { ThresholdProps } from './RightContainer/Threshold/types';
import TimeItems, { timePreferance } from './RightContainer/timeItems';
@@ -80,7 +82,6 @@ import {
placeWidgetAtBottom,
placeWidgetBetweenRows,
} from './utils';
import DiscardChangesModal from './WidgetModals/DiscardChangesModal';
import './NewWidget.styles.scss';
@@ -97,6 +98,8 @@ function NewWidget({
const { dashboardVariables } = useDashboardVariables();
const { t } = useTranslation(['dashboard']);
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const {
@@ -107,6 +110,11 @@ function NewWidget({
setSupersetQuery,
} = useQueryBuilder();
const isQueryModified = useMemo(
() => getIsQueryModified(currentQuery, stagedQuery),
[currentQuery, stagedQuery],
);
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
@@ -131,23 +139,6 @@ function NewWidget({
const query = useUrlQuery();
// For existing widgets, compare currentQuery against the saved widget query
// (stable across Stage-and-Run cycles). For new panels with no saved baseline,
// fall back to stagedQuery so initial edits still trigger the warning.
const savedWidgetQuery = useMemo(() => {
const widgetId = query.get('widgetId');
const match = widgets?.find((w) => w.id === widgetId);
if (!match || match.panelTypes === PANEL_GROUP_TYPES.ROW) {
return null;
}
return (match as Widgets).query ?? null;
}, [widgets, query]);
const isQueryModified = useMemo(
() => getIsQueryModified(currentQuery, savedWidgetQuery ?? stagedQuery),
[currentQuery, savedWidgetQuery, stagedQuery],
);
const [isNewDashboard, setIsNewDashboard] = useState<boolean>(false);
const logEventCalledRef = useRef(false);
@@ -237,6 +228,7 @@ function NewWidget({
Record<string, string>
>(selectedWidget?.customLegendColors || {});
const [saveModal, setSaveModal] = useState(false);
const [discardModal, setDiscardModal] = useState(false);
const [bucketWidth, setBucketWidth] = useState<number>(
@@ -348,6 +340,7 @@ function NewWidget({
]);
const closeModal = (): void => {
setSaveModal(false);
setDiscardModal(false);
};
@@ -600,7 +593,7 @@ function NewWidget({
},
};
return updateDashboardMutation.mutateAsync(dashboard, {
updateDashboardMutation.mutateAsync(dashboard, {
onSuccess: () => {
setToScrollWidgetId(selectedWidget?.id || '');
navigateToDashboardPage();
@@ -695,9 +688,9 @@ function NewWidget({
})),
}),
});
onClickSaveHandler();
setSaveModal(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onClickSaveHandler]);
}, [isNewPanel]);
const isNewTraceLogsAvailable =
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
@@ -958,14 +951,57 @@ function NewWidget({
</ResizablePanel>
</ResizablePanelGroup>
</PanelContainer>
<DiscardChangesModal
<Modal
title={
isQueryModified ? (
<Space>
<SolidAlertTriangle size={16} color="#fdd600" />
Unsaved Changes
</Space>
) : (
'Save Widget'
)
}
focusTriggerAfterClose
forceRender
destroyOnClose
closable
onCancel={closeModal}
onOk={onClickSaveHandler}
confirmLoading={updateDashboardMutation.isLoading}
centered
open={saveModal}
width={600}
>
{!isQueryModified ? (
<Typography>
{t('your_graph_build_with')}{' '}
<QueryTypeTag queryType={currentQuery.queryType} />{' '}
{t('dashboard_ok_confirm')}
</Typography>
) : (
<Typography>{t('dashboard_unsave_changes')} </Typography>
)}
</Modal>
<Modal
title={
<Space>
<SolidAlertTriangle size={16} color="#fdd600" />
Unsaved Changes
</Space>
}
focusTriggerAfterClose
forceRender
destroyOnClose
closable
onCancel={closeModal}
onOk={discardChanges}
centered
open={discardModal}
isNewPanel={isNewPanel}
panelTitle={title}
dashboardTitle={dashboardData?.data?.title}
onDiscard={discardChanges}
onClose={closeModal}
/>
width={600}
>
<Typography>{t('dashboard_unsave_changes')}</Typography>
</Modal>
</Container>
);
}

View File

@@ -1,4 +1,5 @@
import { Layout } from 'react-grid-layout';
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
import { PrecisionOptionsEnum } from 'components/Graph/types';
import { YAxisCategoryNames } from 'components/YAxisUnitSelector/constants';
import {
@@ -25,84 +26,16 @@ import { DataSource } from 'types/common/queryBuilder';
import { getCategoryName } from './RightContainer/dataFormatCategories';
// Asks "would saving the current panel change the persisted widget spec?".
//
// `adjustQueryForV5` is deliberately not reused here: in addition to stripping
// the legacy v4 fields, it also resurrects them onto each metric
// `aggregations[i]`. That migration step is correct on save but bleeds
// asymmetrically across a comparator — the live query still carries the
// legacy defaults from `initialQueryBuilderFormValuesMap` while a previously
// saved widget had them stripped.
const stripQueryDataForCompare = (
queryData: IBuilderQuery,
): Record<string, unknown> => {
const {
aggregateAttribute: _aggregateAttribute,
aggregateOperator: _aggregateOperator,
timeAggregation: _timeAggregation,
spaceAggregation: _spaceAggregation,
reduceTo: _reduceTo,
filters: _filters,
...retained
} = queryData ?? ({} as IBuilderQuery);
const groupBy = (retained.groupBy ?? []).map((entry) => {
const { id: _id, ...rest } = entry;
return rest;
});
return {
...retained,
groupBy,
source: retained.source || '',
};
};
const normalizeForDirtyCheck = (query: Query): Record<string, unknown> => {
const { id: _id, unit, builder, ...rest } = query;
return {
...rest,
// `id` is regenerated on every Stage and Run; `unit` flips between ''
// and undefined depending on whether the user has touched the selector.
unit: unit || '',
builder: {
...builder,
queryData: (builder?.queryData ?? []).map(stripQueryDataForCompare),
},
};
};
// `lodash.isEqual` distinguishes `{a: undefined}` from `{}`; for the dirty
// check those are the same. Initial-values spreads on the live query
// frequently leave such explicit-undefined keys.
const stripUndefined = (value: unknown): unknown => {
if (Array.isArray(value)) {
return value.map(stripUndefined);
}
if (value && typeof value === 'object') {
const out: Record<string, unknown> = {};
Object.entries(value as Record<string, unknown>).forEach(([k, v]) => {
if (v === undefined) {
return;
}
out[k] = stripUndefined(v);
});
return out;
}
return value;
};
export const getIsQueryModified = (
currentQuery: Query,
baselineQuery: Query | null | undefined,
stagedQuery: Query | null,
): boolean => {
if (!baselineQuery) {
if (!stagedQuery) {
return false;
}
return !isEqual(
stripUndefined(normalizeForDirtyCheck(baselineQuery)),
stripUndefined(normalizeForDirtyCheck(currentQuery)),
);
const omitIdFromStageQuery = omitIdFromQuery(stagedQuery);
const omitIdFromCurrentQuery = omitIdFromQuery(currentQuery);
return !isEqual(omitIdFromStageQuery, omitIdFromCurrentQuery);
};
export type PartialPanelTypes = {

View File

@@ -14,7 +14,12 @@ function MaxLinesField({ config }: MaxLinesFieldProps): JSX.Element | null {
return (
<MaxLinesFieldWrapper>
<FieldTitle>{t('options_menu.maxLines')}</FieldTitle>
<MaxLinesInput value={config.value} onChange={config.onChange} />
<MaxLinesInput
controls
size="small"
value={config.value}
onChange={config.onChange}
/>
</MaxLinesFieldWrapper>
);
}

View File

@@ -1,4 +1,4 @@
import InputNumber from 'components/InputNumber';
import { InputNumber } from 'antd';
import styled from 'styled-components';
export const MaxLinesFieldWrapper = styled.div`

View File

@@ -1,6 +1,5 @@
import { RadioProps, SelectProps } from 'antd';
import { InputNumberProps, RadioProps, SelectProps } from 'antd';
import { TelemetryFieldKey } from 'api/v5/v5';
import type { InputNumberProps } from 'components/InputNumber';
import { LogViewMode } from 'container/LogsTable';
export enum FontSize {

View File

@@ -9,7 +9,7 @@ import {
useListDowntimeSchedules,
} from 'api/generated/services/downtimeschedules';
import { useListRules } from 'api/generated/services/rules';
import type { AlertmanagertypesPlannedMaintenanceDTO } from 'api/generated/services/sigNoz.schemas';
import type { RuletypesPlannedMaintenanceDTO } from 'api/generated/services/sigNoz.schemas';
import dayjs from 'dayjs';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useNotifications } from 'hooks/useNotifications';
@@ -24,7 +24,7 @@ import { PlannedDowntimeDeleteModal } from './PlannedDowntimeDeleteModal';
import { PlannedDowntimeForm } from './PlannedDowntimeForm';
import { PlannedDowntimeList } from './PlannedDowntimeList';
import {
defaultInitialValues,
defautlInitialValues,
deleteDowntimeHandler,
} from './PlannedDowntimeutils';
@@ -48,8 +48,8 @@ export function PlannedDowntime(): JSX.Element {
const urlQuery = useUrlQuery();
const [initialValues, setInitialValues] =
useState<Partial<AlertmanagertypesPlannedMaintenanceDTO>>(
defaultInitialValues,
useState<Partial<RuletypesPlannedMaintenanceDTO & { editMode: boolean }>>(
defautlInitialValues,
);
const downtimeSchedules = useListDowntimeSchedules();
@@ -148,7 +148,7 @@ export function PlannedDowntime(): JSX.Element {
<Button
type="primary"
onClick={(): void => {
setInitialValues(defaultInitialValues);
setInitialValues({ ...defautlInitialValues, editMode: false });
setIsOpen(true);
setEditMode(false);
form.resetFields();

View File

@@ -20,9 +20,9 @@ import {
updateDowntimeScheduleByID,
} from 'api/generated/services/downtimeschedules';
import type {
AlertmanagertypesPlannedMaintenanceDTO,
AlertmanagertypesPostablePlannedMaintenanceDTO,
AlertmanagertypesRecurrenceDTO,
RuletypesPlannedMaintenanceDTO,
RuletypesPostablePlannedMaintenanceDTO,
RuletypesRecurrenceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
@@ -46,6 +46,8 @@ import { AlertRuleTags } from './PlannedDowntimeList';
import {
getAlertOptionsFromIds,
getDurationInfo,
getEndTime,
handleTimeConversion,
isScheduleRecurring,
recurrenceOptions,
recurrenceOptionWithSubmenu,
@@ -62,28 +64,24 @@ const TIME_FORMAT = DATE_TIME_FORMATS.TIME;
const DATE_FORMAT = DATE_TIME_FORMATS.ORDINAL_DATE;
const ORDINAL_FORMAT = DATE_TIME_FORMATS.ORDINAL_ONLY;
const TZ_OPTIONS: DefaultOptionType[] = ALL_TIME_ZONES.map(
(timezone: string) => ({
label: timezone,
value: timezone,
key: timezone,
}),
);
interface PlannedDowntimeFormData {
name: string;
startTime: dayjs.Dayjs | null;
endTime: dayjs.Dayjs | null;
recurrence?: AlertmanagertypesRecurrenceDTO;
startTime: dayjs.Dayjs | string;
endTime: dayjs.Dayjs | string;
recurrence?: RuletypesRecurrenceDTO | null;
alertRules: DefaultOptionType[];
recurrenceSelect?: AlertmanagertypesRecurrenceDTO;
recurrenceSelect?: RuletypesRecurrenceDTO;
timezone?: string;
}
const customFormat = DATE_TIME_FORMATS.ORDINAL_DATETIME;
interface PlannedDowntimeFormProps {
initialValues: Partial<AlertmanagertypesPlannedMaintenanceDTO>;
initialValues: Partial<
RuletypesPlannedMaintenanceDTO & {
editMode: boolean;
}
>;
alertOptions: DefaultOptionType[];
isError: boolean;
isLoading: boolean;
@@ -91,7 +89,7 @@ interface PlannedDowntimeFormProps {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
refetchAllSchedules: () => void;
isEditMode: boolean;
form: FormInstance;
form: FormInstance<any>;
}
export function PlannedDowntimeForm(
@@ -109,46 +107,66 @@ export function PlannedDowntimeForm(
form,
} = props;
const [selectedTags, setSelectedTags] = React.useState<DefaultOptionType[]>(
[],
);
const [selectedTags, setSelectedTags] = React.useState<
DefaultOptionType | DefaultOptionType[]
>([]);
const alertRuleFormName = 'alertRules';
const [saveLoading, setSaveLoading] = useState(false);
const [durationUnit, setDurationUnit] = useState<string>(
getDurationInfo(initialValues.schedule?.recurrence?.duration)?.unit || 'm',
getDurationInfo(initialValues.schedule?.recurrence?.duration as string)
?.unit || 'm',
);
const [formData, setFormData] = useState<Partial<PlannedDowntimeFormData>>({
timezone: initialValues.schedule?.timezone,
});
const [recurrenceType, setRecurrenceType] = useState<string>(
initialValues.schedule?.recurrence?.repeatType ||
const [recurrenceType, setRecurrenceType] = useState<string | null>(
(initialValues.schedule?.recurrence?.repeatType as string) ||
recurrenceOptions.doesNotRepeat.value,
);
const timezoneInitialValue = !isEmpty(initialValues.schedule?.timezone)
? (initialValues.schedule?.timezone as string)
: undefined;
const { notifications } = useNotifications();
const { showErrorModal } = useErrorModal();
const requiredFieldRule = [{ required: true }];
const datePickerFooter = (mode: any): any =>
mode === 'time' ? (
<span style={{ color: 'gray' }}>Please select the time</span>
) : null;
const saveHandler = useCallback(
const saveHanlder = useCallback(
async (values: PlannedDowntimeFormData) => {
const data: AlertmanagertypesPostablePlannedMaintenanceDTO = {
const shouldKeepLocalTime = !isEditMode;
const data: RuletypesPostablePlannedMaintenanceDTO = {
alertIds: values.alertRules
.map((alert) => alert.value)
.filter((alert) => alert !== undefined) as string[],
name: values.name,
schedule: {
startTime: values.startTime?.format(),
endTime: values.endTime?.format(),
timezone: values.timezone!,
recurrence: values.recurrence,
startTime: new Date(
handleTimeConversion(
values.startTime,
timezoneInitialValue,
values.timezone,
shouldKeepLocalTime,
),
),
timezone: values.timezone as string,
endTime: values.endTime
? new Date(
handleTimeConversion(
values.endTime,
timezoneInitialValue,
values.timezone,
shouldKeepLocalTime,
),
)
: undefined,
recurrence: values.recurrence as RuletypesRecurrenceDTO,
},
};
@@ -180,58 +198,50 @@ export function PlannedDowntimeForm(
notifications,
refetchAllSchedules,
setIsOpen,
timezoneInitialValue,
showErrorModal,
],
);
const onFinish = async (values: PlannedDowntimeFormData): Promise<void> => {
const { recurrence } = values;
const recurrenceData =
!recurrence ||
recurrence.repeatType === recurrenceOptions.doesNotRepeat.value
values?.recurrence?.repeatType === recurrenceOptions.doesNotRepeat.value
? undefined
: {
duration: recurrence.duration
? `${recurrence.duration}${durationUnit}`
: '',
startTime: values.startTime!.format(),
endTime: values.endTime?.format(),
repeatOn: recurrence.repeatOn,
repeatType: recurrence.repeatType,
duration: values.recurrence?.duration
? `${values.recurrence?.duration}${durationUnit}`
: undefined,
endTime: !isEmpty(values.endTime)
? handleTimeConversion(
values.endTime,
timezoneInitialValue,
values.timezone,
!isEditMode,
)
: undefined,
startTime: handleTimeConversion(
values.startTime,
timezoneInitialValue,
values.timezone,
!isEditMode,
),
repeatOn: !values.recurrence?.repeatOn?.length
? undefined
: values.recurrence?.repeatOn,
repeatType: values.recurrence?.repeatType,
};
await saveHandler({
const payloadValues = {
...values,
recurrence: recurrenceData,
});
recurrence: recurrenceData as RuletypesRecurrenceDTO | undefined,
};
await saveHanlder(payloadValues);
};
const handleFormData = (data: Partial<PlannedDowntimeFormData>): void => {
const { startTime, endTime, timezone } = data;
const update: Partial<PlannedDowntimeFormData> = {};
// If the set timezone doesn't match, update it.
if (
startTime &&
timezone &&
startTime.format() !== startTime.tz(timezone, true).format()
) {
update.startTime = startTime.tz(timezone, true);
}
if (
endTime &&
timezone &&
endTime.format() !== endTime.tz(timezone, true).format()
) {
update.endTime = endTime.tz(timezone, true);
}
if (!isEmpty(update)) {
data = { ...data, ...update };
form.setFieldsValue({ ...update });
}
setFormData(data);
};
const formValidationRules = [
{
required: true,
},
];
const handleOk = async (): Promise<void> => {
await form.validateFields().catch(() => {
@@ -239,11 +249,16 @@ export function PlannedDowntimeForm(
});
};
const handleCancel = (): void => setIsOpen(false);
const handleCancel = (): void => {
setIsOpen(false);
};
const handleAlertRulesChange: SelectProps['onChange'] = (_value, options) => {
const handleChange = (
_value: string,
options: DefaultOptionType | DefaultOptionType[],
): void => {
form.setFieldValue(alertRuleFormName, options);
setSelectedTags(Array.isArray(options) ? options : [options]);
setSelectedTags(options);
};
const noTagRenderer: SelectProps['tagRender'] = () => <></>;
@@ -252,51 +267,113 @@ export function PlannedDowntimeForm(
if (!removedTag) {
return;
}
const newTags = selectedTags.filter((tag) => tag.value !== removedTag);
const newTags = selectedTags.filter(
(tag: DefaultOptionType) => tag.value !== removedTag,
);
form.setFieldValue(alertRuleFormName, newTags);
setSelectedTags(newTags);
};
const formattedInitialValues = useMemo((): PlannedDowntimeFormData => {
const { schedule } = initialValues;
const startTime = schedule?.recurrence?.startTime || schedule?.startTime;
const endTime = schedule?.recurrence?.endTime || schedule?.endTime;
return {
const formatedInitialValues = useMemo(() => {
const formData: PlannedDowntimeFormData = {
name: defaultTo(initialValues.name, ''),
alertRules: getAlertOptionsFromIds(
initialValues.alertIds || [],
alertOptions,
),
startTime: startTime ? dayjs(startTime).tz(schedule.timezone) : null,
endTime: endTime ? dayjs(endTime).tz(schedule.timezone) : null,
endTime: getEndTime(initialValues) ? dayjs(getEndTime(initialValues)) : '',
startTime: initialValues.schedule?.startTime
? dayjs(initialValues.schedule?.startTime)
: '',
recurrence: {
...schedule?.recurrence,
repeatType: !isScheduleRecurring(schedule)
...initialValues.schedule?.recurrence,
repeatType: (!isScheduleRecurring(initialValues?.schedule)
? recurrenceOptions.doesNotRepeat.value
: schedule?.recurrence?.repeatType,
duration: getDurationInfo(schedule?.recurrence?.duration)?.value ?? '',
} as AlertmanagertypesRecurrenceDTO,
timezone: schedule?.timezone as string,
: initialValues.schedule?.recurrence
?.repeatType) as RuletypesRecurrenceDTO['repeatType'],
duration: String(
getDurationInfo(initialValues.schedule?.recurrence?.duration as string)
?.value ?? '',
),
} as RuletypesRecurrenceDTO,
timezone: initialValues.schedule?.timezone as string,
};
return formData;
}, [initialValues, alertOptions]);
useEffect(() => {
setSelectedTags(formattedInitialValues.alertRules);
form.setFieldsValue({ ...formattedInitialValues });
}, [form, formattedInitialValues, initialValues]);
setSelectedTags(formatedInitialValues.alertRules);
form.setFieldsValue({ ...formatedInitialValues });
}, [form, formatedInitialValues, initialValues]);
const timeZoneItems: DefaultOptionType[] = ALL_TIME_ZONES.map(
(timezone: string) => ({
label: timezone,
value: timezone,
key: timezone,
}),
);
const getTimezoneFormattedTime = (
time: string | dayjs.Dayjs,
timeZone?: string,
isEditMode?: boolean,
format?: string,
): string => {
if (!time) {
return '';
}
if (!timeZone) {
return dayjs(time).format(format);
}
return dayjs(time).tz(timeZone, isEditMode).format(format);
};
const startTimeText = useMemo((): string => {
const startTime = formData.startTime;
let startTime = formData?.startTime;
if (recurrenceType !== recurrenceOptions.doesNotRepeat.value) {
startTime =
(formData?.recurrence?.startTime
? dayjs(formData.recurrence.startTime).toISOString()
: '') ||
formData?.startTime ||
'';
}
if (!startTime) {
return '';
}
const daysOfWeek = formData.recurrence?.repeatOn;
if (formData.timezone) {
startTime = handleTimeConversion(
startTime,
timezoneInitialValue,
formData?.timezone,
!isEditMode,
);
}
const daysOfWeek = formData?.recurrence?.repeatOn;
const formattedStartTime = startTime.format(TIME_FORMAT);
const formattedStartDate = startTime.format(DATE_FORMAT);
const ordinalFormat = startTime.format(ORDINAL_FORMAT);
const formattedStartTime = getTimezoneFormattedTime(
startTime,
formData.timezone,
!isEditMode,
TIME_FORMAT,
);
const formattedStartDate = getTimezoneFormattedTime(
startTime,
formData.timezone,
!isEditMode,
DATE_FORMAT,
);
const ordinalFormat = getTimezoneFormattedTime(
startTime,
formData.timezone,
!isEditMode,
ORDINAL_FORMAT,
);
const formattedDaysOfWeek = daysOfWeek?.join(', ');
switch (recurrenceType) {
@@ -311,18 +388,49 @@ export function PlannedDowntimeForm(
default:
return `Scheduled for ${formattedStartDate} starting at ${formattedStartTime}.`;
}
}, [formData, recurrenceType, timezone]);
}, [formData, recurrenceType, isEditMode, timezoneInitialValue]);
const endTimeText = useMemo((): string => {
const endTime = formData.endTime;
let endTime = formData?.endTime;
if (recurrenceType !== recurrenceOptions.doesNotRepeat.value) {
endTime =
(formData?.recurrence?.endTime
? dayjs(formData.recurrence.endTime).toISOString()
: '') || '';
if (!isEditMode && !endTime) {
endTime = formData?.endTime || '';
}
}
if (!endTime) {
return '';
}
const formattedEndTime = endTime.format(TIME_FORMAT);
const formattedEndDate = endTime.format(DATE_FORMAT);
if (formData.timezone) {
endTime = handleTimeConversion(
endTime,
timezoneInitialValue,
formData?.timezone,
!isEditMode,
);
}
const formattedEndTime = getTimezoneFormattedTime(
endTime,
formData.timezone,
!isEditMode,
TIME_FORMAT,
);
const formattedEndDate = getTimezoneFormattedTime(
endTime,
formData.timezone,
!isEditMode,
DATE_FORMAT,
);
return `Scheduled to end maintenance on ${formattedEndDate} at ${formattedEndTime}.`;
}, [formData, recurrenceType, timezone]);
}, [formData, recurrenceType, isEditMode, timezoneInitialValue]);
return (
<Modal
@@ -338,28 +446,33 @@ export function PlannedDowntimeForm(
footer={null}
>
<Form<PlannedDowntimeFormData>
name={isEditMode ? 'edit-form' : 'create-form'}
name={initialValues.editMode ? 'edit-form' : 'create-form'}
form={form}
layout="vertical"
className="createForm"
onFinish={onFinish}
onValuesChange={(): void => {
setRecurrenceType(form.getFieldValue('recurrence')?.repeatType as string);
handleFormData(form.getFieldsValue());
setFormData(form.getFieldsValue());
}}
autoComplete="off"
>
<Form.Item label="Name" name="name" rules={requiredFieldRule}>
<Form.Item label="Name" name="name" rules={formValidationRules}>
<Input placeholder="e.g. Upgrade downtime" />
</Form.Item>
<Form.Item
label="Starts from"
name="startTime"
rules={requiredFieldRule}
rules={formValidationRules}
className={!isEmpty(startTimeText) ? 'formItemWithBullet' : ''}
getValueProps={(value): any => ({
value: value ? dayjs(value).tz(timezoneInitialValue) : undefined,
})}
>
<DatePicker
format={(date) => date.format(customFormat)}
format={(date): string =>
dayjs(date).tz(timezoneInitialValue).format(customFormat)
}
showTime
renderExtraFooter={datePickerFooter}
showNow={false}
@@ -372,7 +485,7 @@ export function PlannedDowntimeForm(
<Form.Item
label="Repeats every"
name={['recurrence', 'repeatType']}
rules={requiredFieldRule}
rules={formValidationRules}
>
<Select
placeholder="Select option..."
@@ -383,7 +496,7 @@ export function PlannedDowntimeForm(
<Form.Item
label="Weekly occurernce"
name={['recurrence', 'repeatOn']}
rules={requiredFieldRule}
rules={formValidationRules}
>
<Select
placeholder="Select option..."
@@ -397,14 +510,16 @@ export function PlannedDowntimeForm(
<Form.Item
label="Duration"
name={['recurrence', 'duration']}
rules={requiredFieldRule}
rules={formValidationRules}
>
<Input
addonAfter={
<Select
defaultValue="m"
value={durationUnit}
onChange={(value): void => setDurationUnit(value)}
onChange={(value): void => {
setDurationUnit(value);
}}
>
<Select.Option value="m">Mins</Select.Option>
<Select.Option value="h">Hours</Select.Option>
@@ -418,8 +533,8 @@ export function PlannedDowntimeForm(
/>
</Form.Item>
)}
<Form.Item label="Timezone" name="timezone" rules={requiredFieldRule}>
<Select options={TZ_OPTIONS} placeholder="Select timezone" showSearch />
<Form.Item label="Timezone" name="timezone" rules={formValidationRules}>
<Select options={timeZoneItems} placeholder="Select timezone" showSearch />
</Form.Item>
<Form.Item
label="Ends on"
@@ -431,9 +546,14 @@ export function PlannedDowntimeForm(
},
]}
className={!isEmpty(endTimeText) ? 'formItemWithBullet' : ''}
getValueProps={(value): any => ({
value: value ? dayjs(value).tz(timezoneInitialValue) : undefined,
})}
>
<DatePicker
format={(date) => date.format(customFormat)}
format={(date): string =>
dayjs(date).tz(timezoneInitialValue).format(customFormat)
}
showTime
showNow={false}
renderExtraFooter={datePickerFooter}
@@ -464,7 +584,7 @@ export function PlannedDowntimeForm(
status={isError ? 'error' : undefined}
loading={isLoading}
tagRender={noTagRenderer}
onChange={handleAlertRulesChange}
onChange={handleChange}
showSearch
options={alertOptions}
filterOption={(input, option): boolean =>

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