Compare commits

...

343 Commits

Author SHA1 Message Date
aks07
319d535af6 Merge branch 'main' of github.com:SigNoz/signoz into feat/interactive-dashbaord-v2 2025-09-07 13:55:42 +05:30
aks07
3e37fdf6af fix: hide drilldown options in view mode for non-builder panels 2025-09-07 13:52:58 +05:30
SagarRajput-7
0f5825a2b3 chore: product link and retires retry condition (#9018)
* feat: product link and retires retry condition

* feat: retry logic for 4xx and 5xx

* chore: tooltip editorial changes (#9019)

* chore: tooltip editorial changes

* feat: fixed lint error and styling

---------

Co-authored-by: SagarRajput-7 <sagar@signoz.io>

* feat: added test cases

* feat: fixed lint error

* feat: added product link in single select for dyn variables

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-09-07 13:36:12 +05:30
aks07
9ec44d3186 Merge branch 'main' of github.com:SigNoz/signoz into feat/interactive-dashbaord-v2 2025-09-07 13:34:04 +05:30
Aditya Singh
eee96503ff feat/interactive dashbaord v2 (#9011)
* feat: add drilldown options in uplot

* feat: add time range to timeseries, bar charts

* feat: remove unwanted code

* feat: minor refactor

* feat: drilldown prop drilldowned

* feat: refactor code

* feat: update click plugin in uplot

* feat: lint fix

* feat: add search to breakout and other refactor

* feat: context menu - increase width and add overlay

* feat: add context links

* feat: context links init

* feat: context links init

* feat: context links init

* feat: update context link modal form init

* feat: add double way sync on urls and param

* feat: minor refactor

* feat: minor refactor

* feat: change contextlinks data structure

* feat: context menu changes init

* feat: context menu hook refactor

* feat: context links processors

* feat: context variables hook added

* feat: add support for field variables

* feat: minor refactor

* feat: minor refactor

* feat: minor refactor

* feat: handle on save

* feat: minor refactor

* feat: snapshot update

* feat: revert qbv5

* feat: aggregation header val

* feat: fix header color

* feat: minor refactor

* feat: minor refactor

* feat: fix breaking changes from qb v5

* feat: change api for breakout opitons

* feat: minor refactor

* feat: minor refactor

* fix: added fix for extractquerypararms when value is string in multivalue operator

* feat: minor refactor

* feat: add back in breakout

* feat: minor refactor

* feat: add substitute var api call to decode vars

* feat: minor fix

* feat: optimize query value comparison in QueryBuilderV2

* feat: minor fix

* feat: minor fix

* feat: test fix

* feat: added dynamic variables creation flow (#7541)

* feat: added dynamic variables creation flow

* feat: added keys and value apis and hooks

* feat: added api and select component changes

* feat: added keys fetching and preview values

* feat: added dynamic variable to variable items

* feat: handled value persistence and tab switches

* feat: added default value and formed a schema for dyn-variables

* feat: added client and server side searches

* feat: corrected the initial load getfieldKey api

* feat: removed fetch on mount restriction

* feat: added dynamic variable to the dashboard details (#7755)

* feat: added dynamic variable to the dashboard details

* feat: added new component to existing variables

* feat: added enhancement to multiselect and select for dyn-variables

* feat: added refetch method between all dynamic-variables

* feat: correct error handling

* feat: correct error handling

* feat: enforced non-empty selectedvalues and default value

* feat: added client and server side searches

* feat: retry on error

* feat: correct error handling

* feat: handle defautl value in existing variables

* feat: lowercase the source for payload

* feat: fixed the incorrect assignment of active indices

* feat: improved handling of all option

* feat: improved the ALL option visuals

* feat: handled default value enforcement in existing variables

* feat: added unix time to values call

* feat: added incomplete data message and info to search

* feat: changed dashboard panel call handling with existing variables

* feat: adjusted the response type and data with the new API schema for values

* feat: code refactor

* feat: made dyn-variable option as the default

* feat: added test cases for dyn variable creation and completion

* feat: updated test cases

* feat: added variable in url and made dashboard sync around that and sharable (#7944)

* feat: added dynamic variable to the dashboard details

* feat: added new component to existing variables

* feat: added enhancement to multiselect and select for dyn-variables

* feat: added refetch method between all dynamic-variables

* feat: correct error handling

* feat: correct error handling

* feat: enforced non-empty selectedvalues and default value

* feat: added client and server side searches

* feat: retry on error

* feat: correct error handling

* feat: handle defautl value in existing variables

* feat: lowercase the source for payload

* feat: fixed the incorrect assignment of active indices

* feat: improved handling of all option

* feat: improved the ALL option visuals

* feat: handled default value enforcement in existing variables

* feat: added unix time to values call

* feat: added incomplete data message and info to search

* feat: changed dashboard panel call handling with existing variables

* feat: adjusted the response type and data with the new API schema for values

* feat: code refactor

* feat: made dyn-variable option as the default

* feat: added test cases for dyn variable creation and completion

* feat: updated test cases

* feat: added variable in url and made dashboard sync around that and sharable

* feat: added test cases

* feat: added safety check

* feat: enabled url setting on first load itself

* feat: code refactor

* feat: cleared options query param when on dashboard list page

* feat: resolved conflicts

* feat: added dynamic variable suggestion in where clause

* feat: added test cases for hooks and api call functions

* feat: added test case for querybuildersearchv2 suggestion changes

* feat: code refactor

* feat: updated test case

* feat: corrected the regex matcher for resolved titles

* feat: added ability to add/remove variable filter to one or more existing panels

* feat: added widgetselector on variable creation

* feat: show labels in widget selector

* feat: added apply to all and variable removal logical

* feat: refectch only related and affected panels in case of dynamic variables

* feat: added button loader for apply-all

* feat: light-mode styles

* feat: minor refactor

* feat: added test cases

* feat: refactor

* feat: remove consoles

* feat: pass panel types to substitutevars

* feat: cross filtering init

* fix: added fix for query builder filters

* feat: cross filtering add set/unset/create functionality

* feat: test update

* fix: added migration to filter expression for crud operations of variable

* feat: format legend name according to existing format

* feat: breakout test init

* feat: breakout test match query

* feat: context links tests

* feat: minor refactor

* feat: show edit only if user has access

* feat: added dynamic variables creation flow (#7541)

* feat: added dynamic variables creation flow

* feat: added keys and value apis and hooks

* feat: added api and select component changes

* feat: added keys fetching and preview values

* feat: added dynamic variable to variable items

* feat: handled value persistence and tab switches

* feat: added default value and formed a schema for dyn-variables

* feat: added client and server side searches

* feat: corrected the initial load getfieldKey api

* feat: removed fetch on mount restriction

* feat: added dynamic variable to the dashboard details (#7755)

* feat: added dynamic variable to the dashboard details

* feat: added new component to existing variables

* feat: added enhancement to multiselect and select for dyn-variables

* feat: added refetch method between all dynamic-variables

* feat: correct error handling

* feat: correct error handling

* feat: enforced non-empty selectedvalues and default value

* feat: added client and server side searches

* feat: retry on error

* feat: correct error handling

* feat: handle defautl value in existing variables

* feat: lowercase the source for payload

* feat: fixed the incorrect assignment of active indices

* feat: improved handling of all option

* feat: improved the ALL option visuals

* feat: handled default value enforcement in existing variables

* feat: added unix time to values call

* feat: added incomplete data message and info to search

* feat: changed dashboard panel call handling with existing variables

* feat: adjusted the response type and data with the new API schema for values

* feat: code refactor

* feat: made dyn-variable option as the default

* feat: added test cases for dyn variable creation and completion

* feat: updated test cases

* feat: added dynamic variable suggestion in where clause

* feat: added test cases for hooks and api call functions

* feat: added test case for querybuildersearchv2 suggestion changes

* feat: code refactor

* feat: updated test case

* feat: corrected the regex matcher for resolved titles

* feat: added ability to add/remove variable filter to one or more existing panels

* feat: added widgetselector on variable creation

* feat: show labels in widget selector

* feat: added apply to all and variable removal logical

* feat: refectch only related and affected panels in case of dynamic variables

* feat: added button loader for apply-all

* feat: light-mode styles

* fix: added migration to filter expression for crud operations of variable

* feat: reverted dynamic variable url config changes (#8877)

* Revert "feat: changed query param name"

This reverts commit 62bee5f003.

* Revert "feat: added user-friendly format to dashboard variable url"

This reverts commit 6de8b1c2e8.

* feat: reverted url var changes

* feat: reverted url changed from usedashboardvarupdate hook

* feat: send empty array for widgetId

* feat: added type in the variables in query_range payload for dynamic

* feat: minor fixes

* fix: added fix for multivalue operator without brackets

* feat: minor fix

* feat: fix failing test

* feat: change revert

* test: added tests for querycontextUtils + querybuilderv2 utils

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

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

* test: fixed querybuilderv2 utils test

* feat: handle number dataType in filters

* feat: correct the variable addition to panel format for new qb expression

* feat: remove other queries in breakout

* feat: add metric to traces mapping

* feat: pass proper time range

* feat: update time range logic

* feat: value panel drilldown init

* feat: value panel drilldown init

* feat: enable context links in value panel

* feat: minor fix

* feat: update snapshot

* feat: hide breakout in value panel

* feat: add panel type to view mode

* feat: add support to change panel in breakouts

* feat: panel change for breakout logic added

* chore: fix style

* chore: show variables suggestion while creating context links

* chore: add timestamp to graphs

* chore: add timestamp to table panel

* chore: fix failing tests

* chore: fix infinite re-rendering due to queryRange

* chore: send appropriate time range when signal is metrics

* chore: show variables suggestion while creating context links

* chore: minor refactor

* chore: show trace details link if filter has trace_id

* chore: fix infinite render of table component

* chore: added tests for v2

* fix: context links set from dropdown

* chore: minor refactor

* chore: minor refactor

* chore: fix test

* chore: fix timerange for apm metrics

* fix: get correct timestamp for clicked data

* chore: comment out change to histogram on breakout by number

* chore: change panel type on panel type change in url

* chore: remove consoles

* feat: added dynamic variables creation flow (#7541)

* feat: added dynamic variables creation flow

* feat: added keys and value apis and hooks

* feat: added api and select component changes

* feat: added keys fetching and preview values

* feat: added dynamic variable to variable items

* feat: handled value persistence and tab switches

* feat: added default value and formed a schema for dyn-variables

* feat: added client and server side searches

* feat: corrected the initial load getfieldKey api

* feat: removed fetch on mount restriction

* feat: added dynamic variable to the dashboard details (#7755)

* feat: added dynamic variable to the dashboard details

* feat: added new component to existing variables

* feat: added enhancement to multiselect and select for dyn-variables

* feat: added refetch method between all dynamic-variables

* feat: correct error handling

* feat: correct error handling

* feat: enforced non-empty selectedvalues and default value

* feat: added client and server side searches

* feat: retry on error

* feat: correct error handling

* feat: handle defautl value in existing variables

* feat: lowercase the source for payload

* feat: fixed the incorrect assignment of active indices

* feat: improved handling of all option

* feat: improved the ALL option visuals

* feat: handled default value enforcement in existing variables

* feat: added unix time to values call

* feat: added incomplete data message and info to search

* feat: changed dashboard panel call handling with existing variables

* feat: adjusted the response type and data with the new API schema for values

* feat: code refactor

* feat: made dyn-variable option as the default

* feat: added test cases for dyn variable creation and completion

* feat: updated test cases

* feat: fix lint and test cases

* feat: fix typo

* feat: fixed test case

* feat: added dynamic variable suggestion in where clause

* feat: added test cases for hooks and api call functions

* feat: added test case for querybuildersearchv2 suggestion changes

* feat: code refactor

* feat: corrected the regex matcher for resolved titles

* feat: fixed test cases

* feat: added ability to add/remove variable filter to one or more existing panels

* feat: added widgetselector on variable creation

* feat: show labels in widget selector

* feat: added apply to all and variable removal logical

* feat: refectch only related and affected panels in case of dynamic variables

* feat: added button loader for apply-all

* feat: light-mode styles

* fix: added migration to filter expression for crud operations of variable

* feat: added type in the variables in query_range payload for dynamic

* feat: correct the variable addition to panel format for new qb expression

* feat: added test cases for dynamic variable and add/remove panel feat

* feat: implemented where clause suggestion in new qb v5

* feat: added retries for dyn variable and fixed on-enter selection issue

* feat: added relatedValues and existing query in param related changes

* feat: sanitized data storage and removed duplicates

* fix: fixed typechecks

* feat: updated panel wait and refetch logic and ALL option selection

* feat: fixed variable tabel reordering issue

* feat: added empty name validation in variable creation

* feat: change value to searchtext in values API

* feat: added option for regex in the component, disabled for now

* feat: added beta and not rec. tag in variable tabs

* feat: added check to prevent api and updates calls with same payload

* feat: optimized localstorage for all selection in dynamic variable and updated __all__ case

* feat: resolved variable tables infinite loop update error

* feat: aded variable name auto-update based on attribute name entered for dynamic variables

* feat: modified only/all click behaviour and set all selection always true for dynamic variable

* feat: fix dropdown closing doesn't reset us back to our all available values when we have a search

* feat: handled all state distinction and carry forward in existing variables

* feat: trucate + n more tooltip content to 10

* feat: fixed infinite loop because of dependency of frequently changing object ref in var table

* feat: fixed inconsist search implementations

* feat: reverted only - all updated area implementation

* feat: added more space for search in multiselect component

* feat: checked for variable id instead of variable key for refetch

* feat: improved performance around multiselect component and added confirm modal for apply to all

* feat: rewrite functionality around add and remove panels

* feat: changed color for apply to all modal

* feat: added changes under flag to handle variable specific removal for removeKeysFromExpression func

* feat: added validation in variable edit panel

* chore: fix dynamic variable update in context menu to latest logic

* chore: minor fix

* chore: type fix

* fix: remove unwanted code

* fix: remove unwanted code

* fix: resolved pr comments

* fix: minor fix

* fix: fix tests

* fix: style fix

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>
Co-authored-by: Abhi Kumar <ahrefabhi@gmail.com>
Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>
Co-authored-by: SagarRajput-7 <sagar@signoz.io>
2025-09-07 11:50:35 +05:30
aks07
573dbba79d fix: style fix 2025-09-07 11:31:07 +05:30
aks07
4a3f6fdd95 fix: fix tests 2025-09-06 23:33:43 +05:30
aks07
cef7679d61 fix: minor fix 2025-09-06 21:52:07 +05:30
aks07
ebe127af91 fix: resolved pr comments 2025-09-06 19:21:32 +05:30
aks07
b0016c9637 fix: remove unwanted code 2025-09-06 18:57:28 +05:30
aks07
a99c58558f fix: remove unwanted code 2025-09-06 18:38:21 +05:30
aks07
53d6cba0af chore: type fix 2025-09-06 15:47:40 +05:30
aks07
01ea7be5ab chore: minor fix 2025-09-06 15:40:19 +05:30
aks07
67308abcdc Merge branch 'main' of github.com:SigNoz/signoz into feat/interactive-dashbaord-v2 2025-09-06 15:36:29 +05:30
SagarRajput-7
7f925bd50e feat: added dynamic variable and other variable enhancements (#8873)
* feat: added dynamic variables creation flow (#7541)

* feat: added dynamic variables creation flow

* feat: added keys and value apis and hooks

* feat: added api and select component changes

* feat: added keys fetching and preview values

* feat: added dynamic variable to variable items

* feat: handled value persistence and tab switches

* feat: added default value and formed a schema for dyn-variables

* feat: added client and server side searches

* feat: corrected the initial load getfieldKey api

* feat: removed fetch on mount restriction

* feat: added dynamic variable to the dashboard details (#7755)

* feat: added dynamic variable to the dashboard details

* feat: added new component to existing variables

* feat: added enhancement to multiselect and select for dyn-variables

* feat: added refetch method between all dynamic-variables

* feat: correct error handling

* feat: correct error handling

* feat: enforced non-empty selectedvalues and default value

* feat: added client and server side searches

* feat: retry on error

* feat: correct error handling

* feat: handle defautl value in existing variables

* feat: lowercase the source for payload

* feat: fixed the incorrect assignment of active indices

* feat: improved handling of all option

* feat: improved the ALL option visuals

* feat: handled default value enforcement in existing variables

* feat: added unix time to values call

* feat: added incomplete data message and info to search

* feat: changed dashboard panel call handling with existing variables

* feat: adjusted the response type and data with the new API schema for values

* feat: code refactor

* feat: made dyn-variable option as the default

* feat: added test cases for dyn variable creation and completion

* feat: updated test cases

* feat: fix lint and test cases

* feat: fix typo

* feat: resolved comments and refactoring

* feat: added dynamic variable suggestion in where clause (#8875)

* feat: added dynamic variable suggestion in where clause

* feat: added test cases for hooks and api call functions

* feat: added test case for querybuildersearchv2 suggestion changes

* feat: code refactor

* feat: corrected the regex matcher for resolved titles

* feat: fixed test cases

* feat: added ability to add/remove variable filter to one or more existing panels (#8876)

* feat: added ability to add/remove variable filter to one or more existing panels

* feat: added widgetselector on variable creation

* feat: show labels in widget selector

* feat: added apply to all and variable removal logical

* feat: refectch only related and affected panels in case of dynamic variables

* feat: added button loader for apply-all

* feat: light-mode styles

* fix: added migration to filter expression for crud operations of variable

* feat: added type in the variables in query_range payload for dynamic

* feat: correct the variable addition to panel format for new qb expression

* feat: added test cases for dynamic variable and add/remove panel feat

* feat: implemented where clause suggestion in new qb v5

* feat: added retries for dyn variable and fixed on-enter selection issue

* feat: added relatedValues and existing query in param related changes

* feat: sanitized data storage and removed duplicates

* fix: fixed typechecks

* feat: updated panel wait and refetch logic and ALL option selection

* feat: fixed variable tabel reordering issue

* feat: added empty name validation in variable creation

* feat: change value to searchtext in values API

* feat: added option for regex in the component, disabled for now

* feat: added beta and not rec. tag in variable tabs

* feat: added check to prevent api and updates calls with same payload

* feat: optimized localstorage for all selection in dynamic variable and updated __all__ case

* feat: resolved variable tables infinite loop update error

* feat: aded variable name auto-update based on attribute name entered for dynamic variables

* feat: modified only/all click behaviour and set all selection always true for dynamic variable

* feat: fix dropdown closing doesn't reset us back to our all available values when we have a search

* feat: handled all state distinction and carry forward in existing variables

* feat: trucate + n more tooltip content to 10

* feat: fixed infinite loop because of dependency of frequently changing object ref in var table

* feat: fixed inconsist search implementations

* feat: reverted only - all updated area implementation

* feat: added more space for search in multiselect component

* feat: checked for variable id instead of variable key for refetch

* feat: improved performance around multiselect component and added confirm modal for apply to all

* feat: rewrite functionality around add and remove panels

* feat: changed color for apply to all modal

* feat: added changes under flag to handle variable specific removal for removeKeysFromExpression func

* feat: added validation in variable edit panel

* feat: fixed failing test cases due to recent logic change

* feat: added doc links in the dynamic variable feat

* feat: resolved comments and refactoring

* feat: resolved comments and refactoring

* feat: fixed test cases

* feat: fixed test cases
2025-09-06 14:48:45 +05:30
SagarRajput-7
1aa7e8b5d9 feat: added new component to existing variables (#7744)
* feat: added new component to existing variables

* feat: added multiselect component and solved dropdown closing on every selection

* feat: fixed incorrect all label

* feat: allow custom value

* feat: better styles

* feat: added maxtagcount placeholder

* feat: added onclear function

* feat: updated regex and handlings

* feat: updated regex and handlings

* feat: added enableall prop control

* feat: fix the rebase conflict

* feat: fixed comments

* feat: fix test case

* feat: added test cases for customMultiselect, customSelect and variableItem integration

* feat: added test cases for behaviour around values added and selections showed

* feat: refactored test cases

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-09-06 12:26:15 +05:30
Abhi kumar
bf704333b3 Feature/trace operators (#8869)
* feat: added traceoperator component and styles

* chore: minor style improvments

* feat: added conditions for traceoperator

* chore: minor UI fixes

* chore: type changes

* chore: added initialvalue for trace operators

* chore: Added changes to prepare request payload

* chore: fixed minor styles + minor ux fix

* feat: added span selector

* chore: added ui changes in the editor

* chore: removed traceoperations and reused queryoperations

* chore: minor changes in queryaddon and aggregation for support

* chore: added traceoperators in alerts

* chore: minor pr review change

* chore: linter fix

* fix: fixed minor ts issues

* fix: added limit support in traceoperator

* chore: minor fix in traceoperator styles

* chore: linting fix + icon changes

* chore: updated type

* chore: lint fixes

* feat: added changes for showing querynames in alerts

* feat: added trace operator grammer + antlr files

* feat: added traceoperator context util

* chore: added traceoperator validation function

* feat: added traceoperator editor

* feat: added queryname boost + operator constants

* fix: pr reviews

* chore: minor ui fix

* fix: updated grammer files

* test: added test for traceoperatorcontext

* chore: removed check for multiple queries in traceexplorer

* test: minor test fix

* test: fixed breaking mapQueryDataFromApi test

* chore: fixed logic to show trace operator

* chore: updated docs link

* chore: minor ui issue fix

* chore: changed trace operator query name

* chore: removed using spans from in trace opeartors

* fix: added fix for order by in trace opeartor

* feat: added changes related to saved in views in trace opeartor

* chore: added changes to keep indirect descendent operator at bottom

* chore: removed returnspansfrom field from traceoperator

* chore: updated file names + regenerated grammer

* chore: added beta tag in trace opeartor

* chore: pr review fixes

* Fix/tsc trace operator + tests (#8942)

* fix: added tsc fixes for trace operator

* chore: moved traceoperator utils

* test: added test for traceopertor util

* chore: tsc fix

* fix: fixed tsc issue

* Feat/trace operator dashboards (#8992)

* chore: added callout message for multiple queries without trace operators

* feat: added changes for supporting trace operators in dashboards

* chore: minor changes for list panel
2025-09-05 16:00:24 +00:00
Vibhu Pandey
f63f175a77 fix(binding): better error messages (#9010) 2025-09-05 15:47:19 +00:00
Aditya Singh
63f18174a8 Merge branch 'main' into feat/interactive-dashbaord-v2 2025-09-05 21:07:46 +05:30
Ekansh Gupta
b6f5c053a0 feat: trace operators BE (#8293)
* feat: [draft] added implementation of trace operators

* feat: [draft] added implementation of trace operators

* feat: [draft] added implementation of trace operators

* feat: [draft] added implementation of trace operators

* feat: added implementation of trace operators

* feat: added implementation of trace operators

* feat: added implementation of trace operators

* feat: added implementation of trace operators

* feat: added implementation of trace operators

* feat: added implementation of trace operators

* feat: added implementation of trace operators

* feat: added implementation of trace operators

* feat: added implementation of trace operators

* feat: added implementation of trace operators

* feat: refactor trace operator

* feat: added postprocess

* feat: added postprocess

* feat: added postprocess

* feat: refactored the consume function

* feat: refactored the consume function

* feat: refactored the consume function

* feat: refactored the consume function

* feat: refactored the consume function

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: replaced info to debug logs

* feat: replaced info to debug logs

* feat: replaced info to debug logs

* feat: updated time series query

* feat: fixed merge conflicts

* feat: fixed merge conflicts

* feat: fixed merge conflicts

* feat: fixed merge conflicts

* feat: added deep copy in ranged queries

* feat: refactored fingerprinting

* feat: refactored fingerprinting

* feat: refactored fingerprinting

* feat: refactored fingerprinting

* feat: refactored fingerprinting

* feat: refactored fingerprinting

* feat: refactored fingerprinting

* feat: added comment for build all spans cte

* feat: added postprocess for timeseries and added limits to memory

* feat: fixed span count in trace view

* feat: fixed span count in trace view

* feat: fixed linting issues

* feat: fixed linting issues

* feat: fixed linting issues

* feat: fixed linting issues

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2025-09-05 21:07:10 +05:30
Aditya Singh
facc95abae chore: fix dynamic variable update in context menu to latest logic 2025-09-05 19:25:46 +05:30
Aditya Singh
f754c6cc6c Merge branch 'SIG-5603-dyn-var' of github.com:SigNoz/signoz into feat/interactive-dashbaord-v2 2025-09-05 19:24:35 +05:30
SagarRajput-7
9ad6ab49f0 feat: added validation in variable edit panel 2025-09-05 17:47:07 +05:30
SagarRajput-7
8f6b853bb9 feat: added changes under flag to handle variable specific removal for removeKeysFromExpression func 2025-09-05 17:46:34 +05:30
SagarRajput-7
4e4f7cc521 feat: changed color for apply to all modal 2025-09-05 17:45:09 +05:30
SagarRajput-7
d794404f31 feat: rewrite functionality around add and remove panels 2025-09-05 17:45:09 +05:30
SagarRajput-7
7d81f1e665 feat: improved performance around multiselect component and added confirm modal for apply to all 2025-09-05 17:45:09 +05:30
SagarRajput-7
3b8033c7ec feat: checked for variable id instead of variable key for refetch 2025-09-05 17:45:09 +05:30
SagarRajput-7
70e6b61660 feat: added more space for search in multiselect component 2025-09-05 17:45:09 +05:30
SagarRajput-7
2b4cd5d1fb feat: reverted only - all updated area implementation 2025-09-05 17:45:09 +05:30
SagarRajput-7
73c279cac9 feat: fixed inconsist search implementations 2025-09-05 17:45:09 +05:30
SagarRajput-7
73ce96e3a6 feat: fixed infinite loop because of dependency of frequently changing object ref in var table 2025-09-05 17:45:09 +05:30
SagarRajput-7
8217e1e0cb feat: trucate + n more tooltip content to 10 2025-09-05 17:45:09 +05:30
SagarRajput-7
e24da43559 feat: handled all state distinction and carry forward in existing variables 2025-09-05 17:45:09 +05:30
SagarRajput-7
6efbce3ea1 feat: fix dropdown closing doesn't reset us back to our all available values when we have a search 2025-09-05 17:45:09 +05:30
SagarRajput-7
eb063f7ac0 feat: modified only/all click behaviour and set all selection always true for dynamic variable 2025-09-05 17:45:09 +05:30
SagarRajput-7
b01f8ae170 feat: aded variable name auto-update based on attribute name entered for dynamic variables 2025-09-05 17:45:09 +05:30
SagarRajput-7
10167f7cd1 feat: resolved variable tables infinite loop update error 2025-09-05 17:45:09 +05:30
SagarRajput-7
a98b56d994 feat: optimized localstorage for all selection in dynamic variable and updated __all__ case 2025-09-05 17:45:09 +05:30
SagarRajput-7
6eb2546398 feat: added check to prevent api and updates calls with same payload 2025-09-05 17:45:09 +05:30
SagarRajput-7
47b447099d feat: added beta and not rec. tag in variable tabs 2025-09-05 17:45:09 +05:30
SagarRajput-7
35a1875d45 feat: added option for regex in the component, disabled for now 2025-09-05 17:45:09 +05:30
SagarRajput-7
d971224169 feat: change value to searchtext in values API 2025-09-05 17:45:09 +05:30
SagarRajput-7
c72ef90209 feat: added empty name validation in variable creation 2025-09-05 17:45:09 +05:30
SagarRajput-7
6c41aa1420 feat: fixed variable tabel reordering issue 2025-09-05 17:45:09 +05:30
SagarRajput-7
d2a175db9c feat: updated panel wait and refetch logic and ALL option selection 2025-09-05 17:45:09 +05:30
SagarRajput-7
5a149a9a4f fix: fixed typechecks 2025-09-05 17:45:09 +05:30
SagarRajput-7
0319e1b816 feat: sanitized data storage and removed duplicates 2025-09-05 17:45:09 +05:30
SagarRajput-7
215707304a feat: added relatedValues and existing query in param related changes 2025-09-05 17:45:09 +05:30
SagarRajput-7
cb22545031 feat: added retries for dyn variable and fixed on-enter selection issue 2025-09-05 17:45:09 +05:30
SagarRajput-7
9f40bd6a9f feat: implemented where clause suggestion in new qb v5 2025-09-05 17:45:09 +05:30
SagarRajput-7
3a78b13e0c feat: added test cases for dynamic variable and add/remove panel feat 2025-09-05 17:45:09 +05:30
SagarRajput-7
2323ce4aeb feat: correct the variable addition to panel format for new qb expression 2025-09-05 17:45:09 +05:30
SagarRajput-7
72272799ee feat: added type in the variables in query_range payload for dynamic 2025-09-05 17:45:09 +05:30
SagarRajput-7
73d635149f fix: added migration to filter expression for crud operations of variable 2025-09-05 17:45:09 +05:30
SagarRajput-7
9bd55dfa6c feat: light-mode styles 2025-09-05 17:45:09 +05:30
SagarRajput-7
822338ace8 feat: added button loader for apply-all 2025-09-05 17:45:09 +05:30
SagarRajput-7
37bb8e95a8 feat: refectch only related and affected panels in case of dynamic variables 2025-09-05 17:45:09 +05:30
SagarRajput-7
7eaab9cd21 feat: added apply to all and variable removal logical 2025-09-05 17:45:08 +05:30
SagarRajput-7
7c97b8f880 feat: show labels in widget selector 2025-09-05 17:45:08 +05:30
SagarRajput-7
6f1dd4d10a feat: added widgetselector on variable creation 2025-09-05 17:45:08 +05:30
SagarRajput-7
8a9f67b17c feat: added ability to add/remove variable filter to one or more existing panels 2025-09-05 17:45:08 +05:30
SagarRajput-7
d16e26b5e4 feat: fixed test cases 2025-09-05 17:44:39 +05:30
SagarRajput-7
36dd024f69 feat: corrected the regex matcher for resolved titles 2025-09-05 17:44:39 +05:30
SagarRajput-7
12f61bdccf feat: code refactor 2025-09-05 17:44:39 +05:30
SagarRajput-7
46307ed4f4 feat: added test case for querybuildersearchv2 suggestion changes 2025-09-05 17:44:39 +05:30
SagarRajput-7
8e20150e48 feat: added test cases for hooks and api call functions 2025-09-05 17:44:39 +05:30
SagarRajput-7
15f857bced feat: added dynamic variable suggestion in where clause 2025-09-05 17:44:39 +05:30
SagarRajput-7
e4b0388de5 feat: fixed test case 2025-09-05 17:43:27 +05:30
SagarRajput-7
bcd2ebed47 feat: fix typo 2025-09-05 17:43:27 +05:30
SagarRajput-7
dae61cfa7a feat: fix lint and test cases 2025-09-05 17:43:26 +05:30
SagarRajput-7
c6b6e84db6 feat: added dynamic variable to the dashboard details (#7755)
* feat: added dynamic variable to the dashboard details

* feat: added new component to existing variables

* feat: added enhancement to multiselect and select for dyn-variables

* feat: added refetch method between all dynamic-variables

* feat: correct error handling

* feat: correct error handling

* feat: enforced non-empty selectedvalues and default value

* feat: added client and server side searches

* feat: retry on error

* feat: correct error handling

* feat: handle defautl value in existing variables

* feat: lowercase the source for payload

* feat: fixed the incorrect assignment of active indices

* feat: improved handling of all option

* feat: improved the ALL option visuals

* feat: handled default value enforcement in existing variables

* feat: added unix time to values call

* feat: added incomplete data message and info to search

* feat: changed dashboard panel call handling with existing variables

* feat: adjusted the response type and data with the new API schema for values

* feat: code refactor

* feat: made dyn-variable option as the default

* feat: added test cases for dyn variable creation and completion

* feat: updated test cases
2025-09-05 17:43:26 +05:30
SagarRajput-7
bf02c6b500 feat: added dynamic variables creation flow (#7541)
* feat: added dynamic variables creation flow

* feat: added keys and value apis and hooks

* feat: added api and select component changes

* feat: added keys fetching and preview values

* feat: added dynamic variable to variable items

* feat: handled value persistence and tab switches

* feat: added default value and formed a schema for dyn-variables

* feat: added client and server side searches

* feat: corrected the initial load getfieldKey api

* feat: removed fetch on mount restriction
2025-09-05 17:43:26 +05:30
Aditya Singh
abb15b05a5 Merge branch 'main' of github.com:SigNoz/signoz into feat/interactive-dashbaord-v2 2025-09-05 14:43:45 +05:30
Aditya Singh
fe3a1ab74b chore: remove consoles 2025-09-05 14:40:54 +05:30
Aditya Singh
a1f7e16d75 chore: change panel type on panel type change in url 2025-09-05 14:24:21 +05:30
Aditya Singh
20fb032503 chore: comment out change to histogram on breakout by number 2025-09-05 14:23:12 +05:30
Aditya Singh
4a0c9cd03a fix: get correct timestamp for clicked data 2025-09-05 14:16:12 +05:30
Aditya Singh
d67c61f332 chore: fix timerange for apm metrics 2025-09-05 13:59:26 +05:30
Abhi kumar
abeadc7672 fix: backward compatibility for explorer in case of aggregateAttribute is not present (#9000) 2025-09-04 13:21:16 +00:00
SagarRajput-7
faadc60c74 fix: fixed table panels not sorting, due to mismatch in lookup (id vs name) for aggregations (#9002)
* fix: fixed table panels not sorting, due to mismatch in id for aggregations

* fix: added test cases for the sort and util for qbv5 aggregation
2025-09-04 18:44:38 +05:30
Vibhu Pandey
360e8309c8 feat(password): implement strong controls for password (#8983)
## 📄 Summary

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

password must be at least 12 characters long, should contain at least one uppercase letter [A-Z], one lowercase letter [a-z], one number [0-9], and one symbol
2025-09-04 17:22:28 +05:30
Aditya Singh
3ea561ab51 chore: fix test 2025-09-04 16:38:02 +05:30
Aditya Singh
6454392ca0 chore: minor refactor 2025-09-04 16:32:24 +05:30
Aditya Singh
9f4d8c79e1 Merge branch 'main' of github.com:SigNoz/signoz into feat/interactive-dashbaord-v2 2025-09-04 16:23:59 +05:30
Aditya Singh
c6c8924fe9 chore: minor refactor 2025-09-04 16:13:21 +05:30
Aditya Singh
be5aa3d638 Merge branch 'feat/cross-filtering' of github.com:SigNoz/signoz into feat/interactive-dashbaord-v2 2025-09-04 16:10:57 +05:30
SagarRajput-7
27580b62ba fix: fixed full view height for table panel (#9004) 2025-09-04 10:40:30 +00:00
Aditya Singh
27e3700e27 Merge branch 'main' of github.com:SigNoz/signoz into feat/cross-filtering 2025-09-04 16:10:11 +05:30
Aditya Singh
572d7efe09 Merge branch 'main' of github.com:SigNoz/signoz into feat/interactive-dashbaord-v2 2025-09-04 16:09:23 +05:30
Aditya Singh
e790c7a2af fix: context links set from dropdown 2025-09-04 16:03:13 +05:30
SagarRajput-7
bcd21cee74 fix: y axis unit not interactive in the panel edit mode (#9003) 2025-09-04 16:00:03 +05:30
Aditya Singh
aea3824f9b chore: added tests for v2 2025-09-04 14:11:35 +05:30
Vikrant Gupta
2dbe0777f4 feat(authz): add openfga authz middleware (#8990)
* feat(authz): add openfga authz middleware

* feat(authz): update the auth context

* feat(authz): update the auth context

* feat(authz): update check request

* feat(authz): update check request

* feat(authz): add lifecycle tests

* feat(authz): add lifecycle tests

* feat(authz): add start-stop tests
2025-09-04 08:37:11 +00:00
Aditya Singh
f5d330b910 chore: fix infinite render of table component 2025-09-04 13:55:39 +05:30
Aditya Singh
7c8ca2bb48 chore: show trace details link if filter has trace_id 2025-09-04 13:28:44 +05:30
Aditya Singh
df18d8c90a chore: minor refactor 2025-09-04 12:31:04 +05:30
Aditya Singh
2c5a2d2e71 chore: show variables suggestion while creating context links 2025-09-04 12:24:30 +05:30
Aditya Singh
b45da5e3b1 chore: send appropriate time range when signal is metrics 2025-09-04 12:14:44 +05:30
Aditya Singh
11b520a088 Merge branch 'main' of github.com:SigNoz/signoz into feat/interactive-dashbaord-v2 2025-09-04 12:10:50 +05:30
Aditya Singh
d4969b7fbe Merge branch 'feat/cross-filtering' of github.com:SigNoz/signoz into feat/interactive-dashbaord-v2 2025-09-04 11:58:48 +05:30
SagarRajput-7
7602d863dd fix: fixed table panel no scroll issue due to style override from calender component (#8996) 2025-09-04 09:55:46 +05:30
Abhi kumar
68d9c6c3cc fix: added fix for incorrect query in dashboard on panel change to list panel (#8994)
* fix: added fix for incorrect query in dashboard on panel change to list issue

* test: added test for handleQueryChange

* chore: pr reviews
2025-09-03 14:00:22 +00:00
manika-signoz
10c6e1fac7 feat: add delete button to invite user flow (#8993) 2025-09-03 19:07:35 +05:30
Shaheer Kochai
3999a64c64 feat: add pinning functionality for span attributes (#8769)
* refactor: adjust the attribute pinning changes based on trace actionables latest changes

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

* chore: overall improvement

* chore: fix the failing tests

* fix: make the changes w.r.t. pinned attributes preferences in in preference.go
2025-09-03 16:36:10 +04:30
Aditya Singh
5c0ece454a chore: fix infinite re-rendering due to queryRange 2025-09-02 16:45:50 +05:30
Vikrant Gupta
729bfb31f1 feat(authz): implement the current usecases in openfga (#8982)
* feat(authz): implement the current usecases in openfga

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

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

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

* fix: remove increase

* fix: move changes to req.go

* fix: add tood

* fix: address comments
2025-09-02 09:54:55 +00:00
nikhilmantri0902
5d9247f591 Allow deletion of multiple panels for dashboard updates made with API key (#8903) 2025-09-02 09:30:22 +00:00
Aditya Singh
4c86c0650c chore: fix failing tests 2025-09-02 13:37:40 +05:30
Aditya Singh
3c380353a3 Merge branch 'main' of github.com:SigNoz/signoz into feat/cross-filtering 2025-09-02 12:40:55 +05:30
Yunus M
c0a9948146 feat: handle active log flow (#8946)
* feat: handle active log flow

* feat: show live logs in logs explorer view

* feat: enable live logs in logs explorer

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

* chore: pass showLiveLogs as false in test cases

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

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

* fix: encode filter expression, show live option only in list view
2025-09-02 11:05:52 +05:30
Aditya Singh
917814e903 chore: add timestamp to table panel 2025-09-02 02:53:28 +05:30
Aditya Singh
ff6f3a382d chore: add timestamp to graphs 2025-09-02 02:25:43 +05:30
Vikrant Gupta
f3569a9a02 chore(meter): remove the meter data validity message for non cloud users (#8972) 2025-09-01 16:18:47 +00:00
Vikrant Gupta
0df1ed3b57 fix(dashboard): remove context from dashboard types (#8971) 2025-09-01 16:08:25 +00:00
Aditya Singh
90a2a82d69 chore: show variables suggestion while creating context links 2025-09-01 19:09:22 +05:30
Nityananda Gohain
d0132f11ae fix: update clickhouse-sql-parser (#8970) 2025-09-01 13:25:27 +00:00
Vikrant Gupta
f61e859901 feat(authz): embed openfga server (#8966)
* feat(access-control): embed openfga in signoz

* feat(authz): rename access control to authz

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

* feat(authz): fix lint

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

* feat(authz): some more lint issues

* feat(authz): some more lint issues

* feat(authz): some more lint issues

* feat(authz): fix more lint issues

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

* feat: added custom retention for logs api

* feat: added implementation of trace operators

* feat: added validation checks for resource keys

* feat: added validation checks for resource keys

* feat: added validation checks for resource keys

* feat: added validation checks for resource keys

* feat: added validation checks for resource keys

* feat: added validation checks for resource keys

* feat: added validation checks for resource keys

* feat: added validation checks for resource keys

* feat: added default checks for custom retention

* feat: added default checks for custom retention

* feat: added default checks for custom retention

* feat: added change for ttl

* feat: v2 api supports both v1 and v2

* feat: v2 api supports both v1 and v2

* feat: v2 api supports both v1 and v2

* feat: v2 api supports both v1 and v2

* feat: added default_ttl in v1

* feat: added set logs ttl v1 from v2

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts
2025-09-01 07:30:57 +00:00
Ekansh Gupta
382d9d4a87 Revert "3rd party API sem conv fix (Supports >1.26) (#8822)" (#8954)
This reverts commit 396e0cdc2d.
2025-09-01 07:20:56 +00:00
Aditya Singh
e6d5221693 chore: fix style 2025-08-31 17:28:53 +05:30
Aditya Singh
fcfc724a1a feat: panel change for breakout logic added 2025-08-31 17:18:15 +05:30
Aditya Singh
b623603d10 feat: add support to change panel in breakouts 2025-08-31 17:06:22 +05:30
Aditya Singh
b5626534b1 feat: add panel type to view mode 2025-08-31 15:42:05 +05:30
Nityananda Gohain
87ce197631 fix: don't skip resource filter in main table for OR queries (#8958)
* fix: don't skip resource filter in main table for OR queries

* fix: dont skip resource table

* fix: make check case insensitive

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

* feat: adding support for 1.26 semconv

* feat: adding support for 1.26 semconv

* feat: adding support for 1.26 semconv

* feat: added native support for 1.26

* feat: added native support for 1.26

* feat: adding support for 1.26 semconv

* feat: adding support for 1.26 semconv

* feat: adding support for 1.26 semconv

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

* feat: resolved conflicts

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

* chore: added pr review fixes

* chore: minor fix

* feat: added api changes for setting retention period

* chore: pr review fixes

* chore: removed return statement

* fix: pr reviews

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

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

* fix: add comment

* fix: add comment

* fix: move logic to validation

* fix: remove requestType trace

* fix: update comment

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

* feat: remove duplicate hook calls

* chore: add @signozhq/table to transformIgnorePatterns

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

* feat: address review comments , add sonner for notifications

* feat: address review comments
2025-08-28 19:03:29 +05:30
Abhi kumar
912a34da8d fix: added fix for context query not getting updated (#8941) 2025-08-28 15:14:18 +05:30
primus-bot[bot]
8b99ba0f9f chore(release): bump SigNoz to v0.93.0, OTel Collector to v0.129.2 (#8939)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-08-28 12:26:46 +05:30
Nityananda Gohain
841abf8c0b fix: update live tail api (#8807)
* fix: update live tail api

* fix: minor fixes

* fix: more minor fixes

* fix: remove zap

* fix: correct spelling

* fix: tests

* fix: lint issues

* fix: remove debug

* fix: address comments

* fix: update name

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-08-28 11:44:43 +05:30
Vishal Sharma
df54e6350d fix: trace explorer and home page keyboard shortcut (#8934) 2025-08-27 18:19:54 +00:00
Srikanth Chekuri
f6bc30050b fix: exclusion operators use AND combinator on ambiguity (#8928) 2025-08-26 14:22:23 +00:00
Srikanth Chekuri
1e76046c7c chore: add value search for related values request (#8925) 2025-08-26 19:37:25 +05:30
SagarRajput-7
910751713d fix: removed transformstringwithprefix and removeprefix utilities (#8922)
* fix: removed transformstringwithprefix and removeprefix utilities

* fix: fixed testcase
2025-08-26 16:13:08 +05:30
Aditya Singh
30e6a3b248 Merge branch 'main' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-26 14:42:33 +05:30
Vikrant Gupta
85c671c8d5 fix(meter): meter regex for comment middleware (#8921) 2025-08-26 08:12:31 +00:00
Yunus M
4d2094b4ce feat: enable global actions (#8906)
* feat: enable global actions

* feat: separate actions for collapse and open sidebar

* fix: remove spaces from shortcuts

* chore: remove console log

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

* chore: change the fallback background for status code badge

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

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

* feat: custom date time range history

* feat: light mode updates and interaction fixes

* fix: improve usability

* chore: add calendar, input and popover to transformIgnorePatterns

* chore: add date-fns to transformIgnorePatterns

* chore: add @signozhq/button to transformIgnorePatterns

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

* fix: address comments

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

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

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

* feat: fixed multiagregation not getting sent in create alert

* fix: fixed failing test cases

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

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

* fix: fix lint error

* fix: stepInterval not updating in panel qb

* fix: added test cases for mapQueryDataFromApi and prepareQueryRangePayloadV5

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

* fix: refactored handleRunQuery

* fix: code refactor

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

* fix: updated test cases

* fix: removed isJSON and isColumn from everywhere

* fix: fixed code and test cases

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

* fix: added querytype boolean check for v5 changes

* fix: fixed typechecks

* fix: fixed typechecks

---------

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

* fix: added fix for multivalue operator without brackets

* test: added tests for querycontextUtils + querybuilderv2 utils

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

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

* test: fixed querybuilderv2 utils test

* chore: added changes for jest to use es6

* test: fixed tests for querycontextutils + querybuilderv2 utils

* test: fixed failing tests
2025-08-25 16:35:16 +05:30
Vishal Sharma
08323e4dfd chore: update dashboard template links to SigNoz website dashboard template landing page (#8897) 2025-08-25 13:29:55 +05:30
Vibhu Pandey
ee19f1749b fix(web): fix panic on nil file info (#8907)
fix panic on nil file info
2025-08-25 09:01:32 +05:30
Aditya Singh
05c58a2b3b feat: hide breakout in value panel 2025-08-24 14:23:31 +05:30
Vikrant Gupta
b21db878e8 chore(meter): added product analytics for meter module (#8898)
* chore(meter): added product analytics for meter package

* chore(meter): added product analytics for meter package
2025-08-24 14:21:45 +05:30
Aditya Singh
c638b3be39 feat: update snapshot 2025-08-24 14:00:19 +05:30
Aditya Singh
e2df0ffc87 feat: minor fix 2025-08-24 13:44:21 +05:30
Aditya Singh
c222350f6e feat: enable context links in value panel 2025-08-24 13:28:02 +05:30
Aditya Singh
0ce9531a7a feat: value panel drilldown init 2025-08-24 13:24:45 +05:30
Aditya Singh
e23a569d53 feat: value panel drilldown init 2025-08-24 13:23:57 +05:30
Aditya Singh
5632f05d51 feat: update time range logic 2025-08-24 13:23:12 +05:30
Aditya Singh
f9512dd37c feat: pass proper time range 2025-08-24 11:55:22 +05:30
Srikanth Chekuri
a7ddd2ddf0 chore: do not send field context as tag for deprecated fields (#8902) 2025-08-24 11:14:12 +05:30
Srikanth Chekuri
4d72f47758 chore: parse into number alias for mat column from statement (#8900) 2025-08-24 09:44:58 +05:30
Aditya Singh
5eb4e54913 feat: add metric to traces mapping 2025-08-23 20:56:54 +05:30
Aditya Singh
2f53a2471d feat: remove other queries in breakout 2025-08-23 19:40:32 +05:30
Vikrant Gupta
b5b513f1e0 chore(meter): add warnings and make meter live in sidenav (#8882)
* chore(meter): add warnings and make meter live in sidenav

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

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

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

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

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

* fix: minor error change

* fix: address comments

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-08-22 20:20:42 +05:30
Aditya Singh
ff38ceaecf Merge branch 'SIG-5603-dyn-var' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-22 18:27:58 +05:30
SagarRajput-7
e28d9977be feat: correct the variable addition to panel format for new qb expression 2025-08-22 18:25:15 +05:30
Aditya Singh
dd0a263008 Merge branch 'feat/custom-destinations' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-22 16:35:15 +05:30
Aditya Singh
0df85ae46b feat: handle number dataType in filters 2025-08-22 16:34:39 +05:30
Srikanth Chekuri
eca13075e9 fix: related links for rule history page (#8883) 2025-08-22 16:19:27 +05:30
Aditya Singh
aadbf6c316 Merge branch 'feat/custom-destinations' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-22 14:53:00 +05:30
Aditya Singh
6cb1ffdbc2 Merge branch 'fix/query-builder-filters' of github.com:SigNoz/signoz into feat/custom-destinations 2025-08-22 14:52:19 +05:30
ahrefabhi
83df91bba5 test: fixed querybuilderv2 utils test 2025-08-22 13:22:34 +05:30
ahrefabhi
796497adfc fix: added fix for replacing filters + datetimepicker composite query 2025-08-22 13:12:49 +05:30
ahrefabhi
049f1f396d fix: added fix for replacing filter with the new value 2025-08-22 12:20:03 +05:30
ahrefabhi
4fb993bb6e test: added tests for querycontextUtils + querybuilderv2 utils 2025-08-22 12:08:26 +05:30
ahrefabhi
6251fd42b2 Merge branch 'main' of https://github.com/SigNoz/signoz into fix/query-builder-filters 2025-08-22 11:39:27 +05:30
Aditya Singh
0b36e17090 feat: change revert 2025-08-21 22:56:13 +05:30
Aditya Singh
f150d320b8 feat: fix failing test 2025-08-21 22:54:42 +05:30
Aditya Singh
1d08233ed4 feat: minor fix 2025-08-21 19:24:19 +05:30
Amlan Kumar Nandy
e5ab664483 fix: resolve sentry issues in alert list (#8878)
* fix: resolve sentry issues in alert list

* chore: update the key

---------

Co-authored-by: srikanthccv <srikanth.chekuri92@gmail.com>
2025-08-21 19:21:15 +05:30
ahrefabhi
0a3d40806a fix: added fix for multivalue operator without brackets 2025-08-21 15:15:29 +05:30
ahrefabhi
be7b3e7f9b Merge branch 'main' of https://github.com/SigNoz/signoz into fix/query-builder-filters 2025-08-21 15:06:41 +05:30
Aditya Singh
3ca0fd8029 Merge branch 'SIG-5603-dyn-var' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-21 13:17:07 +05:30
Aditya Singh
45015c1e9b feat: minor fixes 2025-08-21 13:13:38 +05:30
SagarRajput-7
4b95010f14 feat: added type in the variables in query_range payload for dynamic 2025-08-21 13:11:23 +05:30
Aditya Singh
1e66ce6b63 Merge branch 'SIG-5603-dyn-var' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-21 12:23:01 +05:30
Aditya Singh
4690e201d6 feat: send empty array for widgetId 2025-08-21 12:22:15 +05:30
SagarRajput-7
7780dc3248 feat: reverted dynamic variable url config changes (#8877)
* Revert "feat: changed query param name"

This reverts commit 62bee5f003.

* Revert "feat: added user-friendly format to dashboard variable url"

This reverts commit 6de8b1c2e8.

* feat: reverted url var changes

* feat: reverted url changed from usedashboardvarupdate hook
2025-08-21 12:20:41 +05:30
SagarRajput-7
65609c62cc fix: added migration to filter expression for crud operations of variable 2025-08-21 10:46:33 +05:30
SagarRajput-7
a6790e2997 feat: light-mode styles 2025-08-21 10:46:25 +05:30
SagarRajput-7
c0847285ab feat: added button loader for apply-all 2025-08-21 10:46:18 +05:30
SagarRajput-7
c4d2b70689 feat: refectch only related and affected panels in case of dynamic variables 2025-08-21 10:46:01 +05:30
SagarRajput-7
59702e16e0 feat: added apply to all and variable removal logical 2025-08-21 10:45:52 +05:30
SagarRajput-7
eb3bb41d0a feat: show labels in widget selector 2025-08-21 10:45:42 +05:30
SagarRajput-7
ac44c92ab6 feat: added widgetselector on variable creation 2025-08-21 10:42:10 +05:30
SagarRajput-7
58c8310634 feat: added ability to add/remove variable filter to one or more existing panels 2025-08-21 10:42:03 +05:30
SagarRajput-7
90eebe207e feat: corrected the regex matcher for resolved titles 2025-08-21 10:37:12 +05:30
SagarRajput-7
c09eae6386 feat: updated test case 2025-08-21 10:37:06 +05:30
SagarRajput-7
797f7e2487 feat: code refactor 2025-08-21 10:37:00 +05:30
SagarRajput-7
ef4446cd35 feat: added test case for querybuildersearchv2 suggestion changes 2025-08-21 10:36:52 +05:30
SagarRajput-7
0e0fa9ebea feat: added test cases for hooks and api call functions 2025-08-21 10:36:45 +05:30
SagarRajput-7
5f768fec48 feat: added dynamic variable suggestion in where clause 2025-08-21 10:36:39 +05:30
SagarRajput-7
274fd8b51f feat: added dynamic variable to the dashboard details (#7755)
* feat: added dynamic variable to the dashboard details

* feat: added new component to existing variables

* feat: added enhancement to multiselect and select for dyn-variables

* feat: added refetch method between all dynamic-variables

* feat: correct error handling

* feat: correct error handling

* feat: enforced non-empty selectedvalues and default value

* feat: added client and server side searches

* feat: retry on error

* feat: correct error handling

* feat: handle defautl value in existing variables

* feat: lowercase the source for payload

* feat: fixed the incorrect assignment of active indices

* feat: improved handling of all option

* feat: improved the ALL option visuals

* feat: handled default value enforcement in existing variables

* feat: added unix time to values call

* feat: added incomplete data message and info to search

* feat: changed dashboard panel call handling with existing variables

* feat: adjusted the response type and data with the new API schema for values

* feat: code refactor

* feat: made dyn-variable option as the default

* feat: added test cases for dyn variable creation and completion

* feat: updated test cases
2025-08-21 10:11:10 +05:30
SagarRajput-7
57c8381f68 feat: added dynamic variables creation flow (#7541)
* feat: added dynamic variables creation flow

* feat: added keys and value apis and hooks

* feat: added api and select component changes

* feat: added keys fetching and preview values

* feat: added dynamic variable to variable items

* feat: handled value persistence and tab switches

* feat: added default value and formed a schema for dyn-variables

* feat: added client and server side searches

* feat: corrected the initial load getfieldKey api

* feat: removed fetch on mount restriction
2025-08-21 10:10:59 +05:30
Aditya Singh
067919cd7d Merge branch 'feat/custom-destinations' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-20 22:41:57 +05:30
Aditya Singh
13f2cc8115 feat: show edit only if user has access 2025-08-20 22:40:37 +05:30
Aditya Singh
22a5420340 feat: minor refactor 2025-08-20 21:04:24 +05:30
Aditya Singh
07573e831e Merge branch 'main' of github.com:SigNoz/signoz into feat/custom-destinations 2025-08-20 21:04:09 +05:30
Aditya Singh
42e5aa2dd4 feat: context links tests 2025-08-20 20:57:30 +05:30
Vibhu Pandey
a3f32b3d85 fix(comment): add a dedicated comment parsing middleware (#8855)
## 📄 Summary

- add a dedicated comment parsing middleware. This removes duplication and double parsing of referrer.
2025-08-20 20:20:28 +05:30
Aditya Singh
4e72753c24 feat: breakout test match query 2025-08-20 20:10:24 +05:30
Aditya Singh
6f9ac378e2 feat: breakout test init 2025-08-20 20:00:58 +05:30
Aditya Singh
89135b4d90 feat: format legend name according to existing format 2025-08-20 15:49:13 +05:30
Aditya Singh
f1f446b455 Merge branch 'main' of github.com:SigNoz/signoz into feat/custom-destinations 2025-08-20 14:43:59 +05:30
Amlan Kumar Nandy
9c2f127282 chore: backend changes for y-axis management (#8730) 2025-08-20 04:04:50 +00:00
Aditya Singh
84b3ec0626 Merge branch 'main' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-19 14:04:42 +05:30
Srikanth Chekuri
e30de5f13e chore: do not store query name in cache (#8838) 2025-08-19 13:01:55 +05:30
SagarRajput-7
019083983a fix: added sanity logic for explorer old urls (#8804)
* fix: added sanity logic for explorer old urls

* fix: added test for useSanitizeOrderBy

* fix: added sentry events for orderby validation

* fix: cleanup unused logic and renamed boolean state
2025-08-19 12:46:02 +05:30
Aditya Singh
5445fe8e8c Merge branch 'SIG-5603-2' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-19 12:40:26 +05:30
Shaheer Kochai
fdcad997f5 feat: trace detail page actionables (#8761)
* feat: add table view with actionables to span details drawer attributes

* feat: span actions functionality

* refactor: overall improvements

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

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

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

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

* fix: added fix for exist operator

* chore: minor fix for quick filters

* chore: added tests for convertfilterstoexpression

* chore: added fix for regex to regexp conversion

* test: added test for regex to regexp

* fix: added fix for functions conversion and tests

* fix: added fix for negated non_value_operators
2025-08-19 10:55:45 +05:30
SagarRajput-7
55f9bfbfa8 fix: added migration to filter expression for crud operations of variable 2025-08-19 10:33:17 +05:30
Amlan Kumar Nandy
674556d672 chore: add new y-axis unit selector (#8765) 2025-08-19 04:55:46 +00:00
Amlan Kumar Nandy
af987e53ce chore: add unit tests for k8s entity details (#8774) 2025-08-19 04:28:39 +00:00
Aditya Singh
d70034fbc5 Merge branch 'feat/custom-destinations' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-18 23:56:49 +05:30
Aditya Singh
21fb5876c1 Merge branch 'main' of github.com:SigNoz/signoz into feat/custom-destinations 2025-08-18 23:55:32 +05:30
Aditya Singh
0902dc4b43 Merge branch 'main' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-18 23:54:50 +05:30
Vikrant Gupta
59d5accd33 fix(meter): meter where clause keys fix (#8833) 2025-08-18 22:57:02 +05:30
Aditya Singh
d0e668c6ce feat: test update 2025-08-18 22:48:47 +05:30
Aditya Singh
0a008cd6c7 Merge branch 'main' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-18 22:18:34 +05:30
manika-signoz
5a7ad670d8 feat: change copy /signup route (#8783)
* feat: change copy /signup route

* feat: remove firstName from payload
2025-08-18 16:48:09 +00:00
Aditya Singh
e47d13a237 feat: cross filtering add set/unset/create functionality 2025-08-18 22:17:42 +05:30
Abhi kumar
9d04b397ac fix: added fix for code block in light mode (#8831) 2025-08-18 22:00:58 +05:30
ahrefabhi
e3b0a2e33f fix: added fix for query builder filters 2025-08-18 21:02:52 +05:30
Vikrant Gupta
a4f3be5e46 feat(meter): add pre-defined panels for meter breakdown and improvements (#8827)
* feat(meter): add pre-defined panels for meter breakdown

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

* feat(meter): added graphs for total calculation

* feat(meter): added graphs for total calculation
2025-08-18 19:44:44 +05:30
Srikanth Chekuri
8f833fa62c fix: incorrect query prepared for group by body.{key} (#8823) 2025-08-18 15:11:53 +05:30
dependabot[bot]
7029233596 chore(deps): bump @babel/runtime from 7.21.0 to 7.28.2 in /frontend (#8726)
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.21.0 to 7.28.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.2/packages/babel-runtime)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-18 07:09:52 +00:00
Vibhu Pandey
d26efd2833 feat: address bitnami migration (#8808) 2025-08-14 20:54:28 +05:30
Abhi kumar
0e3ac2a179 fix: added loading indicators in traces pages when running query (#8782) 2025-08-14 13:53:39 +05:30
Aditya Singh
7a319d926f Merge branch 'SIG-5603-2' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-14 13:12:39 +05:30
Amlan Kumar Nandy
249f8be845 fix: resolve infinite loading issue in metric view in messaging queues (#8779) 2025-08-14 04:16:39 +00:00
Aditya Singh
4d7b54382d feat: cross filtering init 2025-08-14 02:35:03 +05:30
Aditya Singh
0950a74e96 Merge branch 'feat/custom-destinations' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-14 02:01:01 +05:30
Aditya Singh
b90ab7fe1b feat: pass panel types to substitutevars 2025-08-14 02:00:13 +05:30
Aditya Singh
1915df8ad7 feat: remove consoles 2025-08-13 21:15:00 +05:30
Aditya Singh
eb37dafcd1 feat: refactor 2025-08-13 21:07:46 +05:30
Aditya Singh
c5682b98c5 Merge branch 'main' of github.com:SigNoz/signoz into feat/custom-destinations 2025-08-13 18:19:02 +05:30
primus-bot[bot]
9c952942ad chore(release): bump to v0.92.1 (#8780)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-08-13 15:10:08 +05:30
Nityananda Gohain
dac46d82ff fix: check ch version (#8778)
Check the clickhouse version, before the setting secondary_indices_enable_bulk_filtering is used.
2025-08-13 14:57:25 +05:30
primus-bot[bot]
802ce6de01 chore(release): bump to v0.92.0 (#8776)
#### Summary
 - Release SigNoz v0.92.0
 - Bump SigNoz OTel Collector to v0.129.0
2025-08-13 12:17:43 +05:30
dependabot[bot]
6853f0c99d chore(deps): bump urllib3 from 2.4.0 to 2.5.0 in /tests/integration (#8296) 2025-08-13 04:58:39 +00:00
Srikanth Chekuri
3f8a2870e4 fix: key CONTAINS value doesn't work for numeric values (#8768) 2025-08-13 09:59:28 +05:30
Srikanth Chekuri
5fa70ea802 chore: use *_keys tables instead of tag_attributes_v2 for suggestions (#8753) 2025-08-12 18:10:35 +05:30
Aditya Singh
7fbe7ab019 Merge branch 'variables-features' of github.com:SigNoz/signoz into feat/cross-filtering 2025-08-12 13:27:43 +05:30
Aditya Singh
b14e77a120 Merge branch 'main' of github.com:SigNoz/signoz into feat/custom-destinations 2025-08-12 13:12:40 +05:30
Aditya Singh
75f30e6117 feat: added test cases 2025-08-12 03:06:41 +05:30
Aditya Singh
c53a599b2e feat: minor refactor 2025-08-11 18:59:19 +05:30
Yunus M
3a952fa330 fix: pass metric name to get value suggestions api (#8671)
* fix: pass metric name to get value suggestions api

* feat: add source to get value suggestions
2025-08-11 08:10:31 +00:00
Yunus M
6d97db1d9d fix: use localstorage value to avoid waiting for pref api to set the toggle state, add shortcut (#8751) 2025-08-11 10:26:27 +05:30
SagarRajput-7
8f4832de3e feat: light-mode styles 2025-08-11 08:24:14 +05:30
SagarRajput-7
a257208254 feat: added button loader for apply-all 2025-08-11 08:24:05 +05:30
SagarRajput-7
55df468435 feat: refectch only related and affected panels in case of dynamic variables 2025-08-11 08:23:52 +05:30
SagarRajput-7
8334b5cb87 feat: added apply to all and variable removal logical 2025-08-11 08:23:36 +05:30
SagarRajput-7
2cdcec9d07 feat: show labels in widget selector 2025-08-11 08:22:05 +05:30
SagarRajput-7
b4a3645d1f feat: added widgetselector on variable creation 2025-08-11 08:21:33 +05:30
SagarRajput-7
f786576895 feat: added ability to add/remove variable filter to one or more existing panels 2025-08-11 08:13:24 +05:30
SagarRajput-7
30d16a3f48 feat: corrected the regex matcher for resolved titles 2025-08-11 08:10:44 +05:30
SagarRajput-7
9745e9e3a2 feat: updated test case 2025-08-11 08:07:46 +05:30
SagarRajput-7
a2deba11af feat: code refactor 2025-08-11 08:07:37 +05:30
SagarRajput-7
d8afa24184 feat: added test case for querybuildersearchv2 suggestion changes 2025-08-11 08:07:26 +05:30
SagarRajput-7
16165c3bd2 feat: added test cases for hooks and api call functions 2025-08-11 08:07:18 +05:30
SagarRajput-7
bcf3b8f1ac feat: added dynamic variable suggestion in where clause 2025-08-11 08:07:08 +05:30
SagarRajput-7
13b39d9b13 feat: resolved conflicts 2025-08-11 07:43:52 +05:30
SagarRajput-7
0be18b7e77 feat: added variable in url and made dashboard sync around that and sharable (#7944)
* feat: added dynamic variable to the dashboard details

* feat: added new component to existing variables

* feat: added enhancement to multiselect and select for dyn-variables

* feat: added refetch method between all dynamic-variables

* feat: correct error handling

* feat: correct error handling

* feat: enforced non-empty selectedvalues and default value

* feat: added client and server side searches

* feat: retry on error

* feat: correct error handling

* feat: handle defautl value in existing variables

* feat: lowercase the source for payload

* feat: fixed the incorrect assignment of active indices

* feat: improved handling of all option

* feat: improved the ALL option visuals

* feat: handled default value enforcement in existing variables

* feat: added unix time to values call

* feat: added incomplete data message and info to search

* feat: changed dashboard panel call handling with existing variables

* feat: adjusted the response type and data with the new API schema for values

* feat: code refactor

* feat: made dyn-variable option as the default

* feat: added test cases for dyn variable creation and completion

* feat: updated test cases

* feat: added variable in url and made dashboard sync around that and sharable

* feat: added test cases

* feat: added safety check

* feat: enabled url setting on first load itself

* feat: code refactor

* feat: cleared options query param when on dashboard list page
2025-08-11 07:27:39 +05:30
SagarRajput-7
cd6105a6b9 feat: added dynamic variable to the dashboard details (#7755)
* feat: added dynamic variable to the dashboard details

* feat: added new component to existing variables

* feat: added enhancement to multiselect and select for dyn-variables

* feat: added refetch method between all dynamic-variables

* feat: correct error handling

* feat: correct error handling

* feat: enforced non-empty selectedvalues and default value

* feat: added client and server side searches

* feat: retry on error

* feat: correct error handling

* feat: handle defautl value in existing variables

* feat: lowercase the source for payload

* feat: fixed the incorrect assignment of active indices

* feat: improved handling of all option

* feat: improved the ALL option visuals

* feat: handled default value enforcement in existing variables

* feat: added unix time to values call

* feat: added incomplete data message and info to search

* feat: changed dashboard panel call handling with existing variables

* feat: adjusted the response type and data with the new API schema for values

* feat: code refactor

* feat: made dyn-variable option as the default

* feat: added test cases for dyn variable creation and completion

* feat: updated test cases
2025-08-11 07:25:59 +05:30
SagarRajput-7
9f23a39abe feat: added dynamic variables creation flow (#7541)
* feat: added dynamic variables creation flow

* feat: added keys and value apis and hooks

* feat: added api and select component changes

* feat: added keys fetching and preview values

* feat: added dynamic variable to variable items

* feat: handled value persistence and tab switches

* feat: added default value and formed a schema for dyn-variables

* feat: added client and server side searches

* feat: corrected the initial load getfieldKey api

* feat: removed fetch on mount restriction
2025-08-11 07:24:45 +05:30
Shaheer Kochai
5412e7f70b feat: show count in span details drawer tabs (#8702)
* feat: show event count in Events tab of SpanDetailsDrawer

* feat: add count badges to all SpanDetailsDrawer tabs
2025-08-10 05:39:20 +00:00
Aditya Singh
19216e107c Merge branch 'main' of github.com:SigNoz/signoz into feat/custom-destinations 2025-08-08 18:10:03 +05:30
Aditya Singh
2aa423de52 feat: test fix 2025-08-08 18:08:47 +05:30
aniketio-ctrl
8e5cb9046d fix(alert): added querier v5 in test notify (#8749) 2025-08-08 18:01:23 +05:30
Srikanth Chekuri
760eabb2dc chore: do not return err for meter source temporality (#8750) 2025-08-08 17:39:06 +05:30
Srikanth Chekuri
35ddaaa2fc chore: add env to override logs keys table name (#8748) 2025-08-08 11:34:09 +00:00
nikhilmantri0902
a51ee66c02 Improvement: Added Otel-collector setup for local dev environment (#8701)
* feat(devenv): add otel-collector support for local development

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

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

* docs: improve collector setup wordings

* chore: fixed comment and service name

* chore: docker service name updated otel-collector -> signoz-otel-collector
2025-08-08 16:54:05 +05:30
Yunus M
75d189162b feat: migrate old saved columns keys to name (#8747) 2025-08-08 14:41:34 +05:30
Aditya Singh
3f7175daa3 feat: minor fix 2025-08-08 13:58:39 +05:30
Aditya Singh
0d7a6794b4 feat: minor fix 2025-08-08 13:52:56 +05:30
Aditya Singh
312f02c318 Merge branch 'fix/extract-query-params' of github.com:SigNoz/signoz into feat/custom-destinations 2025-08-08 13:51:01 +05:30
Abhi Kumar
0dd085c48e feat: optimize query value comparison in QueryBuilderV2 2025-08-08 13:11:20 +05:30
Yunus M
932918e3a4 feat: meter explorer (#8741)
* feat: meter explorer

* feat: meter explorer

* fix: remove meter as data source

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

* chore: delete test file

* fix: failing test cases
2025-08-08 12:03:26 +05:30
Aditya Singh
020bf76570 Merge branch 'main' of github.com:SigNoz/signoz into feat/custom-destinations 2025-08-08 10:51:53 +05:30
Aditya Singh
3191f81046 feat: minor fix 2025-08-08 10:50:51 +05:30
Aditya Singh
9d59fb8d05 feat: add substitute var api call to decode vars 2025-08-08 02:50:34 +05:30
Aditya Singh
dfe024e234 feat: minor refactor 2025-08-08 01:24:09 +05:30
Vibhu Pandey
aa3bc16dcb test(integration): bump requests to 2.32.4 (#8743) 2025-08-08 00:25:38 +05:30
Yunus M
b5098e00a3 fix: logs explorer - should have atleast 1 column, discard empty key columns (#8740) 2025-08-07 20:17:34 +05:30
Abhi kumar
20dc561bfe fix: added fix for query becoming empty on time change (#8739) 2025-08-07 19:42:07 +05:30
Nityananda Gohain
99bbb87738 chore: add option to ignore data skipping indices (#8738)
* chore: add option to ignore data skipping indices

* fix: update example
2025-08-07 13:21:17 +00:00
Aditya Singh
2f4ae5ad05 feat: add back in breakout 2025-08-07 18:13:30 +05:30
Aditya Singh
68714b14c1 feat: minor refactor 2025-08-07 18:09:44 +05:30
Vikrant Gupta
f1ce93171c feat(telemetrymeter): add support for telemetry meter (#8667)
* feat(telemetry/meter): added base setup for telemetry meter signal

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

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

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

* feat(telemetry/meter): improve error messages

* feat(telemetrymeter): step interval improvements

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

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

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

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

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

* feat(telemetrymeter): cleanup the types

* feat(telemetrymeter): introduce source for query

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

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

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

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

* feat(telemetrymeter): use meter aggregate keys

* feat(telemetrymeter): use meter aggregate keys

* feat(telemetrymeter): remove meter from complete bools

* feat(telemetrymeter): remove meter from complete bools

* feat(telemetrymeter): update the quick filters to use meter
2025-08-07 16:50:37 +05:30
Abhi Kumar
531a0a12dd fix: added fix for extractquerypararms when value is string in multivalue operator 2025-08-07 15:52:56 +05:30
Aditya Singh
9a2c74ccbc feat: minor refactor 2025-08-07 11:19:49 +05:30
Aditya Singh
031575cb27 feat: minor refactor 2025-08-07 10:54:06 +05:30
Aditya Singh
c4eefc4935 feat: change api for breakout opitons 2025-08-07 02:18:24 +05:30
Srikanth Chekuri
92794389d6 fix: limit keys for empty search key (#8728) 2025-08-07 00:34:44 +05:30
Aditya Singh
db36f0c336 feat: fix breaking changes from qb v5 2025-08-06 23:22:42 +05:30
Srikanth Chekuri
bd02848623 chore: add sql migration for dashboards, alerts, and saved views (#8642)
## 📄 Summary

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

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

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

* chore: added box-shadows in the dropdowns
2025-08-06 12:48:07 +05:30
Aditya Singh
ddacc77100 Merge branch 'feat/drilldowns' of github.com:SigNoz/signoz into feat/custom-destinations 2025-08-06 12:15:23 +05:30
Abhi kumar
3a2eab2019 fixes: includes fixes required in the new QB (#8675)
* fix: removed unused code for querycontext (#8674)

* Update frontend/src/utils/queryValidationUtils.ts

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

* feat: added tooltips in metric aggregations

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

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

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

* fix: cloned panel query shows previous query

* chore: removed comments

* chore: added null check

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

---------

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

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

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

* feat: add warning notification for query response

* feat: fixed failing test case

* fix: metric histogram UI config state in edit mode

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

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

* fix: added fix for QB filters for functions

* chore: minor fix

---------

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

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

* feat: legend format fixes around single and multiple aggregation

* feat: fixed table unit and metric units

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

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

* feat: fix and cleanup functions across product for v5

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

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

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

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

* fix: fixed keyfetching logic (#8712)

* chore: lint fix

* fix: fixed logs explorer test

* feat: fix type checks

---------

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

* feat: added warning popover content for QB v5 feature

* feat: icon change for warning

* feat: added warning to QB v5 components

* feat: fixed type error

* feat: fix test cases

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-08-05 23:45:39 +05:30
Amlan Kumar Nandy
2901e052ae chore: improve metrics explorer empty state (#8711) 2025-08-05 20:45:21 +07:00
Aditya Singh
d8d1c2ea7a feat: handle on save 2025-08-04 18:53:54 +05:30
Aditya Singh
bf1378f144 feat: minor refactor 2025-08-04 13:32:27 +05:30
Aditya Singh
2207643e21 feat: minor refactor 2025-08-04 13:31:00 +05:30
Aditya Singh
2af035d3cf feat: minor refactor 2025-08-04 12:52:37 +05:30
Aditya Singh
acc4db2ce4 feat: add support for field variables 2025-08-03 17:26:35 +05:30
Aditya Singh
f9dd1d6b69 feat: context variables hook added 2025-08-03 16:05:42 +05:30
Aditya Singh
e9c6513328 feat: context links processors 2025-08-03 16:01:33 +05:30
Aditya Singh
fa047ba7db Merge branch 'feat/drilldown-tables-v2' of github.com:SigNoz/signoz into feat/custom-destinations 2025-08-01 18:26:58 +05:30
Aditya Singh
c80f020145 feat: context menu changes init 2025-07-31 20:51:23 +05:30
Aditya Singh
3748b9d24b feat: change contextlinks data structure 2025-07-31 19:06:49 +05:30
Aditya Singh
28370d219e feat: minor refactor 2025-07-31 16:20:42 +05:30
Aditya Singh
a03d2ba961 feat: minor refactor 2025-07-31 02:32:28 +05:30
Aditya Singh
e08045d413 feat: add double way sync on urls and param 2025-07-31 02:11:18 +05:30
Aditya Singh
fd073d9788 feat: update context link modal form init 2025-07-30 21:19:47 +05:30
Aditya Singh
e57a21dd92 feat: context links init 2025-07-30 01:16:27 +05:30
Aditya Singh
53e10602b6 feat: context links init 2025-07-30 01:09:14 +05:30
Aditya Singh
8168d8bea0 feat: context links init 2025-07-30 01:07:36 +05:30
Aditya Singh
b18f998d0e feat: add context links 2025-07-29 14:53:21 +05:30
806 changed files with 49117 additions and 9363 deletions

View File

@@ -24,7 +24,7 @@ services:
depends_on:
- zookeeper
zookeeper:
image: bitnami/zookeeper:3.7.1
image: signoz/zookeeper:3.7.1
container_name: zookeeper
volumes:
- ${PWD}/fs/tmp/zookeeper:/bitnami/zookeeper
@@ -40,7 +40,7 @@ services:
timeout: 5s
retries: 3
schema-migrator-sync:
image: signoz/signoz-schema-migrator:v0.128.2
image: signoz/signoz-schema-migrator:v0.129.2
container_name: schema-migrator-sync
command:
- sync
@@ -53,7 +53,7 @@ services:
condition: service_healthy
restart: on-failure
schema-migrator-async:
image: signoz/signoz-schema-migrator:v0.128.2
image: signoz/signoz-schema-migrator:v0.129.2
container_name: schema-migrator-async
command:
- async

View File

@@ -0,0 +1,29 @@
services:
signoz-otel-collector:
image: signoz/signoz-otel-collector:v0.128.2
container_name: signoz-otel-collector-dev
command:
- --config=/etc/otel-collector-config.yaml
- --feature-gates=-pkg.translator.prometheus.NormalizeName
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- LOW_CARDINAL_EXCEPTION_GROUPING=false
ports:
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
- "13133:13133" # health check extension
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- localhost:13133
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"

View File

@@ -0,0 +1,96 @@
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-collector
processors:
batch:
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
resourcedetection:
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
detectors: [env, system]
timeout: 2s
signozspanmetrics/delta:
metrics_exporter: signozclickhousemetrics
metrics_flush_interval: 60s
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
dimensions_cache_size: 100000
aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA
enable_exp_histogram: true
dimensions:
- name: service.namespace
default: default
- name: deployment.environment
default: default
# This is added to ensure the uniqueness of the timeseries
# Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics
- name: signoz.collector.id
- name: service.version
- name: browser.platform
- name: browser.mobile
- name: k8s.cluster.name
- name: k8s.node.name
- name: k8s.namespace.name
- name: host.name
- name: host.type
- name: container.name
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 0.0.0.0:1777
exporters:
clickhousetraces:
datasource: tcp://host.docker.internal:9000/signoz_traces
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
use_new_schema: true
signozclickhousemetrics:
dsn: tcp://host.docker.internal:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://host.docker.internal:9000/signoz_logs
timeout: 10s
use_new_schema: true
service:
telemetry:
logs:
encoding: json
extensions:
- health_check
- pprof
pipelines:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter]

4
.github/CODEOWNERS vendored
View File

@@ -42,3 +42,7 @@
/pkg/telemetrymetadata/ @srikanthccv
/pkg/telemetrymetrics/ @srikanthccv
/pkg/telemetrytraces/ @srikanthccv
# AuthN / AuthZ Owners
/pkg/authz/ @vikrantgupta25 @grandwizard28

View File

@@ -62,7 +62,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.23
GO_VERSION: 1.24
GO_NAME: signoz-community
GO_INPUT_ARTIFACT_CACHE_KEY: community-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build

View File

@@ -93,7 +93,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.23
GO_VERSION: 1.24
GO_INPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
GO_BUILD_CONTEXT: ./cmd/enterprise

View File

@@ -92,7 +92,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.23
GO_VERSION: 1.24
GO_INPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
GO_BUILD_CONTEXT: ./cmd/enterprise

View File

@@ -18,7 +18,7 @@ jobs:
with:
PRIMUS_REF: main
GO_TEST_CONTEXT: ./...
GO_VERSION: 1.23
GO_VERSION: 1.24
fmt:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -27,7 +27,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.23
GO_VERSION: 1.24
lint:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -36,7 +36,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.23
GO_VERSION: 1.24
deps:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -45,7 +45,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.23
GO_VERSION: 1.24
build:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -57,7 +57,7 @@ jobs:
- name: go-install
uses: actions/setup-go@v5
with:
go-version: "1.23"
go-version: "1.24"
- name: qemu-install
uses: docker/setup-qemu-action@v3
- name: aarch64-install

View File

@@ -58,7 +58,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
go-version: "1.23"
go-version: "1.24"
- name: cross-compilation-tools
if: matrix.os == 'ubuntu-latest'
run: |
@@ -122,7 +122,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
go-version: "1.23"
go-version: "1.24"
# copy the caches from build
- name: get-sha

View File

@@ -72,7 +72,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
go-version: "1.23"
go-version: "1.24"
- name: cross-compilation-tools
if: matrix.os == 'ubuntu-latest'
run: |
@@ -135,7 +135,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
go-version: "1.23"
go-version: "1.24"
# copy the caches from build
- name: get-sha

2
.gitignore vendored
View File

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

View File

@@ -61,6 +61,17 @@ devenv-postgres: ## Run postgres in devenv
@cd .devenv/docker/postgres; \
docker compose -f compose.yaml up -d
.PHONY: devenv-signoz-otel-collector
devenv-signoz-otel-collector: ## Run signoz-otel-collector in devenv (requires clickhouse to be running)
@cd .devenv/docker/signoz-otel-collector; \
docker compose -f compose.yaml up -d
.PHONY: devenv-up
devenv-up: devenv-clickhouse devenv-signoz-otel-collector ## Start both clickhouse and signoz-otel-collector for local development
@echo "Development environment is ready!"
@echo " - ClickHouse: http://localhost:8123"
@echo " - Signoz OTel Collector: grpc://localhost:4317, http://localhost:4318"
##############################################################
# go commands
##############################################################

View File

@@ -2,10 +2,11 @@ FROM node:18-bullseye AS build
WORKDIR /opt/
COPY ./frontend/ ./
ENV NODE_OPTIONS=--max-old-space-size=8192
RUN CI=1 yarn install
RUN CI=1 yarn build
FROM golang:1.23-bullseye
FROM golang:1.24-bullseye
ARG OS="linux"
ARG TARGETARCH

View File

@@ -121,6 +121,8 @@ telemetrystore:
timeout_before_checking_execution_speed: 0
max_bytes_to_read: 0
max_result_rows: 0
ignore_data_skipping_indices: ""
secondary_indices_enable_bulk_filtering: false
##################### Prometheus #####################
prometheus:

View File

@@ -39,7 +39,7 @@ x-clickhouse-defaults: &clickhouse-defaults
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: bitnami/zookeeper:3.7.1
image: signoz/zookeeper:3.7.1
user: root
deploy:
labels:
@@ -174,7 +174,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.91.0
image: signoz/signoz:v0.93.0
command:
- --config=/root/config/prometheus.yml
ports:
@@ -207,7 +207,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.128.2
image: signoz/signoz-otel-collector:v0.129.2
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -231,7 +231,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.128.2
image: signoz/signoz-schema-migrator:v0.129.2
deploy:
restart_policy:
condition: on-failure

View File

@@ -38,7 +38,7 @@ x-clickhouse-defaults: &clickhouse-defaults
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: bitnami/zookeeper:3.7.1
image: signoz/zookeeper:3.7.1
user: root
deploy:
labels:
@@ -115,7 +115,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.91.0
image: signoz/signoz:v0.93.0
command:
- --config=/root/config/prometheus.yml
ports:
@@ -148,7 +148,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.128.2
image: signoz/signoz-otel-collector:v0.129.2
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -174,7 +174,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.128.2
image: signoz/signoz-schema-migrator:v0.129.2
deploy:
restart_policy:
condition: on-failure

View File

@@ -42,7 +42,7 @@ x-clickhouse-defaults: &clickhouse-defaults
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: bitnami/zookeeper:3.7.1
image: signoz/zookeeper:3.7.1
user: root
labels:
signoz.io/scrape: "true"
@@ -177,7 +177,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.91.0}
image: signoz/signoz:${VERSION:-v0.93.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -211,7 +211,7 @@ services:
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.2}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -237,7 +237,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.2}
container_name: schema-migrator-sync
command:
- sync
@@ -248,7 +248,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.2}
container_name: schema-migrator-async
command:
- async

View File

@@ -38,7 +38,7 @@ x-clickhouse-defaults: &clickhouse-defaults
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: bitnami/zookeeper:3.7.1
image: signoz/zookeeper:3.7.1
user: root
labels:
signoz.io/scrape: "true"
@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.91.0}
image: signoz/signoz:${VERSION:-v0.93.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -143,7 +143,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.2}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -165,7 +165,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.2}
container_name: schema-migrator-sync
command:
- sync
@@ -177,7 +177,7 @@ services:
restart: on-failure
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.2}
container_name: schema-migrator-async
command:
- async

View File

@@ -44,20 +44,35 @@ Before diving in, make sure you have these tools installed:
SigNoz has three main components: Clickhouse, Backend, and Frontend. Let's set them up one by one.
### 1. Setting up Clickhouse
### 1. Setting up ClickHouse
First, we need to get Clickhouse running:
First, we need to get ClickHouse running:
```bash
make devenv-clickhouse
```
This command:
- Starts Clickhouse in a single-shard, single-replica cluster
- Starts ClickHouse in a single-shard, single-replica cluster
- Sets up Zookeeper
- Runs the latest schema migrations
### 2. Starting the Backend
### 2. Setting up SigNoz OpenTelemetry Collector
Next, start the OpenTelemetry Collector to receive telemetry data:
```bash
make devenv-signoz-otel-collector
```
This command:
- Starts the SigNoz OpenTelemetry Collector
- Listens on port 4317 (gRPC) and 4318 (HTTP) for incoming telemetry data
- Forwards data to ClickHouse for storage
> 💡 **Quick Setup**: Use `make devenv-up` to start both ClickHouse and OTel Collector together
### 3. Starting the Backend
1. Run the backend server:
```bash
@@ -73,7 +88,7 @@ This command:
> 💡 **Tip**: The API server runs at `http://localhost:8080/` by default
### 3. Setting up the Frontend
### 4. Setting up the Frontend
1. Navigate to the frontend directory:
```bash
@@ -98,3 +113,25 @@ This command:
> 💡 **Tip**: `yarn dev` will automatically rebuild when you make changes to the code
Now you're all set to start developing! Happy coding! 🎉
## Verifying Your Setup
To verify everything is working correctly:
1. **Check ClickHouse**: `curl http://localhost:8123/ping` (should return "Ok.")
2. **Check OTel Collector**: `curl http://localhost:13133` (should return health status)
3. **Check Backend**: `curl http://localhost:8080/api/v1/health` (should return `{"status":"ok"}`)
4. **Check Frontend**: Open `http://localhost:3301` in your browser
## How to send test data?
You can now send telemetry data to your local SigNoz instance:
- **OTLP gRPC**: `localhost:4317`
- **OTLP HTTP**: `localhost:4318`
For example, using `curl` to send a test trace:
```bash
curl -X POST http://localhost:4318/v1/traces \
-H "Content-Type: application/json" \
-d '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"test-service"}}]},"scopeSpans":[{"spans":[{"traceId":"12345678901234567890123456789012","spanId":"1234567890123456","name":"test-span","startTimeUnixNano":"1609459200000000000","endTimeUnixNano":"1609459201000000000"}]}]}]}'
```

View File

@@ -13,11 +13,11 @@ import (
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/user"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/google/uuid"
"github.com/gorilla/mux"
"go.uber.org/zap"
)
@@ -192,14 +192,14 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
))
}
password, err := types.NewFactorPassword(uuid.NewString())
password := types.MustGenerateFactorPassword(newUser.ID.StringValue())
integrationUser, err := ah.Signoz.Modules.User.CreateUserWithPassword(ctx, newUser, password)
err = ah.Signoz.Modules.User.CreateUser(ctx, newUser, user.WithFactorPassword(password))
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
}
return integrationUser, nil
return newUser, nil
}
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (

View File

@@ -257,6 +257,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
s.config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
r.Use(middleware.NewComment().Wrap)
apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)

View File

@@ -1,7 +1,7 @@
package model
import (
"fmt"
"errors"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
)
@@ -57,7 +57,7 @@ func Unauthorized(err error) *ApiError {
func BadRequestStr(s string) *ApiError {
return &ApiError{
Typ: basemodel.ErrorBadData,
Err: fmt.Errorf(s),
Err: errors.New(s),
}
}
@@ -73,7 +73,7 @@ func InternalError(err error) *ApiError {
func InternalErrorStr(s string) *ApiError {
return &ApiError{
Typ: basemodel.ErrorInternal,
Err: fmt.Errorf(s),
Err: errors.New(s),
}
}

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ const config: Config.InitialOptions = {
'ts-jest': {
useESM: true,
isolatedModules: true,
tsconfig: '<rootDir>/tsconfig.jest.json',
},
},
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
@@ -25,7 +26,7 @@ const config: Config.InitialOptions = {
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -43,11 +43,19 @@
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "8.41.0",
"@sentry/webpack-plugin": "2.22.6",
"@signozhq/badge": "0.0.2",
"@signozhq/calendar": "0.0.0",
"@signozhq/callout": "0.0.2",
"@signozhq/design-tokens": "1.1.4",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
"@signozhq/sonner": "0.1.0",
"@signozhq/table": "0.3.7",
"@signozhq/tooltip": "0.0.2",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-github": "4.24.1",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/codemirror-theme-github": "4.24.1",
"@uiw/react-codemirror": "4.23.10",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
@@ -92,6 +100,7 @@
"i18next-http-backend": "^1.3.2",
"jest": "^27.5.1",
"js-base64": "^3.7.2",
"kbar": "0.1.0-beta.48",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",

View File

@@ -46,5 +46,8 @@
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
"METER": "SigNoz | Meter"
}

View File

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

View File

@@ -4,6 +4,7 @@ import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import AppLoading from 'components/AppLoading/AppLoading';
import KBarCommandPalette from 'components/KBarCommandPalette/KBarCommandPalette';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import UserpilotRouteTracker from 'components/UserpilotRouteTracker/UserpilotRouteTracker';
@@ -25,6 +26,7 @@ import { useAppContext } from 'providers/App/App';
import { IUser } from 'providers/App/types';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { KBarCommandPaletteProvider } from 'providers/KBarCommandPaletteProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
@@ -368,39 +370,42 @@ function App(): JSX.Element {
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<CompatRouter>
<UserpilotRouteTracker />
<NotificationProvider>
<ErrorModalProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</ErrorModalProvider>
</NotificationProvider>
<KBarCommandPaletteProvider>
<UserpilotRouteTracker />
<KBarCommandPalette />
<NotificationProvider>
<ErrorModalProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</ErrorModalProvider>
</NotificationProvider>
</KBarCommandPaletteProvider>
</CompatRouter>
</Router>
</ConfigProvider>

View File

@@ -1,5 +1,6 @@
import ROUTES from 'constants/routes';
import MessagingQueues from 'pages/MessagingQueues';
import MeterExplorer from 'pages/MeterExplorer';
import { RouteProps } from 'react-router-dom';
import {
@@ -434,6 +435,28 @@ const routes: AppRoutes[] = [
key: 'METRICS_EXPLORER_VIEWS',
isPrivate: true,
},
{
path: ROUTES.METER,
exact: true,
component: MeterExplorer,
key: 'METER',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER,
exact: true,
component: MeterExplorer,
key: 'METER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER_VIEWS,
exact: true,
component: MeterExplorer,
key: 'METER_EXPLORER_VIEWS',
isPrivate: true,
},
{
path: ROUTES.API_MONITORING,
exact: true,

View File

@@ -0,0 +1,34 @@
import { ApiV5Instance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { QueryRangePayloadV5 } from 'api/v5/v5';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
interface ISubstituteVars {
compositeQuery: ICompositeMetricQuery;
}
export const getSubstituteVars = async (
props?: Partial<QueryRangePayloadV5>,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponseV2<ISubstituteVars>> => {
try {
const response = await ApiV5Instance.post<{ data: ISubstituteVars }>(
'/substitute_vars',
props,
{
signal,
headers,
},
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};

View File

@@ -0,0 +1,115 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { ApiBaseInstance } from 'api';
import { getFieldKeys } from '../getFieldKeys';
// Mock the API instance
jest.mock('api', () => ({
ApiBaseInstance: {
get: jest.fn(),
},
}));
describe('getFieldKeys API', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const mockSuccessResponse = {
status: 200,
data: {
status: 'success',
data: {
keys: {
'service.name': [],
'http.status_code': [],
},
complete: true,
},
},
};
it('should call API with correct parameters when no args provided', async () => {
// Mock successful API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
// Call function with no parameters
await getFieldKeys();
// Verify API was called correctly with empty params object
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
params: {},
});
});
it('should call API with signal parameter when provided', async () => {
// Mock successful API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
// Call function with signal parameter
await getFieldKeys('traces');
// Verify API was called with signal parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
params: { signal: 'traces' },
});
});
it('should call API with name parameter when provided', async () => {
// Mock successful API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
data: {
keys: { service: [] },
complete: false,
},
},
});
// Call function with name parameter
await getFieldKeys(undefined, 'service');
// Verify API was called with name parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
params: { name: 'service' },
});
});
it('should call API with both signal and name when provided', async () => {
// Mock successful API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
data: {
keys: { service: [] },
complete: false,
},
},
});
// Call function with both parameters
await getFieldKeys('logs', 'service');
// Verify API was called with both parameters
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
params: { signal: 'logs', name: 'service' },
});
});
it('should return properly formatted response', async () => {
// Mock API to return our response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
// Call the function
const result = await getFieldKeys('traces');
// Verify the returned structure matches SuccessResponseV2 format
expect(result).toEqual({
httpStatusCode: 200,
data: mockSuccessResponse.data.data,
});
});
});

View File

@@ -0,0 +1,214 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { ApiBaseInstance } from 'api';
import { getFieldValues } from '../getFieldValues';
// Mock the API instance
jest.mock('api', () => ({
ApiBaseInstance: {
get: jest.fn(),
},
}));
describe('getFieldValues API', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should call the API with correct parameters (no options)', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
});
// Call function without parameters
await getFieldValues();
// Verify API was called correctly with empty params
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: {},
});
});
it('should call the API with signal parameter', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
});
// Call function with signal parameter
await getFieldValues('traces');
// Verify API was called with signal parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: { signal: 'traces' },
});
});
it('should call the API with name parameter', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
});
// Call function with name parameter
await getFieldValues(undefined, 'service.name');
// Verify API was called with name parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: { name: 'service.name' },
});
});
it('should call the API with value parameter', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend'],
},
complete: false,
},
},
});
// Call function with value parameter
await getFieldValues(undefined, 'service.name', 'front');
// Verify API was called with value parameter
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: { name: 'service.name', searchText: 'front' },
});
});
it('should call the API with time range parameters', async () => {
// Mock API response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
});
// Call function with time range parameters
const startUnixMilli = 1625097600000000; // Note: nanoseconds
const endUnixMilli = 1625184000000000;
await getFieldValues(
'logs',
'service.name',
undefined,
startUnixMilli,
endUnixMilli,
);
// Verify API was called with time range parameters (converted to milliseconds)
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
params: {
signal: 'logs',
name: 'service.name',
startUnixMilli: '1625097600', // Should be converted to seconds (divided by 1000000)
endUnixMilli: '1625184000', // Should be converted to seconds (divided by 1000000)
},
});
});
it('should normalize the response values', async () => {
// Mock API response with multiple value types
const mockResponse = {
status: 200,
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
numberValues: [200, 404],
boolValues: [true, false],
},
complete: true,
},
},
};
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockResponse);
// Call the function
const result = await getFieldValues('traces', 'mixed.values');
// Verify the response has normalized values array
expect(result.data?.normalizedValues).toContain('frontend');
expect(result.data?.normalizedValues).toContain('backend');
expect(result.data?.normalizedValues).toContain('200');
expect(result.data?.normalizedValues).toContain('404');
expect(result.data?.normalizedValues).toContain('true');
expect(result.data?.normalizedValues).toContain('false');
expect(result.data?.normalizedValues?.length).toBe(6);
});
it('should return a properly formatted success response', async () => {
// Create mock response
const mockApiResponse = {
status: 200,
data: {
status: 'success',
data: {
values: {
stringValues: ['frontend', 'backend'],
},
complete: true,
},
},
};
// Mock API to return our response
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockApiResponse);
// Call the function
const result = await getFieldValues('traces', 'service.name');
// Verify the returned structure matches SuccessResponseV2 format
expect(result).toEqual({
httpStatusCode: 200,
data: expect.objectContaining({
values: expect.any(Object),
normalizedValues: expect.any(Array),
complete: true,
}),
});
});
});

View File

@@ -0,0 +1,38 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { FieldKeyResponse } from 'types/api/dynamicVariables/getFieldKeys';
/**
* Get field keys for a given signal type
* @param signal Type of signal (traces, logs, metrics)
* @param name Optional search text
*/
export const getFieldKeys = async (
signal?: 'traces' | 'logs' | 'metrics',
name?: string,
): Promise<SuccessResponseV2<FieldKeyResponse>> => {
const params: Record<string, string> = {};
if (signal) {
params.signal = encodeURIComponent(signal);
}
if (name) {
params.name = encodeURIComponent(name);
}
try {
const response = await ApiBaseInstance.get('/fields/keys', { params });
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getFieldKeys;

View File

@@ -0,0 +1,87 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { FieldValueResponse } from 'types/api/dynamicVariables/getFieldValues';
/**
* Get field values for a given signal type and field name
* @param signal Type of signal (traces, logs, metrics)
* @param name Name of the attribute for which values are being fetched
* @param value Optional search text
* @param existingQuery Optional existing query - across all present dynamic variables
*/
export const getFieldValues = async (
signal?: 'traces' | 'logs' | 'metrics',
name?: string,
searchText?: string,
startUnixMilli?: number,
endUnixMilli?: number,
existingQuery?: string,
): Promise<SuccessResponseV2<FieldValueResponse>> => {
const params: Record<string, string> = {};
if (signal) {
params.signal = encodeURIComponent(signal);
}
if (name) {
params.name = encodeURIComponent(name);
}
if (searchText) {
params.searchText = encodeURIComponent(searchText);
}
if (startUnixMilli) {
params.startUnixMilli = Math.floor(startUnixMilli / 1000000).toString();
}
if (endUnixMilli) {
params.endUnixMilli = Math.floor(endUnixMilli / 1000000).toString();
}
if (existingQuery) {
params.existingQuery = existingQuery;
}
try {
const response = await ApiBaseInstance.get('/fields/values', { params });
// Normalize values from different types (stringValues, boolValues, etc.)
if (response.data?.data?.values) {
const allValues: string[] = [];
Object.entries(response.data?.data?.values).forEach(
([key, valueArray]: [string, any]) => {
// Skip RelatedValues as they should be kept separate
if (key === 'relatedValues') {
return;
}
if (Array.isArray(valueArray)) {
allValues.push(...valueArray.map(String));
}
},
);
// Add a normalized values array to the response
response.data.data.normalizedValues = allValues;
// Add relatedValues to the response as per FieldValueResponse
if (response.data?.data?.values?.relatedValues) {
response.data.data.relatedValues =
response.data?.data?.values?.relatedValues;
}
}
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getFieldValues;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
import { ApiV2Instance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/settings/getRetention';
// Only works for logs
const getRetentionV2 = async (): Promise<
SuccessResponseV2<PayloadProps<'logs'>>
> => {
try {
const response = await ApiV2Instance.get<PayloadProps<'logs'>>(
`/settings/ttl`,
);
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getRetentionV2;

View File

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

View File

@@ -0,0 +1,32 @@
import { ApiV2Instance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadPropsV2, PropsV2 } from 'types/api/settings/setRetention';
const setRetentionV2 = async ({
type,
defaultTTLDays,
coldStorageVolume,
coldStorageDuration,
ttlConditions,
}: PropsV2): Promise<SuccessResponseV2<PayloadPropsV2>> => {
try {
const response = await ApiV2Instance.post<PayloadPropsV2>(`/settings/ttl`, {
type,
defaultTTLDays,
coldStorageVolume,
coldStorageDuration,
ttlConditions,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default setRetentionV2;

View File

@@ -0,0 +1,284 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { SuccessResponse } from 'types/api';
import {
MetricRangePayloadV5,
QueryBuilderFormula,
QueryRangeRequestV5,
QueryRangeResponseV5,
RequestType,
ScalarData,
TelemetryFieldKey,
TimeSeries,
TimeSeriesData,
TimeSeriesValue,
} from 'types/api/v5/queryRange';
import { convertV5ResponseToLegacy } from './convertV5Response';
describe('convertV5ResponseToLegacy', () => {
function makeBaseSuccess<T>(
payload: T,
params: QueryRangeRequestV5,
): SuccessResponse<T, QueryRangeRequestV5> {
return {
statusCode: 200,
message: 'success',
payload,
error: null,
params,
};
}
function makeBaseParams(
requestType: RequestType,
queries: QueryRangeRequestV5['compositeQuery']['queries'],
): QueryRangeRequestV5 {
return {
schemaVersion: 'v1',
start: 1,
end: 2,
requestType,
compositeQuery: { queries },
variables: {},
formatOptions: { formatTableResultForUI: false, fillGaps: false },
};
}
it('converts time_series response into legacy series structure', () => {
const timeSeries: TimeSeriesData = {
queryName: 'A',
aggregations: [
{
index: 0,
alias: '__result_0',
meta: {},
series: [
({
labels: [
{
key: ({ name: 'service.name' } as unknown) as TelemetryFieldKey,
value: 'adservice',
},
],
values: [
({ timestamp: 1000, value: 10 } as unknown) as TimeSeriesValue,
({ timestamp: 2000, value: 12 } as unknown) as TimeSeriesValue,
],
} as unknown) as TimeSeries,
],
},
],
};
const v5Data: QueryRangeResponseV5 = {
type: 'time_series',
data: { results: [timeSeries] },
meta: { rowsScanned: 0, bytesScanned: 0, durationMs: 0 },
};
const params = makeBaseParams('time_series', [
{
type: 'builder_query',
spec: {
name: 'A',
signal: 'traces',
stepInterval: 60,
disabled: false,
aggregations: [{ expression: 'count()' }],
},
},
]);
const input: SuccessResponse<
MetricRangePayloadV5,
QueryRangeRequestV5
> = makeBaseSuccess({ data: v5Data }, params);
const legendMap = { A: '{{service.name}}' };
const result = convertV5ResponseToLegacy(input, legendMap, false);
expect(result.payload.data.resultType).toBe('time_series');
expect(result.payload.data.result).toHaveLength(1);
const q = result.payload.data.result[0];
expect(q.queryName).toBe('A');
expect(q.legend).toBe('{{service.name}}');
expect(q.series?.[0]).toEqual(
expect.objectContaining({
labels: { 'service.name': 'adservice' },
values: [
{ timestamp: 1000, value: '10' },
{ timestamp: 2000, value: '12' },
],
metaData: expect.objectContaining({
alias: '__result_0',
index: 0,
queryName: 'A',
}),
}),
);
});
it('converts scalar to legacy table (formatForWeb=false) with names/ids resolved from aggregations', () => {
const scalar: ScalarData = {
columns: [
// group column
({
name: 'service.name',
queryName: 'A',
aggregationIndex: 0,
columnType: 'group',
} as unknown) as ScalarData['columns'][number],
// aggregation 0
({
name: '__result_0',
queryName: 'A',
aggregationIndex: 0,
columnType: 'aggregation',
} as unknown) as ScalarData['columns'][number],
// aggregation 1
({
name: '__result_1',
queryName: 'A',
aggregationIndex: 1,
columnType: 'aggregation',
} as unknown) as ScalarData['columns'][number],
// formula F1
({
name: '__result',
queryName: 'F1',
aggregationIndex: 0,
columnType: 'aggregation',
} as unknown) as ScalarData['columns'][number],
],
data: [['adservice', 606, 1.452, 151.5]],
};
const v5Data: QueryRangeResponseV5 = {
type: 'scalar',
data: { results: [scalar] },
meta: { rowsScanned: 0, bytesScanned: 0, durationMs: 0 },
};
const params = makeBaseParams('scalar', [
{
type: 'builder_query',
spec: {
name: 'A',
signal: 'traces',
stepInterval: 60,
disabled: false,
aggregations: [
{ expression: 'count()' },
{ expression: 'avg(app.ads.count)', alias: 'avg' },
],
},
},
{
type: 'builder_formula',
spec: ({
name: 'F1',
expression: 'A * 0.25',
} as unknown) as QueryBuilderFormula,
},
]);
const input: SuccessResponse<
MetricRangePayloadV5,
QueryRangeRequestV5
> = makeBaseSuccess({ data: v5Data }, params);
const legendMap = { A: '{{service.name}}', F1: '' };
const result = convertV5ResponseToLegacy(input, legendMap, false);
expect(result.payload.data.resultType).toBe('scalar');
const [tableEntry] = result.payload.data.result;
expect(tableEntry.table?.columns).toEqual([
{
name: 'service.name',
queryName: 'A',
isValueColumn: false,
id: 'service.name',
},
{ name: 'count()', queryName: 'A', isValueColumn: true, id: 'A.count()' },
{
name: 'avg',
queryName: 'A',
isValueColumn: true,
id: 'A.avg(app.ads.count)',
},
{ name: 'F1', queryName: 'F1', isValueColumn: true, id: 'F1' },
]);
expect(tableEntry.table?.rows?.[0]).toEqual({
data: {
'service.name': 'adservice',
'A.count()': 606,
'A.avg(app.ads.count)': 1.452,
F1: 151.5,
},
});
});
it('converts scalar with formatForWeb=true to UI-friendly table', () => {
const scalar: ScalarData = {
columns: [
{
name: 'service.name',
queryName: 'A',
aggregationIndex: 0,
columnType: 'group',
} as any,
{
name: '__result_0',
queryName: 'A',
aggregationIndex: 0,
columnType: 'aggregation',
} as any,
],
data: [['adservice', 580]],
};
const v5Data: QueryRangeResponseV5 = {
type: 'scalar',
data: { results: [scalar] },
meta: { rowsScanned: 0, bytesScanned: 0, durationMs: 0 },
};
const params = makeBaseParams('scalar', [
{
type: 'builder_query',
spec: {
name: 'A',
signal: 'traces',
stepInterval: 60,
disabled: false,
aggregations: [{ expression: 'count()' }],
},
},
]);
const input: SuccessResponse<
MetricRangePayloadV5,
QueryRangeRequestV5
> = makeBaseSuccess({ data: v5Data }, params);
const legendMap = { A: '{{service.name}}' };
const result = convertV5ResponseToLegacy(input, legendMap, true);
expect(result.payload.data.resultType).toBe('scalar');
const [tableEntry] = result.payload.data.result;
expect(tableEntry.table?.columns).toEqual([
{
name: 'service.name',
queryName: 'A',
isValueColumn: false,
id: 'service.name',
},
// Single aggregation: name resolves to legend, id resolves to queryName
{ name: '{{service.name}}', queryName: 'A', isValueColumn: true, id: 'A' },
]);
expect(tableEntry.table?.rows?.[0]).toEqual({
data: {
'service.name': 'adservice',
A: 580,
},
});
});
});

View File

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

View File

@@ -0,0 +1,637 @@
/* eslint-disable sonarjs/no-duplicate-string, simple-import-sort/imports, @typescript-eslint/indent, no-mixed-spaces-and-tabs */
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
IBuilderFormula,
IBuilderQuery,
} from 'types/api/queryBuilder/queryBuilderData';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import {
ClickHouseQuery,
LogAggregation,
LogBuilderQuery,
MetricBuilderQuery,
PromQuery,
QueryBuilderFormula as V5QueryBuilderFormula,
QueryEnvelope,
QueryRangePayloadV5,
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { prepareQueryRangePayloadV5 } from './prepareQueryRangePayloadV5';
jest.mock('lib/getStartEndRangeTime', () => ({
__esModule: true,
default: jest.fn(() => ({ start: '100', end: '200' })),
}));
describe('prepareQueryRangePayloadV5', () => {
const start = 1_710_000_000; // seconds
const end = 1_710_000_600; // seconds
const baseBuilderQuery = (
overrides?: Partial<IBuilderQuery>,
): IBuilderQuery => ({
queryName: 'A',
dataSource: DataSource.METRICS,
aggregations: [
{
metricName: 'cpu_usage',
temporality: '',
timeAggregation: 'sum',
spaceAggregation: 'avg',
reduceTo: 'avg',
},
],
timeAggregation: 'sum',
spaceAggregation: 'avg',
temporality: '',
functions: [
{
name: 'timeShift',
args: [{ value: '5m' }],
},
],
filter: { expression: '' },
filters: { items: [], op: 'AND' },
groupBy: [],
expression: 'A',
disabled: false,
having: [],
limit: null,
stepInterval: 600,
orderBy: [],
reduceTo: 'avg',
legend: 'Legend A',
...overrides,
});
const baseFormula = (
overrides?: Partial<IBuilderFormula>,
): IBuilderFormula => ({
expression: 'A + 1',
disabled: false,
queryName: 'F1',
legend: 'Formula Legend',
limit: undefined,
having: [],
stepInterval: undefined,
orderBy: [],
...overrides,
});
it('builds payload for builder queries with formulas and variables', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'q1',
unit: undefined,
promql: [],
clickhouse_sql: [],
builder: {
queryData: [baseBuilderQuery()],
queryFormulas: [baseFormula()],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
start,
end,
variables: { svc: 'api', count: 5, flag: true },
fillGaps: true,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'Legend A', F1: 'Formula Legend' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: expect.arrayContaining([
expect.objectContaining({
type: 'builder_query',
spec: expect.objectContaining({
name: 'A',
signal: 'metrics',
stepInterval: 600,
functions: [{ name: 'timeShift', args: [{ value: '5m' }] }],
aggregations: [
expect.objectContaining({
metricName: 'cpu_usage',
timeAggregation: 'sum',
spaceAggregation: 'avg',
reduceTo: undefined,
}),
],
}),
}),
expect.objectContaining({
type: 'builder_formula',
spec: expect.objectContaining({
name: 'F1',
expression: 'A + 1',
legend: 'Formula Legend',
}),
}),
]),
}),
requestType: 'time_series',
formatOptions: expect.objectContaining({
formatTableResultForUI: false,
fillGaps: true,
}),
start: start * 1000,
end: end * 1000,
variables: expect.objectContaining({
svc: { value: 'api' },
count: { value: 5 },
flag: { value: true },
}),
}),
}),
);
// Legend map combines builder and formulas
expect(result.legendMap).toEqual({ A: 'Legend A', F1: 'Formula Legend' });
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.schemaVersion).toBe('v1');
expect(payload.start).toBe(start * 1000);
expect(payload.end).toBe(end * 1000);
expect(payload.requestType).toBe('time_series');
expect(payload.formatOptions?.formatTableResultForUI).toBe(false);
expect(payload.formatOptions?.fillGaps).toBe(true);
// Variables mapped as { key: { value } }
expect(payload.variables).toEqual({
svc: { value: 'api' },
count: { value: 5 },
flag: { value: true },
});
// Queries include one builder_query and one builder_formula
expect(payload.compositeQuery.queries).toHaveLength(2);
const builderQuery = payload.compositeQuery.queries.find(
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const builderSpec = builderQuery.spec as MetricBuilderQuery;
expect(builderSpec.name).toBe('A');
expect(builderSpec.signal).toBe('metrics');
expect(builderSpec.aggregations?.[0]).toMatchObject({
metricName: 'cpu_usage',
timeAggregation: 'sum',
spaceAggregation: 'avg',
});
// reduceTo should not be present for non-scalar panels
expect(builderSpec.aggregations?.[0].reduceTo).toBeUndefined();
// functions should be preserved/normalized
expect(builderSpec.functions?.[0]?.name).toBe('timeShift');
const formulaQuery = payload.compositeQuery.queries.find(
(q) => q.type === 'builder_formula',
) as QueryEnvelope;
const formulaSpec = formulaQuery.spec as V5QueryBuilderFormula;
expect(formulaSpec.name).toBe('F1');
expect(formulaSpec.expression).toBe('A + 1');
expect(formulaSpec.legend).toBe('Formula Legend');
});
it('builds payload for PromQL queries and respects originalGraphType for formatting', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.PROM,
id: 'q2',
unit: undefined,
promql: [
{
name: 'A',
query: 'up',
disabled: false,
legend: 'LP',
},
],
clickhouse_sql: [],
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
},
graphType: PANEL_TYPES.TIME_SERIES,
originalGraphType: PANEL_TYPES.TABLE,
selectedTime: 'GLOBAL_TIME',
start,
end,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'LP' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: [
{
type: 'promql',
spec: expect.objectContaining({
name: 'A',
query: 'up',
legend: 'LP',
stats: false,
}),
},
],
}),
requestType: 'time_series',
formatOptions: expect.objectContaining({
formatTableResultForUI: true,
fillGaps: false,
}),
start: start * 1000,
end: end * 1000,
variables: {},
}),
}),
);
expect(result.legendMap).toEqual({ A: 'LP' });
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.requestType).toBe('time_series');
expect(payload.formatOptions?.formatTableResultForUI).toBe(true);
expect(payload.compositeQuery.queries).toHaveLength(1);
const prom = payload.compositeQuery.queries[0];
expect(prom.type).toBe('promql');
const promSpec = prom.spec as PromQuery;
expect(promSpec.name).toBe('A');
expect(promSpec.query).toBe('up');
expect(promSpec.legend).toBe('LP');
expect(promSpec.stats).toBe(false);
});
it('builds payload for ClickHouse queries and maps requestType from panel', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.CLICKHOUSE,
id: 'q3',
unit: undefined,
promql: [],
clickhouse_sql: [
{
name: 'Q',
query: 'SELECT 1',
disabled: false,
legend: 'LC',
},
],
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
},
graphType: PANEL_TYPES.TABLE,
selectedTime: 'GLOBAL_TIME',
start,
end,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { Q: 'LC' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: [
{
type: 'clickhouse_sql',
spec: expect.objectContaining({
name: 'Q',
query: 'SELECT 1',
legend: 'LC',
}),
},
],
}),
requestType: 'scalar',
formatOptions: expect.objectContaining({
formatTableResultForUI: true,
fillGaps: false,
}),
start: start * 1000,
end: end * 1000,
variables: {},
}),
}),
);
expect(result.legendMap).toEqual({ Q: 'LC' });
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.requestType).toBe('scalar');
expect(payload.compositeQuery.queries).toHaveLength(1);
const ch = payload.compositeQuery.queries[0];
expect(ch.type).toBe('clickhouse_sql');
const chSpec = ch.spec as ClickHouseQuery;
expect(chSpec.name).toBe('Q');
expect(chSpec.query).toBe('SELECT 1');
expect(chSpec.legend).toBe('LC');
});
it('uses getStartEndRangeTime when start/end are not provided', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'q4',
unit: undefined,
promql: [],
clickhouse_sql: [],
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
},
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: {},
queryPayload: expect.objectContaining({
compositeQuery: { queries: [] },
requestType: 'time_series',
formatOptions: expect.objectContaining({
formatTableResultForUI: false,
fillGaps: false,
}),
start: 100 * 1000,
end: 200 * 1000,
variables: {},
}),
}),
);
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.start).toBe(100 * 1000);
expect(payload.end).toBe(200 * 1000);
});
it('includes reduceTo for metrics in scalar panels (TABLE)', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'q5',
unit: undefined,
promql: [],
clickhouse_sql: [],
builder: {
queryData: [baseBuilderQuery()],
queryFormulas: [],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.TABLE,
selectedTime: 'GLOBAL_TIME',
start,
end,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'Legend A' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: [
{
type: 'builder_query',
spec: expect.objectContaining({
name: 'A',
signal: 'metrics',
stepInterval: 600,
functions: [{ name: 'timeShift', args: [{ value: '5m' }] }],
aggregations: [
expect.objectContaining({
metricName: 'cpu_usage',
timeAggregation: 'sum',
spaceAggregation: 'avg',
reduceTo: 'avg',
temporality: undefined,
}),
],
}),
},
],
}),
requestType: 'scalar',
formatOptions: expect.objectContaining({
formatTableResultForUI: true,
fillGaps: false,
}),
start: start * 1000,
end: end * 1000,
variables: {},
}),
}),
);
const payload: QueryRangePayloadV5 = result.queryPayload;
const builderQuery = payload.compositeQuery.queries.find(
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const builderSpec = builderQuery.spec as MetricBuilderQuery;
expect(builderSpec.aggregations?.[0].reduceTo).toBe('avg');
});
it('omits aggregations for raw request type (LIST panel)', () => {
const logAgg: LogAggregation[] = [{ expression: 'count()' }];
const logsQuery = baseBuilderQuery({
dataSource: DataSource.LOGS,
aggregations: logAgg,
} as Partial<IBuilderQuery>);
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'q6',
unit: undefined,
promql: [],
clickhouse_sql: [],
builder: {
queryData: [logsQuery],
queryFormulas: [],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
start,
end,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'Legend A' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: [
{
type: 'builder_query',
spec: expect.objectContaining({
name: 'A',
signal: 'logs',
stepInterval: 600,
functions: [{ name: 'timeShift', args: [{ value: '5m' }] }],
aggregations: undefined,
}),
},
],
}),
requestType: 'raw',
formatOptions: expect.objectContaining({
formatTableResultForUI: false,
fillGaps: false,
}),
start: start * 1000,
end: end * 1000,
variables: {},
}),
}),
);
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.requestType).toBe('raw');
const builderQuery = payload.compositeQuery.queries.find(
(q) => q.type === 'builder_query',
) as QueryEnvelope;
// For RAW request type, aggregations should be omitted
const logSpec = builderQuery.spec as LogBuilderQuery;
expect(logSpec.aggregations).toBeUndefined();
});
it('maps groupBy, order, having, aggregations and filter for logs builder query', () => {
const getStartEndRangeTime = jest.requireMock('lib/getStartEndRangeTime')
.default as jest.Mock;
getStartEndRangeTime.mockReturnValueOnce({
start: '1754623641',
end: '1754645241',
});
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'e643e387-1996-4449-97b6-9ef4498a0573',
unit: undefined,
promql: [{ name: 'A', query: '', legend: '', disabled: false }],
clickhouse_sql: [{ name: 'A', legend: '', disabled: false, query: '' }],
builder: {
queryData: [
{
dataSource: DataSource.LOGS,
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
key: '',
dataType: DataTypes.EMPTY,
type: '',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
filter: { expression: "service.name = 'adservice'" },
aggregations: [
{ expression: 'count() as cnt avg(code.lineno) ' } as LogAggregation,
],
functions: [],
filters: {
items: [
{
id: '14c790ec-54d1-42f0-a889-3b4f0fb79852',
op: '=',
key: { id: 'service.name', key: 'service.name', type: '' },
value: 'adservice',
},
],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 80,
having: { expression: 'count() > 0' },
limit: 600,
orderBy: [{ columnName: 'service.name', order: 'desc' }],
groupBy: [
{
key: 'service.name',
type: '',
},
],
legend: '{{service.name}}',
reduceTo: 'avg',
offset: 0,
pageSize: 100,
},
],
queryFormulas: [],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: 'custom' as never,
variables: {},
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: '{{service.name}}' },
queryPayload: expect.objectContaining({
schemaVersion: 'v1',
start: 1754623641000,
end: 1754645241000,
requestType: 'time_series',
compositeQuery: expect.objectContaining({
queries: [
{
type: 'builder_query',
spec: expect.objectContaining({
name: 'A',
signal: 'logs',
stepInterval: 80,
disabled: false,
filter: { expression: "service.name = 'adservice'" },
groupBy: [
{
name: 'service.name',
fieldDataType: '',
fieldContext: '',
},
],
limit: 600,
order: [
{
key: { name: 'service.name' },
direction: 'desc',
},
],
legend: '{{service.name}}',
having: { expression: 'count() > 0' },
aggregations: [
{ expression: 'count()', alias: 'cnt' },
{ expression: 'avg(code.lineno)' },
],
}),
},
],
}),
formatOptions: { formatTableResultForUI: false, fillGaps: false },
variables: {},
}),
}),
);
});
});

View File

@@ -1,4 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable sonarjs/no-identical-functions */
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
@@ -7,7 +8,7 @@ import { isEmpty } from 'lodash-es';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
QueryFunctionProps,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
@@ -27,9 +28,11 @@ import {
TelemetryFieldKey,
TraceAggregation,
VariableItem,
VariableType,
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { normalizeFunctionName } from 'utils/functionNameNormalizer';
type PrepareQueryRangePayloadV5Result = {
queryPayload: QueryRangePayloadV5;
@@ -68,9 +71,46 @@ function getSignalType(dataSource: string): 'traces' | 'logs' | 'metrics' {
return 'metrics';
}
/**
* Creates base spec for builder queries
*/
function isDeprecatedField(fieldName: string): boolean {
const deprecatedIntrinsicFields = [
'traceID',
'spanID',
'parentSpanID',
'spanKind',
'durationNano',
'statusCode',
'statusMessage',
'statusCodeString',
];
const deprecatedCalculatedFields = [
'responseStatusCode',
'externalHttpUrl',
'httpUrl',
'externalHttpMethod',
'httpMethod',
'httpHost',
'dbName',
'dbOperation',
'hasError',
'isRemote',
'serviceName',
'httpRoute',
'msgSystem',
'msgOperation',
'dbSystem',
'rpcSystem',
'rpcService',
'rpcMethod',
'peerService',
];
return (
deprecatedIntrinsicFields.includes(fieldName) ||
deprecatedCalculatedFields.includes(fieldName)
);
}
function createBaseSpec(
queryData: IBuilderQuery,
requestType: RequestType,
@@ -82,7 +122,7 @@ function createBaseSpec(
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
return {
stepInterval: queryData?.stepInterval || undefined,
stepInterval: queryData?.stepInterval || null,
disabled: queryData.disabled,
filter: queryData?.filter?.expression ? queryData.filter : undefined,
groupBy:
@@ -90,8 +130,8 @@ function createBaseSpec(
? queryData.groupBy.map(
(item: any): GroupByKey => ({
name: item.key,
fieldDataType: item?.dataType,
fieldContext: item?.type,
fieldDataType: item?.dataType || '',
fieldContext: item?.type || '',
description: item?.description,
unit: item?.unit,
signal: item?.signal,
@@ -123,34 +163,52 @@ function createBaseSpec(
functions: isEmpty(queryData.functions)
? undefined
: queryData.functions.map(
(func: QueryFunctionProps): QueryFunction => ({
name: func.name as FunctionName,
args: isEmpty(func.namedArgs)
? func.args.map((arg) => ({
value: arg,
}))
: Object.entries(func.namedArgs).map(([name, value]) => ({
name,
value,
})),
}),
(func: QueryFunction): QueryFunction => {
// Normalize function name to handle case sensitivity
const normalizedName = normalizeFunctionName(func?.name);
return {
name: normalizedName as FunctionName,
args: isEmpty(func.namedArgs)
? func.args?.map((arg) => ({
value: arg?.value,
}))
: Object.entries(func?.namedArgs || {}).map(([name, value]) => ({
name,
value,
})),
};
},
),
selectFields: isEmpty(nonEmptySelectColumns)
? undefined
: nonEmptySelectColumns?.map(
(column: any): TelemetryFieldKey => ({
name: column.name ?? column.key,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
signal: column?.signal ?? undefined,
}),
(column: any): TelemetryFieldKey => {
const fieldName = column.name ?? column.key;
const isDeprecated = isDeprecatedField(fieldName);
const fieldObj: TelemetryFieldKey = {
name: fieldName,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
signal: column?.signal ?? undefined,
};
// Only add fieldContext if the field is NOT deprecated
if (!isDeprecated && fieldName !== 'name') {
fieldObj.fieldContext =
column?.fieldContext ?? (column?.type as FieldContext);
}
return fieldObj;
},
),
};
}
// Utility to parse aggregation expressions with optional alias
export function parseAggregations(
expression: string,
availableAlias?: string,
): { expression: string; alias?: string }[] {
const result: { expression: string; alias?: string }[] = [];
// Matches function calls like "count()" or "sum(field)" with optional alias like "as 'alias'"
@@ -159,7 +217,7 @@ export function parseAggregations(
let match = regex.exec(expression);
while (match !== null) {
const expr = match[1];
let alias = match[2];
let alias = match[2] || availableAlias; // Use provided alias or availableAlias if not matched
if (alias) {
// Remove quotes if present
alias = alias.replace(/^['"]|['"]$/g, '');
@@ -210,9 +268,14 @@ export function createAggregation(
}
if (queryData.aggregations?.length > 0) {
return isEmpty(parseAggregations(queryData.aggregations?.[0].expression))
? [{ expression: 'count()' }]
: parseAggregations(queryData.aggregations?.[0].expression);
return queryData.aggregations.flatMap(
(agg: { expression: string; alias?: string }) => {
const parsedAggregations = parseAggregations(agg.expression, agg?.alias);
return isEmpty(parsedAggregations)
? [{ expression: 'count()' }]
: parsedAggregations;
},
);
}
return [{ expression: 'count()' }];
@@ -258,6 +321,7 @@ export function convertBuilderQueriesToV5(
spec = {
name: queryName,
signal: 'metrics' as const,
source: queryData.source || '',
...baseSpec,
aggregations: aggregations as MetricAggregation[],
// reduceTo: queryData.reduceTo,
@@ -273,6 +337,109 @@ export function convertBuilderQueriesToV5(
);
}
function createTraceOperatorBaseSpec(
queryData: IBuilderTraceOperator,
requestType: RequestType,
panelType?: PANEL_TYPES,
): BaseBuilderQuery {
const nonEmptySelectColumns = (queryData.selectColumns as (
| BaseAutocompleteData
| TelemetryFieldKey
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
const {
stepInterval,
groupBy,
limit,
offset,
legend,
having,
orderBy,
pageSize,
} = queryData;
return {
stepInterval: stepInterval || undefined,
groupBy:
groupBy?.length > 0
? groupBy.map(
(item: any): GroupByKey => ({
name: item.key,
fieldDataType: item?.dataType,
fieldContext: item?.type,
description: item?.description,
unit: item?.unit,
signal: item?.signal,
materialized: item?.materialized,
}),
)
: undefined,
limit:
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
? limit || pageSize || undefined
: limit || undefined,
offset: requestType === 'raw' || requestType === 'trace' ? offset : undefined,
order:
orderBy?.length > 0
? orderBy.map(
(order: any): OrderBy => ({
key: {
name: order.columnName,
},
direction: order.order,
}),
)
: undefined,
legend: isEmpty(legend) ? undefined : legend,
having: isEmpty(having) ? undefined : (having as Having),
selectFields: isEmpty(nonEmptySelectColumns)
? undefined
: nonEmptySelectColumns?.map(
(column: any): TelemetryFieldKey => ({
name: column.name ?? column.key,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
signal: column?.signal ?? undefined,
}),
),
};
}
export function convertTraceOperatorToV5(
traceOperator: Record<string, IBuilderTraceOperator>,
requestType: RequestType,
panelType?: PANEL_TYPES,
): QueryEnvelope[] {
return Object.entries(traceOperator).map(
([queryName, traceOperatorData]): QueryEnvelope => {
const baseSpec = createTraceOperatorBaseSpec(
traceOperatorData,
requestType,
panelType,
);
// Skip aggregation for raw request type
const aggregations =
requestType === 'raw'
? undefined
: createAggregation(traceOperatorData, panelType);
const spec: QueryEnvelope['spec'] = {
name: queryName,
...baseSpec,
expression: traceOperatorData.expression || '',
aggregations: aggregations as TraceAggregation[],
};
return {
type: 'builder_trace_operator' as QueryType,
spec,
};
},
);
}
/**
* Converts PromQL queries to V5 format
*/
@@ -347,6 +514,7 @@ export const prepareQueryRangePayloadV5 = ({
formatForWeb,
originalGraphType,
fillGaps,
dynamicVariables,
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
let legendMap: Record<string, string> = {};
const requestType = mapPanelTypeToRequestType(graphType);
@@ -354,14 +522,28 @@ export const prepareQueryRangePayloadV5 = ({
switch (query.queryType) {
case EQueryType.QUERY_BUILDER: {
const { queryData: data, queryFormulas } = query.builder;
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
const filteredTraceOperator =
queryTraceOperator && queryTraceOperator.length > 0
? queryTraceOperator.filter((traceOperator) =>
Boolean(traceOperator.expression.trim()),
)
: [];
const currentTraceOperator = mapQueryDataToApi(
filteredTraceOperator,
'queryName',
tableParams,
);
// Combine legend maps
legendMap = {
...currentQueryData.newLegendMap,
...currentFormulas.newLegendMap,
...currentTraceOperator.newLegendMap,
};
// Convert builder queries
@@ -394,8 +576,14 @@ export const prepareQueryRangePayloadV5 = ({
}),
);
// Combine both types
queries = [...builderQueries, ...formulaQueries];
const traceOperatorQueries = convertTraceOperatorToV5(
currentTraceOperator.data,
requestType,
graphType,
);
// Combine all query types
queries = [...builderQueries, ...formulaQueries, ...traceOperatorQueries];
break;
}
case EQueryType.PROM: {
@@ -438,7 +626,12 @@ export const prepareQueryRangePayloadV5 = ({
fillGaps: fillGaps || false,
},
variables: Object.entries(variables).reduce((acc, [key, value]) => {
acc[key] = { value };
acc[key] = {
value,
type: dynamicVariables
?.find((v) => v.name === key)
?.type?.toLowerCase() as VariableType,
};
return acc;
}, {} as Record<string, VariableItem>),
};

View File

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

View File

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

View File

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

View File

@@ -39,8 +39,6 @@ export function getFiltersFromQueryParams(
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',
@@ -100,8 +98,7 @@ export const createFiltersFromData = (
key: string;
dataType: DataTypes;
type: string;
isColumn: boolean;
isJSON: boolean;
id: string;
};
op: string;
@@ -119,8 +116,6 @@ export const createFiltersFromData = (
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',

View File

@@ -19,6 +19,7 @@ export interface NavigateToExplorerProps {
endTime?: number;
sameTab?: boolean;
shouldResolveQuery?: boolean;
widgetQuery?: Query;
}
export function useNavigateToExplorer(): (
@@ -30,27 +31,34 @@ export function useNavigateToExplorer(): (
);
const prepareQuery = useCallback(
(selectedFilters: TagFilterItem[], dataSource: DataSource): Query => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData
.map((item) => ({
...item,
dataSource,
aggregateOperator: MetricAggregateOperator.NOOP,
filters: {
...item.filters,
items: [...(item.filters?.items || []), ...selectedFilters],
op: item.filters?.op || 'AND',
},
groupBy: [],
disabled: false,
}))
.slice(0, 1),
queryFormulas: [],
},
}),
(
selectedFilters: TagFilterItem[],
dataSource: DataSource,
query?: Query,
): Query => {
const widgetQuery = query || currentQuery;
return {
...widgetQuery,
builder: {
...widgetQuery.builder,
queryData: widgetQuery.builder.queryData
.map((item) => ({
...item,
dataSource,
aggregateOperator: MetricAggregateOperator.NOOP,
filters: {
...item.filters,
items: [...(item.filters?.items || []), ...selectedFilters],
op: item.filters?.op || 'AND',
},
groupBy: [],
disabled: false,
}))
.slice(0, 1),
queryFormulas: [],
},
};
},
[currentQuery],
);
@@ -67,6 +75,7 @@ export function useNavigateToExplorer(): (
endTime,
sameTab,
shouldResolveQuery,
widgetQuery,
} = props;
const urlParams = new URLSearchParams();
if (startTime && endTime) {
@@ -77,7 +86,7 @@ export function useNavigateToExplorer(): (
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
}
let preparedQuery = prepareQuery(filters, dataSource);
let preparedQuery = prepareQuery(filters, dataSource, widgetQuery);
if (shouldResolveQuery) {
await getUpdatedQuery({

View File

@@ -137,5 +137,11 @@
h6 {
color: var(--text-ink-500);
}
code {
background-color: var(--bg-vanilla-300);
border: 1px solid var(--bg-vanilla-300);
color: var(--text-ink-500);
}
}
}

View File

@@ -241,8 +241,6 @@ function ClientSideQBSearch(
key: 'body',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
id: 'body--string----true',
},
op: OPERATORS.CONTAINS,

View File

@@ -0,0 +1,33 @@
.error-state-container {
height: 240px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
border-radius: 3px;
.error-state-container-content {
display: flex;
flex-direction: column;
gap: 8px;
.error-state-text {
font-size: 14px;
font-weight: 500;
}
.error-state-additional-messages {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 4px;
.error-state-additional-text {
font-size: 12px;
font-weight: 400;
margin-left: 8px;
}
}
}
}

View File

@@ -0,0 +1,59 @@
import './Common.styles.scss';
import { Typography } from 'antd';
import APIError from '../../types/api/error';
interface ErrorStateComponentProps {
message?: string;
error?: APIError;
}
const defaultProps: Partial<ErrorStateComponentProps> = {
message: undefined,
error: undefined,
};
function ErrorStateComponent({
message,
error,
}: ErrorStateComponentProps): JSX.Element {
// Handle API Error object
if (error) {
const mainMessage = error.getErrorMessage();
const additionalErrors = error.getErrorDetails().error.errors || [];
return (
<div className="error-state-container">
<div className="error-state-container-content">
<Typography className="error-state-text">{mainMessage}</Typography>
{additionalErrors.length > 0 && (
<div className="error-state-additional-messages">
{additionalErrors.map((additionalError) => (
<Typography
key={`error-${additionalError.message}`}
className="error-state-additional-text"
>
{additionalError.message}
</Typography>
))}
</div>
)}
</div>
</div>
);
}
// Handle simple string message (backwards compatibility)
return (
<div className="error-state-container">
<div className="error-state-container-content">
<Typography className="error-state-text">{message}</Typography>
</div>
</div>
);
}
ErrorStateComponent.defaultProps = defaultProps;
export default ErrorStateComponent;

View File

@@ -1,6 +1,16 @@
.custom-time-picker {
display: flex;
flex-direction: column;
.timeSelection-input {
&:hover {
border-color: #1d212d !important;
}
}
.time-input-suffix {
display: flex;
}
}
.time-options-container {
@@ -135,6 +145,7 @@
align-items: center;
color: var(--bg-vanilla-400);
gap: 6px;
.timezone {
display: flex;
align-items: center;
@@ -163,6 +174,52 @@
cursor: pointer;
}
.time-input-prefix {
.live-dot-icon {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: var(--bg-forest-500);
animation: ripple 1s infinite;
margin-right: 4px;
margin-left: 4px;
}
}
@keyframes ripple {
0% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4);
}
70% {
box-shadow: 0 0 0 6px rgba(245, 158, 11, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
}
}
.time-input-suffix-icon-badge {
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
cursor: pointer;
height: 20px;
width: 20px;
&:hover {
background: rgba(171, 189, 255, 0.08);
}
}
.lightMode {
.date-time-popover__footer {
border-color: var(--bg-vanilla-400);
@@ -180,8 +237,26 @@
}
}
}
.custom-time-picker {
.timeSelection-input {
&:hover {
border-color: var(--bg-vanilla-300) !important;
}
}
}
.timezone-badge {
color: var(--bg-ink-100);
background: rgb(179 179 179 / 15%);
}
.time-input-suffix-icon-badge {
color: var(--bg-ink-100);
background: rgb(179 179 179 / 15%);
&:hover {
background: rgb(179 179 179 / 20%);
}
}
}

View File

@@ -5,13 +5,12 @@ import './CustomTimePicker.styles.scss';
import { Input, Popover, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
CustomTimeType,
FixedDurationSuggestionOptions,
Options,
RelativeDurationSuggestionOptions,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { isValidTimeFormat } from 'lib/getMinMax';
@@ -28,7 +27,10 @@ import {
useMemo,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { popupContainer } from 'utils/selectPopupContainer';
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
@@ -57,11 +59,9 @@ interface CustomTimePickerProps {
customDateTimeVisible?: boolean;
setCustomDTPickerVisible?: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler?: (dateTimeRange: DateTimeRangeType) => void;
handleGoLive?: () => void;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
showLiveLogs?: boolean;
onGoLive?: () => void;
onExitLiveLogs?: () => void;
}
function CustomTimePicker({
@@ -78,14 +78,19 @@ function CustomTimePicker({
customDateTimeVisible,
setCustomDTPickerVisible,
onCustomDateHandler,
handleGoLive,
onTimeChange,
onGoLive,
onExitLiveLogs,
showLiveLogs,
}: CustomTimePickerProps): JSX.Element {
const [
selectedTimePlaceholderValue,
setSelectedTimePlaceholderValue,
] = useState('Select / Enter Time Range');
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [inputValue, setInputValue] = useState('');
const [inputStatus, setInputStatus] = useState<'' | 'error' | 'success'>('');
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
@@ -164,9 +169,13 @@ function CustomTimePicker({
};
useEffect(() => {
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setSelectedTimePlaceholderValue(value);
}, [selectedTime, selectedValue]);
if (showLiveLogs) {
setSelectedTimePlaceholderValue('Live');
} else {
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setSelectedTimePlaceholderValue(value);
}
}, [selectedTime, selectedValue, showLiveLogs]);
const hide = (): void => {
setOpen(false);
@@ -256,6 +265,11 @@ function CustomTimePicker({
};
const handleSelect = (label: string, value: string): void => {
if (label === 'Custom') {
setCustomDTPickerVisible?.(true);
return;
}
onSelect(value);
setSelectedTimePlaceholderValue(label);
setInputStatus('');
@@ -318,84 +332,118 @@ function CustomTimePicker({
);
};
const getTooltipTitle = (): string => {
if (selectedTime === 'custom' && inputValue === '' && !open) {
return `${dayjs(minTime / 1000_000)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)} - ${dayjs(
maxTime / 1000_000,
)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)}`;
}
return '';
};
const getInputPrefix = (): JSX.Element => {
if (showLiveLogs) {
return (
<div className="time-input-prefix">
<div className="live-dot-icon" />
</div>
);
}
return (
<div className="time-input-prefix">
{inputValue && inputStatus === 'success' ? (
<CheckCircle size={14} color="#51E7A8" />
) : (
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
<Clock size={14} className="cursor-pointer" />
</Tooltip>
)}
</div>
);
};
return (
<div className="custom-time-picker">
<Popover
className={cx(
'timeSelection-input-container',
selectedTime === 'custom' && inputValue === '' ? 'custom-time' : '',
)}
placement="bottomRight"
getPopupContainer={popupContainer}
rootClassName="date-time-root"
content={
newPopover ? (
<CustomTimePickerPopoverContent
setIsOpen={setOpen}
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
onSelectHandler={handleSelect}
handleGoLive={defaultTo(handleGoLive, noop)}
options={items}
selectedTime={selectedTime}
activeView={activeView}
setActiveView={setActiveView}
setIsOpenedFromFooter={setIsOpenedFromFooter}
isOpenedFromFooter={isOpenedFromFooter}
onTimeChange={onTimeChange}
/>
) : (
content
)
}
arrow={false}
trigger="click"
open={open}
onOpenChange={handleOpenChange}
style={{
padding: 0,
}}
>
<Input
className="timeSelection-input"
type="text"
status={inputValue && inputStatus === 'error' ? 'error' : ''}
placeholder={
isInputFocused
? 'Time Format (1m or 2h or 3d or 4w)'
: selectedTimePlaceholderValue
}
value={inputValue}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={handleInputChange}
data-1p-ignore
prefix={
inputValue && inputStatus === 'success' ? (
<CheckCircle size={14} color="#51E7A8" />
<Tooltip title={getTooltipTitle()} placement="top">
<Popover
className={cx(
'timeSelection-input-container',
selectedTime === 'custom' && inputValue === '' ? 'custom-time' : '',
)}
placement="bottomRight"
getPopupContainer={popupContainer}
rootClassName="date-time-root"
content={
newPopover ? (
<CustomTimePickerPopoverContent
setIsOpen={setOpen}
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
onSelectHandler={handleSelect}
onGoLive={defaultTo(onGoLive, noop)}
onExitLiveLogs={defaultTo(onExitLiveLogs, noop)}
options={items}
selectedTime={selectedTime}
activeView={activeView}
setActiveView={setActiveView}
setIsOpenedFromFooter={setIsOpenedFromFooter}
isOpenedFromFooter={isOpenedFromFooter}
/>
) : (
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
<Clock size={14} />
</Tooltip>
content
)
}
suffix={
<>
{!!isTimezoneOverridden && activeTimezoneOffset && (
<div className="timezone-badge" onClick={handleTimezoneHintClick}>
<span>{activeTimezoneOffset}</span>
</div>
)}
<ChevronDown
size={14}
onClick={(): void => handleViewChange('datetime')}
/>
</>
}
/>
</Popover>
arrow={false}
trigger="click"
open={open}
onOpenChange={handleOpenChange}
style={{
padding: 0,
}}
>
<Input
className="timeSelection-input"
type="text"
status={inputValue && inputStatus === 'error' ? 'error' : ''}
placeholder={
isInputFocused
? 'Time Format (1m or 2h or 3d or 4w)'
: selectedTimePlaceholderValue
}
value={inputValue}
onFocus={handleFocus}
onClick={handleFocus}
onBlur={handleBlur}
onChange={handleInputChange}
data-1p-ignore
prefix={getInputPrefix()}
suffix={
<div className="time-input-suffix">
{!!isTimezoneOverridden && activeTimezoneOffset && (
<div className="timezone-badge" onClick={handleTimezoneHintClick}>
<span>{activeTimezoneOffset}</span>
</div>
)}
<ChevronDown
size={14}
className="cursor-pointer time-input-suffix-icon-badge"
onClick={(e): void => {
e.stopPropagation();
handleViewChange('datetime');
}}
/>
</div>
}
/>
</Popover>
</Tooltip>
{inputStatus === 'error' && inputErrorMessage && (
<Typography.Title level={5} className="valid-format-error">
{inputErrorMessage}
@@ -412,7 +460,8 @@ CustomTimePicker.defaultProps = {
customDateTimeVisible: false,
setCustomDTPickerVisible: noop,
onCustomDateHandler: noop,
handleGoLive: noop,
onGoLive: noop,
onCustomTimeStatusUpdate: noop,
onTimeChange: undefined,
onExitLiveLogs: noop,
showLiveLogs: false,
};

View File

@@ -4,21 +4,30 @@ import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import DatePickerV2 from 'components/DatePickerV2/DatePickerV2';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
CustomTimeType,
LexicalContext,
Option,
RelativeDurationSuggestionOptions,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { Clock, PenLine } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { Dispatch, SetStateAction, useMemo } from 'react';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { getCustomTimeRanges } from 'utils/customTimeRangeUtils';
import RangePickerModal from './RangePickerModal';
import TimezonePicker from './TimezonePicker';
interface CustomTimePickerPopoverContentProps {
@@ -31,16 +40,21 @@ interface CustomTimePickerPopoverContentProps {
lexicalContext?: LexicalContext,
) => void;
onSelectHandler: (label: string, value: string) => void;
handleGoLive: () => void;
onGoLive: () => void;
selectedTime: string;
activeView: 'datetime' | 'timezone';
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
isOpenedFromFooter: boolean;
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
onExitLiveLogs: () => void;
}
interface RecentlyUsedDateTimeRange {
label: string;
value: number;
timestamp: number;
from: string;
to: string;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -51,22 +65,68 @@ function CustomTimePickerPopoverContent({
setCustomDTPickerVisible,
onCustomDateHandler,
onSelectHandler,
handleGoLive,
onGoLive,
selectedTime,
activeView,
setActiveView,
isOpenedFromFooter,
setIsOpenedFromFooter,
onTimeChange,
onExitLiveLogs,
}: CustomTimePickerPopoverContentProps): JSX.Element {
const { pathname } = useLocation();
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname,
]);
const url = new URLSearchParams(window.location.search);
let panelTypeFromURL = url.get(QueryParams.panelTypes);
try {
panelTypeFromURL = JSON.parse(panelTypeFromURL as string);
} catch {
// fallback → leave as-is
}
const isLogsListView =
panelTypeFromURL !== 'table' && panelTypeFromURL !== 'graph'; // we do not select list view in the url
const { timezone } = useTimezone();
const activeTimezoneOffset = timezone.offset;
const [recentlyUsedTimeRanges, setRecentlyUsedTimeRanges] = useState<
RecentlyUsedDateTimeRange[]
>([]);
const handleExitLiveLogs = useCallback((): void => {
if (isLogsExplorerPage) {
onExitLiveLogs();
}
}, [isLogsExplorerPage, onExitLiveLogs]);
useEffect(() => {
if (!customDateTimeVisible) {
const customTimeRanges = getCustomTimeRanges();
const formattedCustomTimeRanges: RecentlyUsedDateTimeRange[] = customTimeRanges.map(
(range) => ({
label: `${dayjs(range.from)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)} - ${dayjs(range.to)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)}`,
from: range.from,
to: range.to,
value: range.timestamp,
timestamp: range.timestamp,
}),
);
setRecentlyUsedTimeRanges(formattedCustomTimeRanges);
}
}, [customDateTimeVisible, timezone.value]);
function getTimeChips(options: Option[]): JSX.Element {
return (
<div className="relative-date-time-section">
@@ -76,6 +136,7 @@ function CustomTimePickerPopoverContent({
className="time-btns"
key={option.label + option.value}
onClick={(): void => {
handleExitLiveLogs();
onSelectHandler(option.label, option.value);
}}
>
@@ -109,53 +170,87 @@ function CustomTimePickerPopoverContent({
);
}
const handleGoLive = (): void => {
onGoLive();
setIsOpen(false);
};
return (
<>
<div className="date-time-popover">
<div className="date-time-options">
{isLogsExplorerPage && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
</div>
{!customDateTimeVisible && (
<div className="date-time-options">
{isLogsExplorerPage && isLogsListView && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
handleExitLiveLogs();
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
</div>
)}
<div
className={cx(
'relative-date-time',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
customDateTimeVisible ? 'date-picker' : 'relative-times',
)}
>
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
{customDateTimeVisible ? (
<DatePickerV2
onSetCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
onTimeChange={onTimeChange}
/>
) : (
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
<div className="time-selector-container">
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
<div className="recently-used-container">
<div className="time-heading">RECENTLY USED</div>
<div className="recently-used-range">
{recentlyUsedTimeRanges.map((range: RecentlyUsedDateTimeRange) => (
<div
className="recently-used-range-item"
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
handleExitLiveLogs();
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
setIsOpen(false);
}
}}
key={range.value}
onClick={(): void => {
handleExitLiveLogs();
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
setIsOpen(false);
}}
>
{range.label}
</div>
))}
</div>
</div>
</div>
)}
</div>
@@ -189,8 +284,4 @@ function CustomTimePickerPopoverContent({
);
}
CustomTimePickerPopoverContent.defaultProps = {
onTimeChange: undefined,
};
export default CustomTimePickerPopoverContent;

View File

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

View File

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

View File

@@ -0,0 +1,79 @@
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import { ReactNode } from 'react';
import APIError from 'types/api/error';
interface ErrorInPlaceProps {
/** The error object to display */
error: APIError;
/** Custom class name */
className?: string;
/** Custom style */
style?: React.CSSProperties;
/** Whether to show a border */
bordered?: boolean;
/** Background color */
background?: string;
/** Padding */
padding?: string | number;
/** Height - defaults to 100% to take available space */
height?: string | number;
/** Width - defaults to 100% to take available space */
width?: string | number;
/** Custom content instead of ErrorContent */
children?: ReactNode;
}
/**
* ErrorInPlace - A component that renders error content directly in the available space
* of its parent container. Perfect for displaying errors in widgets, cards, or any
* container where you want the error to take up the full available space.
*
* @example
* <ErrorInPlace error={error} />
*
* @example
* <ErrorInPlace error={error} bordered background="#f5f5f5" padding={16} />
*/
function ErrorInPlace({
error,
className = '',
style,
bordered = false,
background,
padding = 16,
height = '100%',
width = '100%',
children,
}: ErrorInPlaceProps): JSX.Element {
const containerStyle: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
width,
height,
padding: typeof padding === 'number' ? `${padding}px` : padding,
backgroundColor: background,
border: bordered ? '1px solid var(--bg-slate-400, #374151)' : 'none',
borderRadius: bordered ? '4px' : '0',
overflow: 'auto',
...style,
};
return (
<div className={`error-in-place ${className}`.trim()} style={containerStyle}>
{children || <ErrorContent error={error} />}
</div>
);
}
ErrorInPlace.defaultProps = {
className: undefined,
style: undefined,
bordered: undefined,
background: undefined,
padding: undefined,
height: undefined,
width: undefined,
children: undefined,
};
export default ErrorInPlace;

View File

@@ -0,0 +1,33 @@
/* eslint-disable react/jsx-props-no-spreading */
import { Popover, PopoverProps } from 'antd';
import { ReactNode } from 'react';
interface ErrorPopoverProps extends Omit<PopoverProps, 'content'> {
/** Content to display in the popover */
content: ReactNode;
/** Element that triggers the popover */
children: ReactNode;
}
/**
* ErrorPopover - A clean wrapper around Ant Design's Popover
* that provides a simple interface for displaying content in a popover.
*
* @example
* <ErrorPopover content={<ErrorContent error={error} />}>
* <CircleX />
* </ErrorPopover>
*/
function ErrorPopover({
content,
children,
...popoverProps
}: ErrorPopoverProps): JSX.Element {
return (
<Popover content={content} {...popoverProps}>
{children}
</Popover>
);
}
export default ErrorPopover;

View File

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

View File

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

View File

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

View File

@@ -78,7 +78,7 @@ function Metrics({
signal,
}: QueryFunctionContext): Promise<
SuccessResponse<MetricRangePayloadProps>
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, signal),
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, undefined, signal),
enabled: !!payload && visibilities[index],
keepPreviousData: true,
})),

View File

@@ -0,0 +1,50 @@
import { Badge } from '@signozhq/badge';
type BadgeColor =
| 'vanilla'
| 'robin'
| 'forest'
| 'amber'
| 'sienna'
| 'cherry'
| 'sakura'
| 'aqua';
interface HttpStatusBadgeProps {
statusCode: string | number;
}
function getStatusCodeColor(statusCode: number): BadgeColor {
if (statusCode >= 200 && statusCode < 300) {
return 'forest'; // Success - green
}
if (statusCode >= 300 && statusCode < 400) {
return 'robin'; // Redirect - blue
}
if (statusCode >= 400 && statusCode < 500) {
return 'amber'; // Client error - amber
}
if (statusCode >= 500) {
return 'cherry'; // Server error - red
}
if (statusCode >= 100 && statusCode < 200) {
return 'vanilla'; // Informational - neutral
}
return 'robin'; // Default fallback
}
function HttpStatusBadge({
statusCode,
}: HttpStatusBadgeProps): JSX.Element | null {
const numericStatusCode = Number(statusCode);
if (!numericStatusCode || numericStatusCode <= 0) {
return null;
}
const color = getStatusCodeColor(numericStatusCode);
return <Badge color={color}>{statusCode}</Badge>;
}
export default HttpStatusBadge;

View File

@@ -17,7 +17,7 @@ function InputWithLabel({
closeIcon,
}: {
label: string;
initialValue?: string | number;
initialValue?: string | number | null;
placeholder: string;
type?: string;
onClose?: () => void;

View File

@@ -0,0 +1,152 @@
.kbar-command-palette__positioner {
position: fixed;
inset: 0;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 1rem;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(6px);
z-index: 50;
}
.kbar-command-palette__animator {
width: 100%;
max-width: 600px;
}
.kbar-command-palette__card {
background: var(--bg-ink-500);
color: var(--text-vanilla-100);
border-radius: 3px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
overflow: hidden;
display: flex;
flex-direction: column;
}
.kbar-command-palette__search {
padding: 12px 16px;
font-size: 13px;
border: none;
border-bottom: 1px solid var(--border-ink-200);
color: var(--text-vanilla-100);
outline: none;
background-color: var(--bg-ink-500);
}
.kbar-command-palette__section {
padding: 8px 16px 4px;
font-size: 12px;
font-weight: 600;
color: var(--text-robin-500);
font-family: 'Inter', sans-serif;
text-transform: uppercase;
letter-spacing: 0.02em;
}
.kbar-command-palette__item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
font-size: 13px;
cursor: pointer;
transition: background 0.15s ease;
}
.kbar-command-palette__item:hover,
.kbar-command-palette__item--active {
background: var(--bg-ink-400);
}
.kbar-command-palette__icon {
flex-shrink: 0;
width: 18px;
height: 18px;
color: #444;
}
.kbar-command-palette__shortcut {
margin-left: auto;
display: flex;
gap: 4px;
}
.kbar-command-palette__key {
padding: 2px 6px;
font-size: 12px;
border-radius: 4px;
background: var(--bg-ink-300);
color: var(--text-vanilla-300);
text-transform: uppercase;
font-family: 'Space Mono', monospace;
}
.kbar-command-palette__results-container {
div {
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}
}
.lightMode {
.kbar-command-palette__positioner {
background: rgba(0, 0, 0, 0.5);
}
.kbar-command-palette__card {
background: #fff;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
}
.kbar-command-palette__search {
border-bottom: 1px solid #e5e5e5;
color: var(--text-ink-500);
background-color: var(--bg-vanilla-100);
}
.kbar-command-palette__item {
color: var(--text-ink-500);
}
.kbar-command-palette__item:hover,
.kbar-command-palette__item--active {
background: #f5f5f5;
}
.kbar-command-palette__icon {
color: #444;
}
.kbar-command-palette__key {
background: #eee;
color: #555;
}
.kbar-command-palette__results-container {
div {
&::-webkit-scrollbar-thumb {
background: var(--bg-vanilla-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-vanilla-300);
}
}
}
}

View File

@@ -0,0 +1,69 @@
import './KBarCommandPalette.scss';
import {
KBarAnimator,
KBarPortal,
KBarPositioner,
KBarResults,
KBarSearch,
useMatches,
} from 'kbar';
function Results(): JSX.Element {
const { results } = useMatches();
const renderResults = ({
item,
active,
}: {
item: any;
active: boolean;
}): JSX.Element =>
typeof item === 'string' ? (
<div className="kbar-command-palette__section">{item}</div>
) : (
<div
className={`kbar-command-palette__item ${
active ? 'kbar-command-palette__item--active' : ''
}`}
>
{item.icon}
<span>{item.name}</span>
{item.shortcut?.length ? (
<span className="kbar-command-palette__shortcut">
{item.shortcut.map((sc: string) => (
<kbd key={sc} className="kbar-command-palette__key">
{sc}
</kbd>
))}
</span>
) : null}
</div>
);
return (
<div className="kbar-command-palette__results-container">
<KBarResults items={results} onRender={renderResults} />
</div>
);
}
function KBarCommandPalette(): JSX.Element {
return (
<KBarPortal>
<KBarPositioner className="kbar-command-palette__positioner">
<KBarAnimator className="kbar-command-palette__animator">
<div className="kbar-command-palette__card">
<KBarSearch
className="kbar-command-palette__search"
placeholder="Search or type a command..."
/>
<Results />
</div>
</KBarAnimator>
</KBarPositioner>
</KBarPortal>
);
}
export default KBarCommandPalette;

View File

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

View File

@@ -23,6 +23,7 @@ import {
} from 'container/LogDetailedView/utils';
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
@@ -39,7 +40,7 @@ import {
TextSelect,
X,
} from 'lucide-react';
import { useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useCopyToClipboard, useLocation } from 'react-use';
import { AppState } from 'store/reducers';
@@ -94,6 +95,8 @@ function LogDetailInner({
const { notifications } = useNotifications();
const { onLogCopy } = useCopyLogLink(log?.id);
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
const handleModeChange = (e: RadioChangeEvent): void => {
@@ -146,6 +149,34 @@ function LogDetailInner({
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
};
const handleQueryExpressionChange = useCallback(
(value: string, queryIndex: number) => {
// update the query at the given index
setContextQuery((prev) => {
if (!prev) return prev;
return {
...prev,
builder: {
...prev.builder,
queryData: prev.builder.queryData.map((query, idx) =>
idx === queryIndex
? {
...query,
filter: {
...query.filter,
expression: value,
},
}
: query,
),
},
};
});
},
[],
);
const handleRunQuery = (expression: string): void => {
let updatedContextQuery = cloneDeep(contextQuery);
@@ -305,11 +336,19 @@ function LogDetailInner({
onClick={handleFilterVisible}
/>
)}
<Tooltip title="Copy Log Link" placement="left" aria-label="Copy Log Link">
<Button
className="action-btn"
icon={<Copy size={16} />}
onClick={onLogCopy}
/>
</Tooltip>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
<QuerySearch
onChange={(): void => {}}
onChange={(value): void => handleQueryExpressionChange(value, 0)}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}

View File

@@ -17,7 +17,7 @@ function AddToQueryHOC({
}: AddToQueryHOCProps): JSX.Element {
const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => {
event.stopPropagation();
onAddToQuery(fieldKey, fieldValue, OPERATORS['='], undefined, dataType);
onAddToQuery(fieldKey, fieldValue, OPERATORS['='], dataType);
};
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
@@ -41,7 +41,6 @@ export interface AddToQueryHOCProps {
fieldKey: string,
fieldValue: string,
operator: string,
isJSON?: boolean,
dataType?: DataTypes,
) => void;
fontSize: FontSize;

View File

@@ -56,6 +56,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
.map(({ name }) => ({
title: name,
dataIndex: name,
accessorKey: name,
id: name.toLowerCase().replace(/\./g, '_'),
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
@@ -83,7 +85,10 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
// We do not need any title and data index for the log state indicator
title: '',
dataIndex: '',
// eslint-disable-next-line sonarjs/no-duplicate-string
key: 'state-indicator',
accessorKey: 'state-indicator',
id: 'state-indicator',
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
children: (
<div className={cx('state-indicator', fontSize)}>
@@ -101,6 +106,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
title: 'timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
accessorKey: 'timestamp',
id: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (
field: string | number,
@@ -135,6 +142,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
title: 'body',
dataIndex: 'body',
key: 'body',
accessorKey: 'body',
id: 'body',
render: (
field: string | number,
): ColumnTypeRender<Record<string, unknown>> => ({

View File

@@ -1,3 +1,5 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable no-nested-ternary */
@@ -12,9 +14,11 @@ import {
import { Color } from '@signozhq/design-tokens';
import { Button, Checkbox, Select, Typography } from 'antd';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip/TextToolTip';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { capitalize, isEmpty } from 'lodash-es';
import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp } from 'lucide-react';
import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, Info } from 'lucide-react';
import type { BaseSelectRef } from 'rc-select';
import React, {
useCallback,
@@ -23,11 +27,13 @@ import React, {
useRef,
useState,
} from 'react';
import { Virtuoso } from 'react-virtuoso';
import { popupContainer } from 'utils/selectPopupContainer';
import { CustomMultiSelectProps, CustomTagProps, OptionData } from './types';
import {
filterOptionsBySearch,
handleScrollToBottom,
prioritizeOrAddOptionForMultiSelect,
SPACEKEY,
} from './utils';
@@ -37,7 +43,7 @@ enum ToggleTagValue {
All = 'All',
}
const ALL_SELECTED_VALUE = '__all__'; // Constant for the special value
const ALL_SELECTED_VALUE = '__ALL__'; // Constant for the special value
const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
placeholder = 'Search...',
@@ -62,6 +68,12 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
allowClear = false,
onRetry,
maxTagTextLength,
onDropdownVisibleChange,
showIncompleteDataMessage = false,
showLabels = false,
enableRegexOption = false,
isDynamicVariable = false,
showRetryButton = true,
...rest
}) => {
// ===== State & Refs =====
@@ -78,6 +90,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({});
const [visibleOptions, setVisibleOptions] = useState<OptionData[]>([]);
const isClickInsideDropdownRef = useRef(false);
const justOpenedRef = useRef<boolean>(false);
const [isScrolledToBottom, setIsScrolledToBottom] = useState(false);
const isDarkMode = useIsDarkMode();
// Convert single string value to array for consistency
const selectedValues = useMemo(
@@ -124,6 +140,12 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
return allAvailableValues.every((val) => selectedValues.includes(val));
}, [selectedValues, allAvailableValues, enableAllSelection]);
// Define allOptionShown earlier in the code
const allOptionShown = useMemo(
() => value === ALL_SELECTED_VALUE || value === 'ALL',
[value],
);
// Value passed to the underlying Ant Select component
const displayValue = useMemo(
() => (isAllSelected ? [ALL_SELECTED_VALUE] : selectedValues),
@@ -132,10 +154,18 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// ===== Internal onChange Handler =====
const handleInternalChange = useCallback(
(newValue: string | string[]): void => {
(newValue: string | string[], directCaller?: boolean): void => {
// Ensure newValue is an array
const currentNewValue = Array.isArray(newValue) ? newValue : [];
if (
(allOptionShown || isAllSelected) &&
!directCaller &&
currentNewValue.length === 0
) {
return;
}
if (!onChange) return;
// Case 1: Cleared (empty array or undefined)
@@ -144,7 +174,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
return;
}
// Case 2: "__all__" is selected (means select all actual values)
// Case 2: "__ALL__" is selected (means select all actual values)
if (currentNewValue.includes(ALL_SELECTED_VALUE)) {
const allActualOptions = allAvailableValues.map(
(v) => options.flat().find((o) => o.value === v) || { label: v, value: v },
@@ -175,7 +205,14 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}
}
},
[onChange, allAvailableValues, options, enableAllSelection],
[
allOptionShown,
isAllSelected,
onChange,
allAvailableValues,
options,
enableAllSelection,
],
);
// ===== Existing Callbacks (potentially needing adjustment later) =====
@@ -272,7 +309,8 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
: filteredOptions,
);
}
}, [filteredOptions, searchText, options, selectedValues]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filteredOptions, searchText, options]);
// ===== Text Selection Utilities =====
@@ -510,13 +548,46 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}
// Normal single value handling
setSearchText(value.trim());
const trimmedValue = value.trim();
setSearchText(trimmedValue);
if (!isOpen) {
setIsOpen(true);
justOpenedRef.current = true;
}
if (onSearch) onSearch(value.trim());
// Reset active index when search changes if dropdown is open
if (isOpen && trimmedValue) {
setActiveIndex(-1);
// see if the trimmed value matched any option and set that active index
const matchedOption = filteredOptions.find(
(option) =>
option.label.toLowerCase() === trimmedValue.toLowerCase() ||
option.value?.toLowerCase() === trimmedValue.toLowerCase(),
);
if (matchedOption) {
setActiveIndex(1);
} else {
// check if the trimmed value is a regex pattern and set that active index
const isRegex =
trimmedValue.startsWith('.*') && trimmedValue.endsWith('.*');
if (isRegex && enableRegexOption) {
setActiveIndex(0);
} else {
setActiveIndex(enableRegexOption ? 1 : 0);
}
}
}
if (onSearch) onSearch(trimmedValue);
},
[onSearch, isOpen, selectedValues, onChange],
[
onSearch,
isOpen,
selectedValues,
onChange,
filteredOptions,
enableRegexOption,
],
);
// ===== UI & Rendering Functions =====
@@ -528,28 +599,34 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
(text: string, searchQuery: string): React.ReactNode => {
if (!searchQuery || !highlightSearch) return text;
const parts = text.split(
new RegExp(
`(${searchQuery.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')})`,
'gi',
),
);
return (
<>
{parts.map((part, i) => {
// Create a unique key that doesn't rely on array index
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
try {
const parts = text.split(
new RegExp(
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
'gi',
),
);
return (
<>
{parts.map((part, i) => {
// Create a unique key that doesn't rely on array index
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">
{part}
</span>
) : (
part
);
})}
</>
);
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">
{part}
</span>
) : (
part
);
})}
</>
);
} catch (error) {
// If regex fails, return the original text without highlighting
console.error('Error in text highlighting:', error);
return text;
}
},
[highlightSearch],
);
@@ -560,10 +637,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
if (isAllSelected) {
// If all are selected, deselect all
handleInternalChange([]);
handleInternalChange([], true);
} else {
// Otherwise, select all
handleInternalChange([ALL_SELECTED_VALUE]);
handleInternalChange([ALL_SELECTED_VALUE], true);
}
}, [options, isAllSelected, handleInternalChange]);
@@ -738,6 +815,26 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// Enhanced keyboard navigation with support for maxTagCount
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLElement>): void => {
// Simple early return if ALL is selected - block all possible keyboard interactions
// that could remove the ALL tag, but still allow dropdown navigation and search
if (
(allOptionShown || isAllSelected) &&
(e.key === 'Backspace' || e.key === 'Delete')
) {
// Only prevent default if the input is empty or cursor is at start position
const activeElement = document.activeElement as HTMLInputElement;
const isInputActive = activeElement?.tagName === 'INPUT';
const isInputEmpty = isInputActive && !activeElement?.value;
const isCursorAtStart =
isInputActive && activeElement?.selectionStart === 0;
if (isInputEmpty || isCursorAtStart) {
e.preventDefault();
e.stopPropagation();
return;
}
}
// Get flattened list of all selectable options
const getFlatOptions = (): OptionData[] => {
if (!visibleOptions) return [];
@@ -752,13 +849,13 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
if (hasAll) {
flatList.push({
label: 'ALL',
value: '__all__', // Special value for the ALL option
value: ALL_SELECTED_VALUE, // Special value for the ALL option
type: 'defined',
});
}
// Add Regex to flat list
if (!isEmpty(searchText)) {
if (!isEmpty(searchText) && enableRegexOption) {
// Only add regex wrapper if it doesn't already look like a regex pattern
const isAlreadyRegex =
searchText.startsWith('.*') && searchText.endsWith('.*');
@@ -784,6 +881,17 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const flatOptions = getFlatOptions();
// If we just opened the dropdown and have options, set first option as active
if (justOpenedRef.current && flatOptions.length > 0) {
setActiveIndex(0);
justOpenedRef.current = false;
}
// If no option is active but we have options and dropdown is open, activate the first one
if (isOpen && activeIndex === -1 && flatOptions.length > 0) {
setActiveIndex(0);
}
// Get the active input element to check cursor position
const activeElement = document.activeElement as HTMLInputElement;
const isInputActive = activeElement?.tagName === 'INPUT';
@@ -1129,7 +1237,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// If there's an active option in the dropdown, prioritize selecting it
if (activeIndex >= 0 && activeIndex < flatOptions.length) {
const selectedOption = flatOptions[activeIndex];
if (selectedOption.value === '__all__') {
if (selectedOption.value === ALL_SELECTED_VALUE) {
handleSelectAll();
} else if (selectedOption.value && onChange) {
const newValues = selectedValues.includes(selectedOption.value)
@@ -1159,6 +1267,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
e.preventDefault();
setIsOpen(false);
setActiveIndex(-1);
// Call onDropdownVisibleChange when Escape is pressed to close dropdown
if (onDropdownVisibleChange) {
onDropdownVisibleChange(false);
}
break;
case SPACEKEY:
@@ -1168,7 +1280,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const selectedOption = flatOptions[activeIndex];
// Check if it's the ALL option
if (selectedOption.value === '__all__') {
if (selectedOption.value === ALL_SELECTED_VALUE) {
handleSelectAll();
} else if (selectedOption.value && onChange) {
const newValues = selectedValues.includes(selectedOption.value)
@@ -1214,7 +1326,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
e.stopPropagation();
e.preventDefault();
setIsOpen(true);
setActiveIndex(0);
justOpenedRef.current = true; // Set flag to initialize active option on next render
setActiveChipIndex(-1);
break;
@@ -1260,9 +1372,14 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}
},
[
allOptionShown,
isAllSelected,
isOpen,
activeIndex,
getVisibleChipIndices,
getLastVisibleChipIndex,
selectedChips,
isSelectionMode,
isOpen,
activeChipIndex,
selectedValues,
visibleOptions,
@@ -1278,10 +1395,9 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
startSelection,
selectionEnd,
extendSelection,
activeIndex,
onDropdownVisibleChange,
handleSelectAll,
getVisibleChipIndices,
getLastVisibleChipIndex,
enableRegexOption,
],
);
@@ -1306,6 +1422,14 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
setIsOpen(false);
}, []);
// Add a scroll handler for the dropdown
const handleDropdownScroll = useCallback(
(e: React.UIEvent<HTMLDivElement>): void => {
setIsScrolledToBottom(handleScrollToBottom(e));
},
[],
);
// Custom dropdown render with sections support
const customDropdownRender = useCallback((): React.ReactElement => {
// Process options based on current search
@@ -1324,7 +1448,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const customOptions: OptionData[] = [];
// add regex options first since they appear first in the UI
if (!isEmpty(searchText)) {
if (!isEmpty(searchText) && enableRegexOption) {
// Only add regex wrapper if it doesn't already look like a regex pattern
const isAlreadyRegex =
searchText.startsWith('.*') && searchText.endsWith('.*');
@@ -1347,8 +1471,17 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
});
}
// Now add all custom options at the beginning
const enhancedNonSectionOptions = [...customOptions, ...nonSectionOptions];
// Now add all custom options at the beginning, removing duplicates based on value
const allOptions = [...customOptions, ...nonSectionOptions];
const seenValues = new Set<string>();
const enhancedNonSectionOptions = allOptions.filter((option) => {
const value = option.value || '';
if (seenValues.has(value)) {
return false;
}
seenValues.add(value);
return true;
});
const allOptionValues = getAllAvailableValues(processedOptions);
const allOptionsSelected =
@@ -1382,6 +1515,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
onMouseDown={handleDropdownMouseDown}
onClick={handleDropdownClick}
onKeyDown={handleKeyDown}
onScroll={handleDropdownScroll}
onBlur={handleBlur}
role="listbox"
aria-multiselectable="true"
@@ -1423,14 +1557,39 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}
}}
>
<Checkbox
checked={allOptionsSelected}
style={{ width: '100%', height: '100%' }}
>
<div className="option-content">
<div>ALL</div>
<div style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Checkbox checked={allOptionsSelected} className="option-checkbox">
<div className="option-content">
<div className="all-option-text">ALL</div>
</div>
</Checkbox>
<div
onClick={(e): void => {
e.stopPropagation();
}}
onMouseDown={(e): void => {
e.stopPropagation();
}}
>
{isDynamicVariable && (
<TextToolTip
text="ALL in dynamic variable = No filter applied (unlike other variable types where ALL sends all selected values). Learn more"
url="https://signoz.io/docs/userguide/manage-variables/#note-about-all"
urlText="here"
useFilledIcon={false}
outlinedIcon={
<Info
size={14}
style={{
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
marginLeft: 5,
}}
/>
}
/>
)}
</div>
</Checkbox>
</div>
</div>
<div className="divider" />
</>
@@ -1439,7 +1598,19 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
{/* Non-section options when not searching */}
{enhancedNonSectionOptions.length > 0 && (
<div className="no-section-options">
{mapOptions(enhancedNonSectionOptions)}
<Virtuoso
style={{
minHeight: Math.min(300, enhancedNonSectionOptions.length * 40),
maxHeight: enhancedNonSectionOptions.length * 40,
}}
data={enhancedNonSectionOptions}
itemContent={(index, item): React.ReactNode =>
(mapOptions([item]) as unknown) as React.ReactElement
}
totalCount={enhancedNonSectionOptions.length}
itemSize={(): number => 40}
overscan={5}
/>
</div>
)}
@@ -1450,31 +1621,65 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
<div className="select-group" key={section.label}>
<div className="group-label" role="heading" aria-level={2}>
{section.label}
{isDynamicVariable && (
<TextToolTip
text="Related values: Filtered by other variable selections. All values: Unfiltered complete list. Learn more"
url="https://signoz.io/docs/userguide/manage-variables/#dynamic-variable-dropdowns-display-values-in-two-sections"
urlText="here"
useFilledIcon={false}
outlinedIcon={
<Info
size={14}
style={{
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
marginTop: 1,
}}
/>
}
/>
)}
</div>
<div role="group" aria-label={`${section.label} options`}>
{section.options && mapOptions(section.options)}
<Virtuoso
style={{
minHeight: Math.min(300, (section.options?.length || 0) * 40),
maxHeight: (section.options?.length || 0) * 40,
}}
data={section.options || []}
itemContent={(index, item): React.ReactNode =>
(mapOptions([item]) as unknown) as React.ReactElement
}
totalCount={section.options?.length || 0}
itemSize={(): number => 40}
overscan={5}
/>
</div>
</div>
) : null,
) : (
<div key={section.label} />
),
)}
{/* Navigation help footer */}
<div className="navigation-footer" role="note">
{!loading && !errorMessage && !noDataMessage && (
<section className="navigate">
<ArrowDown size={8} className="icons" />
<ArrowUp size={8} className="icons" />
<ArrowLeft size={8} className="icons" />
<ArrowRight size={8} className="icons" />
<span className="keyboard-text">to navigate</span>
</section>
)}
{!loading &&
!errorMessage &&
!noDataMessage &&
!(showIncompleteDataMessage && isScrolledToBottom) && (
<section className="navigate">
<ArrowDown size={8} className="icons" />
<ArrowUp size={8} className="icons" />
<ArrowLeft size={8} className="icons" />
<ArrowRight size={8} className="icons" />
<span className="keyboard-text">to navigate</span>
</section>
)}
{loading && (
<div className="navigation-loading">
<div className="navigation-icons">
<LoadingOutlined />
</div>
<div className="navigation-text">We are updating the values...</div>
<div className="navigation-text">Refreshing values...</div>
</div>
)}
{errorMessage && !loading && (
@@ -1482,21 +1687,33 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
<div className="navigation-text">
{errorMessage || SOMETHING_WENT_WRONG}
</div>
<div className="navigation-icons">
<ReloadOutlined
twoToneColor={Color.BG_CHERRY_400}
onClick={(e): void => {
e.stopPropagation();
if (onRetry) onRetry();
}}
/>
</div>
{onRetry && showRetryButton && (
<div className="navigation-icons">
<ReloadOutlined
twoToneColor={Color.BG_CHERRY_400}
onClick={(e): void => {
e.stopPropagation();
onRetry();
}}
/>
</div>
)}
</div>
)}
{noDataMessage && !loading && (
<div className="navigation-text">{noDataMessage}</div>
)}
{showIncompleteDataMessage &&
isScrolledToBottom &&
!loading &&
!errorMessage && (
<div className="navigation-text-incomplete">
Don&apos;t see the value? Use search
</div>
)}
{noDataMessage &&
!loading &&
!(showIncompleteDataMessage && isScrolledToBottom) &&
!errorMessage && <div className="navigation-text">{noDataMessage}</div>}
</div>
</div>
);
@@ -1513,6 +1730,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
handleDropdownMouseDown,
handleDropdownClick,
handleKeyDown,
handleDropdownScroll,
handleBlur,
activeIndex,
loading,
@@ -1522,8 +1740,35 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
renderOptionWithIndex,
handleSelectAll,
onRetry,
showIncompleteDataMessage,
isScrolledToBottom,
enableRegexOption,
isDarkMode,
isDynamicVariable,
showRetryButton,
]);
// Custom handler for dropdown visibility changes
const handleDropdownVisibleChange = useCallback(
(visible: boolean): void => {
setIsOpen(visible);
if (visible) {
justOpenedRef.current = true;
setActiveIndex(0);
setActiveChipIndex(-1);
} else {
setSearchText('');
setActiveIndex(-1);
// Don't clear activeChipIndex when dropdown closes to maintain tag focus
}
// Pass through to the parent component's handler if provided
if (onDropdownVisibleChange) {
onDropdownVisibleChange(visible);
}
},
[onDropdownVisibleChange],
);
// ===== Side Effects =====
// Clear search when dropdown closes
@@ -1585,55 +1830,16 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// Custom Tag Render (needs significant updates)
const tagRender = useCallback(
(props: CustomTagProps): React.ReactElement => {
const { label, value, closable, onClose } = props;
const { label: labelProp, value, closable, onClose } = props;
const label = showLabels
? options.find((option) => option.value === value)?.label || labelProp
: labelProp;
// If the display value is the special ALL value, render the ALL tag
if (value === ALL_SELECTED_VALUE && isAllSelected) {
const handleAllTagClose = (
e: React.MouseEvent | React.KeyboardEvent,
): void => {
e.stopPropagation();
e.preventDefault();
handleInternalChange([]); // Clear selection when ALL tag is closed
};
const handleAllTagKeyDown = (e: React.KeyboardEvent): void => {
if (e.key === 'Enter' || e.key === SPACEKEY) {
handleAllTagClose(e);
}
// Prevent Backspace/Delete propagation if needed, handle in main keydown handler
};
return (
<div
className={cx('ant-select-selection-item', {
'ant-select-selection-item-active': activeChipIndex === 0, // Treat ALL tag as index 0 when active
'ant-select-selection-item-selected': selectedChips.includes(0),
})}
style={
activeChipIndex === 0 || selectedChips.includes(0)
? {
borderColor: Color.BG_ROBIN_500,
backgroundColor: Color.BG_SLATE_400,
}
: undefined
}
>
<span className="ant-select-selection-item-content">ALL</span>
{closable && (
<span
className="ant-select-selection-item-remove"
onClick={handleAllTagClose}
onKeyDown={handleAllTagKeyDown}
role="button"
tabIndex={0}
aria-label="Remove ALL tag (deselect all)"
>
×
</span>
)}
</div>
);
if (allOptionShown) {
// Don't render a visible tag - will be shown as placeholder
return <div style={{ display: 'none' }} />;
}
// If not isAllSelected, render individual tags using previous logic
@@ -1713,52 +1919,69 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// Fallback for safety, should not be reached
return <div />;
},
[
isAllSelected,
handleInternalChange,
activeChipIndex,
selectedChips,
selectedValues,
maxTagCount,
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[isAllSelected, activeChipIndex, selectedChips, selectedValues, maxTagCount],
);
// Simple onClear handler to prevent clearing ALL
const onClearHandler = useCallback((): void => {
// Skip clearing if ALL is selected
if (allOptionShown || isAllSelected) {
return;
}
// Normal clear behavior
handleInternalChange([], true);
if (onClear) onClear();
}, [onClear, handleInternalChange, allOptionShown, isAllSelected]);
// ===== Component Rendering =====
return (
<Select
ref={selectRef}
className={cx('custom-multiselect', className, {
'has-selection': selectedChips.length > 0 && !isAllSelected,
'is-all-selected': isAllSelected,
<div
className={cx('custom-multiselect-wrapper', {
'all-selected': allOptionShown || isAllSelected,
})}
placeholder={placeholder}
mode="multiple"
showSearch
filterOption={false}
onSearch={handleSearch}
value={displayValue}
onChange={handleInternalChange}
onClear={(): void => handleInternalChange([])}
onDropdownVisibleChange={setIsOpen}
open={isOpen}
defaultActiveFirstOption={defaultActiveFirstOption}
popupMatchSelectWidth={dropdownMatchSelectWidth}
allowClear={allowClear}
getPopupContainer={getPopupContainer ?? popupContainer}
suffixIcon={<DownOutlined style={{ cursor: 'default' }} />}
dropdownRender={customDropdownRender}
menuItemSelectedIcon={null}
popupClassName={cx('custom-multiselect-dropdown-container', popupClassName)}
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
onKeyDown={handleKeyDown}
tagRender={tagRender as any}
placement={placement}
listHeight={300}
searchValue={searchText}
maxTagTextLength={maxTagTextLength}
maxTagCount={isAllSelected ? 1 : maxTagCount}
{...rest}
/>
>
{(allOptionShown || isAllSelected) && !searchText && (
<div className="all-text">ALL</div>
)}
<Select
ref={selectRef}
className={cx('custom-multiselect', className, {
'has-selection': selectedChips.length > 0 && !isAllSelected,
'is-all-selected': isAllSelected,
})}
placeholder={placeholder}
mode="multiple"
showSearch
filterOption={false}
onSearch={handleSearch}
value={displayValue}
onChange={(newValue): void => {
handleInternalChange(newValue, false);
}}
onClear={onClearHandler}
onDropdownVisibleChange={handleDropdownVisibleChange}
open={isOpen}
defaultActiveFirstOption={defaultActiveFirstOption}
popupMatchSelectWidth={dropdownMatchSelectWidth}
allowClear={allowClear}
getPopupContainer={getPopupContainer ?? popupContainer}
suffixIcon={<DownOutlined style={{ cursor: 'default' }} />}
dropdownRender={customDropdownRender}
menuItemSelectedIcon={null}
popupClassName={cx('custom-multiselect-dropdown-container', popupClassName)}
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
onKeyDown={handleKeyDown}
tagRender={tagRender as any}
placement={placement}
listHeight={300}
searchValue={searchText}
maxTagTextLength={maxTagTextLength}
maxTagCount={isAllSelected ? undefined : maxTagCount}
{...rest}
/>
</div>
);
};

View File

@@ -13,9 +13,11 @@ import {
import { Color } from '@signozhq/design-tokens';
import { Select } from 'antd';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { capitalize, isEmpty } from 'lodash-es';
import { ArrowDown, ArrowUp } from 'lucide-react';
import { ArrowDown, ArrowUp, Info } from 'lucide-react';
import type { BaseSelectRef } from 'rc-select';
import React, {
useCallback,
@@ -29,6 +31,7 @@ import { popupContainer } from 'utils/selectPopupContainer';
import { CustomSelectProps, OptionData } from './types';
import {
filterOptionsBySearch,
handleScrollToBottom,
prioritizeOrAddOptionForSingleSelect,
SPACEKEY,
} from './utils';
@@ -57,17 +60,33 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
errorMessage,
allowClear = false,
onRetry,
showIncompleteDataMessage = false,
showRetryButton = true,
isDynamicVariable = false,
...rest
}) => {
// ===== State & Refs =====
const [isOpen, setIsOpen] = useState(false);
const [searchText, setSearchText] = useState('');
const [activeOptionIndex, setActiveOptionIndex] = useState<number>(-1);
const [isScrolledToBottom, setIsScrolledToBottom] = useState(false);
const isDarkMode = useIsDarkMode();
// Refs for element access and scroll behavior
const selectRef = useRef<BaseSelectRef>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({});
// Flag to track if dropdown just opened
const justOpenedRef = useRef<boolean>(false);
// Add a scroll handler for the dropdown
const handleDropdownScroll = useCallback(
(e: React.UIEvent<HTMLDivElement>): void => {
setIsScrolledToBottom(handleScrollToBottom(e));
},
[],
);
// ===== Option Filtering & Processing Utilities =====
@@ -130,23 +149,33 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
(text: string, searchQuery: string): React.ReactNode => {
if (!searchQuery || !highlightSearch) return text;
const parts = text.split(new RegExp(`(${searchQuery})`, 'gi'));
return (
<>
{parts.map((part, i) => {
// Create a deterministic but unique key
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
try {
const parts = text.split(
new RegExp(
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
'gi',
),
);
return (
<>
{parts.map((part, i) => {
// Create a deterministic but unique key
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">
{part}
</span>
) : (
part
);
})}
</>
);
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">
{part}
</span>
) : (
part
);
})}
</>
);
} catch (error) {
console.error('Error in text highlighting:', error);
return text;
}
},
[highlightSearch],
);
@@ -246,9 +275,14 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
const trimmedValue = value.trim();
setSearchText(trimmedValue);
// Reset active option index when search changes
if (isOpen) {
setActiveOptionIndex(0);
}
if (onSearch) onSearch(trimmedValue);
},
[onSearch],
[onSearch, isOpen],
);
/**
@@ -272,14 +306,23 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
const flatList: OptionData[] = [];
// Process options
let processedOptions = isEmpty(value)
? filteredOptions
: prioritizeOrAddOptionForSingleSelect(filteredOptions, value);
if (!isEmpty(searchText)) {
processedOptions = filterOptionsBySearch(processedOptions, searchText);
}
const { sectionOptions, nonSectionOptions } = splitOptions(
isEmpty(value)
? filteredOptions
: prioritizeOrAddOptionForSingleSelect(filteredOptions, value),
processedOptions,
);
// Add custom option if needed
if (!isEmpty(searchText) && !isLabelPresent(filteredOptions, searchText)) {
if (
!isEmpty(searchText) &&
!isLabelPresent(processedOptions, searchText)
) {
flatList.push({
label: searchText,
value: searchText,
@@ -300,33 +343,52 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
const options = getFlatOptions();
// If we just opened the dropdown and have options, set first option as active
if (justOpenedRef.current && options.length > 0) {
setActiveOptionIndex(0);
justOpenedRef.current = false;
}
// If no option is active but we have options, activate the first one
if (activeOptionIndex === -1 && options.length > 0) {
setActiveOptionIndex(0);
}
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveOptionIndex((prev) =>
prev < options.length - 1 ? prev + 1 : 0,
);
if (options.length > 0) {
setActiveOptionIndex((prev) =>
prev < options.length - 1 ? prev + 1 : 0,
);
}
break;
case 'ArrowUp':
e.preventDefault();
setActiveOptionIndex((prev) =>
prev > 0 ? prev - 1 : options.length - 1,
);
if (options.length > 0) {
setActiveOptionIndex((prev) =>
prev > 0 ? prev - 1 : options.length - 1,
);
}
break;
case 'Tab':
// Tab navigation with Shift key support
if (e.shiftKey) {
e.preventDefault();
setActiveOptionIndex((prev) =>
prev > 0 ? prev - 1 : options.length - 1,
);
if (options.length > 0) {
setActiveOptionIndex((prev) =>
prev > 0 ? prev - 1 : options.length - 1,
);
}
} else {
e.preventDefault();
setActiveOptionIndex((prev) =>
prev < options.length - 1 ? prev + 1 : 0,
);
if (options.length > 0) {
setActiveOptionIndex((prev) =>
prev < options.length - 1 ? prev + 1 : 0,
);
}
}
break;
@@ -339,6 +401,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
onChange(selectedOption.value, selectedOption);
setIsOpen(false);
setActiveOptionIndex(-1);
setSearchText('');
}
} else if (!isEmpty(searchText)) {
// Add custom value when no option is focused
@@ -351,6 +414,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
onChange(customOption.value, customOption);
setIsOpen(false);
setActiveOptionIndex(-1);
setSearchText('');
}
}
break;
@@ -359,6 +423,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
e.preventDefault();
setIsOpen(false);
setActiveOptionIndex(-1);
setSearchText('');
break;
case ' ': // Space key
@@ -369,6 +434,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
onChange(selectedOption.value, selectedOption);
setIsOpen(false);
setActiveOptionIndex(-1);
setSearchText('');
}
}
break;
@@ -379,7 +445,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
// Open dropdown when Down or Tab is pressed while closed
e.preventDefault();
setIsOpen(true);
setActiveOptionIndex(0);
justOpenedRef.current = true; // Set flag to initialize active option on next render
}
},
[
@@ -444,6 +510,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
className="custom-select-dropdown"
onClick={handleDropdownClick}
onKeyDown={handleKeyDown}
onScroll={handleDropdownScroll}
role="listbox"
tabIndex={-1}
aria-activedescendant={
@@ -454,7 +521,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
<div className="no-section-options">
{nonSectionOptions.length > 0 && mapOptions(nonSectionOptions)}
</div>
{/* Section options */}
{sectionOptions.length > 0 &&
sectionOptions.map((section) =>
@@ -462,6 +528,23 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
<div className="select-group" key={section.label}>
<div className="group-label" role="heading" aria-level={2}>
{section.label}
{isDynamicVariable && (
<TextToolTip
text="Related values: Filtered by other variable selections. All values: Unfiltered complete list. Learn more"
url="https://signoz.io/docs/userguide/manage-variables/#dynamic-variable-dropdowns-display-values-in-two-sections"
urlText="here"
useFilledIcon={false}
outlinedIcon={
<Info
size={14}
style={{
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
marginTop: 1,
}}
/>
}
/>
)}
</div>
<div role="group" aria-label={`${section.label} options`}>
{section.options && mapOptions(section.options)}
@@ -472,19 +555,22 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
{/* Navigation help footer */}
<div className="navigation-footer" role="note">
{!loading && !errorMessage && !noDataMessage && (
<section className="navigate">
<ArrowDown size={8} className="icons" />
<ArrowUp size={8} className="icons" />
<span className="keyboard-text">to navigate</span>
</section>
)}
{!loading &&
!errorMessage &&
!noDataMessage &&
!(showIncompleteDataMessage && isScrolledToBottom) && (
<section className="navigate">
<ArrowDown size={8} className="icons" />
<ArrowUp size={8} className="icons" />
<span className="keyboard-text">to navigate</span>
</section>
)}
{loading && (
<div className="navigation-loading">
<div className="navigation-icons">
<LoadingOutlined />
</div>
<div className="navigation-text">We are updating the values...</div>
<div className="navigation-text">Refreshing values...</div>
</div>
)}
{errorMessage && !loading && (
@@ -492,21 +578,33 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
<div className="navigation-text">
{errorMessage || SOMETHING_WENT_WRONG}
</div>
<div className="navigation-icons">
<ReloadOutlined
twoToneColor={Color.BG_CHERRY_400}
onClick={(e): void => {
e.stopPropagation();
if (onRetry) onRetry();
}}
/>
</div>
{onRetry && showRetryButton && (
<div className="navigation-icons">
<ReloadOutlined
twoToneColor={Color.BG_CHERRY_400}
onClick={(e): void => {
e.stopPropagation();
onRetry();
}}
/>
</div>
)}
</div>
)}
{noDataMessage && !loading && (
<div className="navigation-text">{noDataMessage}</div>
)}
{showIncompleteDataMessage &&
isScrolledToBottom &&
!loading &&
!errorMessage && (
<div className="navigation-text-incomplete">
Don&apos;t see the value? Use search
</div>
)}
{noDataMessage &&
!loading &&
!(showIncompleteDataMessage && isScrolledToBottom) &&
!errorMessage && <div className="navigation-text">{noDataMessage}</div>}
</div>
</div>
);
@@ -520,6 +618,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
isLabelPresent,
handleDropdownClick,
handleKeyDown,
handleDropdownScroll,
activeOptionIndex,
loading,
errorMessage,
@@ -527,8 +626,25 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
dropdownRender,
renderOptionWithIndex,
onRetry,
showIncompleteDataMessage,
isScrolledToBottom,
showRetryButton,
isDarkMode,
isDynamicVariable,
]);
// Handle dropdown visibility changes
const handleDropdownVisibleChange = useCallback((visible: boolean): void => {
setIsOpen(visible);
if (visible) {
justOpenedRef.current = true;
setActiveOptionIndex(0);
} else {
setSearchText('');
setActiveOptionIndex(-1);
}
}, []);
// ===== Side Effects =====
// Clear search text when dropdown closes
@@ -582,7 +698,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
onSearch={handleSearch}
value={value}
onChange={onChange}
onDropdownVisibleChange={setIsOpen}
onDropdownVisibleChange={handleDropdownVisibleChange}
open={isOpen}
options={optionsWithHighlight}
defaultActiveFirstOption={defaultActiveFirstOption}

View File

@@ -0,0 +1,127 @@
import {
fireEvent,
render,
RenderResult,
screen,
waitFor,
} from '@testing-library/react';
import { VirtuosoMockContext } from 'react-virtuoso';
import CustomMultiSelect from '../CustomMultiSelect';
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Helper function to render with VirtuosoMockContext
const renderWithVirtuoso = (component: React.ReactElement): RenderResult =>
render(
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
{component}
</VirtuosoMockContext.Provider>,
);
// Mock options data
const mockOptions = [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
];
// CSS selector for retry button
const RETRY_BUTTON_SELECTOR = '.navigation-icons .anticon-reload';
describe('CustomMultiSelect - Retry Functionality', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should show retry button when 5xx error occurs and error message is displayed', async () => {
const mockOnRetry = jest.fn();
const errorMessage = 'Internal Server Error (500)';
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
errorMessage={errorMessage}
onRetry={mockOnRetry}
showRetryButton
loading={false}
/>,
);
// Open dropdown to see error state
const selectElement = screen.getByRole('combobox');
fireEvent.mouseDown(selectElement);
// Wait for dropdown to appear with error message
await waitFor(() => {
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
// Check that retry button (ReloadOutlined icon) is present
const retryButton = document.querySelector(RETRY_BUTTON_SELECTOR);
expect(retryButton).toBeInTheDocument();
});
it('should show retry button when 4xx error occurs and error message is displayed (current behavior)', async () => {
const mockOnRetry = jest.fn();
const errorMessage = 'Bad Request (400)';
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
errorMessage={errorMessage}
onRetry={mockOnRetry}
showRetryButton={false}
loading={false}
/>,
);
// Open dropdown
const selectElement = screen.getByRole('combobox');
fireEvent.mouseDown(selectElement);
// Wait for dropdown to appear with error message
await waitFor(() => {
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
const retryButton = document.querySelector(RETRY_BUTTON_SELECTOR);
expect(retryButton).not.toBeInTheDocument();
});
it('should call onRetry function when retry button is clicked', async () => {
const mockOnRetry = jest.fn();
const errorMessage = 'Internal Server Error (500)';
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
errorMessage={errorMessage}
onRetry={mockOnRetry}
showRetryButton
loading={false}
/>,
);
// Open dropdown
const selectElement = screen.getByRole('combobox');
fireEvent.mouseDown(selectElement);
// Wait for dropdown to appear
await waitFor(() => {
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
// Find and click the retry button
const retryButton = document.querySelector(RETRY_BUTTON_SELECTOR);
expect(retryButton).toBeInTheDocument();
fireEvent.click(retryButton as Element);
// Verify onRetry was called
expect(mockOnRetry).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,10 +1,27 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
fireEvent,
render,
RenderResult,
screen,
waitFor,
} from '@testing-library/react';
import { VirtuosoMockContext } from 'react-virtuoso';
import CustomMultiSelect from '../CustomMultiSelect';
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Helper function to render with VirtuosoMockContext
const renderWithVirtuoso = (component: React.ReactElement): RenderResult =>
render(
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
{component}
</VirtuosoMockContext.Provider>,
);
// Mock options data
const mockOptions = [
{ label: 'Option 1', value: 'option1' },
@@ -32,7 +49,7 @@ const mockGroupedOptions = [
describe('CustomMultiSelect Component', () => {
it('renders with placeholder', () => {
const handleChange = jest.fn();
render(
renderWithVirtuoso(
<CustomMultiSelect
placeholder="Select multiple options"
options={mockOptions}
@@ -47,7 +64,9 @@ describe('CustomMultiSelect Component', () => {
it('opens dropdown when clicked', async () => {
const handleChange = jest.fn();
render(<CustomMultiSelect options={mockOptions} onChange={handleChange} />);
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={handleChange} />,
);
// Click to open the dropdown
const selectElement = screen.getByRole('combobox');
@@ -66,7 +85,7 @@ describe('CustomMultiSelect Component', () => {
const handleChange = jest.fn();
// Start with option1 already selected
render(
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
onChange={handleChange}
@@ -93,7 +112,7 @@ describe('CustomMultiSelect Component', () => {
it('selects ALL options when ALL is clicked', async () => {
const handleChange = jest.fn();
render(
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
onChange={handleChange}
@@ -126,7 +145,7 @@ describe('CustomMultiSelect Component', () => {
});
it('displays selected options as tags', async () => {
render(
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} value={['option1', 'option2']} />,
);
@@ -137,7 +156,7 @@ describe('CustomMultiSelect Component', () => {
it('removes a tag when clicked', async () => {
const handleChange = jest.fn();
render(
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
value={['option1', 'option2']}
@@ -159,7 +178,7 @@ describe('CustomMultiSelect Component', () => {
});
it('filters options when searching', async () => {
render(<CustomMultiSelect options={mockOptions} />);
renderWithVirtuoso(<CustomMultiSelect options={mockOptions} />);
// Open dropdown
const selectElement = screen.getByRole('combobox');
@@ -193,7 +212,7 @@ describe('CustomMultiSelect Component', () => {
});
it('renders grouped options correctly', async () => {
render(<CustomMultiSelect options={mockGroupedOptions} />);
renderWithVirtuoso(<CustomMultiSelect options={mockGroupedOptions} />);
// Open dropdown
const selectElement = screen.getByRole('combobox');
@@ -211,18 +230,18 @@ describe('CustomMultiSelect Component', () => {
});
it('shows loading state', () => {
render(<CustomMultiSelect options={mockOptions} loading />);
renderWithVirtuoso(<CustomMultiSelect options={mockOptions} loading />);
// Open dropdown
const selectElement = screen.getByRole('combobox');
fireEvent.mouseDown(selectElement);
// Check loading text is displayed
expect(screen.getByText('We are updating the values...')).toBeInTheDocument();
expect(screen.getByText('Refreshing values...')).toBeInTheDocument();
});
it('shows error message', () => {
render(
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
errorMessage="Test error message"
@@ -238,7 +257,9 @@ describe('CustomMultiSelect Component', () => {
});
it('shows no data message', () => {
render(<CustomMultiSelect options={[]} noDataMessage="No data available" />);
renderWithVirtuoso(
<CustomMultiSelect options={[]} noDataMessage="No data available" />,
);
// Open dropdown
const selectElement = screen.getByRole('combobox');
@@ -249,7 +270,7 @@ describe('CustomMultiSelect Component', () => {
});
it('shows "ALL" tag when all options are selected', () => {
render(
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
value={['option1', 'option2', 'option3']}

File diff suppressed because it is too large Load Diff

View File

@@ -140,7 +140,7 @@ describe('CustomSelect Component', () => {
fireEvent.mouseDown(selectElement);
// Check loading text is displayed
expect(screen.getByText('We are updating the values...')).toBeInTheDocument();
expect(screen.getByText('Refreshing values...')).toBeInTheDocument();
});
it('shows error message', () => {

View File

@@ -0,0 +1,624 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
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';
// Mock the dashboard variables query
jest.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
__esModule: true,
default: jest.fn(() =>
Promise.resolve({
payload: {
variableValues: ['option1', 'option2', 'option3', 'option4'],
},
}),
),
}));
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Constants
const TEST_VARIABLE_NAME = 'test_variable';
const TEST_VARIABLE_ID = 'test-var-id';
// Create a mock store
const mockStore = configureStore([])({
globalTime: {
minTime: Date.now() - 3600000, // 1 hour ago
maxTime: Date.now(),
},
});
// Test data
const createMockVariable = (
overrides: Partial<IDashboardVariable> = {},
): IDashboardVariable => ({
id: TEST_VARIABLE_ID,
name: TEST_VARIABLE_NAME,
description: 'Test variable description',
type: 'QUERY',
queryValue: 'SELECT DISTINCT value FROM table',
customValue: '',
sort: 'ASC',
multiSelect: false,
showALLOption: true,
selectedValue: [],
allSelected: false,
...overrides,
});
function TestWrapper({ children }: { children: React.ReactNode }): JSX.Element {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return (
<Provider store={mockStore}>
<QueryClientProvider client={queryClient}>
<VirtuosoMockContext.Provider
// eslint-disable-next-line react/jsx-no-constructed-context-values
value={{ viewportHeight: 300, itemHeight: 40 }}
>
{children}
</VirtuosoMockContext.Provider>
</QueryClientProvider>
</Provider>
);
}
describe('VariableItem Integration Tests', () => {
let user: ReturnType<typeof userEvent.setup>;
let mockOnValueUpdate: jest.Mock;
let mockSetVariablesToGetUpdated: jest.Mock;
beforeEach(() => {
user = userEvent.setup();
mockOnValueUpdate = jest.fn();
mockSetVariablesToGetUpdated = jest.fn();
jest.clearAllMocks();
});
// ===== 1. INTEGRATION WITH CUSTOMSELECT =====
describe('CustomSelect Integration (VI)', () => {
test('VI-01: Single select variable integration', async () => {
const variable = createMockVariable({
multiSelect: false,
type: 'CUSTOM',
customValue: 'option1,option2,option3',
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Should render with CustomSelect
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
await user.click(combobox);
await waitFor(() => {
expect(screen.getByText('option1')).toBeInTheDocument();
expect(screen.getByText('option2')).toBeInTheDocument();
expect(screen.getByText('option3')).toBeInTheDocument();
});
// Select an option
const option1 = screen.getByText('option1');
await user.click(option1);
expect(mockOnValueUpdate).toHaveBeenCalledWith(
TEST_VARIABLE_NAME,
TEST_VARIABLE_ID,
'option1',
false,
);
});
});
// ===== 2. INTEGRATION WITH CUSTOMMULTISELECT =====
describe('CustomMultiSelect Integration (VI)', () => {
test('VI-02: Multi select variable integration', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
customValue: 'option1,option2,option3,option4',
showALLOption: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Should render with CustomMultiSelect
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
await user.click(combobox);
await waitFor(() => {
// Should show ALL option
expect(screen.getByText('ALL')).toBeInTheDocument();
});
// Wait for Virtuoso to render the custom options
await waitFor(
() => {
expect(screen.getByText('option1')).toBeInTheDocument();
expect(screen.getByText('option2')).toBeInTheDocument();
expect(screen.getByText('option3')).toBeInTheDocument();
expect(screen.getByText('option4')).toBeInTheDocument();
},
{ timeout: 5000 },
);
});
});
// ===== 3. TEXTBOX VARIABLE TYPE =====
describe('Textbox Variable Integration', () => {
test('VI-03: Textbox variable handling', async () => {
const variable = createMockVariable({
type: 'TEXTBOX',
selectedValue: 'initial-value',
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Should render a regular input
const textInput = screen.getByDisplayValue('initial-value');
expect(textInput).toBeInTheDocument();
expect(textInput.tagName).toBe('INPUT');
// Clear and type new value
await user.clear(textInput);
await user.type(textInput, 'new-text-value');
// Should call onValueUpdate after debounce
await waitFor(
() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
TEST_VARIABLE_NAME,
TEST_VARIABLE_ID,
'new-text-value',
false,
);
},
{ timeout: 1000 },
);
});
});
// ===== 4. VALUE PERSISTENCE AND STATE MANAGEMENT =====
describe('Value Persistence and State Management', () => {
test('VI-04: All selected state handling', () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
customValue: 'service1,service2,service3',
selectedValue: ['service1', 'service2', 'service3'],
allSelected: true,
showALLOption: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Should show "ALL" instead of individual values
expect(screen.getByText('ALL')).toBeInTheDocument();
});
test('VI-05: Dropdown behavior with temporary selections', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
customValue: 'item1,item2,item3',
selectedValue: ['item1'],
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
const combobox = screen.getByRole('combobox');
await user.click(combobox);
// Wait for dropdown to open
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toBeInTheDocument();
});
// Verify the component renders without crashing
expect(combobox).toBeInTheDocument();
});
});
// ===== 6. ACCESSIBILITY AND USER EXPERIENCE =====
describe('Accessibility and User Experience', () => {
test('VI-06: Variable description tooltip', async () => {
const variable = createMockVariable({
description: 'This variable controls the service selection',
type: 'CUSTOM',
customValue: 'service1,service2',
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Should show info icon
const infoIcon = document.querySelector('.info-icon');
expect(infoIcon).toBeInTheDocument();
// Hover to show tooltip
if (infoIcon) {
await user.hover(infoIcon);
}
await waitFor(() => {
expect(
screen.getByText('This variable controls the service selection'),
).toBeInTheDocument();
});
});
test('VI-07: Variable name display', () => {
const variable = createMockVariable({
name: 'service_name',
type: 'CUSTOM',
customValue: 'service1,service2',
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Should show variable name with $ prefix
expect(screen.getByText('$service_name')).toBeInTheDocument();
});
test('VI-08: Max tag count behavior', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
customValue: 'tag1,tag2,tag3,tag4,tag5,tag6,tag7',
selectedValue: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'],
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Wait for component to render
await waitFor(() => {
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
});
// Should show limited number of tags with "+ X more"
const tags = document.querySelectorAll('.ant-select-selection-item');
// The component should render without crashing
expect(tags.length).toBeGreaterThanOrEqual(0);
});
});
// ===== 8. SEARCH INTERACTION TESTS =====
describe('Search Interaction Tests', () => {
test('VI-14: Search persistence across dropdown open/close', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',
multiSelect: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
const combobox = screen.getByRole('combobox');
await user.click(combobox);
await waitFor(() => {
expect(screen.getByText('ALL')).toBeInTheDocument();
});
const searchInput = document.querySelector(
'.ant-select-selection-search-input',
);
expect(searchInput).toBeInTheDocument();
if (searchInput) {
await user.type(searchInput, 'search-text');
}
// Verify search text is in input
await waitFor(() => {
expect(searchInput).toHaveValue('search-text');
});
// Press Escape to close dropdown
await user.keyboard('{Escape}');
// Dropdown should close and search text should be cleared
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expect(searchInput).toHaveValue('');
});
});
});
// ===== 9. ADVANCED KEYBOARD NAVIGATION =====
describe('Advanced Keyboard Navigation (VI)', () => {
test('VI-15: Shift + Arrow + Del chip deletion in multiselect', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',
multiSelect: true,
selectedValue: ['option1', 'option2', 'option3'],
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
const combobox = screen.getByRole('combobox');
await user.click(combobox);
// Navigate to chips using arrow keys
await user.keyboard('{ArrowLeft}');
// Use Shift + Arrow to navigate between chips
await user.keyboard('{Shift>}{ArrowLeft}{/Shift}');
// Use Del to delete the active chip
await user.keyboard('{Delete}');
// Note: The component may not immediately call onValueUpdate
// This test verifies the chip deletion behavior
await waitFor(() => {
// Check if a chip was removed from the selection
const selectionItems = document.querySelectorAll(
'.ant-select-selection-item',
);
expect(selectionItems.length).toBeLessThan(3);
});
});
});
// ===== 11. ADVANCED UI STATES =====
describe('Advanced UI States (VI)', () => {
test('VI-19: No data with previous value selected in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: '',
multiSelect: true,
selectedValue: ['previous-value'],
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Wait for component to initialize
await waitFor(() => {
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
});
const combobox = screen.getByRole('combobox');
await user.click(combobox);
// Should show no data message (the component may not show this exact text)
await waitFor(() => {
// Check if dropdown is empty or shows no data indication
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toBeInTheDocument();
});
// Verify the component renders without crashing
expect(combobox).toBeInTheDocument();
});
test('VI-20: Always editable accessibility in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2',
multiSelect: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
const combobox = screen.getByRole('combobox');
// Should be editable
expect(combobox).not.toBeDisabled();
await user.click(combobox);
expect(combobox).toHaveFocus();
// Should still be interactive
expect(combobox).not.toBeDisabled();
});
});
// ===== 13. DROPDOWN PERSISTENCE =====
describe('Dropdown Persistence (VI)', () => {
test('VI-24: Dropdown stays open for non-save actions in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',
multiSelect: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]}
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
dependencyData={null}
/>
</TestWrapper>,
);
// Wait for component to initialize
await waitFor(() => {
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
});
const combobox = screen.getByRole('combobox');
await user.click(combobox);
await waitFor(() => {
expect(screen.getByText('ALL')).toBeInTheDocument();
});
// Navigate with arrow keys (non-save action)
await user.keyboard('{ArrowDown}');
await user.keyboard('{ArrowDown}');
// Dropdown should still be open
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toBeInTheDocument();
});
// Verify the component renders without crashing
expect(combobox).toBeInTheDocument();
// Only ESC should close the dropdown
await user.keyboard('{Escape}');
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
});
});
});
});

View File

@@ -35,12 +35,50 @@ $custom-border-color: #2c3044;
width: 100%;
position: relative;
&.is-all-selected {
.ant-select-selection-search-input {
caret-color: transparent;
}
.ant-select-selection-placeholder {
opacity: 1 !important;
color: var(--bg-vanilla-400) !important;
font-weight: 500;
visibility: visible !important;
pointer-events: none;
z-index: 2;
.lightMode & {
color: rgba(0, 0, 0, 0.85) !important;
}
}
&.ant-select-focused .ant-select-selection-placeholder {
opacity: 0.45 !important;
}
}
.all-selected-text {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--bg-vanilla-400);
z-index: 1;
pointer-events: none;
.lightMode & {
color: rgba(0, 0, 0, 0.85);
}
}
.ant-select-selector {
max-height: 200px;
overflow: auto;
scrollbar-width: thin;
background-color: var(--bg-ink-400);
border-color: var(--bg-slate-400);
cursor: text;
&::-webkit-scrollbar {
width: 6px;
@@ -56,6 +94,16 @@ $custom-border-color: #2c3044;
}
}
// Ensure adequate space for input area
.ant-select-selection-search {
min-width: 60px !important;
flex: 1 1 auto;
.ant-select-selection-search-input {
min-width: 60px !important;
cursor: text;
}
}
&.ant-select-focused {
.ant-select-selector {
border-color: var(--bg-robin-500);
@@ -158,7 +206,7 @@ $custom-border-color: #2c3044;
// Custom dropdown styles for single select
.custom-select-dropdown {
padding: 8px 0 0 0;
max-height: 500px;
max-height: 300px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
@@ -276,6 +324,10 @@ $custom-border-color: #2c3044;
font-size: 12px;
}
.navigation-text-incomplete {
color: var(--bg-amber-600) !important;
}
.navigation-error {
.navigation-text,
.navigation-icons {
@@ -322,7 +374,7 @@ $custom-border-color: #2c3044;
// Custom dropdown styles for multi-select
.custom-multiselect-dropdown {
padding: 8px 0 0 0;
max-height: 500px;
max-height: 350px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
@@ -355,8 +407,13 @@ $custom-border-color: #2c3044;
.select-group {
margin-bottom: 12px;
overflow: hidden;
margin-top: 4px;
.group-label {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 500;
padding: 4px 12px;
font-size: 13px;
@@ -404,6 +461,13 @@ $custom-border-color: #2c3044;
width: 100%;
}
.all-option-text {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.option-content {
display: flex;
justify-content: space-between;
@@ -637,6 +701,7 @@ $custom-border-color: #2c3044;
.ant-select-selector {
background-color: var(--bg-vanilla-100);
border-color: #e9e9e9;
cursor: text; // Make entire selector clickable for input focus
&::-webkit-scrollbar-thumb {
background-color: #ccc;
@@ -647,6 +712,20 @@ $custom-border-color: #2c3044;
}
}
.ant-select-selection-search {
min-width: 60px !important;
flex: 1 1 auto;
.ant-select-selection-search-input {
min-width: 60px !important;
cursor: text;
}
}
.ant-select-selector {
cursor: text;
}
.ant-select-selection-placeholder {
color: rgba(0, 0, 0, 0.45);
}
@@ -656,6 +735,10 @@ $custom-border-color: #2c3044;
border: 1px solid #e8e8e8;
color: rgba(0, 0, 0, 0.85);
font-size: 12px !important;
height: 20px;
line-height: 18px;
.ant-select-selection-item-content {
color: rgba(0, 0, 0, 0.85);
}
@@ -718,6 +801,10 @@ $custom-border-color: #2c3044;
.select-group {
.group-label {
display: flex;
align-items: center;
justify-content: space-between;
color: rgba(0, 0, 0, 0.85);
background-color: #fafafa;
border-bottom: 1px solid #f0f0f0;
@@ -836,3 +923,38 @@ $custom-border-color: #2c3044;
}
}
}
.custom-multiselect-wrapper {
position: relative;
width: 100%;
&.all-selected {
.all-text {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--bg-vanilla-400);
font-weight: 500;
z-index: 2;
pointer-events: none;
transition: opacity 0.2s ease, visibility 0.2s ease;
.lightMode & {
color: rgba(0, 0, 0, 0.85);
}
}
&:focus-within .all-text {
opacity: 0.45;
}
.ant-select-selection-search-input {
caret-color: auto;
}
.ant-select-selection-placeholder {
display: none;
}
}
}

View File

@@ -24,9 +24,12 @@ export interface CustomSelectProps extends Omit<SelectProps, 'options'> {
highlightSearch?: boolean;
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
popupMatchSelectWidth?: boolean;
errorMessage?: string;
errorMessage?: string | null;
allowClear?: SelectProps['allowClear'];
onRetry?: () => void;
showIncompleteDataMessage?: boolean;
showRetryButton?: boolean;
isDynamicVariable?: boolean;
}
export interface CustomTagProps {
@@ -51,10 +54,16 @@ export interface CustomMultiSelectProps
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
dropdownRender?: (menu: React.ReactElement) => React.ReactElement;
highlightSearch?: boolean;
errorMessage?: string;
errorMessage?: string | null;
popupClassName?: string;
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
maxTagCount?: number;
allowClear?: SelectProps['allowClear'];
onRetry?: () => void;
maxTagTextLength?: number;
showIncompleteDataMessage?: boolean;
showLabels?: boolean;
enableRegexOption?: boolean;
isDynamicVariable?: boolean;
showRetryButton?: boolean;
}

View File

@@ -1,4 +1,6 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { uniqueOptions } from 'container/NewDashboard/DashboardVariablesSelection/util';
import { OptionData } from './types';
export const SPACEKEY = ' ';
@@ -98,8 +100,10 @@ export const prioritizeOrAddOptionForMultiSelect = (
label: labels?.[value] ?? value, // Use provided label or default to value
}));
const flatOutSelectedOptions = uniqueOptions([...newOptions, ...foundOptions]);
// Add found & new options to the top
return [...newOptions, ...foundOptions, ...filteredOptions];
return [...flatOutSelectedOptions, ...filteredOptions];
};
/**
@@ -133,3 +137,15 @@ export const filterOptionsBySearch = (
})
.filter(Boolean) as OptionData[];
};
/**
* Utility function to handle dropdown scroll and detect when scrolled to bottom
* Returns true when scrolled to within 20px of the bottom
*/
export const handleScrollToBottom = (
e: React.UIEvent<HTMLDivElement>,
): boolean => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
// Consider "scrolled to bottom" when within 20px of the bottom or at the bottom
return scrollHeight - scrollTop - clientHeight < 20;
};

View File

@@ -102,7 +102,7 @@ function ListViewOrderBy({
onChange={onChange}
onSearch={handleSearch}
notFoundContent={<Loader isLoading={isLoading} />}
placeholder="Select an attribute"
placeholder="Select a field"
style={{ width: 200 }}
options={selectOptions}
filterOption={(input, option): boolean =>

View File

@@ -0,0 +1,19 @@
.loading-panel-data {
padding: 24px 0;
height: 240px;
display: flex;
justify-content: center;
align-items: flex-start;
.loading-panel-data-content {
display: flex;
align-items: flex-start;
flex-direction: column;
.loading-gif {
height: 72px;
margin-left: -24px;
}
}
}

View File

@@ -0,0 +1,19 @@
import './PanelDataLoading.styles.scss';
import { Typography } from 'antd';
export function PanelDataLoading(): JSX.Element {
return (
<div className="loading-panel-data">
<div className="loading-panel-data-content">
<img
className="loading-gif"
src="/Icons/loading-plane.gif"
alt="wait-icon"
/>
<Typography.Text>Fetching data...</Typography.Text>
</div>
</div>
);
}

View File

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

View File

@@ -5,11 +5,13 @@ import { Formula } from 'container/QueryBuilder/components/Formula';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useEffect, useMemo, useRef } from 'react';
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
import { QueryV2 } from './QueryV2/QueryV2';
import TraceOperator from './QueryV2/TraceOperator/TraceOperator';
export const QueryBuilderV2 = memo(function QueryBuilderV2({
config,
@@ -18,6 +20,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryComponents,
isListViewPanel = false,
showOnlyWhereClause = false,
showTraceOperator = false,
version,
}: QueryBuilderProps): JSX.Element {
const {
@@ -25,6 +28,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
addNewBuilderQuery,
addNewFormula,
handleSetConfig,
addTraceOperator,
panelType,
initialDataSource,
} = useQueryBuilder();
@@ -54,6 +58,11 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
newPanelType,
]);
const isMultiQueryAllowed = useMemo(
() => !isListViewPanel || showTraceOperator,
[showTraceOperator, isListViewPanel],
);
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: true, isDisabled: true },
@@ -97,11 +106,60 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
listViewTracesFilterConfigs,
]);
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
if (
currentQuery.builder.queryTraceOperator &&
currentQuery.builder.queryTraceOperator.length > 0
) {
return currentQuery.builder.queryTraceOperator[0];
}
return undefined;
}, [currentQuery.builder.queryTraceOperator]);
const hasAtLeastOneTraceQuery = useMemo(
() =>
currentQuery.builder.queryData.some(
(query) => query.dataSource === DataSource.TRACES,
),
[currentQuery.builder.queryData],
);
const hasTraceOperator = useMemo(
() => showTraceOperator && hasAtLeastOneTraceQuery && Boolean(traceOperator),
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
);
const shouldShowFooter = useMemo(
() =>
(!showOnlyWhereClause && !isListViewPanel) ||
(currentDataSource === DataSource.TRACES && showTraceOperator),
[isListViewPanel, showTraceOperator, showOnlyWhereClause, currentDataSource],
);
const showQueryList = useMemo(
() => (!showOnlyWhereClause && !isListViewPanel) || showTraceOperator,
[isListViewPanel, showOnlyWhereClause, showTraceOperator],
);
const showFormula = useMemo(() => {
if (currentDataSource === DataSource.TRACES) {
return !isListViewPanel;
}
return true;
}, [isListViewPanel, currentDataSource]);
const showAddTraceOperator = useMemo(
() => showTraceOperator && !traceOperator && hasAtLeastOneTraceQuery,
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
);
return (
<QueryBuilderV2Provider>
<div className="query-builder-v2">
<div className="qb-content-container">
{isListViewPanel && (
{!isMultiQueryAllowed ? (
<QueryV2
ref={containerRef}
key={currentQuery.builder.queryData[0].queryName}
@@ -109,15 +167,16 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
query={currentQuery.builder.queryData[0]}
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
isMultiQueryAllowed={isMultiQueryAllowed}
showTraceOperator={showTraceOperator}
hasTraceOperator={hasTraceOperator}
version={version}
isAvailableToDisable={false}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
/>
)}
{!isListViewPanel &&
) : (
currentQuery.builder.queryData.map((query, index) => (
<QueryV2
ref={containerRef}
@@ -127,12 +186,17 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
version={version}
isMultiQueryAllowed={isMultiQueryAllowed}
isAvailableToDisable={false}
showTraceOperator={showTraceOperator}
hasTraceOperator={hasTraceOperator}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
signalSource={config?.signalSource || ''}
/>
))}
))
)}
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
<div className="qb-formulas-container">
@@ -157,15 +221,25 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
</div>
)}
{!showOnlyWhereClause && !isListViewPanel && (
{shouldShowFooter && (
<QueryFooter
showAddFormula={showFormula}
addNewBuilderQuery={addNewBuilderQuery}
addNewFormula={addNewFormula}
addTraceOperator={addTraceOperator}
showAddTraceOperator={showAddTraceOperator}
/>
)}
{hasTraceOperator && (
<TraceOperator
isListViewPanel={isListViewPanel}
traceOperator={traceOperator as IBuilderTraceOperator}
/>
)}
</div>
{!showOnlyWhereClause && !isListViewPanel && (
{showQueryList && (
<div className="query-names-section">
{currentQuery.builder.queryData.map((query) => (
<div key={query.queryName} className="query-name">

View File

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

View File

@@ -7,7 +7,6 @@ import { ATTRIBUTE_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import SpaceAggregationOptions from 'container/QueryBuilder/components/SpaceAggregationOptions/SpaceAggregationOptions';
import { GroupByFilter, OperatorsSelect } from 'container/QueryBuilder/filters';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Info } from 'lucide-react';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
@@ -19,11 +18,13 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
index,
version,
panelType,
signalSource = '',
}: {
query: IBuilderQuery;
index: number;
version: string;
panelType: PANEL_TYPES | null;
signalSource: string;
}): JSX.Element {
const { setAggregationOptions } = useQueryBuilderV2Context();
const {
@@ -50,17 +51,17 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
);
useEffect(() => {
setAggregationOptions([
setAggregationOptions(query.queryName, [
{
func: queryAggregation.spaceAggregation || 'count',
arg: queryAggregation.metricName || '',
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
queryAggregation.spaceAggregation,
queryAggregation.metricName,
setAggregationOptions,
query,
query.queryName,
]);
const handleChangeGroupByKeys = useCallback(
@@ -100,12 +101,22 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
<div className="metrics-time-aggregation-section">
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE BY TIME{' '}
<Tooltip title="AGGREGATE BY TIME">
<Info size={12} />
</Tooltip>
</div>
<Tooltip
title={
<a
href="https://signoz.io/docs/metrics-management/types-and-aggregation/#aggregation"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
Learn more about temporal aggregation
</a>
}
>
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE WITHIN TIME SERIES{' '}
</div>
</Tooltip>
<div className="metrics-aggregation-section-content-item-value">
<OperatorsSelect
value={queryAggregation.timeAggregation || ''}
@@ -118,9 +129,30 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
{showAggregationInterval && (
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label">
every
</div>
<Tooltip
title={
<div>
Set the time interval for aggregation
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
Learn about step intervals
</a>
</div>
}
placement="top"
>
<div
className="metrics-aggregation-section-content-item-label"
style={{ cursor: 'help' }}
>
every
</div>
</Tooltip>
<div className="metrics-aggregation-section-content-item-value">
<InputWithLabel
@@ -128,7 +160,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
label="Seconds"
placeholder="Auto"
labelAfter
initialValue={query?.stepInterval ?? undefined}
initialValue={query?.stepInterval ?? null}
/>
</div>
</div>
@@ -138,12 +170,22 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
<div className="metrics-space-aggregation-section">
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE LABELS
<Tooltip title="AGGREGATE LABELS">
<Info size={12} />
</Tooltip>
</div>
<Tooltip
title={
<a
href="https://signoz.io/docs/metrics-management/types-and-aggregation/#aggregation"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
Learn more about spatial aggregation
</a>
}
>
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE ACROSS TIME SERIES
</div>
</Tooltip>
<div className="metrics-aggregation-section-content-item-value">
<SpaceAggregationOptions
panelType={panelType}
@@ -168,6 +210,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
disabled={!queryAggregation.metricName}
query={query}
onChange={handleChangeGroupByKeys}
signalSource={signalSource}
/>
</div>
</div>
@@ -204,13 +247,35 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
disabled={!queryAggregation.metricName}
query={query}
onChange={handleChangeGroupByKeys}
signalSource={signalSource}
/>
</div>
</div>
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label">
every
</div>
<Tooltip
title={
<div>
Set the time interval for aggregation
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
Learn about step intervals
</a>
</div>
}
placement="top"
>
<div
className="metrics-aggregation-section-content-item-label"
style={{ cursor: 'help' }}
>
every
</div>
</Tooltip>
<div className="metrics-aggregation-section-content-item-value">
<InputWithLabel
@@ -218,7 +283,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
label="Seconds"
placeholder="Auto"
labelAfter
initialValue={query?.stepInterval ?? undefined}
initialValue={query?.stepInterval ?? null}
className="histogram-every-input"
/>
</div>

View File

@@ -44,13 +44,14 @@
.lightMode {
.metrics-select-container {
.ant-select-selector {
border: 1px solid var(--bg-slate-300) !important;
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100);
color: var(--text-ink-100);
}
.ant-select-dropdown {
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300) !important;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
backdrop-filter: none;

View File

@@ -9,10 +9,12 @@ export const MetricsSelect = memo(function MetricsSelect({
query,
index,
version,
signalSource,
}: {
query: IBuilderQuery;
index: number;
version: string;
signalSource: 'meter' | '';
}): JSX.Element {
const { handleChangeAggregatorAttribute } = useQueryOperations({
index,
@@ -22,7 +24,12 @@ export const MetricsSelect = memo(function MetricsSelect({
return (
<div className="metrics-select-container">
<AggregatorFilter onChange={handleChangeAggregatorAttribute} query={query} />
<AggregatorFilter
onChange={handleChangeAggregatorAttribute}
query={query}
index={index}
signalSource={signalSource || ''}
/>
</div>
);
});

View File

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

View File

@@ -1,7 +1,11 @@
.query-add-ons {
width: 100%;
}
.add-ons-list {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
.add-ons-tabs {
display: flex;
@@ -111,17 +115,13 @@
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: -2px !important;
width: 100% !important;
position: absolute !important;
top: 38px !important;
top: calc(100% + 6px) !important;
left: 0px !important;
right: 0px !important;
border-radius: 4px;
border: 1px solid var(--bg-slate-200, #1d212d);
border-top: none !important;
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
@@ -129,7 +129,9 @@
) !important;
backdrop-filter: blur(20px);
box-sizing: border-box;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6);
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
ul {
width: 100% !important;
@@ -165,7 +167,6 @@
overflow: hidden;
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
.cm-completionIcon {
display: none !important;
@@ -330,16 +331,18 @@
background: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
ul {
li {
color: var(--bg-ink-300) !important;
&:hover {
background: var(--bg-vanilla-300) !important;
}
&[aria-selected='true'] {
color: var(--bg-ink-500) !important;
background: var(--bg-vanilla-300) !important;
font-weight: 600 !important;
}
}
}

View File

@@ -1,6 +1,7 @@
/* eslint-disable react/require-default-props */
import './QueryAddOns.styles.scss';
import { Button, Radio, RadioChangeEvent } from 'antd';
import { Button, Radio, RadioChangeEvent, Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
@@ -9,7 +10,7 @@ import { ReduceToFilter } from 'container/QueryBuilder/filters/ReduceToFilter/Re
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { isEmpty } from 'lodash-es';
import { BarChart2, ChevronUp, ScrollText } from 'lucide-react';
import { BarChart2, ChevronUp, ExternalLink, ScrollText } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
@@ -21,6 +22,8 @@ interface AddOn {
icon: React.ReactNode;
label: string;
key: string;
description?: string;
docLink?: string;
}
const ADD_ONS_KEYS = {
@@ -36,26 +39,45 @@ const ADD_ONS = [
icon: <BarChart2 size={14} />,
label: 'Group By',
key: 'group_by',
description:
'Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments.',
docLink: 'https://signoz.io/docs/userguide/query-builder-v5/#grouping',
},
{
icon: <ScrollText size={14} />,
label: 'Having',
key: 'having',
description:
'Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#conditional-filtering-with-having',
},
{
icon: <ScrollText size={14} />,
label: 'Order By',
key: 'order_by',
description:
'Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers.',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting',
},
{
icon: <ScrollText size={14} />,
label: 'Limit',
key: 'limit',
description:
'Show only the top/bottom N results. Perfect for focusing on outliers, reducing noise, and improving dashboard performance.',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting',
},
{
icon: <ScrollText size={14} />,
label: 'Legend format',
key: 'legend_format',
description:
'Customize series labels using variables like {{service.name}}-{{endpoint}}. Makes charts readable at a glance during incident investigation.',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#legend-formatting',
},
];
@@ -63,8 +85,58 @@ const REDUCE_TO = {
icon: <ScrollText size={14} />,
label: 'Reduce to',
key: 'reduce_to',
description:
'Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value.',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations',
};
// Custom tooltip content component
function TooltipContent({
label,
description,
docLink,
}: {
label: string;
description?: string;
docLink?: string;
}): JSX.Element {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
maxWidth: '300px',
}}
>
<strong style={{ fontSize: '14px' }}>{label}</strong>
{description && (
<span style={{ fontSize: '12px', lineHeight: '1.5' }}>{description}</span>
)}
{docLink && (
<a
href={docLink}
target="_blank"
rel="noopener noreferrer"
onClick={(e): void => e.stopPropagation()}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
color: '#4096ff',
fontSize: '12px',
marginTop: '4px',
}}
>
Learn more
<ExternalLink size={12} />
</a>
)}
</div>
);
}
function QueryAddOns({
query,
version,
@@ -72,6 +144,7 @@ function QueryAddOns({
showReduceTo,
panelType,
index,
isForTraceOperator = false,
}: {
query: IBuilderQuery;
version: string;
@@ -79,6 +152,7 @@ function QueryAddOns({
showReduceTo: boolean;
panelType: PANEL_TYPES | null;
index: number;
isForTraceOperator?: boolean;
}): JSX.Element {
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
@@ -88,6 +162,7 @@ function QueryAddOns({
index,
query,
entityVersion: '',
isForTraceOperator,
});
const { handleSetQueryData } = useQueryBuilder();
@@ -212,7 +287,21 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'group_by') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Group By</div>
<Tooltip
title={
<TooltipContent
label="Group By"
description="Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments."
docLink="https://signoz.io/docs/userguide/query-builder-v5/#grouping"
/>
}
placement="top"
mouseEnterDelay={0.5}
>
<div className="label" style={{ cursor: 'help' }}>
Group By
</div>
</Tooltip>
<div className="input">
<GroupByFilter
disabled={
@@ -234,7 +323,21 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'having') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Having</div>
<Tooltip
title={
<TooltipContent
label="Having"
description="Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500"
docLink="https://signoz.io/docs/userguide/query-builder-v5/#conditional-filtering-with-having"
/>
}
placement="top"
mouseEnterDelay={0.5}
>
<div className="label" style={{ cursor: 'help' }}>
Having
</div>
</Tooltip>
<div className="input">
<HavingFilter
onClose={(): void => {
@@ -266,7 +369,21 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'order_by') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Order By</div>
<Tooltip
title={
<TooltipContent
label="Order By"
description="Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers."
docLink="https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting"
/>
}
placement="top"
mouseEnterDelay={0.5}
>
<div className="label" style={{ cursor: 'help' }}>
Order By
</div>
</Tooltip>
<div className="input">
<OrderByFilter
entityVersion={version}
@@ -290,7 +407,21 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Reduce to</div>
<Tooltip
title={
<TooltipContent
label="Reduce to"
description="Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value."
docLink="https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations"
/>
}
placement="top"
mouseEnterDelay={0.5}
>
<div className="label" style={{ cursor: 'help' }}>
Reduce to
</div>
</Tooltip>
<div className="input">
<ReduceToFilter query={query} onChange={handleChangeReduceToV5} />
</div>
@@ -330,20 +461,32 @@ function QueryAddOns({
value={selectedViews}
>
{addOns.map((addOn) => (
<Radio.Button
key={addOn.label}
className={
selectedViews.find((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
<Tooltip
key={addOn.key}
title={
<TooltipContent
label={addOn.label}
description={addOn.description}
docLink={addOn.docLink}
/>
}
value={addOn}
placement="top"
mouseEnterDelay={0.5}
>
<div className="add-on-tab-title">
{addOn.icon}
{addOn.label}
</div>
</Radio.Button>
<Radio.Button
className={
selectedViews.find((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
}
value={addOn}
>
<div className="add-on-tab-title">
{addOn.icon}
{addOn.label}
</div>
</Radio.Button>
</Tooltip>
))}
</Radio.Group>
</div>

View File

@@ -63,17 +63,14 @@
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: 8px !important;
min-width: 400px !important;
position: absolute !important;
top: calc(100% + 6px) !important;
left: 0px !important;
width: 100% !important;
right: 0px !important;
border-radius: 4px;
border: 1px solid var(--bg-slate-200, #1d212d);
border-top: none !important;
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
@@ -81,6 +78,7 @@
) !important;
backdrop-filter: blur(20px);
box-sizing: border-box;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6);
font-family: 'Space Mono', monospace !important;
ul {
@@ -269,19 +267,17 @@
background: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
ul {
li {
&:hover {
background-color: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600;
}
color: var(--bg-ink-300) !important;
&:hover,
&[aria-selected='true'] {
background: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600;
font-weight: 600 !important;
}
}
}

View File

@@ -1,9 +1,13 @@
import './QueryAggregation.styles.scss';
import { Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import QueryAggregationSelect from './QueryAggregationSelect';
@@ -19,7 +23,7 @@ function QueryAggregationOptions({
panelType?: string;
onAggregationIntervalChange: (value: number) => void;
onChange?: (value: string) => void;
queryData: IBuilderQuery;
queryData: IBuilderQuery | IBuilderTraceOperator;
}): JSX.Element {
const showAggregationInterval = useMemo(() => {
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
@@ -53,12 +57,34 @@ function QueryAggregationOptions({
{showAggregationInterval && (
<div className="query-aggregation-interval">
<div className="query-aggregation-interval-label">every</div>
<Tooltip
title={
<div>
Set the time interval for aggregation
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
Learn about step intervals
</a>
</div>
}
placement="top"
>
<div
className="metrics-aggregation-section-content-item-label"
style={{ cursor: 'help' }}
>
every
</div>
</Tooltip>
<div className="query-aggregation-interval-input-container">
<InputWithLabel
initialValue={
queryData?.stepInterval ? queryData?.stepInterval : undefined
}
initialValue={queryData?.stepInterval ? queryData?.stepInterval : null}
className="query-aggregation-interval-input"
label="Seconds"
placeholder="Auto"

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