Compare commits

...

143 Commits

Author SHA1 Message Date
Abhishek Kumar Singh
b8da56b4f4 Merge branch 'main' into fix/validate_alert_payload_before_saving 2026-01-19 16:15:25 +05:30
SagarRajput-7
325974292f feat: auth revamp flow (#9901)
* feat: auth revamp base setup

* feat: auth revamp signin changes

* feat: auth revamp signup changes

* feat: auth revamp reset password changes

* feat: auth revamp light mode changes

* feat: clean up and added get help link

* feat: cleanup and addressed comments

* feat: cleanup

* feat: comment addressed

* feat: auth revamp for onboarding flow (#9915)

* feat: auth revamp for onboarding flow

* feat: updated invite team member step

* feat: updated light mode

* feat: comment addressed

* feat: added onboarding flow test cases

* feat: fixed feedbacks

* feat: resolved comments and refactoring

* feat: resolved comments and refactoring

* fix: svg import fix

* feat: added test cases for the new auth flow (#9914)

* feat: added test cases for the new auth flow

* feat: updated test cases

* feat: updated test cases

* fix: updated test cases

* fix: updated test cases

* fix: use error content for auth revamp error

* feat: refactor and cleanup

* feat: refactor and cleanup

* fix: add shake animation and light mode fix

* feat: feedback resolved

* feat: feedback resolved

* feat: updated test cases

* feat: feedback resolved

* feat: shake animation update
2026-01-19 13:40:37 +05:30
Vishal Sharma
7c1a531d01 Add context7.json with URL and public key (#10037) 2026-01-19 11:35:37 +05:30
Vikrant Gupta
5a45532a72 chore: update codeowners file (#10031) 2026-01-18 22:00:38 +05:30
Srikanth Chekuri
e9501d2e0f test(integration): add cumulative counter rate tests (#9976) 2026-01-18 16:13:41 +05:30
Srikanth Chekuri
c306e66bcd chore: update CODEOWNERS (#10028) 2026-01-18 15:42:46 +05:30
Yunus M
767a0cc28e chore: improve pull request template clarity and visibility (#10021) 2026-01-16 16:17:17 +00:00
Ashwin Bhatkal
4f9efcc133 chore: remove airbnb from ESLint, update prettier rules and update VS Code settings (#10013)
* chore: remove airbnb from ESLint, update prettier rules and update VS Code settings

* fix: lint errors
2026-01-16 12:05:23 +00:00
Amlan Kumar Nandy
bf2dd612e0 chore: fill y axis unit in alerts coming from dashboards (#9982) 2026-01-16 09:37:16 +00:00
Aditya Singh
d3f15022a4 fix: overflowing tag input on antd (#10018) 2026-01-16 09:15:22 +00:00
Pandey
a5c021e96c fix(kafka): fix nil pointer error in evaluation api (#10015) 2026-01-16 02:24:20 +05:30
Abhi kumar
8b9fcae0cb fix: added layout fix for changelog modal (#10012) 2026-01-15 12:30:10 +00:00
Abhi kumar
e75abcb108 Fix/animation offload to cpu (#9961)
* fix: transition is getting offloaded to cpu because gpu can't handle it in composition

* chore: changed transition from all to transform only
2026-01-15 13:43:44 +05:30
primus-bot[bot]
ffa15f9ec1 chore(release): bump to v0.107.0 (#10007)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-01-15 12:18:52 +05:30
Ishan
87562f5387 feat: onGroupBy - show timeseries panel - #3403 (#9921)
* feat: onGroupBy - show timeseries

* feat: updated testcases for tableview actions

* feat: updated pipeline file to remove groupBy as not used

* feat: updated better code checks

* feat: updated better code checks

* feat: updated tableview actions to update query

* feat: updated types and test cases

* feat: updated dataType (noramlized)
2026-01-15 08:53:08 +05:30
Ishan
11a7512a25 feat: trace id pinned (#9981) 2026-01-13 19:59:14 +05:30
Yunus M
f925a15f30 feat: improve date range selection ux (#9994)
* feat: improve date range selection ux
2026-01-13 13:55:57 +00:00
Abhishek Kumar Singh
e1f2a25f99 feat: added unit validation in compositeQuery and ruleThreshold 2026-01-13 17:34:49 +05:30
Abhishek Kumar Singh
80f04da274 chore: removed having + expression check as not respected from frontend 2026-01-13 16:57:54 +05:30
Ashwin Bhatkal
a3fe2c2589 chore: rename Variables folder to DashboardVariableSettings (#9990)
* chore: rename Variables folder to DashboardVariableSettings

* fix: fix tests
2026-01-13 11:25:42 +00:00
Abhishek Kumar Singh
5b169dc3b2 refactor: use model.BadRequest in manager, replaced html template library to text template 2026-01-13 16:31:07 +05:30
Aditya Singh
2719c9b6a7 Fix: Make exists in small case recognise as valid non value op (#9989)
* fix: make exists in small case recognise as valid non value op

* fix: lint fix
2026-01-13 10:56:38 +00:00
Abhishek Kumar Singh
ae50f7d29b chore: remove unused fields form tests 2026-01-13 15:50:53 +05:30
Ashwin Bhatkal
b4282be3ac chore: make settings drawer reusable (#9985)
* chore: make settings drawer reusable

* chore: clean up styles as well

* fix: resolve comments

* chore: trigger build

* chore: resolve comments
2026-01-13 09:33:05 +00:00
Ashwin Bhatkal
15fe3a7163 fix: close button when importing dashboard json (#9987)
* fix: close button when importing dashboard json

* fix: remove x icon
2026-01-13 05:59:11 +00:00
Amlan Kumar Nandy
30b98582b4 chore: fix undefined labels error in alerts (#9589)
* chore: fix undefined labels error in alerts

* chore: fix CI

* chore: minor fix

* chore: add tests

* chore: additional changes

* chore: additonal cleanup

* chore: update tests

* chore: update mock

* chore: update checks

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-01-13 05:27:16 +00:00
Abhishek Kumar Singh
68f315b13e Merge branch 'main' into fix/validate_alert_payload_before_saving 2026-01-13 10:10:56 +05:30
Vikrant Gupta
99b9e27bca fix(dashboard): delete dashboard shouldn't break on license check (#9984)
* fix(dashboard): delete dashboard shouldn't break on license check

* fix(dashboard): add integration tests

* fix(dashboard): add integration tests

* fix(dashboard): remove license check from public dashboard delete

* fix(dashboard): update delete public
2026-01-12 23:40:54 +05:30
Yunus M
7e72f501a7 feat: Improved UX for Date Time Picker (#9944)
* feat: enable inline edit for selected time

* feat: handle absolute and relative time formats

* feat: support epoch timeranges

* feat: match styles for datetime shorthand pill

* feat: handle time range error flows

* feat: hide last refreshed for custom time ranges

* feat: cleanup

* feat: use calendar range for custom date time picker

* fix: update tests to check for run query rather than stage & run query

* feat: pass modalStartTime & modalEndTime in cases where isModalSelection is true

* feat: pass minTime & maxTime for CustomPicker in topnav component

* fix: add safety check for selected time

* feat: show recently use custom time ranges

* fix: cancel query button light mode text color

* feat: move calendar content to a container

* feat: address review comments
2026-01-12 21:18:58 +05:30
Ashwin Bhatkal
97d7b81a73 chore: rename NewDashboard to DashboardContainer (#9980)
* chore: rename NewDashboard to DashboardContainer

* chore: resolve comments and fix tests
2026-01-12 15:25:43 +05:30
Srikanth Chekuri
3a2c38888f Merge branch 'main' into fix/validate_alert_payload_before_saving 2026-01-09 23:39:35 +05:30
Abhishek Kumar Singh
40151b4472 Merge branch 'feat/exclude_new_series_from_alert' into fix/validate_alert_payload_before_saving 2026-01-08 15:53:45 +05:30
Abhishek Kumar Singh
f239212745 chore: fix diff in openapi spec 2026-01-08 08:39:05 +05:30
Srikanth Chekuri
28498a0419 Merge branch 'main' into feat/exclude_new_series_from_alert 2026-01-08 02:03:22 +05:30
Srikanth Chekuri
d0c7e20832 Merge branch 'main' into feat/exclude_new_series_from_alert 2026-01-07 22:28:05 +05:30
Abhishek Kumar Singh
3155762978 refactor: added support for formula query in AnalyzeQueryEnvelopes of queryparser 2026-01-07 19:36:32 +05:30
Abhishek Kumar Singh
ffb6b0af67 refactor: updated telemetry metadata store to use tuples for col search 2026-01-07 15:35:18 +05:30
Abhishek Kumar Singh
d2e296ce79 fix: correct first_seen calculation and add skipped series logging 2026-01-07 15:18:00 +05:30
Abhishek Kumar Singh
41ae5b7319 refactor: use correct grouping key for preparing lookup keys 2026-01-06 21:37:14 +05:30
Abhishek Kumar Singh
ec593f02e5 refactor: update CH filter extractor to not process the function column expr in stripTableAlias 2026-01-06 21:10:30 +05:30
Abhishek Kumar Singh
b3e446755f chore: comments for query filter extractor result sorting 2026-01-06 19:04:34 +05:30
Abhishek Kumar Singh
4f4c628905 refactor: update extractMetricAndGroupBys to return a map of metrics to groupBy keys 2026-01-06 18:49:29 +05:30
Abhishek Kumar Singh
9da0cf48ee refactor: change return type for AnalyzeQueryEnvelopes 2026-01-06 16:53:56 +05:30
Abhishek Kumar Singh
061540e824 refactor: updated AnalyzeCompositeQuery to be AnalyzeQueryEnvelopes 2026-01-06 15:11:58 +05:30
Abhishek Kumar Singh
c159f484ba refactor: used gosqlbuilder to generate query for telemetryMetaStore 2026-01-06 12:54:37 +05:30
Abhishek Kumar Singh
ce96494f01 refactor: moved composite query validation to struct + added supported types in error messages 2026-01-05 21:17:47 +05:30
Srikanth Chekuri
53fdb60c72 Merge branch 'main' into feat/exclude_new_series_from_alert 2026-01-02 23:19:37 +05:30
Abhishek Kumar Singh
f34c8f2084 refactor: removed new series filteration from non v5 queries 2026-01-02 13:05:25 +05:30
Abhishek Kumar Singh
bb93ef3c1f Merge branch 'main' into feat/exclude_new_series_from_alert 2026-01-02 13:01:06 +05:30
Abhishek Kumar Singh
024e351d6d refactor: moved CH metadata related query from telemetry store to metadata store 2026-01-02 12:53:42 +05:30
Abhishek Kumar Singh
0b8e68f0b7 fix: on error from FilterNewSeries, continue with original series 2025-12-30 21:03:37 +05:30
Abhishek Kumar Singh
5256ace34f refactor: update AnalyzeCompositeQuery to return an array of FilterResult 2025-12-30 20:58:43 +05:30
Abhishek Kumar Singh
8ef3b89ab3 test: changed logic to compare series in TestBaseRule_FilterNewSeries 2025-12-30 20:48:13 +05:30
Abhishek Kumar Singh
7ff332f184 refactor: updated baseRule.FilterNewSeries to accept and return array of pointer to Series 2025-12-30 20:38:25 +05:30
Abhishek Kumar Singh
aec969c897 feat: integrated ValidateCompositeQuery in API layer in rule update APIs 2025-12-29 13:40:54 +05:30
Abhishek Kumar Singh
75e67a7e35 feat: apply templates on Clickhouse query before parsing 2025-12-26 18:22:53 +05:30
Abhishek Kumar Singh
8c67f6ff7a chore: fixed tests for ValidateCompositeQuery 2025-12-26 13:21:05 +05:30
Abhishek Kumar Singh
d62ed6f003 feat: added validation for QueryBuilderFormula 2025-12-26 13:07:34 +05:30
Abhishek Kumar Singh
ef4ef47634 feat: added new error type for Query parsing, added validation for QueryBuilderJoin 2025-12-26 12:52:30 +05:30
Srikanth Chekuri
e036d928c4 Merge branch 'main' into feat/exclude_new_series_from_alert 2025-12-26 09:48:14 +05:30
Abhishek Kumar Singh
0a42c77ca7 test: added test for ValidateCompositeQuery 2025-12-24 19:16:20 +05:30
Abhishek Kumar Singh
a89bb71f2c feat: added ValidateCompositeQuery in queryparser 2025-12-24 19:14:39 +05:30
Abhishek Kumar Singh
521e5d92e7 test: fixed breaking tests post PostableRule validations 2025-12-24 17:27:09 +05:30
Abhishek Kumar Singh
09b7360513 feat: added struct based validation for PostableRule and it's child structs 2025-12-24 17:20:57 +05:30
Abhishek Kumar Singh
0fd926b8a1 feat: exclude filtering new series in Logs and Traces queries with corresponding tests 2025-12-23 13:21:30 +05:30
Abhishek Kumar Singh
e4214309f4 refactor: moved series filteration logic to new function for prom_rule 2025-12-22 14:30:50 +05:30
Abhishek Kumar Singh
297383ddca feat: don't skip series with missing metadata as we can't decide in this case if series is old/new 2025-12-22 13:51:08 +05:30
Abhishek Kumar Singh
6871eccd28 feat: return series early on no skipped index 2025-12-22 12:01:12 +05:30
Abhishek Kumar Singh
0a272b5b43 Merge branch 'main' into feat/exclude_new_series_from_alert 2025-12-22 11:47:41 +05:30
Abhishek Kumar Singh
4c4387b6d2 test: added test for FilterNewSeries 2025-12-19 23:32:26 +05:30
Abhishek Kumar Singh
cb242e2d4c refactor: removed un-used check from filterNewSeries + fix CH metadata table name 2025-12-19 18:16:16 +05:30
Abhishek Kumar Singh
c98cdc174b refactor: code reuse 2025-12-19 17:00:29 +05:30
Abhishek Kumar Singh
6fc38bac79 refactor: updated FilterNewSeries to use v3.Series a common collection to filter new series 2025-12-19 16:42:03 +05:30
Abhishek Kumar Singh
ddba7e71b7 refactor: improve type assertion for filtered collections in anomaly, prom, and threshold rules 2025-12-19 14:18:21 +05:30
Abhishek Kumar Singh
23f9ff50a7 chore: added helpful comments for FilterNewSeries 2025-12-19 13:30:54 +05:30
Abhishek Kumar Singh
55e5c871fe refactor: user real QP for FilterNewSeries test 2025-12-19 12:53:22 +05:30
Abhishek Kumar Singh
511bb176dd Merge branch 'main' into feat/exclude_new_series_from_alert 2025-12-19 11:50:55 +05:30
Abhishek Kumar Singh
4e0c0319d0 Merge branch 'main' into feat/exclude_new_series_from_alert 2025-12-18 14:49:28 +05:30
Srikanth Chekuri
9e5ea4de9c Merge branch 'main' into feat/exclude_new_series_from_alert 2025-12-15 08:27:00 +05:30
Abhishek Kumar Singh
81e0df09b8 refactor: merge conflict fix 2025-12-03 19:06:18 +05:30
Abhishek Kumar Singh
a522f39b9b Merge branch 'main' into feat/exclude_new_series_from_alert 2025-12-03 19:04:18 +05:30
Abhishek Kumar Singh
affb6eee05 feat: added function in query parser to parse composite query and get filter result 2025-12-02 18:56:19 +05:30
Abhishek Kumar Singh
13a5e9dd24 Merge branch 'feat/groups_in_ch_and_promql_queries' into feat/exclude_new_series_from_alert 2025-12-02 18:02:40 +05:30
Abhishek Kumar Singh
f620767876 refactor: used binding.JSON.BindBody to parse body 2025-12-02 17:58:39 +05:30
Srikanth Chekuri
9fb8b2bb1b Merge branch 'main' into feat/groups_in_ch_and_promql_queries 2025-12-02 17:26:54 +05:30
Abhishek Kumar Singh
30494c9196 chore: updated comments 2025-12-02 16:58:06 +05:30
Abhishek Kumar Singh
cae4cf0777 refactor: use query parser in baseRule 2025-12-02 16:55:58 +05:30
Abhishek Kumar Singh
c9538b0604 Merge branch 'feat/groups_in_ch_and_promql_queries' into feat/exclude_new_series_from_alert 2025-12-02 15:57:15 +05:30
Abhishek Kumar Singh
204cc4e5c5 Merge branch 'main' into feat/groups_in_ch_and_promql_queries 2025-12-02 15:47:30 +05:30
Abhishek Kumar Singh
6dd2ffcb64 refactor: update API handler to use new queryparser package for query parsing 2025-11-27 20:11:16 +05:30
Abhishek Kumar Singh
13c15249c5 Merge branch 'feat/groups_in_ch_and_promql_queries' into feat/exclude_new_series_from_alert 2025-11-27 19:51:27 +05:30
Abhishek Kumar Singh
8419ca7982 Merge branch 'main' into feat/exclude_new_series_from_alert 2025-11-27 19:49:57 +05:30
Abhishek Kumar Singh
6b189b14c6 chore: updated series collection to labelledcollection 2025-11-27 19:45:01 +05:30
Abhishek Kumar Singh
550c49fab0 feat: created queryparser package with APIs 2025-11-27 19:37:41 +05:30
Abhishek Kumar Singh
5b6ff92648 refactor: moved package queryfilterextractor in pkg/queryparser 2025-11-27 19:14:55 +05:30
Abhishek Kumar Singh
45954b38fa Merge branch 'main' into feat/exclude_new_series_from_alert 2025-11-25 12:27:00 +05:30
Abhishek Kumar Singh
ceade6c7d7 refactor: moved query parser APIs to queryfilterextractor package 2025-11-24 14:15:44 +05:30
Srikanth Chekuri
f15c88836c Merge branch 'main' into feat/groups_in_ch_and_promql_queries 2025-11-21 15:26:44 +05:30
Abhishek Kumar Singh
9af45643a9 refactor: created valuer enum for extractor types + exposed alias along with column name for groups 2025-11-21 15:16:03 +05:30
Srikanth Chekuri
d15e974e9f Merge branch 'main' into feat/groups_in_ch_and_promql_queries 2025-11-20 22:59:29 +05:30
Abhishek Kumar Singh
71e752a015 refactor: moved api request and response to types 2025-11-20 20:00:51 +05:30
Abhishek Kumar Singh
3407760585 Merge branch 'feat/queryfilterextractor' into feat/groups_in_ch_and_promql_queries 2025-11-20 19:02:11 +05:30
Abhishek Kumar Singh
58a0e36869 refactor: return relevant errors when parsing query 2025-11-20 19:01:40 +05:30
Abhishek Kumar Singh
5d688eb919 test: updated test case to include CH query error case 2025-11-20 18:55:45 +05:30
Abhishek Kumar Singh
c0f237a7c4 refactor: use render.Error instead of RespondError 2025-11-20 18:12:47 +05:30
Abhishek Kumar Singh
8ce8bc940a Merge branch 'feat/queryfilterextractor' into feat/groups_in_ch_and_promql_queries 2025-11-20 17:42:46 +05:30
Abhishek Kumar Singh
abce05b289 feat: exposed group name from column info 2025-11-20 17:38:00 +05:30
Abhishek Kumar Singh
ccd25c3b67 refactor: removed redundant checks 2025-11-20 17:24:36 +05:30
Abhishek Kumar Singh
ddb98da217 refactor: change function visibility for extractMetricAndGroupBys 2025-11-20 15:56:45 +05:30
Abhishek Kumar Singh
18d63d2e66 fix: close CH rows on reading each chunk 2025-11-20 15:44:07 +05:30
Abhishek Kumar Singh
67c108f021 feat: added new series filter in anomaly rule as well 2025-11-20 15:26:14 +05:30
Abhishek Kumar Singh
02939cafa4 refactor: added GetFirstSeenFromMetricMetadata in clickhouse reader, removed caching for query result 2025-11-20 15:24:14 +05:30
Abhishek Kumar Singh
e62b070c1e fix: created interface for standard series filtration from both prom and threshold rule 2025-11-20 14:35:25 +05:30
Srikanth Chekuri
be0a7d8fd4 Merge branch 'main' into feat/queryfilterextractor 2025-11-20 02:48:43 +05:30
Srikanth Chekuri
419044dc9e Merge branch 'main' into feat/queryfilterextractor 2025-11-19 22:42:12 +05:30
Abhishek Kumar Singh
223465d6d5 Merge branch 'feat/queryfilterextractor' into feat/exclude_new_series_from_alert 2025-11-19 20:56:01 +05:30
Abhishek Kumar Singh
cec99674fa feat: exclude new samples from alert evals 2025-11-19 20:52:59 +05:30
Abhishek Kumar Singh
0ccf58ac7a fix: common behaviour across CH and PromQL originField and originExpr 2025-11-19 20:36:00 +05:30
Abhishek Kumar Singh
b08d636d6a refactor: updated query type check with constants 2025-11-19 20:12:20 +05:30
Abhishek Kumar Singh
f6141bc6c5 feat: added API to extract metric names and groups from CH or PromQL query 2025-11-19 16:48:03 +05:30
Srikanth Chekuri
bfe49f0f1b Merge branch 'main' into feat/queryfilterextractor 2025-11-19 14:21:35 +05:30
Abhishek Kumar Singh
8e8064c5c1 fix: ci lint issues 2025-11-19 13:28:04 +05:30
Abhishek Kumar Singh
4392341467 improv: added comments for CH originparser + some code improv 2025-11-19 12:58:34 +05:30
Abhishek Kumar Singh
521d8e4f4d improv: added more tests for promql and added comments 2025-11-19 12:15:20 +05:30
Abhishek Kumar Singh
b6103f371f Merge branch 'main' into feat/queryfilterextractor 2025-11-18 21:09:45 +05:30
Abhishek Kumar Singh
43283506db Merge branch 'feat/queryfilterextractor_complex' into feat/queryfilterextractor 2025-11-18 15:22:24 +05:30
Abhishek Kumar Singh
694d9958db improv: integrated origin field extraction and updated tests to check for origin fields 2025-11-18 15:03:24 +05:30
Abhishek Kumar Singh
addee4c0a5 feat: added origin field extractor for ch query 2025-11-18 14:36:03 +05:30
Abhishek Kumar Singh
f10cf7ac04 refactor: code organisation 2025-11-17 16:27:17 +05:30
Abhishek Kumar Singh
b336678639 fix: CH test cases 2025-11-17 15:01:32 +05:30
Abhishek Kumar Singh
c438b3444e refactor: removed GroupBy from FilterResult 2025-11-17 14:34:46 +05:30
Abhishek Kumar Singh
b624414507 feat: extract column origin from subquery and join before searching directly 2025-11-17 13:42:47 +05:30
Abhishek Kumar Singh
bde7963444 feat: implemented extractOriginFromSelectItem which will find the given columnName till the very end to return the origin column with given name 2025-11-17 09:00:18 +05:30
Abhishek Kumar Singh
2df93ff217 feat: extract column origin from query and add in column info 2025-11-16 10:20:38 +05:30
Abhishek Kumar Singh
f496a6ecde improv: updated result for queryfilterextractor to return column with alias 2025-11-16 08:58:33 +05:30
Abhishek Kumar Singh
599e230a72 feat: added NewExtractor function for creating extractor 2025-11-13 13:52:32 +05:30
Abhishek Kumar Singh
9a0e32ff3b refactor: removed redundant non nil checks 2025-11-13 13:41:51 +05:30
Abhishek Kumar Singh
5fe2732698 refactor: removed unused extractFromAnyFunction 2025-11-13 13:20:59 +05:30
Abhishek Kumar Singh
4993a44ecc refactor: removed unused cases + added comments 2025-11-13 12:59:35 +05:30
Abhishek Kumar Singh
ebd575a16b chore: comments + remove usage of seen map in extractGroupFromGroupByClause 2025-11-12 19:26:44 +05:30
Abhishek Kumar Singh
666582337e feat: support for CTE in clickhouse queryfilterextractor 2025-11-12 18:58:30 +05:30
Abhishek Kumar Singh
23512ab05c feat: added support for promql in queryfilterextractor 2025-11-10 20:50:42 +05:30
Abhishek Kumar Singh
1423749529 feat: added filter extractor interface and clickhouse impl with tests 2025-11-10 20:05:39 +05:30
237 changed files with 9227 additions and 2512 deletions

69
.github/CODEOWNERS vendored
View File

@@ -16,13 +16,13 @@
# Scaffold Owners
/pkg/config/ @therealpandey
/pkg/errors/ @therealpandey
/pkg/factory/ @therealpandey
/pkg/types/ @therealpandey
/pkg/valuer/ @therealpandey
/cmd/ @therealpandey
.golangci.yml @therealpandey
/pkg/config/ @vikrantgupta25
/pkg/errors/ @vikrantgupta25
/pkg/factory/ @vikrantgupta25
/pkg/types/ @vikrantgupta25
/pkg/valuer/ @vikrantgupta25
/cmd/ @vikrantgupta25
.golangci.yml @vikrantgupta25
# Zeus Owners
@@ -48,22 +48,71 @@
/pkg/querier/ @srikanthccv
/pkg/variables/ @srikanthccv
/pkg/types/querybuildertypes/ @srikanthccv
/pkg/types/telemetrytypes/ @srikanthccv
/pkg/querybuilder/ @srikanthccv
/pkg/telemetrylogs/ @srikanthccv
/pkg/telemetrymetadata/ @srikanthccv
/pkg/telemetrymetrics/ @srikanthccv
/pkg/telemetrytraces/ @srikanthccv
# Metrics
/pkg/types/metrictypes/ @srikanthccv
/pkg/types/metricsexplorertypes/ @srikanthccv
/pkg/modules/metricsexplorer/ @srikanthccv
/pkg/prometheus/ @srikanthccv
# APM
/pkg/types/servicetypes/ @srikanthccv
/pkg/types/apdextypes/ @srikanthccv
/pkg/modules/apdex/ @srikanthccv
/pkg/modules/services/ @srikanthccv
# Dashboard
/pkg/types/dashboardtypes/ @srikanthccv
/pkg/modules/dashboard/ @srikanthccv
# Rule/Alertmanager
/pkg/types/ruletypes/ @srikanthccv
/pkg/types/alertmanagertypes @srikanthccv
/pkg/alertmanager/ @srikanthccv
/pkg/ruler/ @srikanthccv
# Correlation-adjacent
/pkg/contextlinks/ @srikanthccv
/pkg/types/parsertypes/ @srikanthccv
/pkg/queryparser/ @srikanthccv
# AuthN / AuthZ Owners
/pkg/authz/ @vikrantgupta25 @therealpandey
/pkg/authz/ @vikrantgupta25
# Integration tests
/tests/integration/ @therealpandey
/tests/integration/ @vikrantgupta25
# Dashboard Owners
/frontend/src/hooks/dashboard/ @SigNoz/pulse-frontend
## Dashboard List
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
## Dashboard Page
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
/frontend/src/container/NewDashboard/ @SigNoz/pulse-frontend
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
## Public Dashboard Page
/frontend/src/pages/PublicDashboard/ @SigNoz/pulse-frontend
/frontend/src/container/PublicDashboardContainer/ @SigNoz/pulse-frontend

View File

@@ -1,86 +1,76 @@
## 📄 Summary
<!-- Describe the purpose of the PR in a few sentences. What does it fix/add/update? -->
## Pull Request
---
## ✅ Changes
- [ ] Feature: Brief description
- [ ] Bug fix: Brief description
### 📄 Summary
> Why does this change exist?
> What problem does it solve, and why is this the right approach?
---
### ✅ Change Type
_Select all that apply_
## 📝 Changelog
> Fill this only if the change affects users, APIs, UI, or documented behavior.
Mention as N/A for internal refactors or non-user-visible changes.
**Deployment Type:** Cloud / OSS / Enterprise
**Type:** Feature / Bug Fix / Maintenance
**Description:** Short, user-facing summary of the change
- [ ] ✨ Feature
- [ ] 🐛 Bug fix
- [ ] ♻️ Refactor
- [ ] 🛠️ Infra / Tooling
- [ ] 🧪 Test-only
---
## 🏷️ Required: Add Relevant Labels
### 🐛 Bug Context
> Required if this PR fixes a bug
> ⚠️ **Manually add appropriate labels in the PR sidebar**
Please select one or more labels (as applicable):
#### Root Cause
> What caused the issue?
> Regression, faulty assumption, edge case, refactor, etc.
ex:
- `frontend`
- `backend`
- `devops`
- `bug`
- `enhancement`
- `ui`
- `test`
#### Fix Strategy
> How does this PR address the root cause?
---
## 👥 Reviewers
### 🧪 Testing Strategy
> How was this change validated?
> Tag the relevant teams for review:
- frontend / backend / devops
- Tests added/updated:
- Manual verification:
- Edge cases covered:
---
## 🧪 How to Test
### ⚠️ Risk & Impact Assessment
> What could break? How do we recover?
<!-- Describe how reviewers can test this PR -->
1. ...
2. ...
3. ...
- Blast radius:
- Potential regressions:
- Rollback plan:
---
## 🔍 Related Issues
### 📝 Changelog
> Fill only if this affects users, APIs, UI, or documented behavior
> Use **N/A** for internal or non-user-facing changes
<!-- Reference any related issues (e.g. Fixes #123, Closes #456) -->
Closes #
| Field | Value |
|------|-------|
| Deployment Type | Cloud / OSS / Enterprise |
| Change Type | Feature / Bug Fix / Maintenance |
| Description | User-facing summary |
---
## 📸 Screenshots / Screen Recording (if applicable / mandatory for UI related changes)
<!-- Add screenshots or GIFs to help visualize changes -->
---
## 📋 Checklist
- [ ] Dev Review
- [ ] Test cases added (Unit/ Integration / E2E)
- [ ] Manually tested the changes
### 📋 Checklist
- [ ] Tests added or explicitly not required
- [ ] Manually tested
- [ ] Breaking changes documented
- [ ] Backward compatibility considered
---
## 👀 Notes for Reviewers
<!-- Anything reviewers should keep in mind while reviewing -->
---

2
.gitignore vendored
View File

@@ -12,7 +12,6 @@ frontend/coverage
# production
frontend/build
frontend/.vscode
frontend/.yarnclean
frontend/.temp_cache
frontend/test-results
@@ -31,7 +30,6 @@ frontend/src/constants/env.ts
.idea
**/.vscode
**/build
**/storage
**/locust-scripts/__pycache__/

9
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"eslint.workingDirectories": ["./frontend"],
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"prettier.requireConfig": true
}

4
context7.json Normal file
View File

@@ -0,0 +1,4 @@
{
"url": "https://context7.com/signoz/signoz",
"public_key": "pk_6g9GfjdkuPEIDuTGAxnol"
}

View File

@@ -176,7 +176,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.106.0
image: signoz/signoz:v0.107.0
command:
- --config=/root/config/prometheus.yml
ports:

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.106.0
image: signoz/signoz:v0.107.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -179,7 +179,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.106.0}
image: signoz/signoz:${VERSION:-v0.107.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -111,7 +111,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.106.0}
image: signoz/signoz:${VERSION:-v0.107.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -163,7 +163,7 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
}
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
err := module.DeletePublic(ctx, orgID, id)
err := module.deletePublic(ctx, orgID, id)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return err
}
@@ -263,3 +263,35 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.U
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, role types.Role, lock bool) error {
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, role, lock)
}
func (module *module) deletePublic(ctx context.Context, orgID valuer.UUID, dashboardID valuer.UUID) error {
publicDashboard, err := module.store.GetPublic(ctx, dashboardID.String())
if err != nil {
return err
}
role, err := module.role.GetOrCreate(ctx, roletypes.NewRole(roletypes.AnonymousUserRoleName, roletypes.AnonymousUserRoleDescription, roletypes.RoleTypeManaged.StringValue(), orgID))
if err != nil {
return err
}
deletionObject := authtypes.MustNewObject(
authtypes.Resource{
Name: dashboardtypes.TypeableMetaResourcePublicDashboard.Name(),
Type: authtypes.TypeMetaResource,
},
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
)
err = module.role.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
if err != nil {
return err
}
err = module.store.DeletePublic(ctx, dashboardID.StringValue())
if err != nil {
return err
}
return nil
}

View File

@@ -7,8 +7,6 @@ module.exports = {
'jest/globals': true,
},
extends: [
'airbnb',
'airbnb-typescript',
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
@@ -35,6 +33,7 @@ module.exports = {
'react-hooks',
'prettier',
'jest',
'jsx-a11y',
],
settings: {
react: {
@@ -72,9 +71,6 @@ module.exports = {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
// airbnb
'no-underscore-dangle': 'off',
'no-console': 'off',
'import/prefer-default-export': 'off',
'import/extensions': [
'error',
@@ -87,6 +83,9 @@ module.exports = {
},
],
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
// Disabled because TypeScript already handles this check more accurately,
// and the rule has false positives with type-only imports (e.g., TooltipProps from antd)
'import/named': 'off',
'no-plusplus': 'off',
'jsx-a11y/label-has-associated-control': [
'error',
@@ -104,7 +103,10 @@ module.exports = {
},
},
],
'@typescript-eslint/no-unused-vars': 'error',
// Allow empty functions for mocks, default context values, and noop callbacks
'@typescript-eslint/no-empty-function': 'off',
// Allow underscore prefix for intentionally unused variables (e.g., const { id: _id, ...rest } = props)
'@typescript-eslint/no-unused-vars': 'warn',
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'arrow-body-style': ['error', 'as-needed'],

View File

@@ -4,5 +4,14 @@
"tabWidth": 1,
"singleQuote": true,
"jsxSingleQuote": false,
"semi": true
"semi": true,
"printWidth": 80,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf",
"quoteProps": "as-needed",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"embeddedLanguageFormatting": "auto"
}

8
frontend/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"prettier.requireConfig": true
}

View File

@@ -34,12 +34,18 @@ Embrace the spirit of collaboration and contribute to the success of our open-so
### Linting and Setup
- It is crucial to refrain from disabling ESLint and TypeScript errors within the project. If there is a specific rule that needs to be disabled, provide a clear and justified explanation for doing so. Maintaining the integrity of the linting and type-checking processes ensures code quality and consistency throughout the codebase.
- In our project, we rely on several essential ESLint plugins, namely:
- [plugin:@typescript-eslint](https://typescript-eslint.io/rules/)
- [airbnb styleguide](https://github.com/airbnb/javascript)
- [plugin:sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs)
- In our project, we rely on several essential ESLint plugins and configurations:
To ensure compliance with our coding standards and best practices, we encourage you to refer to the documentation of these plugins. Familiarizing yourself with the ESLint rules they provide will help maintain code quality and consistency throughout the project.
- [eslint:recommended](https://eslint.org/docs/latest/rules/) - Core ESLint rules for JavaScript best practices
- [plugin:@typescript-eslint](https://typescript-eslint.io/rules/) - TypeScript-specific linting rules
- [plugin:react](https://github.com/jsx-eslint/eslint-plugin-react) - React best practices and patterns
- [plugin:react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) - Rules of Hooks enforcement
- [plugin:sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs) - Code quality and complexity analysis
- [plugin:prettier](https://github.com/prettier/eslint-plugin-prettier) - Code formatting via Prettier
- [simple-import-sort](https://github.com/lydell/eslint-plugin-simple-import-sort) - Automatic import organization
- [plugin:jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) - Accessibility rules for JSX elements
To ensure compliance with our coding standards and best practices, we encourage you to refer to the documentation of these plugins. Familiarizing yourself with the ESLint rules they provide will help maintain code quality and consistency throughout the project.
### Naming Conventions

View File

@@ -219,16 +219,11 @@
"compression-webpack-plugin": "9.0.0",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.4",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jest": "^26.9.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32"><path fill="#fff" d="m9.05 20.616 13.968-.178-.06-9.283-14.417.178z"/><path fill="#d80d1a" d="m10.165 20.736-1.302-2.253.127-5.209 5.133 7.491z"/><path fill="#1d86fa" d="m16.72 20.616-6.416-9.401 4.298.06 6.536 9.372z"/><path fill="#d80d1a" d="m22.958 19.125-5.58-7.88 4.118.12 2.12 2.956z"/><path fill="#a1d2d6" d="m8.75 11.722 14.208-.09-.098-.86-14.14.114zM8.99 21.214l14.237-.18-.329-.864-14.237.177z"/><path fill="#c8c8c8" d="M22.772 21.163c.122.147.882.175.882.175s.444.825 1.41.707c.865-.106 1.2-.829 1.2-.829s1.463-.338 2.259-1.464c.846-1.2.882-2.383.882-4.076 0-1.836-.143-3.088-1.076-4.076-1.073-1.134-2.186-1.218-2.186-1.218s-.39-.654-1.271-.636c-.883.018-1.218.74-1.218.74s-.704.151-.882.282c-.091.067-.067 2.703-.054 5.294.012 2.5-.042 4.988.054 5.1"/><path fill="#858585" d="M28.541 16.538c.353.018.345-1.851.036-3.229-.354-1.57-1.694-2.082-1.8-1.993s-.071 1.675.017 1.764c.09.09.918.494 1.2 1.182.283.69.278 2.262.547 2.276M25.299 21.99l-.056-12.213s.258.04.509.207c.242.162.389.395.389.395l.127 10.828s-.176.325-.35.465c-.381.309-.62.318-.62.318"/><path fill="#e1e0e0" d="m24.692 22.043-.157-12.255s-.323.04-.565.258c-.204.182-.318.44-.318.44l.107 11.01s.164.223.353.354c.23.157.58.193.58.193"/><path fill="#c8c8c8" d="M9.13 21.32c.158-.142.036-10.216-.053-10.392-.089-.175-.829-1.069-2.01-1-1.167.069-1.536.876-1.536.876s-1.169.155-2.065 1.5c-.635.954-.82 2.19-.806 3.74.015 1.712.12 3.319 1.142 4.341.982.982 1.94 1.022 1.94 1.022s.267.87 1.573.87c1.289.002 1.816-.956 1.816-.956"/><path fill="#858585" d="M7.548 22.274s-.373-1.907-.427-6.158.256-6.186.256-6.186.258-.017.504.067c.276.093.458.24.458.24s-.244 3.656-.227 5.879c.018 2.222.37 5.85.37 5.85s-.265.153-.423.206c-.164.051-.51.102-.51.102"/><path fill="#e1e0e0" d="M6.858 22.234s-.48-2.45-.533-6.032.157-6.163.157-6.163-.422.127-.633.322c-.21.196-.318.44-.318.44s-.122 3.359-.14 5.39c-.02 2.454.351 5.216.351 5.216s.085.296.345.496c.367.285.771.331.771.331"/><path fill="#fff" d="M6.1 16.763c.19 0 .3 1.251.38 2.58.045.744.411 2.058-.08 2.058-.49 0-.584-.476-.617-2.09-.034-1.612.064-2.548.317-2.548M3.173 16.667c.12-.042.35.634.903 1.156.39.369.76.49.855.744.096.254.222 1.613-.062 1.631-.396.023-1.022-.262-1.393-.933-.59-1.062-.525-2.518-.303-2.598"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32"><path fill="#455a64" d="m15.255 10.911-4.82-6.048a.204.204 0 0 1 .025-.278.2.2 0 0 1 .284.018l5.127 5.775a.2.2 0 0 1-.018.284l-.307.271a.2.2 0 0 1-.291-.022M16.494 10.878l-.276-.245a.22.22 0 0 1-.017-.313l5.097-5.746a.22.22 0 0 1 .313-.018c.09.078.1.213.027.307l-4.822 5.99a.227.227 0 0 1-.322.025"/><path fill="#455a64" d="M12.389 11.214c0-.775 1.72-1.406 3.842-1.406s3.842.629 3.842 1.406z"/><path fill="#82aec0" d="M28.353 29.337H3.652c-.544 0-.985-.458-.985-1.02V11.8c0-.564.44-1.02.985-1.02h24.703c.542 0 .985.458.985 1.02v16.516c-.003.562-.443 1.02-.987 1.02"/><path fill="#212121" d="M27.637 28.368H4.438a.79.79 0 0 1-.789-.79V12.456a.79.79 0 0 1 .79-.79h23.198c.436 0 .789.354.789.79v15.124a.79.79 0 0 1-.79.789"/><path fill="#b9e4ea" d="m5.2 25.995-1.068 1.677c-.186.293.014.691.36.694h18.414a.536.536 0 0 0 .478-.77l-.667-1.373z"/><path fill="#455a64" d="M27.312 27.662h-2.53a.464.464 0 0 1-.465-.464V21.92c0-.258.209-.464.464-.464h2.531c.258 0 .465.209.465.464v5.278a.464.464 0 0 1-.465.464M26.048 20.226a1.531 1.531 0 1 0 0-3.062 1.531 1.531 0 0 0 0 3.062"/><path fill="#82aec0" d="M24.966 17.798a.254.254 0 0 0-.009.313l.504.678c.158.21.363.382.6.497l.76.37a.254.254 0 0 0 .316-.38l-.505-.678a1.65 1.65 0 0 0-.6-.498l-.76-.369a.26.26 0 0 0-.306.067"/><path fill="#455a64" d="M26.048 16.268a1.531 1.531 0 1 0 0-3.062 1.531 1.531 0 0 0 0 3.062"/><path fill="#82aec0" d="M26.049 13.33c-.118 0-.22.08-.247.194l-.2.822a1.65 1.65 0 0 0 0 .78l.2.822a.254.254 0 0 0 .493 0l.2-.822a1.65 1.65 0 0 0 0-.78l-.2-.822a.256.256 0 0 0-.247-.194"/><path fill="#2f7889" d="M22.992 25.219c-.17.77-.742 1.366-1.475 1.529-1.205.269-3.45.604-7.148.604-3.883 0-6.487-.369-7.89-.642-.775-.151-1.384-.787-1.536-1.607-.348-1.885-.786-5.652.018-10.357.15-.871.814-1.54 1.643-1.653 1.495-.207 4.167-.483 7.765-.483 3.414 0 5.7.25 6.997.45.797.124 1.435.77 1.598 1.608.962 4.957.43 8.733.028 10.55"/><path fill="#212121" fill-rule="evenodd" d="M14.37 13.055c-3.576 0-6.227.274-7.705.478-.623.085-1.147.593-1.265 1.288-.794 4.64-.361 8.353-.02 10.201.121.653.6 1.138 1.184 1.252 1.375.267 3.951.634 7.805.634 3.672 0 5.884-.334 7.051-.594.553-.122 1.002-.576 1.139-1.192.392-1.772.917-5.485-.032-10.369-.13-.67-.633-1.161-1.23-1.254-1.272-.197-3.536-.444-6.928-.444m-7.827-.403c1.514-.209 4.206-.486 7.826-.486 3.436 0 5.746.25 7.064.454h.001c1 .156 1.771.959 1.966 1.964.976 5.028.439 8.867.027 10.73-.206.928-.9 1.665-1.814 1.868-1.242.277-3.52.615-7.244.615-3.91 0-6.544-.372-7.975-.65-.967-.19-1.705-.976-1.887-1.963m2.036-12.532c-1.035.142-1.84.972-2.02 2.02-.815 4.768-.372 8.59-.016 10.512" clip-rule="evenodd"/><path fill="url(#a)" d="M8.686 14.175c.373-.045 1.08-.013 1.329.744.248.758-.298.963-.591 1.187-.843.642-1.26.887-1.767 1.587-.404.56-1.111.384-1.322.015-.169-.298-.293-1.253.064-1.884.771-1.351 1.914-1.605 2.287-1.649"/><defs><linearGradient id="a" x1="8.303" x2="8.07" y1="9.59" y2="18.014" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,145 @@
.auth-error-container {
margin-top: 24px;
width: 100%;
animation: horizontal-shaking 300ms ease-out;
.error-content {
background: rgba(229, 72, 77, 0.1);
border: 1px solid rgba(229, 72, 77, 0.2);
border-radius: 4px;
&__summary-section {
border-bottom: 1px solid rgba(229, 72, 77, 0.2);
}
&__summary {
padding: 16px;
}
&__summary-left {
gap: 10px;
}
&__icon-wrapper {
width: 12px;
height: 12px;
flex-shrink: 0;
}
&__summary-text {
gap: 6px;
}
&__error-code {
color: #fadadb;
font-size: 13px;
font-weight: 500;
line-height: 1;
letter-spacing: -0.065px;
}
&__error-message {
color: #f5b6b8;
font-size: 13px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.065px;
}
&__message-badge {
padding: 0px 16px 16px;
}
&__message-badge-label-text {
color: #fadadb;
}
&__message-badge-line {
background-image: radial-gradient(
circle,
rgba(229, 72, 77, 0.3) 1px,
transparent 2px
);
}
&__messages-section {
padding: 0;
}
&__message-list {
max-height: 200px;
}
&__message-item {
color: #f5b6b8;
font-size: 13px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.065px;
&::before {
background: #f5b6b8;
}
}
&__scroll-hint {
background: rgba(229, 72, 77, 0.2);
}
&__scroll-hint-text {
color: #fadadb;
}
}
.auth-error-icon {
color: var(--bg-cherry-300);
padding-top: 1px;
}
}
.lightMode {
.auth-error-container {
.error-content {
background: rgba(229, 72, 77, 0.1);
border-color: rgba(229, 72, 77, 0.2);
&__error-code {
color: var(--bg-ink-100);
}
&__error-message {
color: var(--bg-ink-400);
}
&__message-item {
color: var(--bg-ink-400);
&::before {
background: var(--bg-ink-400);
}
}
&__scroll-hint-text {
color: var(--bg-ink-100);
}
}
}
}
@keyframes horizontal-shaking {
0% {
transform: translateX(0);
}
25% {
transform: translateX(5px);
}
50% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
100% {
transform: translateX(0);
}
}

View File

@@ -0,0 +1,22 @@
import './AuthError.styles.scss';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import { CircleAlert } from 'lucide-react';
import APIError from 'types/api/error';
interface AuthErrorProps {
error: APIError;
}
function AuthError({ error }: AuthErrorProps): JSX.Element {
return (
<div className="auth-error-container">
<ErrorContent
error={error}
icon={<CircleAlert size={12} className="auth-error-icon" />}
/>
</div>
);
}
export default AuthError;

View File

@@ -0,0 +1,115 @@
@import '@signozhq/design-tokens/dist/style.css';
.auth-footer {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 24px 0;
position: relative;
z-index: 10;
}
.auth-footer-content {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
padding: 12px;
background: var(--bg-ink-400, #121317);
border: 1px solid var(--bg-ink-200, #23262e);
border-radius: 4px;
}
.auth-footer-item {
display: flex;
align-items: center;
gap: 6px;
height: 12px;
}
.auth-footer-status-indicator {
width: 6px;
height: 6px;
border-radius: 9999px;
background: #25e192;
flex-shrink: 0;
}
.auth-footer-icon {
aspect-ratio: 1.93;
width: 29px;
flex-shrink: 0;
object-fit: contain;
opacity: 1;
}
.auth-footer-text {
font-family: var(--font-family-inter, Inter, sans-serif);
font-size: 11px;
font-weight: 400;
line-height: 1;
color: var(--text-neutral-dark-100, #adb4c2);
text-align: center;
}
.auth-footer-link {
display: flex;
align-items: center;
gap: 6px;
text-decoration: none;
transition: opacity 0.2s ease;
&:hover {
opacity: 0.8;
}
}
.auth-footer-link-icon {
flex-shrink: 0;
color: var(--text-neutral-dark-50, #eceef2);
}
.auth-footer-link-status {
.auth-footer-text {
color: #25e192;
}
.auth-footer-link-icon {
color: #25e192;
}
}
.auth-footer-separator {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--bg-ink-200, #23262e);
flex-shrink: 0;
}
.lightMode {
.auth-footer-content {
background: var(--bg-base-white, #ffffff);
border-color: var(--bg-vanilla-300, #e9e9e9);
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.08);
}
.auth-footer-icon {
filter: brightness(0) saturate(100%) invert(25%) sepia(8%) saturate(518%)
hue-rotate(192deg) brightness(80%) contrast(95%);
opacity: 0.9;
}
.auth-footer-text {
color: var(--text-neutral-light-200, #80828d);
}
.auth-footer-link-icon {
color: var(--text-neutral-light-100, #62636c);
}
.auth-footer-separator {
background: var(--bg-vanilla-300, #e9e9e9);
}
}

View File

@@ -0,0 +1,75 @@
import './AuthFooter.styles.scss';
import { ArrowUpRight } from 'lucide-react';
import React from 'react';
interface FooterItem {
icon?: string;
text: string;
url?: string;
statusIndicator?: boolean;
}
const footerItems: FooterItem[] = [
{
text: 'All systems operational',
url: 'https://status.signoz.io/',
statusIndicator: true,
},
{
text: 'Privacy',
url: 'https://www.signoz.io/privacy',
},
{
text: 'Security',
url: 'https://www.signoz.io/security',
},
];
function AuthFooter(): JSX.Element {
return (
<footer className="auth-footer">
<div className="auth-footer-content">
{footerItems.map((item, index) => (
<React.Fragment key={item.text}>
<div className="auth-footer-item">
{item.statusIndicator && (
<div className="auth-footer-status-indicator" />
)}
{item.icon && (
<img
loading="lazy"
src={item.icon}
alt=""
className="auth-footer-icon"
/>
)}
{item.url ? (
<a
href={item.url}
className={`auth-footer-link ${
item.statusIndicator ? 'auth-footer-link-status' : ''
}`}
target="_blank"
rel="noopener noreferrer"
>
<span className="auth-footer-text">{item.text}</span>
{!item.statusIndicator && (
<ArrowUpRight size={12} className="auth-footer-link-icon" />
)}
</a>
) : (
<span className="auth-footer-text">{item.text}</span>
)}
</div>
{index < footerItems.length - 1 && (
<div className="auth-footer-separator" />
)}
</React.Fragment>
))}
</div>
</footer>
);
}
export default AuthFooter;

View File

@@ -0,0 +1,82 @@
@import '@signozhq/design-tokens/dist/style.css';
.auth-header {
width: 100%;
max-width: 1176px;
margin: 0 auto;
padding: 12px 0;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
z-index: 10;
}
.auth-header-logo {
display: flex;
align-items: center;
gap: 4.9px;
text-decoration: none;
}
.auth-header-logo-icon {
width: 17.5px;
height: 17.5px;
flex-shrink: 0;
}
.auth-header-logo-text {
font-family: Satoshi, var(--font-family-inter, Inter), sans-serif;
font-size: 15.4px;
font-weight: 500;
line-height: 17.5px;
color: var(--text-neutral-dark-50, #eceef2);
white-space: nowrap;
}
.auth-header-help-button {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 32px;
padding: 10px 16px;
background: var(--bg-ink-400, #121317);
border: none;
border-radius: 2px;
cursor: pointer;
transition: opacity 0.2s ease;
span {
font-family: var(--font-family-inter, Inter, sans-serif);
font-size: 11px;
font-weight: 500;
line-height: 1;
color: var(--text-neutral-dark-100, #adb4c2);
text-align: center;
}
svg {
flex-shrink: 0;
color: var(--text-neutral-dark-100, #adb4c2);
}
&:hover {
opacity: 0.8;
}
}
.lightMode {
.auth-header-logo-text {
color: var(--text-neutral-light-100, #62636c);
}
.auth-header-help-button {
background: var(--bg-vanilla-200, #f5f5f5);
span,
svg {
color: var(--text-neutral-light-200, #80828d);
}
}
}

View File

@@ -0,0 +1,33 @@
import './AuthHeader.styles.scss';
import { Button } from '@signozhq/button';
import { LifeBuoy } from 'lucide-react';
import { useCallback } from 'react';
function AuthHeader(): JSX.Element {
const handleGetHelp = useCallback((): void => {
window.open('https://signoz.io/support/', '_blank');
}, []);
return (
<header className="auth-header">
<div className="auth-header-logo">
<img
src="/Logos/signoz-brand-logo.svg"
alt="SigNoz"
className="auth-header-logo-icon"
/>
<span className="auth-header-logo-text">SigNoz</span>
</div>
<Button
className="auth-header-help-button"
prefixIcon={<LifeBuoy size={12} />}
onClick={handleGetHelp}
>
Get Help
</Button>
</header>
);
}
export default AuthHeader;

View File

@@ -0,0 +1,181 @@
@import '@signozhq/design-tokens/dist/style.css';
.auth-page-wrapper {
position: relative;
min-height: 100vh;
width: 100%;
background: var(--bg-neutral-dark-1000, #0a0c10);
display: flex;
flex-direction: column;
}
.auth-page-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
}
.auth-page-dots {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.bg-dot-pattern {
background: radial-gradient(
circle,
var(--bg-neutral-dark-50, #eceef2) 1px,
transparent 1px
);
background-size: 12px 12px;
opacity: 1;
}
.masked-dots {
mask-image: radial-gradient(
circle at 50% 0%,
rgba(11, 12, 14, 0.1) 0%,
rgba(11, 12, 14, 0) 56.77%
);
-webkit-mask-image: radial-gradient(
circle at 50% 0%,
rgba(11, 12, 14, 0.1) 0%,
rgba(11, 12, 14, 0) 56.77%
);
}
.auth-page-gradient {
position: absolute;
left: 0;
right: 0;
top: 0;
margin: 0 auto;
height: 450px;
width: 100%;
flex-shrink: 0;
border-radius: 956px;
background: radial-gradient(
ellipse at center -500px,
rgba(78, 116, 248, 0.3) 0%,
transparent 70%
);
opacity: 0.3;
filter: blur(150px);
@media (min-width: 768px) {
height: 956px;
filter: blur(300px);
}
}
.auth-page-line-left,
.auth-page-line-right {
position: absolute;
top: 0;
width: 1px;
height: 100%;
background-image: repeating-linear-gradient(
to bottom,
var(--bg-ink-200, #23262e) 0px,
var(--bg-ink-200, #23262e) 4px,
transparent 4px,
transparent 8px
);
pointer-events: none;
@media (max-width: 1440px) {
display: none;
}
}
.auth-page-line-left {
left: calc(50% - 600px);
}
.auth-page-line-right {
left: calc(50% + 600px);
}
.auth-page-layout {
position: relative;
z-index: 1;
width: 100%;
max-width: 1440px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
@media (max-width: 1440px) {
padding: 0 24px;
}
}
.auth-page-content {
flex: 1;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 8vh;
padding-bottom: 24px;
width: 100%;
position: relative;
@media (max-width: 768px) {
padding-top: 15vh;
align-items: center;
}
&.onboarding-flow {
padding-top: 0;
@media (max-width: 768px) {
padding-top: 0;
}
}
}
.lightMode {
.auth-page-wrapper {
background: var(--bg-base-white, #ffffff);
}
.bg-dot-pattern {
background: radial-gradient(circle, rgba(35, 38, 46, 1) 1px, transparent 1px);
background-size: 12px 12px;
}
.auth-page-gradient {
background: radial-gradient(
ellipse at center top,
rgba(78, 116, 248, 0.12) 0%,
transparent 60%
);
opacity: 0.8;
filter: blur(200px);
@media (min-width: 768px) {
filter: blur(300px);
}
}
.auth-page-line-left,
.auth-page-line-right {
background-image: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300, #e9e9e9) 0px,
var(--bg-vanilla-300, #e9e9e9) 4px,
transparent 4px,
transparent 8px
);
}
}

View File

@@ -0,0 +1,41 @@
import './AuthPageContainer.styles.scss';
import { PropsWithChildren } from 'react';
import AuthFooter from './AuthFooter';
import AuthHeader from './AuthHeader';
type AuthPageContainerProps = PropsWithChildren<{
isOnboarding?: boolean;
}>;
function AuthPageContainer({
children,
isOnboarding = false,
}: AuthPageContainerProps): JSX.Element {
return (
<div className="auth-page-wrapper">
<div className="auth-page-background">
<div className="auth-page-dots bg-dot-pattern masked-dots" />
<div className="auth-page-gradient" />
<div className="auth-page-line-left" />
<div className="auth-page-line-right" />
</div>
<div className="auth-page-layout">
<AuthHeader />
<main
className={`auth-page-content ${isOnboarding ? 'onboarding-flow' : ''}`}
>
{children}
</main>
<AuthFooter />
</div>
</div>
);
}
AuthPageContainer.defaultProps = {
isOnboarding: false,
};
export default AuthPageContainer;

View File

@@ -58,6 +58,7 @@
flex-direction: column;
gap: 16px;
padding-left: 30px;
margin-bottom: 1rem;
li {
position: relative;

View File

@@ -0,0 +1,102 @@
import { Calendar } from '@signozhq/calendar';
import { Button } from 'antd';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import dayjs from 'dayjs';
import { CalendarIcon, Check, X } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { DateRange } from './CustomTimePickerPopoverContent';
function CalendarContainer({
dateRange,
onSelectDateRange,
onCancel,
onApply,
}: {
dateRange: DateRange;
onSelectDateRange: (dateRange: DateRange) => void;
onCancel: () => void;
onApply: () => void;
}): JSX.Element {
const { timezone } = useTimezone();
// this is to override the default behavior of the shadcn calendar component
// if a range is already selected, clicking on a date will reset selection and set the new date as the start date
const handleSelect = (
_selected: DateRange | undefined,
clickedDate?: Date,
): void => {
if (!clickedDate) {
return;
}
// No dates selected → start new
if (!dateRange?.from) {
onSelectDateRange({ from: clickedDate });
return;
}
// Only start selected → complete the range
if (dateRange.from && !dateRange.to) {
if (clickedDate < dateRange.from) {
onSelectDateRange({ from: clickedDate, to: dateRange.from });
} else {
onSelectDateRange({ from: dateRange.from, to: clickedDate });
}
return;
}
onSelectDateRange({ from: clickedDate, to: undefined });
};
return (
<div className="calendar-container">
<div className="calendar-container-header">
<CalendarIcon size={12} />
<div className="calendar-container-header-title">
{dayjs(dateRange?.from)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.MONTH_DATE_SHORT)}{' '}
-{' '}
{dayjs(dateRange?.to)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.MONTH_DATE_SHORT)}
</div>
</div>
<div className="calendar-container-body">
<Calendar
mode="range"
required
defaultMonth={dateRange?.from}
selected={dateRange}
disabled={{
after: dayjs().toDate(),
}}
onSelect={handleSelect}
/>
<div className="calendar-actions">
<Button
type="primary"
className="periscope-btn secondary cancel-btn"
onClick={onCancel}
icon={<X size={12} />}
>
Cancel
</Button>
<Button
type="primary"
className="periscope-btn primary apply-btn"
onClick={onApply}
icon={<Check size={12} />}
>
Apply
</Button>
</div>
</div>
</div>
);
}
export default CalendarContainer;

View File

@@ -36,7 +36,6 @@
}
.time-selection-dropdown-content {
min-width: 172px;
width: 100%;
}
@@ -48,18 +47,16 @@
padding: 4px 8px;
padding-left: 0px !important;
&.custom-time {
input:not(:focus) {
min-width: 280px;
input {
width: 280px;
&::placeholder {
color: white;
}
}
input::placeholder {
color: white;
}
input:focus::placeholder {
color: rgba($color: #ffffff, $alpha: 0.4);
&:focus::placeholder {
color: rgba($color: #ffffff, $alpha: 0.4);
}
}
}
@@ -175,9 +172,26 @@
}
.time-input-prefix {
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
border-radius: 3px;
width: 36px;
font-size: 11px;
color: var(--bg-vanilla-400);
background-color: var(--bg-ink-200);
&.is-live {
background-color: transparent;
color: var(--bg-forest-500);
}
.live-dot-icon {
width: 6px;
height: 6px;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--bg-forest-500);
animation: ripple 1s infinite;
@@ -191,7 +205,7 @@
0% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4);
}
70% {
60% {
box-shadow: 0 0 0 6px rgba(245, 158, 11, 0);
}
100% {
@@ -251,6 +265,11 @@
background: rgb(179 179 179 / 15%);
}
.time-input-prefix {
background-color: var(--bg-vanilla-300);
color: var(--bg-ink-400);
}
.time-input-suffix-icon-badge {
color: var(--bg-ink-100);
background: rgb(179 179 179 / 15%);

View File

@@ -1,8 +1,9 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import './CustomTimePicker.styles.scss';
import { Input, Popover, Tooltip, Typography } from 'antd';
import { Input, InputRef, Popover, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
@@ -13,10 +14,9 @@ import {
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { isValidTimeFormat } from 'lib/getMinMax';
import { isValidShortHandDateTimeFormat } from 'lib/getMinMax';
import { defaultTo, isFunction, noop } from 'lodash-es';
import debounce from 'lodash-es/debounce';
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import {
ChangeEvent,
@@ -25,20 +25,26 @@ import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getTimeDifference, validateEpochRange } from 'utils/epochUtils';
import { popupContainer } from 'utils/selectPopupContainer';
import { TimeRangeValidationResult, validateTimeRange } from 'utils/timeUtils';
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
const maxAllowedMinTimeInMonths = 6;
const maxAllowedMinTimeInMonths = 15;
type ViewType = 'datetime' | 'timezone';
const DEFAULT_VIEW: ViewType = 'datetime';
export enum CustomTimePickerInputStatus {
SUCCESS = 'success',
ERROR = 'error',
UNSET = '',
}
interface CustomTimePickerProps {
onSelect: (value: string) => void;
onError: (value: boolean) => void;
@@ -64,6 +70,8 @@ interface CustomTimePickerProps {
onExitLiveLogs?: () => void;
/** When false, hides the "Recently Used" time ranges section */
showRecentlyUsed?: boolean;
minTime: number;
maxTime: number;
}
function CustomTimePicker({
@@ -84,23 +92,24 @@ function CustomTimePicker({
onExitLiveLogs,
showLiveLogs,
showRecentlyUsed = true,
minTime,
maxTime,
}: CustomTimePickerProps): JSX.Element {
const [
selectedTimePlaceholderValue,
setSelectedTimePlaceholderValue,
] = useState('Select / Enter Time Range');
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [inputValue, setInputValue] = useState('');
const [inputStatus, setInputStatus] = useState<'' | 'error' | 'success'>('');
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
null,
const [inputStatus, setInputStatus] = useState<CustomTimePickerInputStatus>(
CustomTimePickerInputStatus.UNSET,
);
const [inputErrorDetails, setInputErrorDetails] = useState<
TimeRangeValidationResult['errorDetails'] | null
>(null);
const location = useLocation();
const [isInputFocused, setIsInputFocused] = useState(false);
const inputRef = useRef<InputRef>(null);
const [activeView, setActiveView] = useState<ViewType>(DEFAULT_VIEW);
@@ -123,12 +132,48 @@ function CustomTimePicker({
const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false);
// function to get selected time in Last 1m, Last 2h, Last 3d, Last 4w format
// 1m, 2h, 3d, 4w -> Last 1 minute, Last 2 hours, Last 3 days, Last 4 weeks
const getSelectedTimeRangeLabelInRelativeFormat = (
selectedTime: string,
): string => {
if (!selectedTime || selectedTime === 'custom') {
return selectedTime || '';
}
// Check if the format matches the relative time format (e.g., 1m, 2h, 3d, 4w)
const match = selectedTime.match(/^(\d+)([mhdw])$/);
if (!match) {
// If it doesn't match the format, return as is
return `Last ${selectedTime}`;
}
const value = parseInt(match[1], 10);
const unit = match[2];
// Map unit abbreviations to full words
const unitMap: Record<string, { singular: string; plural: string }> = {
m: { singular: 'minute', plural: 'minutes' },
h: { singular: 'hour', plural: 'hours' },
d: { singular: 'day', plural: 'days' },
w: { singular: 'week', plural: 'weeks' },
};
const unitLabel = value === 1 ? unitMap[unit].singular : unitMap[unit].plural;
return `Last ${value} ${unitLabel}`;
};
const getSelectedTimeRangeLabel = (
selectedTime: string,
selectedTimeValue: string,
): string => {
if (!selectedTime) {
return '';
}
if (selectedTime === 'custom') {
// TODO(shaheer): if the user preference is 12 hour format, then convert the date range string to 12-hour format (pick this up while working on 12/24 hour preference feature)
// TODO: if the user preference is 12 hour format, then convert the date range string to 12-hour format (pick this up while working on 12/24 hour preference feature)
// // Convert the date range string to 12-hour format
// const dates = selectedTimeValue.split(' - ');
// if (dates.length === 2) {
@@ -164,42 +209,90 @@ function CustomTimePicker({
}
}
if (isValidTimeFormat(selectedTime)) {
return selectedTime;
if (isValidShortHandDateTimeFormat(selectedTime)) {
return getSelectedTimeRangeLabelInRelativeFormat(selectedTime);
}
return '';
};
const resetErrorStatus = (): void => {
setInputStatus(CustomTimePickerInputStatus.UNSET);
onError(false);
setInputErrorDetails(null);
};
useEffect(() => {
if (showLiveLogs) {
setSelectedTimePlaceholderValue('Live');
setInputValue('Live');
resetErrorStatus();
} else {
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setSelectedTimePlaceholderValue(value);
setInputValue(value);
resetErrorStatus();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedTime, selectedValue, showLiveLogs]);
const hide = (): void => {
setOpen(false);
};
const getInputPrefix = (): JSX.Element => {
if (showLiveLogs) {
return (
<span className="time-input-prefix is-live">
<span className="live-dot-icon" />
</span>
);
}
const timeDifference = getTimeDifference(
Number(minTime / 1000_000),
Number(maxTime / 1000_000),
);
return <span className="time-input-prefix">{timeDifference}</span>;
};
const handleOpenChange = (newOpen: boolean): void => {
setOpen(newOpen);
if (!newOpen) {
setCustomDTPickerVisible?.(false);
setActiveView('datetime');
if (showLiveLogs) {
setSelectedTimePlaceholderValue('Live');
setInputValue('Live');
return;
}
// set the input value to a relative format if the selected time is not custom
const inputValue = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setInputValue(inputValue);
}
};
const debouncedHandleInputChange = debounce((inputValue): void => {
const isValidFormat = /^(\d+)([mhdw])$/.test(inputValue);
if (isValidFormat) {
setInputStatus('success');
onError(false);
setInputErrorMessage(null);
const handleInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
const inputValue = event.target.value;
setInputValue(inputValue);
const match = inputValue.match(/^(\d+)([mhdw])$/);
resetErrorStatus();
};
const handleInputPressEnter = (): void => {
// check if the entered time is in the format of 1m, 2h, 3d, 4w
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);
if (isTimeDurationShortHandFormat) {
setInputStatus(CustomTimePickerInputStatus.SUCCESS);
onError(false);
setInputErrorDetails(null);
const match = inputValue.match(/^(\d+)([mhdw])$/) as RegExpMatchArray;
const value = parseInt(match[1], 10);
const unit = match[2];
@@ -230,9 +323,13 @@ function CustomTimePicker({
}
if (minTime && (!minTime.isValid() || minTime < maxAllowedMinTime)) {
setInputStatus('error');
setInputStatus(CustomTimePickerInputStatus.ERROR);
onError(true);
setInputErrorMessage('Please enter time less than 6 months');
setInputErrorDetails({
message: `Please enter time less than ${maxAllowedMinTimeInMonths} months`,
code: 'TIME_LESS_THAN_MAX_ALLOWED_TIME_IN_MONTHS',
description: `Please enter time less than ${maxAllowedMinTimeInMonths} months`,
});
if (isFunction(onCustomTimeStatusUpdate)) {
onCustomTimeStatusUpdate(true);
}
@@ -241,44 +338,64 @@ function CustomTimePicker({
time: [minTime, currentTime],
timeStr: inputValue,
});
setOpen(false);
}
} else {
setInputStatus('error');
onError(true);
setInputErrorMessage(null);
if (isFunction(onCustomTimeStatusUpdate)) {
onCustomTimeStatusUpdate(false);
}
return;
}
}, 300);
const handleInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
const inputValue = event.target.value;
// parse the input value to get the start and end time
const [startTime, endTime] = inputValue.split(/\s[-]\s/);
// check if startTime and endTime are epoch format
const { isValid: isValidStartTime, range: epochRange } = validateEpochRange(
Number(startTime),
Number(endTime),
);
if (isValidStartTime && epochRange?.startTime && epochRange?.endTime) {
onCustomDateHandler?.([epochRange?.startTime, epochRange?.endTime]);
if (inputValue.length > 0) {
setOpen(false);
} else {
setOpen(true);
return;
}
setInputValue(inputValue);
const {
isValid: isValidTimeRange,
errorDetails,
startTimeMs,
endTimeMs,
} = validateTimeRange(
startTime,
endTime,
DATE_TIME_FORMATS.UK_DATETIME_SECONDS,
);
// Call the debounced function with the input value
debouncedHandleInputChange(inputValue);
if (!isValidTimeRange) {
setInputStatus(CustomTimePickerInputStatus.ERROR);
onError(true);
setInputErrorDetails(errorDetails || null);
return;
}
onCustomDateHandler?.([dayjs(startTimeMs), dayjs(endTimeMs)]);
setOpen(false);
};
const handleSelect = (label: string, value: string): void => {
if (label === 'Custom') {
if (value === 'custom') {
setCustomDTPickerVisible?.(true);
return;
}
onSelect(value);
setSelectedTimePlaceholderValue(label);
setInputStatus('');
onError(false);
setInputErrorMessage(null);
resetErrorStatus();
setInputValue('');
if (value !== 'custom') {
hide();
}
@@ -305,20 +422,48 @@ function CustomTimePicker({
</div>
);
const handleFocus = (): void => {
setIsInputFocused(true);
setActiveView('datetime');
const handleOpen = (e: React.SyntheticEvent): void => {
e.stopPropagation();
if (showLiveLogs) {
setOpen(true);
setSelectedTimePlaceholderValue('Live');
setInputValue('Live');
return;
}
setOpen(true);
// reset the input status and error message as we reset the time to previous correct value
resetErrorStatus();
const startTime = dayjs(minTime / 1000_000).format(
DATE_TIME_FORMATS.UK_DATETIME_SECONDS,
);
const endTime = dayjs(maxTime / 1000_000).format(
DATE_TIME_FORMATS.UK_DATETIME_SECONDS,
);
setInputValue(`${startTime} - ${endTime}`);
};
const handleBlur = (): void => {
setIsInputFocused(false);
const handleClose = (e: React.MouseEvent): void => {
e.stopPropagation();
setOpen(false);
setCustomDTPickerVisible?.(false);
if (showLiveLogs) {
setInputValue('Live');
return;
}
// set the input value to a relative format if the selected time is not custom
const inputValue = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setInputValue(inputValue);
};
// this is required as TopNav component wraps the components and we need to clear the state on path change
useEffect(() => {
setInputStatus('');
onError(false);
setInputErrorMessage(null);
resetErrorStatus();
setInputValue('');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname]);
@@ -335,6 +480,10 @@ function CustomTimePicker({
);
};
const handleInputBlur = (): void => {
resetErrorStatus();
};
const getTooltipTitle = (): string => {
if (selectedTime === 'custom' && inputValue === '' && !open) {
return `${dayjs(minTime / 1000_000)
@@ -349,27 +498,19 @@ function CustomTimePicker({
return '';
};
const getInputPrefix = (): JSX.Element => {
if (showLiveLogs) {
return (
<div className="time-input-prefix">
<div className="live-dot-icon" />
</div>
);
// Focus and select input text when popover opens
useEffect(() => {
if (open && inputRef.current) {
// Use setTimeout to wait for React to update the DOM and make input editable
setTimeout(() => {
const inputElement = inputRef.current?.input;
if (inputElement) {
inputElement.focus();
inputElement.select();
}
}, 0);
}
return (
<div className="time-input-prefix">
{inputValue && inputStatus === 'success' ? (
<CheckCircle size={14} color="#51E7A8" />
) : (
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
<Clock size={14} className="cursor-pointer" />
</Tooltip>
)}
</div>
);
};
}, [open]);
return (
<div className="custom-time-picker">
@@ -385,9 +526,10 @@ function CustomTimePicker({
content={
newPopover ? (
<CustomTimePickerPopoverContent
isLiveLogsEnabled={!!showLiveLogs}
setIsOpen={setOpen}
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
onSelectHandler={handleSelect}
onGoLive={defaultTo(onGoLive, noop)}
@@ -399,6 +541,10 @@ function CustomTimePicker({
setIsOpenedFromFooter={setIsOpenedFromFooter}
isOpenedFromFooter={isOpenedFromFooter}
showRecentlyUsed={showRecentlyUsed}
customDateTimeInputStatus={inputStatus}
inputErrorDetails={inputErrorDetails}
minTime={minTime}
maxTime={maxTime}
/>
) : (
content
@@ -407,25 +553,32 @@ function CustomTimePicker({
arrow={false}
trigger="click"
open={open}
destroyTooltipOnHide
onOpenChange={handleOpenChange}
style={{
padding: 0,
}}
>
<Input
className="timeSelection-input"
ref={inputRef}
className={cx(
'timeSelection-input',
inputStatus === CustomTimePickerInputStatus.ERROR ? 'error' : '',
)}
type="text"
status={inputValue && inputStatus === 'error' ? 'error' : ''}
placeholder={
isInputFocused
? 'Time Format (1m or 2h or 3d or 4w)'
: selectedTimePlaceholderValue
status={
inputValue && inputStatus === CustomTimePickerInputStatus.ERROR
? 'error'
: ''
}
readOnly={!open || showLiveLogs}
placeholder={selectedTimePlaceholderValue}
value={inputValue}
onFocus={handleFocus}
onClick={handleFocus}
onBlur={handleBlur}
onFocus={handleOpen}
onClick={handleOpen}
onChange={handleInputChange}
onPressEnter={handleInputPressEnter}
onBlur={handleInputBlur}
data-1p-ignore
prefix={getInputPrefix()}
suffix={
@@ -435,24 +588,25 @@ function CustomTimePicker({
<span>{activeTimezoneOffset}</span>
</div>
)}
<ChevronDown
size={14}
className="cursor-pointer time-input-suffix-icon-badge"
onClick={(e): void => {
e.stopPropagation();
handleViewChange('datetime');
}}
/>
{open ? (
<ChevronUp
size={14}
className="cursor-pointer time-input-suffix-icon-badge"
onClick={handleClose}
/>
) : (
<ChevronDown
size={14}
className="cursor-pointer time-input-suffix-icon-badge"
onClick={handleOpen}
/>
)}
</div>
}
/>
</Popover>
</Tooltip>
{inputStatus === 'error' && inputErrorMessage && (
<Typography.Title level={5} className="valid-format-error">
{inputErrorMessage}
</Typography.Title>
)}
</div>
);
}

View File

@@ -4,7 +4,6 @@ import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import DatePickerV2 from 'components/DatePickerV2/DatePickerV2';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
@@ -15,7 +14,7 @@ import {
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { Clock, PenLine } from 'lucide-react';
import { Clock, PenLine, TriangleAlertIcon } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import {
Dispatch,
@@ -27,10 +26,23 @@ import {
} from 'react';
import { useLocation } from 'react-router-dom';
import { getCustomTimeRanges } from 'utils/customTimeRangeUtils';
import { TimeRangeValidationResult } from 'utils/timeUtils';
import CalendarContainer from './CalendarContainer';
import { CustomTimePickerInputStatus } from './CustomTimePicker';
import TimezonePicker from './TimezonePicker';
const TO_MILLISECONDS_FACTOR = 1000_000;
export type DateRange = {
from: Date | undefined;
to?: Date | undefined;
};
interface CustomTimePickerPopoverContentProps {
isLiveLogsEnabled: boolean;
minTime: number;
maxTime: number;
options: any[];
setIsOpen: Dispatch<SetStateAction<boolean>>;
customDateTimeVisible: boolean;
@@ -48,6 +60,8 @@ interface CustomTimePickerPopoverContentProps {
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
onExitLiveLogs: () => void;
showRecentlyUsed: boolean;
customDateTimeInputStatus: CustomTimePickerInputStatus;
inputErrorDetails: TimeRangeValidationResult['errorDetails'] | null;
}
interface RecentlyUsedDateTimeRange {
@@ -58,8 +72,29 @@ interface RecentlyUsedDateTimeRange {
to: string;
}
const getDateRange = (
minTime: number,
maxTime: number,
timezone: string,
): DateRange => {
const from = dayjs(minTime / TO_MILLISECONDS_FACTOR)
.tz(timezone)
.startOf('day')
.toDate();
const to = dayjs(maxTime / TO_MILLISECONDS_FACTOR)
.tz(timezone)
.endOf('day')
.toDate();
return { from, to };
};
// eslint-disable-next-line sonarjs/cognitive-complexity
function CustomTimePickerPopoverContent({
isLiveLogsEnabled,
minTime,
maxTime,
options,
setIsOpen,
customDateTimeVisible,
@@ -74,6 +109,8 @@ function CustomTimePickerPopoverContent({
setIsOpenedFromFooter,
onExitLiveLogs,
showRecentlyUsed = true,
customDateTimeInputStatus = CustomTimePickerInputStatus.UNSET,
inputErrorDetails,
}: CustomTimePickerPopoverContentProps): JSX.Element {
const { pathname } = useLocation();
@@ -83,6 +120,9 @@ function CustomTimePickerPopoverContent({
const url = new URLSearchParams(window.location.search);
const { timezone } = useTimezone();
const activeTimezoneOffset = timezone.offset;
let panelTypeFromURL = url.get(QueryParams.panelTypes);
try {
@@ -94,8 +134,9 @@ function CustomTimePickerPopoverContent({
const isLogsListView =
panelTypeFromURL !== 'table' && panelTypeFromURL !== 'graph'; // we do not select list view in the url
const { timezone } = useTimezone();
const activeTimezoneOffset = timezone.offset;
const [dateRange, setDateRange] = useState<DateRange>(() =>
getDateRange(minTime, maxTime, timezone.value),
);
const [recentlyUsedTimeRanges, setRecentlyUsedTimeRanges] = useState<
RecentlyUsedDateTimeRange[]
@@ -177,36 +218,66 @@ function CustomTimePickerPopoverContent({
setIsOpen(false);
};
const handleSelectDateRange = (dateRange: DateRange): void => {
setDateRange(dateRange);
};
const handleCalendarRangeApply = (): void => {
if (dateRange) {
const from = dayjs(dateRange.from)
.tz(timezone.value)
.startOf('day')
.toDate();
const to = dayjs(dateRange.to).tz(timezone.value).endOf('day').toDate();
onCustomDateHandler([dayjs(from), dayjs(to)]);
}
setIsOpen(false);
};
const handleCalendarRangeCancel = (): void => {
setCustomDTPickerVisible(false);
};
return (
<>
<div className="date-time-popover">
{!customDateTimeVisible && (
<div className="date-time-options">
{isLogsExplorerPage && isLogsListView && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
handleExitLiveLogs();
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
</div>
)}
<div className="date-time-options">
{isLogsExplorerPage && isLogsListView && (
<Button
className={cx('data-time-live', isLiveLogsEnabled ? 'active' : '')}
type="text"
onClick={handleGoLive}
>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(e: React.MouseEvent<HTMLButtonElement>): void => {
e.stopPropagation();
e.preventDefault();
handleExitLiveLogs();
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && !isLiveLogsEnabled && 'active'
: selectedTime === option.value && !isLiveLogsEnabled && 'active',
)}
>
<span className="time-label">{option.label}</span>
{option.value !== 'custom' && option.value !== '1month' && (
<span className="time-value">{option.value}</span>
)}
</Button>
))}
</div>
<div
className={cx(
'relative-date-time',
@@ -214,19 +285,38 @@ function CustomTimePickerPopoverContent({
)}
>
{customDateTimeVisible ? (
<DatePickerV2
onSetCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
<CalendarContainer
dateRange={dateRange}
onSelectDateRange={handleSelectDateRange}
onCancel={handleCalendarRangeCancel}
onApply={handleCalendarRangeApply}
/>
) : (
<div className="time-selector-container">
{customDateTimeInputStatus === CustomTimePickerInputStatus.ERROR &&
inputErrorDetails && (
<div className="input-error-message-container">
<div className="input-error-message-title">
<TriangleAlertIcon color={Color.BG_CHERRY_400} size={16} />
<span className="input-error-message-text">
{inputErrorDetails.message}
</span>
</div>
{inputErrorDetails.description && (
<p className="input-error-message-description">
{inputErrorDetails.description}
</p>
)}
</div>
)}
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
{showRecentlyUsed && (
{showRecentlyUsed && recentlyUsedTimeRanges.length > 0 && (
<div className="recently-used-container">
<div className="time-heading">RECENTLY USED</div>
<div className="recently-used-range">

View File

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

View File

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

View File

@@ -6,13 +6,15 @@ import ErrorIcon from 'assets/Error';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { BookOpenText, ChevronsDown } from 'lucide-react';
import KeyValueLabel from 'periscope/components/KeyValueLabel';
import { ReactNode } from 'react';
import APIError from 'types/api/error';
interface ErrorContentProps {
error: APIError;
icon?: ReactNode;
}
function ErrorContent({ error }: ErrorContentProps): JSX.Element {
function ErrorContent({ error, icon }: ErrorContentProps): JSX.Element {
const {
url: errorUrl,
errors: errorMessages,
@@ -25,9 +27,7 @@ function ErrorContent({ error }: ErrorContentProps): JSX.Element {
<section className="error-content__summary-section">
<header className="error-content__summary">
<div className="error-content__summary-left">
<div className="error-content__icon-wrapper">
<ErrorIcon />
</div>
<div className="error-content__icon-wrapper">{icon || <ErrorIcon />}</div>
<div className="error-content__summary-text">
<h2 className="error-content__error-code">{errorCode}</h2>
@@ -95,4 +95,8 @@ function ErrorContent({ error }: ErrorContentProps): JSX.Element {
);
}
ErrorContent.defaultProps = {
icon: undefined,
};
export default ErrorContent;

View File

@@ -1,4 +1,5 @@
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
// eslint-disable-next-line import/namespace -- side-effect import that registers Chart.js date adapter
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';

View File

@@ -176,6 +176,8 @@ function HostMetricTraces({
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
</div>
</div>

View File

@@ -87,6 +87,8 @@ function HostMetricLogsDetailedView({
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
</div>
</div>

View File

@@ -211,6 +211,8 @@ function Metrics({
defaultRelativeTime="5m"
isModalTimeSelection={isModalTimeSelection}
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
</div>
</div>

View File

@@ -1,16 +1,16 @@
import { DrawerProps } from 'antd';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { VIEWS } from './constants';
export type LogDetailProps = {
log: ILog | null;
selectedTab: VIEWS;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
handleChangeSelectedView?: ChangeViewFunctionType;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &

View File

@@ -55,11 +55,11 @@ function LogDetailInner({
log,
onClose,
onAddToQuery,
onGroupByAttribute,
onClickActionItem,
selectedTab,
isListViewPanel = false,
listViewPanelSelectedFields,
handleChangeSelectedView,
}: LogDetailInnerProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query | undefined>(
@@ -365,10 +365,10 @@ function LogDetailInner({
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
onGroupByAttribute={onGroupByAttribute}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}

View File

@@ -6,6 +6,7 @@ import cx from 'classnames';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
@@ -108,6 +109,7 @@ type ListLogViewProps = {
activeLog?: ILog | null;
linesPerRow: number;
fontSize: FontSize;
handleChangeSelectedView?: ChangeViewFunctionType;
};
function ListLogView({
@@ -118,6 +120,7 @@ function ListLogView({
activeLog,
linesPerRow,
fontSize,
handleChangeSelectedView,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@@ -131,7 +134,6 @@ function ListLogView({
onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
onGroupByAttribute,
} = useActiveLog();
const isDarkMode = useIsDarkMode();
@@ -255,7 +257,7 @@ function ListLogView({
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog}
onGroupByAttribute={onGroupByAttribute}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</>
@@ -264,6 +266,7 @@ function ListLogView({
ListLogView.defaultProps = {
activeLog: null,
handleChangeSelectedView: undefined,
};
LogGeneralField.defaultProps = {

View File

@@ -39,6 +39,7 @@ function RawLogView({
selectedFields = [],
fontSize,
onLogClick,
handleChangeSelectedView,
}: RawLogViewProps): JSX.Element {
const {
isHighlighted: isUrlHighlighted,
@@ -52,7 +53,6 @@ function RawLogView({
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<VIEWS | undefined>();
@@ -224,13 +224,12 @@ function RawLogView({
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</RawLogViewContainer>
);
}
RawLogView.defaultProps = {
isActiveLog: false,
isReadOnly: false,

View File

@@ -1,3 +1,4 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { FontSize } from 'container/OptionsMenu/types';
import { MouseEvent } from 'react';
import { IField } from 'types/api/logs/fields';
@@ -14,6 +15,7 @@ export interface RawLogViewProps {
fontSize: FontSize;
selectedFields?: IField[];
onLogClick?: (log: ILog, event: MouseEvent) => void;
handleChangeSelectedView?: ChangeViewFunctionType;
}
export interface RawLogContentProps {

View File

@@ -7,7 +7,7 @@ import { VirtuosoMockContext } from 'react-virtuoso';
import configureStore from 'redux-mock-store';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import VariableItem from '../../../container/NewDashboard/DashboardVariablesSelection/VariableItem';
import VariableItem from '../../../container/DashboardContainer/DashboardVariablesSelection/VariableItem';
// Mock the dashboard variables query
jest.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({

View File

@@ -1,5 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { uniqueOptions } from 'container/NewDashboard/DashboardVariablesSelection/util';
import { uniqueOptions } from 'container/DashboardContainer/DashboardVariablesSelection/util';
import { OptionData } from './types';

View File

@@ -53,4 +53,5 @@ export enum QueryParams {
variables = 'variables',
version = 'version',
showNewCreateAlertsPage = 'showNewCreateAlertsPage',
source = 'source',
}

View File

@@ -301,6 +301,7 @@ export const initialQueryState: QueryState = {
builder: initialQueryBuilderData,
clickhouse_sql: [initialClickHouseData],
promql: [initialQueryPromQLData],
unit: '',
};
const initialQueryWithType: Query = {

View File

@@ -1,47 +1,3 @@
.settings-container-root {
.ant-drawer-wrapper-body {
border-left: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
.ant-drawer-header {
height: 48px;
border-bottom: 1px solid var(--bg-slate-500);
padding: 14px 14px 14px 11px;
.ant-drawer-header-title {
gap: 16px;
.ant-drawer-title {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
padding-left: 16px;
border-left: 1px solid #161922;
}
.ant-drawer-close {
height: 16px;
width: 16px;
margin-inline-end: 0px !important;
}
}
}
.ant-drawer-body {
padding: 16px;
&::-webkit-scrollbar {
width: 0.1rem;
}
}
}
}
.dashboard-description-container {
box-shadow: none;
border: none;
@@ -576,28 +532,6 @@
}
.lightMode {
.settings-container-root {
.ant-drawer-wrapper-body {
border-left: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.ant-drawer-header {
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-drawer-header-title {
.ant-drawer-title {
color: var(--bg-ink-400);
border-left: 1px solid var(--bg-vanilla-300);
}
.ant-drawer-close {
color: var(--bg-ink-300);
}
}
}
}
}
.dashboard-description-container {
color: var(--bg-ink-400);

View File

@@ -0,0 +1,67 @@
.settings-container-root {
.ant-drawer-wrapper-body {
border-left: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
.ant-drawer-header {
height: 48px;
border-bottom: 1px solid var(--bg-slate-500);
padding: 14px 14px 14px 11px;
.ant-drawer-header-title {
gap: 16px;
.ant-drawer-title {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
padding-left: 16px;
border-left: 1px solid var(--bg-slate-500);
}
.ant-drawer-close {
height: 16px;
width: 16px;
margin-inline-end: 0px !important;
}
}
}
.ant-drawer-body {
padding: 16px;
&::-webkit-scrollbar {
width: 0.1rem;
}
}
}
}
.lightMode {
.settings-container-root {
.ant-drawer-wrapper-body {
border-left: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.ant-drawer-header {
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-drawer-header-title {
.ant-drawer-title {
color: var(--bg-ink-400);
border-left: 1px solid var(--bg-vanilla-300);
}
.ant-drawer-close {
color: var(--bg-ink-300);
}
}
}
}
}
}

View File

@@ -0,0 +1,34 @@
import './SettingsDrawer.styles.scss';
import { Drawer } from 'antd';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { memo, PropsWithChildren, ReactElement } from 'react';
type SettingsDrawerProps = PropsWithChildren<{
drawerTitle: string;
isOpen: boolean;
onClose: () => void;
}>;
function SettingsDrawer({
children,
drawerTitle,
isOpen,
onClose,
}: SettingsDrawerProps): JSX.Element {
return (
<Drawer
title={drawerTitle}
placement="right"
width="50%"
onClose={onClose}
open={isOpen}
rootClassName="settings-container-root"
>
{/* Need to type cast because of OverlayScrollbar type definition. We should be good once we remove it. */}
<OverlayScrollbar>{children as ReactElement}</OverlayScrollbar>
</Drawer>
);
}
export default memo(SettingsDrawer);

View File

@@ -12,6 +12,7 @@ import {
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent';
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
@@ -40,7 +41,7 @@ import {
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { sortLayout } from 'providers/Dashboard/util';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { FullScreenHandle } from 'react-full-screen';
import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next';
@@ -52,9 +53,11 @@ import { ComponentTypes } from 'utils/permission';
import { v4 as uuid } from 'uuid';
import DashboardGraphSlider from '../ComponentsSlider';
import DashboardSettings from '../DashboardSettings';
import { Base64Icons } from '../DashboardSettings/General/utils';
import DashboardVariableSelection from '../DashboardVariablesSelection';
import SettingsDrawer from './SettingsDrawer';
import { VariablesSettingsTab } from './types';
import { DEFAULT_ROW_NAME, downloadObjectAsJson } from './utils';
interface DashboardDescriptionProps {
@@ -101,6 +104,11 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
handleDashboardLockToggle,
} = useDashboard();
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
false,
);
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
const isPublicDashboardEnabled = isCloudUser || isEnterpriseSelfHostedUser;
@@ -340,6 +348,18 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
publicDashboardResponse?.data,
]);
const onConfigureClick = useCallback((): void => {
setIsSettingsDrawerOpen(true);
}, []);
const onSettingsDrawerClose = useCallback((): void => {
setIsSettingsDrawerOpen(false);
// good use case for a state library like Jotai
if (variablesSettingsTabHandle.current) {
variablesSettingsTabHandle.current.resetState();
}
}, []);
return (
<Card className="dashboard-description-container">
<div className="dashboard-header">
@@ -504,7 +524,26 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
/>
</Popover>
{!isDashboardLocked && editDashboard && (
<SettingsDrawer drawerTitle="Dashboard Configuration" />
<>
<Button
type="text"
className="configure-button"
icon={<ConfigureIcon />}
data-testid="show-drawer"
onClick={onConfigureClick}
>
Configure
</Button>
<SettingsDrawer
drawerTitle="Dashboard Configuration"
isOpen={isSettingsDrawerOpen}
onClose={onSettingsDrawerClose}
>
<DashboardSettings
variablesSettingsTabHandle={variablesSettingsTabHandle}
/>
</SettingsDrawer>
</>
)}
{!isDashboardLocked && addPanelPermission && (
<Button

View File

@@ -0,0 +1,7 @@
import { MutableRefObject } from 'react';
export interface VariablesSettingsTab {
resetState: () => void;
}
export type VariablesSettingsTabHandle = MutableRefObject<VariablesSettingsTab | null>;

View File

@@ -14,7 +14,8 @@ import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, Modal, Row, Space, Table, Typography } from 'antd';
import { RowProps } from 'antd/lib';
import { convertVariablesToDbFormat } from 'container/NewDashboard/DashboardVariablesSelection/util';
import { VariablesSettingsTabHandle } from 'container/DashboardContainer/DashboardDescription/types';
import { convertVariablesToDbFormat } from 'container/DashboardContainer/DashboardVariablesSelection/util';
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
@@ -77,10 +78,10 @@ function TableRow({ children, ...props }: RowProps): JSX.Element {
);
}
function VariablesSetting({
variableViewModeRef,
function VariablesSettings({
variablesSettingsTabHandle,
}: {
variableViewModeRef: React.MutableRefObject<(() => void) | undefined>;
variablesSettingsTabHandle: VariablesSettingsTabHandle;
}): JSX.Element {
const variableToDelete = useRef<IDashboardVariable | null>(null);
const [deleteVariableModal, setDeleteVariableModal] = useState(false);
@@ -126,11 +127,13 @@ function VariablesSetting({
};
useEffect(() => {
if (variableViewModeRef) {
// eslint-disable-next-line no-param-reassign
variableViewModeRef.current = onDoneVariableViewMode;
}
}, [variableViewModeRef]);
if (!variablesSettingsTabHandle) return;
// eslint-disable-next-line no-param-reassign
variablesSettingsTabHandle.current = {
resetState: onDoneVariableViewMode,
};
}, [variablesSettingsTabHandle]);
const updateMutation = useUpdateDashboard();
@@ -510,4 +513,4 @@ function VariablesSetting({
);
}
export default VariablesSetting;
export default VariablesSettings;

View File

@@ -1,7 +1,7 @@
import './GeneralSettings.styles.scss';
import { Col, Input, Select, Space, Typography } from 'antd';
import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags';
import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddTags';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { isEqual } from 'lodash-es';
import { Check, X } from 'lucide-react';

View File

@@ -6,14 +6,15 @@ import { Braces, Globe, Table } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
import { VariablesSettingsTabHandle } from '../DashboardDescription/types';
import DashboardVariableSettings from './DashboardVariableSettings';
import GeneralDashboardSettings from './General';
import PublicDashboardSetting from './PublicDashboard';
import VariablesSetting from './Variables';
function DashboardSettingsContent({
variableViewModeRef,
function DashboardSettings({
variablesSettingsTabHandle,
}: {
variableViewModeRef: React.MutableRefObject<(() => void) | undefined>;
variablesSettingsTabHandle: VariablesSettingsTabHandle;
}): JSX.Element {
const { user } = useAppContext();
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
@@ -63,7 +64,11 @@ function DashboardSettingsContent({
</Button>
),
key: 'variables',
children: <VariablesSetting variableViewModeRef={variableViewModeRef} />,
children: (
<DashboardVariableSettings
variablesSettingsTabHandle={variablesSettingsTabHandle}
/>
),
},
...(enablePublicDashboard ? [publicDashboardItem] : []),
];
@@ -71,4 +76,4 @@ function DashboardSettingsContent({
return <Tabs items={items} animated className="settings-tabs" />;
}
export default DashboardSettingsContent;
export default DashboardSettings;

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