Compare commits

..

139 Commits

Author SHA1 Message Date
Srikanth Chekuri
2ab83a0e96 Merge branch 'main' into e2e/alert_manager 2026-07-16 01:25:46 +05:30
Niladri Adhikary
821dc233bf fix(alertmanager): integration tests fixes (#11710)
* fix: integration tests

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

* test(alertmanager): add logs and traces e2e tests for custom templating

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

* fix: missing annotations

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

* fix: annotations and templating fixes

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

* chore: removed annotations

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

* fix: tests removed annotations

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

* fix: integration tests

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

* chore: py-lint run

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

---------

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>
2026-06-15 11:21:39 +05:30
Srikanth Chekuri
05c3657ff9 Merge branch 'main' into e2e/alert_manager 2026-06-13 17:06:51 +05:30
Niladri Adhikary
9c066a3389 test(alertmanager): fix integration tests 2026-06-13 17:06:04 +05:30
srikanthccv
5cb04161c2 chore: stale changes 2026-05-25 23:07:21 +05:30
srikanthccv
1a23dbf4df Merge branch 'main' into e2e/alert_manager 2026-05-25 22:58:19 +05:30
Abhishek Kumar Singh
7ab9272a71 chore: moved alertmanager test to new package 2026-04-24 08:50:38 +05:30
Abhishek Kumar Singh
df35a8e0a4 chore: increased alert time to avoid flaky tests 2026-04-23 21:53:09 +05:30
Abhishek Kumar Singh
bee369309c chore: updated email test data with newline changes 2026-04-23 21:38:22 +05:30
Abhishek Kumar Singh
096602cf8b chore: supress line size linter for notification template 2026-04-23 21:32:55 +05:30
Abhishek Kumar Singh
96d423eca3 chore: updates test data based on new changes in templater 2026-04-23 21:29:18 +05:30
Abhishek Kumar Singh
64c736d549 chore: refactor as per new code 2026-04-23 21:03:01 +05:30
Abhishek Kumar Singh
04ee77d533 Merge branch 'chore/alert_templater_connecting_pieces' into e2e/alert_manager 2026-04-23 20:43:42 +05:30
Abhishek Kumar Singh
d40b0cb5da Merge branch 'main' into chore/alert_templater_connecting_pieces 2026-04-23 20:43:21 +05:30
Abhishek Kumar Singh
bcc17f1c0d Merge branch 'chore/alert_templater_connecting_pieces' into e2e/alert_manager 2026-04-23 20:41:06 +05:30
Abhishek Kumar Singh
05de5c6a17 Merge branch 'main' into chore/alert_templater_connecting_pieces 2026-04-23 20:22:02 +05:30
Abhishek Kumar Singh
ffe05af591 chore: updated email template based on new template struct 2026-04-23 20:21:20 +05:30
Abhishek Kumar Singh
64b45a36a8 Merge branch 'chore/alert_templater_connecting_pieces' into e2e/alert_manager 2026-04-23 16:56:38 +05:30
Abhishek Kumar Singh
75728c9918 chore: remove private annotations from pagerduty notifier 2026-04-23 16:55:53 +05:30
Abhishek Kumar Singh
b08578acb4 Merge branch 'chore/alert_templater_connecting_pieces' into e2e/alert_manager 2026-04-23 12:05:28 +05:30
Srikanth Chekuri
99208b89c1 Merge branch 'main' into chore/alert_templater_connecting_pieces 2026-04-23 03:05:05 +05:30
Abhishek Kumar Singh
8454cc8609 refactor: msteams skip logs and traces as factsset, slack code refactor 2026-04-22 20:02:15 +05:30
Abhishek Kumar Singh
bf9220596c chore: updated webhook notifier to send templated title and body in notification 2026-04-22 19:56:01 +05:30
Abhishek Kumar Singh
89fc758e7f chore: removed notification processor 2026-04-22 19:06:24 +05:30
Abhishek Kumar Singh
53f17ac362 refactor: changes as per internal review 2026-04-22 19:04:10 +05:30
Abhishek Kumar Singh
87e66b041d Merge branch 'feat/markdown_renderer' into chore/alert_templater_connecting_pieces 2026-04-20 12:34:33 +05:30
Abhishek Kumar Singh
36e3bad95d Merge branch 'main' into feat/markdown_renderer 2026-04-20 12:24:27 +05:30
Abhishek Kumar Singh
9b7a3a46ee refactor: changed markdown renderer from interface to package-level functions 2026-04-20 12:23:18 +05:30
Abhishek Kumar Singh
fadd421f9d Merge branch 'feat/markdown_renderer' into chore/alert_templater_connecting_pieces 2026-04-17 19:41:33 +05:30
Abhishek Kumar Singh
c36c18f79e Merge branch 'main' into feat/markdown_renderer 2026-04-17 19:37:36 +05:30
Abhishek Kumar Singh
5bb4079951 refactor: removed logger as markdown renderer dependency 2026-04-17 19:36:06 +05:30
Abhishek Kumar Singh
15a036904f chore: removed special handling for softline break 2026-04-17 19:32:09 +05:30
Abhishek Kumar Singh
af607bd249 Merge branch 'feat/alert_manager_template' into feat/markdown_renderer 2026-04-17 18:35:35 +05:30
Srikanth Chekuri
2eadc895a3 Merge branch 'main' into feat/alert_manager_template 2026-04-17 18:05:31 +05:30
Abhishek Kumar Singh
66e34c9b5e chore: lint issue 2026-04-17 17:50:05 +05:30
Abhishek Kumar Singh
799de1ece3 refactor: changes as per internal review 2026-04-17 17:15:07 +05:30
Abhishek Kumar Singh
c5c450c58c fix: concurrent rendering in markdown renderer 2026-04-16 18:20:25 +05:30
Abhishek Kumar Singh
dc67f8551f Merge branch 'feat/alert_manager_template' into feat/markdown_renderer 2026-04-16 17:59:31 +05:30
Abhishek Kumar Singh
c46c0e105a chore: removed notifier test files 2026-04-16 17:29:34 +05:30
Abhishek Kumar Singh
4aaa6ae5a1 fix: increase wait time for alertmanager server setup in notifier tests 2026-04-16 17:22:56 +05:30
Abhishek Kumar Singh
82abb9b113 chore: applied formatter + lint supressed for long config lines 2026-04-16 17:01:44 +05:30
Abhishek Kumar Singh
cc5a0b93ae Merge branch 'main' into feat/alert_manager_template 2026-04-16 16:46:44 +05:30
Abhishek Kumar Singh
e5d67f87eb chore: add alertmanager to integration CI workflow 2026-04-16 14:55:08 +05:30
Abhishek Kumar Singh
f2c56a9978 chore: maildev version bump 2026-04-16 14:52:35 +05:30
Abhishek Kumar Singh
15c593d797 chore: added maildev and notification channel to teardown 2026-04-16 14:44:35 +05:30
Abhishek Kumar Singh
9a47d3e553 Merge branch 'chore/alert_templater_connecting_pieces' into e2e/alert_manager 2026-04-16 14:29:22 +05:30
Abhishek Kumar Singh
b3fe077deb chore: remove static templates from pagerduty notifications 2026-04-16 14:28:27 +05:30
Abhishek Kumar Singh
14d30fa754 fix: email template directory for notification processor 2026-04-16 14:28:03 +05:30
Abhishek Kumar Singh
556bfe44d2 test: added e2e tests for notification content templating 2026-04-16 14:22:05 +05:30
Abhishek Kumar Singh
a9ab0bc480 chore: moved default notification channels to fixtures 2026-04-16 13:06:46 +05:30
Abhishek Kumar Singh
5eda220f88 test: alert manager supported notifier test 2026-04-14 20:25:16 +05:30
Abhishek Kumar Singh
a6ef54d6b9 Merge branch 'feat/markdown_renderer' into chore/alert_templater_connecting_pieces 2026-04-14 17:51:17 +05:30
Abhishek Kumar Singh
d1e332fb16 Merge branch 'feat/alert_manager_template' into feat/markdown_renderer 2026-04-14 17:51:05 +05:30
Abhishek Kumar Singh
c9f3e1ae26 Merge branch 'chore/am_custom_notifiers' into feat/alert_manager_template 2026-04-14 17:50:45 +05:30
Abhishek Kumar Singh
41ded342a1 Merge branch 'main' into chore/am_custom_notifiers 2026-04-14 17:50:27 +05:30
Abhishek Kumar Singh
7f22cb0442 chore: integrated slack mrkdwn renderer and added NoOp formatter 2026-04-14 17:46:33 +05:30
Abhishek Kumar Singh
6b77835050 feat: custom raw html renderer to escape <no value> 2026-04-14 17:44:57 +05:30
Abhishek Kumar Singh
909c3a80b1 feat: slack mrkdwn renderer 2026-04-14 17:44:15 +05:30
Abhishek Kumar Singh
42726747d8 Merge branch 'feat/alert_manager_template' into feat/markdown_renderer 2026-04-14 17:35:19 +05:30
Abhishek Kumar Singh
64ce90e418 fix: variables with symbols in template 2026-04-14 17:27:41 +05:30
Abhishek Kumar Singh
2fcffb7cdc feat: return single templating result from with flag for template type 2026-04-14 17:24:40 +05:30
Abhishek Kumar Singh
5ceb9255d1 chore: error logging + NoOp type definition 2026-04-14 16:28:54 +05:30
Abhishek Kumar Singh
1df7d75d43 feat: added Literal for CompareOperator and MatchType and expose from ruleManager 2026-04-14 15:18:21 +05:30
Abhishek Kumar Singh
1bbee9bc63 chore: fix linter and merge conflict issues 2026-04-14 14:59:55 +05:30
Abhishek Kumar Singh
581e7c8b19 Merge branch 'feat/markdown_renderer' into chore/alert_templater_connecting_pieces 2026-04-14 14:28:37 +05:30
Abhishek Kumar Singh
782eee23d2 Merge branch 'feat/alert_manager_template' into feat/markdown_renderer 2026-04-14 14:16:08 +05:30
Abhishek Kumar Singh
abc0d71c16 Merge branch 'chore/am_custom_notifiers' into feat/alert_manager_template 2026-04-13 22:13:25 +05:30
Abhishek Kumar Singh
2e2dd4c42b Merge branch 'main' into chore/am_custom_notifiers 2026-04-13 22:04:08 +05:30
Abhishek Kumar Singh
51621a3131 chore: added action links to email and slack notifiers 2026-04-06 20:44:24 +05:30
Abhishek Kumar Singh
0fd3979de5 chore: integration of custom templating in rule manager 2026-04-02 11:51:03 +05:30
Abhishek Kumar Singh
4f75075df0 feat: email rendering with custom template in notification processor 2026-03-31 20:28:02 +05:30
Abhishek Kumar Singh
b905d5cc5d feat: added no value extension to render <no value> in html 2026-03-31 20:16:32 +05:30
Abhishek Kumar Singh
6d1b9738b5 chore: lint fixes 2026-03-31 18:27:03 +05:30
Abhishek Kumar Singh
710cd8bdb3 Merge branch 'feat/markdown_renderer' into chore/alert_templater_connecting_pieces 2026-03-31 18:22:15 +05:30
Abhishek Kumar Singh
629929c6a6 Merge branch 'feat/alert_manager_template' into feat/markdown_renderer 2026-03-31 17:57:21 +05:30
Abhishek Kumar Singh
0ce76a94d6 Merge branch 'chore/am_custom_notifiers' into feat/alert_manager_template 2026-03-31 17:57:02 +05:30
Abhishek Kumar Singh
46ae74ced5 chore: updated email notifier from upstream 2026-03-31 11:52:08 +05:30
Abhishek Kumar Singh
2d8c1b7c86 chore: updated licenses for notifiers 2026-03-31 11:51:45 +05:30
Abhishek Kumar Singh
6602c8c523 refactor: lint fixes 2026-03-31 10:46:52 +05:30
Abhishek Kumar Singh
c22dbcbf74 refactor: review comments 2026-03-31 10:44:37 +05:30
Abhishek Kumar Singh
250bd9abeb Merge branch 'main' into chore/am_custom_notifiers 2026-03-31 10:15:39 +05:30
Abhishek Kumar Singh
605b218836 test: added test in notification procesor for no value 2026-03-30 19:53:49 +05:30
Abhishek Kumar Singh
99af679a62 fix: handled <no value> in templated response 2026-03-30 19:47:40 +05:30
Abhishek Kumar Singh
46123f925f fix: added handling for labels and annotations with . and - 2026-03-30 18:25:07 +05:30
Abhishek Kumar Singh
3e5e90f904 fix: webhook notifier update annotations before preparing data 2026-03-30 15:17:36 +05:30
Abhishek Kumar Singh
f8a614478c feat: updated slack notifier with slack mrkdwn format 2026-03-29 21:12:39 +05:30
Abhishek Kumar Singh
ffc54137ca test: add new test cases for Slack MRKDWN rendering 2026-03-29 20:08:08 +05:30
Abhishek Kumar Singh
34655db8cc test: simplify TestRenderSlackMrkdwn 2026-03-29 19:34:54 +05:30
Abhishek Kumar Singh
020140643c feat: added new format in markdown renderer 2026-03-29 19:22:28 +05:30
Abhishek Kumar Singh
6b8a4e4441 feat: slack mrkdwn renderer 2026-03-29 19:06:49 +05:30
Abhishek Kumar Singh
c345f579bb chore: updated alertmanagernotify package with updated notifier signature 2026-03-27 20:18:01 +05:30
Abhishek Kumar Singh
819c7e1103 feat: added notification processor in webhook notifier 2026-03-27 20:17:34 +05:30
Abhishek Kumar Singh
f0a1d07213 chore: added IsCustomTemplated helper function in result struct 2026-03-27 20:06:55 +05:30
Abhishek Kumar Singh
895e10b986 feat: added notification processor in pagerduty notifier 2026-03-27 20:05:04 +05:30
Abhishek Kumar Singh
78228b97ff feat: added notification processor in slack notifier 2026-03-27 19:37:13 +05:30
Abhishek Kumar Singh
826d763b89 feat: added notification processor in opsgenie notifier 2026-03-27 17:38:37 +05:30
Abhishek Kumar Singh
cb74acefc7 chore: msteams note 2026-03-27 17:00:24 +05:30
Abhishek Kumar Singh
eb79494e73 refactor: ms teams notifier 2026-03-27 16:58:52 +05:30
Abhishek Kumar Singh
28698d1af4 feat: update ms team notifier with notification processor 2026-03-27 16:45:33 +05:30
Abhishek Kumar Singh
be55cef462 feat: updated email notifier 2026-03-27 16:05:52 +05:30
Abhishek Kumar Singh
183e400280 chore: return isDefaultTemplated true even in case of blank default template 2026-03-26 19:14:01 +05:30
Abhishek Kumar Singh
5f0b43d975 chore: refactor notification processor and send processor in ReceiverIntegrations 2026-03-26 19:03:39 +05:30
Abhishek Kumar Singh
09adb8bef0 feat: alert notification processor 2026-03-26 16:04:28 +05:30
Abhishek Kumar Singh
77f5522e47 chore: return missing variables as sorted list 2026-03-26 16:03:41 +05:30
Abhishek Kumar Singh
c68154a031 feat: added no-op formatter in markdown rederer 2026-03-26 14:27:55 +05:30
Abhishek Kumar Singh
ec94a6555b refactor: alert manager templater 2026-03-25 20:01:12 +05:30
Abhishek Kumar Singh
f132dc28c3 chore: updated br with new line in test and logs added 2026-03-23 17:02:30 +05:30
Abhishek Kumar Singh
834df680f0 Merge branch 'feat/alert_manager_template' into feat/markdown_renderer 2026-03-23 16:48:26 +05:30
Abhishek Kumar Singh
48b9f15e18 feat: integrated slack blockit in markdownrenderer package and removed plaintext format 2026-03-23 16:45:29 +05:30
Abhishek Kumar Singh
55fa03fe7e test: added test for html rendering 2026-03-23 16:32:25 +05:30
Abhishek Kumar Singh
933717f309 feat: slack blockkit renderer using goldmark 2026-03-23 15:36:32 +05:30
Abhishek Kumar Singh
9ffc1203da chore: updated newline to markdown format 2026-03-19 22:50:24 +05:30
Abhishek Kumar Singh
205a78f0e6 feat: added basic html markdown templater 2026-03-17 20:17:57 +05:30
Abhishek Kumar Singh
79518b6823 Merge branch 'chore/am_custom_notifiers' into feat/alert_manager_template 2026-03-17 20:15:00 +05:30
Abhishek Kumar Singh
e6a9f49cec Merge branch 'main' into chore/am_custom_notifiers 2026-03-17 20:14:30 +05:30
Abhishek Kumar Singh
fd5fc40823 chore: updated comments 2026-03-16 18:19:03 +05:30
Abhishek Kumar Singh
db2e2a4617 chore: lint fix 2026-03-16 15:54:22 +05:30
Abhishek Kumar Singh
9368d3f393 refactor: comments and test improvements 2026-03-16 15:47:45 +05:30
Abhishek Kumar Singh
0c97ba36d6 refactor: test case and sb related changed 2026-03-16 15:12:23 +05:30
Abhishek Kumar Singh
2e1bdbc2fd chore: added test for missing function 2026-03-16 14:49:52 +05:30
Abhishek Kumar Singh
330737f779 chore: renamed the interface 2026-03-13 19:20:49 +05:30
Abhishek Kumar Singh
f0c531ae2b chore: lint fix 2026-03-13 19:11:34 +05:30
Abhishek Kumar Singh
54477ee786 feat: added support for and in templating 2026-03-13 19:09:39 +05:30
Abhishek Kumar Singh
d281f7b6a2 test: fix preprocessor test case 2026-03-13 17:31:28 +05:30
Abhishek Kumar Singh
378dc350ef refactor: added extractCommonKV instead of 2 different functions 2026-03-13 17:10:13 +05:30
Abhishek Kumar Singh
89c38ed9bc feat: converted alerttemplater to interface and updated tests 2026-03-13 17:02:26 +05:30
Abhishek Kumar Singh
04c4869b12 chore: added handling for missing variable used in template 2026-03-13 14:10:13 +05:30
Abhishek Kumar Singh
388a1184ca chore: fix lint issues 2026-03-12 21:51:13 +05:30
Abhishek Kumar Singh
03901b353b chore: hooked preProcess function in expandTitle and body, added labels and annotations in alertdata 2026-03-12 21:47:43 +05:30
Abhishek Kumar Singh
74441c74a8 feat: added preprocessor for alert templater 2026-03-12 20:54:28 +05:30
Abhishek Kumar Singh
93d332bef2 chore: exposed templates for alertmanager types 2026-03-12 18:52:40 +05:30
Abhishek Kumar Singh
1e730cae8c chore: added utils for using variables with $ notation 2026-03-12 16:34:43 +05:30
Abhishek Kumar Singh
01a09cf6d2 chore: updated test name + code for timeout errors 2026-03-12 10:22:42 +05:30
Abhishek Kumar Singh
403dddab85 feat: alert manager template to template title and notification body 2026-03-11 21:55:09 +05:30
Abhishek Kumar Singh
d07a833574 chore: added tracing to msteamsv2 notifier 2026-03-11 16:05:00 +05:30
Abhishek Kumar Singh
b39bec7245 Merge branch 'main' into chore/am_custom_notifiers 2026-03-10 22:37:24 +05:30
Abhishek Kumar Singh
6ff55c48be chore: fix email linter 2026-03-10 22:19:05 +05:30
Abhishek Kumar Singh
b15fa0f88f chore: lint fixs 2026-03-10 21:57:53 +05:30
Abhishek Kumar Singh
19fe4f860e chore: custom notifiers in alert manager 2026-03-10 13:20:04 +05:30
811 changed files with 15987 additions and 45455 deletions

View File

@@ -1,6 +1,6 @@
services:
init-clickhouse:
image: clickhouse/clickhouse-server:25.12.5
image: clickhouse/clickhouse-server:25.5.6
container_name: init-clickhouse
command:
- bash
@@ -18,7 +18,7 @@ services:
volumes:
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
clickhouse:
image: clickhouse/clickhouse-server:25.12.5
image: clickhouse/clickhouse-server:25.5.6
container_name: clickhouse
volumes:
- ${PWD}/fs/etc/clickhouse-server/config.d/config.xml:/etc/clickhouse-server/config.d/config.xml
@@ -67,7 +67,7 @@ services:
timeout: 5s
retries: 3
telemetrystore-migrator:
image: signoz/signoz-otel-collector:v0.144.6
image: signoz/signoz-otel-collector:v0.142.0
container_name: telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

View File

@@ -39,6 +39,7 @@ jobs:
matrix:
suite:
- alerts
- alertmanager
- basepath
- callbackauthn
- cloudintegrations
@@ -54,25 +55,22 @@ jobs:
- querierscalar
- queriercommon
- rawexportdata
- querierauthz
- role
- rootuser
- serviceaccount
- spanmapper
- querier_json_body
- querier_skip_resource_fingerprint
- ttl
- clickhousecluster
- metricreduction
sqlstore-provider:
- postgres
- sqlite
sqlite-mode:
- wal
clickhouse-version:
- 25.5.6
- 25.12.5
schema-migrator-version:
- v0.144.6
- v0.144.3
postgres-version:
- 15
if: |

View File

@@ -9,11 +9,6 @@ global:
# the path component (e.g. /signoz in https://example.com/signoz) is used
# as the base path for all HTTP routes (both API and web frontend).
external_url: <unset>
# origins (scheme://host[:port], no path) allowed as login redirect targets. include
# the origin the signoz ui is served on. when not configured, redirect targets are
# not validated.
# allowed_origins:
# - https://signoz.example.com
# the url where the SigNoz backend receives telemetry data (traces, metrics, logs) from instrumented applications.
ingestion_url: <unset>
# the url of the SigNoz MCP server. when unset, the MCP settings page is hidden in the frontend.

View File

@@ -1495,7 +1495,6 @@ components:
- cassandradb
- redis
- cloudsql_postgres
- memorystore_redis
type: string
CloudintegrationtypesServiceMetadata:
properties:
@@ -2597,6 +2596,19 @@ components:
repeatVariable:
type: string
type: object
DashboardLink:
properties:
name:
type: string
renderVariables:
type: boolean
targetBlank:
type: boolean
tooltip:
type: string
url:
type: string
type: object
DashboardtypesAxes:
properties:
isLogScale:
@@ -2735,7 +2747,6 @@ components:
datasources:
additionalProperties:
$ref: '#/components/schemas/DashboardtypesDatasourceSpec'
nullable: true
type: object
display:
$ref: '#/components/schemas/DashboardtypesDisplay'
@@ -2747,7 +2758,7 @@ components:
type: array
links:
items:
$ref: '#/components/schemas/DashboardtypesLink'
$ref: '#/components/schemas/DashboardLink'
type: array
panels:
additionalProperties:
@@ -2764,7 +2775,6 @@ components:
- variables
- panels
- layouts
- links
type: object
DashboardtypesDashboardView:
properties:
@@ -2843,22 +2853,14 @@ components:
required:
- name
type: object
DashboardtypesDynamicVariableSignal:
enum:
- traces
- logs
- metrics
- all
type: string
DashboardtypesDynamicVariableSpec:
properties:
name:
type: string
signal:
$ref: '#/components/schemas/DashboardtypesDynamicVariableSignal'
$ref: '#/components/schemas/TelemetrytypesSignal'
required:
- name
- signal
type: object
DashboardtypesFillMode:
enum:
@@ -3027,19 +3029,6 @@ components:
- solid
- dashed
type: string
DashboardtypesLink:
properties:
name:
type: string
renderVariables:
type: boolean
targetBlank:
type: boolean
tooltip:
type: string
url:
type: string
type: object
DashboardtypesListOrder:
enum:
- asc
@@ -3050,7 +3039,6 @@ components:
selectFields:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
nullable: true
type: array
type: object
DashboardtypesListSort:
@@ -3400,7 +3388,7 @@ components:
$ref: '#/components/schemas/DashboardtypesDisplay'
links:
items:
$ref: '#/components/schemas/DashboardtypesLink'
$ref: '#/components/schemas/DashboardLink'
type: array
plugin:
$ref: '#/components/schemas/DashboardtypesPanelPlugin'
@@ -3412,7 +3400,6 @@ components:
- display
- plugin
- queries
- links
type: object
DashboardtypesPatchOp:
enum:
@@ -4307,6 +4294,8 @@ components:
type: object
nodeCountsByReadiness:
$ref: '#/components/schemas/InframonitoringtypesNodeCountsByReadiness'
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
required:
@@ -4316,6 +4305,7 @@ components:
- clusterMemory
- clusterMemoryAllocatable
- nodeCountsByReadiness
- podCountsByPhase
- podCountsByStatus
- counts
- meta
@@ -4525,6 +4515,8 @@ components:
type: object
misscheduledNodes:
type: integer
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
readyNodes:
@@ -4541,6 +4533,7 @@ components:
- currentNodes
- readyNodes
- misscheduledNodes
- podCountsByPhase
- podCountsByStatus
- meta
type: object
@@ -4595,6 +4588,8 @@ components:
type: string
nullable: true
type: object
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
required:
@@ -4607,6 +4602,7 @@ components:
- deploymentMemoryLimit
- desiredPods
- availablePods
- podCountsByPhase
- podCountsByStatus
- meta
type: object
@@ -4738,6 +4734,8 @@ components:
type: string
nullable: true
type: object
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
successfulPods:
@@ -4754,6 +4752,7 @@ components:
- activePods
- failedPods
- successfulPods
- podCountsByPhase
- podCountsByStatus
- meta
type: object
@@ -4863,12 +4862,15 @@ components:
type: number
namespaceName:
type: string
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
required:
- namespaceName
- namespaceCPU
- namespaceMemory
- podCountsByPhase
- podCountsByStatus
- counts
- meta
@@ -4934,12 +4936,15 @@ components:
type: number
nodeName:
type: string
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
required:
- nodeName
- condition
- nodeCountsByReadiness
- podCountsByPhase
- podCountsByStatus
- nodeCPU
- nodeCPUAllocatable
@@ -4967,6 +4972,25 @@ components:
- total
- endTimeBeforeRetention
type: object
InframonitoringtypesPodCountsByPhase:
properties:
failed:
type: integer
pending:
type: integer
running:
type: integer
succeeded:
type: integer
unknown:
type: integer
required:
- pending
- running
- succeeded
- failed
- unknown
type: object
InframonitoringtypesPodCountsByStatus:
properties:
completed:
@@ -5025,6 +5049,15 @@ components:
- shutdown
- unexpectedAdmissionError
type: object
InframonitoringtypesPodPhase:
enum:
- pending
- running
- succeeded
- failed
- unknown
- no_data
type: string
InframonitoringtypesPodRecord:
properties:
meta:
@@ -5044,6 +5077,8 @@ components:
podCPURequest:
format: double
type: number
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
podMemory:
@@ -5055,6 +5090,8 @@ components:
podMemoryRequest:
format: double
type: number
podPhase:
$ref: '#/components/schemas/InframonitoringtypesPodPhase'
podRestarts:
format: int64
type: integer
@@ -5070,6 +5107,8 @@ components:
- podMemory
- podMemoryRequest
- podMemoryLimit
- podPhase
- podCountsByPhase
- podStatus
- podCountsByStatus
- podRestarts
@@ -5420,6 +5459,8 @@ components:
type: string
nullable: true
type: object
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
statefulSetCPU:
@@ -5452,6 +5493,7 @@ components:
- statefulSetMemoryLimit
- desiredPods
- currentPods
- podCountsByPhase
- podCountsByStatus
- meta
type: object
@@ -6291,7 +6333,6 @@ components:
- delta
- cumulative
- unspecified
- ""
type: string
MetrictypesTimeAggregation:
enum:
@@ -6304,7 +6345,6 @@ components:
- count_distinct
- rate
- increase
- ""
type: string
MetrictypesType:
enum:
@@ -6512,7 +6552,6 @@ components:
args:
items:
$ref: '#/components/schemas/Querybuildertypesv5FunctionArg'
nullable: true
type: array
name:
$ref: '#/components/schemas/Querybuildertypesv5FunctionName'
@@ -6683,7 +6722,6 @@ components:
functions:
items:
$ref: '#/components/schemas/Querybuildertypesv5Function'
nullable: true
type: array
having:
$ref: '#/components/schemas/Querybuildertypesv5Having'
@@ -6696,7 +6734,6 @@ components:
order:
items:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
nullable: true
type: array
type: object
Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregation:
@@ -6704,7 +6741,6 @@ components:
aggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5LogAggregation'
nullable: true
type: array
cursor:
type: string
@@ -6715,12 +6751,10 @@ components:
functions:
items:
$ref: '#/components/schemas/Querybuildertypesv5Function'
nullable: true
type: array
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
nullable: true
type: array
having:
$ref: '#/components/schemas/Querybuildertypesv5Having'
@@ -6737,17 +6771,14 @@ components:
order:
items:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
nullable: true
type: array
secondaryAggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5SecondaryAggregation'
nullable: true
type: array
selectFields:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
nullable: true
type: array
signal:
enum:
@@ -6765,7 +6796,6 @@ components:
aggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5MetricAggregation'
nullable: true
type: array
cursor:
type: string
@@ -6776,12 +6806,10 @@ components:
functions:
items:
$ref: '#/components/schemas/Querybuildertypesv5Function'
nullable: true
type: array
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
nullable: true
type: array
having:
$ref: '#/components/schemas/Querybuildertypesv5Having'
@@ -6798,17 +6826,14 @@ components:
order:
items:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
nullable: true
type: array
secondaryAggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5SecondaryAggregation'
nullable: true
type: array
selectFields:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
nullable: true
type: array
signal:
enum:
@@ -6826,7 +6851,6 @@ components:
aggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5TraceAggregation'
nullable: true
type: array
cursor:
type: string
@@ -6837,12 +6861,10 @@ components:
functions:
items:
$ref: '#/components/schemas/Querybuildertypesv5Function'
nullable: true
type: array
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
nullable: true
type: array
having:
$ref: '#/components/schemas/Querybuildertypesv5Having'
@@ -6859,17 +6881,14 @@ components:
order:
items:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
nullable: true
type: array
secondaryAggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5SecondaryAggregation'
nullable: true
type: array
selectFields:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
nullable: true
type: array
signal:
enum:
@@ -6887,7 +6906,6 @@ components:
aggregations:
items:
$ref: '#/components/schemas/Querybuildertypesv5TraceAggregation'
nullable: true
type: array
cursor:
type: string
@@ -6900,12 +6918,10 @@ components:
functions:
items:
$ref: '#/components/schemas/Querybuildertypesv5Function'
nullable: true
type: array
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
nullable: true
type: array
having:
$ref: '#/components/schemas/Querybuildertypesv5Having'
@@ -6920,14 +6936,12 @@ components:
order:
items:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
nullable: true
type: array
returnSpansFrom:
type: string
selectFields:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
nullable: true
type: array
stepInterval:
$ref: '#/components/schemas/Querybuildertypesv5Step'
@@ -7167,7 +7181,6 @@ components:
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
nullable: true
type: array
limit:
type: integer
@@ -7176,7 +7189,6 @@ components:
order:
items:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
nullable: true
type: array
stepInterval:
$ref: '#/components/schemas/Querybuildertypesv5Step'
@@ -7559,7 +7571,6 @@ components:
groupBy:
items:
type: string
nullable: true
type: array
newGroupEvalDelay:
type: string
@@ -7631,7 +7642,6 @@ components:
alertStates:
items:
$ref: '#/components/schemas/RuletypesAlertState'
nullable: true
type: array
enabled:
type: boolean
@@ -8038,19 +8048,6 @@ components:
required:
- items
type: object
SpantypesGettableSpanMapperTest:
properties:
collectorLogs:
items:
type: string
nullable: true
type: array
spans:
items:
$ref: '#/components/schemas/SpantypesSpanMapperTestSpan'
nullable: true
type: array
type: object
SpantypesGettableSpanMappers:
properties:
items:
@@ -8147,39 +8144,6 @@ components:
- name
- condition
type: object
SpantypesPostableSpanMapperTest:
properties:
groups:
items:
$ref: '#/components/schemas/SpantypesPostableSpanMapperTestGroup'
nullable: true
type: array
spans:
items:
$ref: '#/components/schemas/SpantypesSpanMapperTestSpan'
nullable: true
type: array
required:
- spans
- groups
type: object
SpantypesPostableSpanMapperTestGroup:
properties:
condition:
$ref: '#/components/schemas/SpantypesSpanMapperGroupCondition'
enabled:
type: boolean
mappers:
items:
$ref: '#/components/schemas/SpantypesPostableSpanMapper'
nullable: true
type: array
name:
type: string
required:
- name
- condition
type: object
SpantypesPostableTraceAggregations:
properties:
aggregations:
@@ -8341,17 +8305,6 @@ components:
- operation
- priority
type: object
SpantypesSpanMapperTestSpan:
properties:
attributes:
additionalProperties: {}
nullable: true
type: object
resource:
additionalProperties: {}
nullable: true
type: object
type: object
SpantypesUpdatableSpanMapper:
properties:
config:
@@ -8569,7 +8522,6 @@ components:
- resource
- attribute
- body
- ""
type: string
TelemetrytypesFieldDataType:
enum:
@@ -8578,7 +8530,6 @@ components:
- float64
- int64
- number
- ""
type: string
TelemetrytypesGettableFieldKeys:
properties:
@@ -8610,12 +8561,10 @@ components:
- traces
- logs
- metrics
- ""
type: string
TelemetrytypesSource:
enum:
- meter
- ""
type: string
TelemetrytypesTelemetryFieldKey:
properties:
@@ -14085,69 +14034,6 @@ paths:
summary: Update a span mapper
tags:
- spanmapper
/api/v1/span_mapper_groups/test:
post:
deprecated: false
description: Tests how span mappers would transform sample spans
operationId: TestSpanMappers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SpantypesPostableSpanMapperTest'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/SpantypesGettableSpanMapperTest'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Test span mappers against sample spans
tags:
- spanmapper
/api/v1/stats:
get:
deprecated: false
@@ -16105,19 +15991,17 @@ paths:
metrics derived by summing per-node values within the group: CPU usage, CPU
allocatable, memory working set, memory allocatable. Each row also reports
per-group nodeCountsByReadiness ({ ready, notReady } from each node''s latest
k8s.node.condition_ready value) and per-group podCountsByStatus ({ pending,
running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull,
createContainerConfigError, containerCreating, oomKilled, completed, error,
containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError
} reflecting each pod''s latest kubectl-style display status). Each cluster
includes metadata attributes (k8s.cluster.name). The response type is ''list''
for the default k8s.cluster.name grouping or ''grouped_list'' for custom groupBy
keys; in both modes every row aggregates nodes and pods in the group. Supports
filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable
/ memory / memory_allocatable, and pagination via offset/limit. Also reports
whether the requested time range falls before the data retention boundary.
Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable)
return -1 as a sentinel when no data is available for that field.'
k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending,
running, succeeded, failed, unknown } from each pod''s latest k8s.pod.phase
value). Each cluster includes metadata attributes (k8s.cluster.name). The
response type is ''list'' for the default k8s.cluster.name grouping or ''grouped_list''
for custom groupBy keys; in both modes every row aggregates nodes and pods
in the group. Supports filtering via a filter expression, custom groupBy,
ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination
via offset/limit. Also reports whether the requested time range falls before
the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable,
clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data
is available for that field.'
operationId: ListClusters
requestBody:
content:
@@ -16185,16 +16069,13 @@ paths:
the number of nodes running at least one ready daemon pod) and misscheduledNodes
(k8s.daemonset.misscheduled_nodes, the number of nodes running the daemon
pod but not supposed to) — note these are node counts, not pod counts. It
also reports per-group podCountsByStatus ({ pending, running, failed, unknown,
crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError,
containerCreating, oomKilled, completed, error, containerCannotRun, evicted,
nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each
pod''s latest kubectl-style display status). Each daemonset includes metadata
attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The
response type is ''list'' for the default k8s.daemonset.name grouping or ''grouped_list''
for custom groupBy keys; in both modes every row aggregates pods owned by
daemonsets in the group. Supports filtering via a filter expression, custom
groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request
also reports per-group podCountsByPhase ({ pending, running, succeeded, failed,
unknown } from each pod''s latest k8s.pod.phase value). Each daemonset includes
metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name).
The response type is ''list'' for the default k8s.daemonset.name grouping
or ''grouped_list'' for custom groupBy keys; in both modes every row aggregates
pods owned by daemonsets in the group. Supports filtering via a filter expression,
custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request
/ memory_limit / desired_nodes / current_nodes / ready_nodes / misscheduled_nodes,
and pagination via offset/limit. Also reports whether the requested time range
falls before the data retention boundary. Numeric metric fields (daemonSetCPU,
@@ -16262,19 +16143,17 @@ paths:
the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest,
deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each
row also reports the latest known desiredPods (k8s.deployment.desired) and
availablePods (k8s.deployment.available) replica counts and per-group podCountsByStatus
({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff,
errImagePull, createContainerConfigError, containerCreating, oomKilled, completed,
error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError
} reflecting each pod''s latest kubectl-style display status). Each deployment
includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name).
The response type is ''list'' for the default k8s.deployment.name grouping
or ''grouped_list'' for custom groupBy keys; in both modes every row aggregates
pods owned by deployments in the group. Supports filtering via a filter expression,
custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request
/ memory_limit / desired_pods / available_pods, and pagination via offset/limit.
Also reports whether the requested time range falls before the data retention
boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit,
availablePods (k8s.deployment.available) replica counts and per-group podCountsByPhase
({ pending, running, succeeded, failed, unknown } from each pod''s latest
k8s.pod.phase value). Each deployment includes metadata attributes (k8s.deployment.name,
k8s.namespace.name, k8s.cluster.name). The response type is ''list'' for the
default k8s.deployment.name grouping or ''grouped_list'' for custom groupBy
keys; in both modes every row aggregates pods owned by deployments in the
group. Supports filtering via a filter expression, custom groupBy, ordering
by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit
/ desired_pods / available_pods, and pagination via offset/limit. Also reports
whether the requested time range falls before the data retention boundary.
Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit,
deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods,
availablePods) return -1 as a sentinel when no data is available for that
field.'
@@ -16409,12 +16288,9 @@ paths:
(k8s.job.desired_successful_pods, the target completion count), activePods
(k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across
the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative).
It also reports per-group podCountsByStatus ({ pending, running, failed, unknown,
crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError,
containerCreating, oomKilled, completed, error, containerCannotRun, evicted,
nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each
pod''s latest kubectl-style display status); note podCountsByStatus.failed
(current pod status) is distinct from failedPods (cumulative job kube-state-metric).
It also reports per-group podCountsByPhase ({ pending, running, succeeded,
failed, unknown } from each pod''s latest k8s.pod.phase value); note podCountsByPhase.failed
(current pod-phase) is distinct from failedPods (cumulative job kube-state-metric).
Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name).
The response type is ''list'' for the default k8s.job.name grouping or ''grouped_list''
for custom groupBy keys; in both modes every row aggregates pods owned by
@@ -16564,18 +16440,16 @@ paths:
deprecated: false
description: 'Returns a paginated list of Kubernetes namespaces with key aggregated
pod metrics: CPU usage and memory working set (summed across pods in the group),
plus per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff,
imagePullBackOff, errImagePull, createContainerConfigError, containerCreating,
oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost,
shutdown, unexpectedAdmissionError } reflecting each pod''s latest kubectl-style
display status in the window). Each namespace includes metadata attributes
(k8s.namespace.name, k8s.cluster.name). The response type is ''list'' for
the default k8s.namespace.name grouping or ''grouped_list'' for custom groupBy
keys; in both modes every row aggregates pods in the group. Supports filtering
via a filter expression, custom groupBy, ordering by cpu / memory, and pagination
via offset/limit. Also reports whether the requested time range falls before
the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory)
return -1 as a sentinel when no data is available for that field.'
plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown
} from each pod''s latest k8s.pod.phase value in the window). Each namespace
includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response
type is ''list'' for the default k8s.namespace.name grouping or ''grouped_list''
for custom groupBy keys; in both modes every row aggregates pods in the group.
Supports filtering via a filter expression, custom groupBy, ordering by cpu
/ memory, and pagination via offset/limit. Also reports whether the requested
time range falls before the data retention boundary. Numeric metric fields
(namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available
for that field.'
operationId: ListNamespaces
requestBody:
content:
@@ -16635,21 +16509,18 @@ paths:
description: 'Returns a paginated list of Kubernetes nodes with key metrics:
CPU usage, CPU allocatable, memory working set, memory allocatable, per-group
nodeCountsByReadiness ({ ready, notReady } from each node''s latest k8s.node.condition_ready
in the window) and per-group podCountsByStatus ({ pending, running, failed,
unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError,
containerCreating, oomKilled, completed, error, containerCannotRun, evicted,
nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } for pods scheduled
on the listed nodes, reflecting each pod''s latest kubectl-style display status).
Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The
response type is ''list'' for the default k8s.node.name grouping (each row
is one node with its current condition string: ready / not_ready / no_data)
or ''grouped_list'' for custom groupBy keys (each row aggregates nodes in
the group; condition stays no_data). Supports filtering via a filter expression,
custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable,
and pagination via offset/limit. Also reports whether the requested time range
falls before the data retention boundary. Numeric metric fields (nodeCPU,
nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel
when no data is available for that field.'
in the window) and per-group podCountsByPhase ({ pending, running, succeeded,
failed, unknown } for pods scheduled on the listed nodes). Each node includes
metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is
''list'' for the default k8s.node.name grouping (each row is one node with
its current condition string: ready / not_ready / no_data) or ''grouped_list''
for custom groupBy keys (each row aggregates nodes in the group; condition
stays no_data). Supports filtering via a filter expression, custom groupBy,
ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination
via offset/limit. Also reports whether the requested time range falls before
the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable,
nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is
available for that field.'
operationId: ListNodes
requestBody:
content:
@@ -16708,23 +16579,20 @@ paths:
deprecated: false
description: 'Returns a paginated list of Kubernetes pods with key metrics:
CPU usage, CPU request/limit utilization, memory working set, memory request/limit
utilization, current pod status (kubectl-style display status such as Running/Pending/CrashLoopBackOff,
or no_data), and pod age (ms since start time). Each pod includes metadata
attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob,
utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data),
and pod age (ms since start time). Each pod includes metadata attributes (namespace,
node, workload owner such as deployment/statefulset/daemonset/job/cronjob,
cluster). Supports filtering via a filter expression, custom groupBy to aggregate
pods by any attribute, ordering by any of the six metrics (cpu, cpu_request,
cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit.
The response type is ''list'' for the default k8s.pod.uid grouping (each row
is one pod with its current status) or ''grouped_list'' for custom groupBy
keys (each row aggregates pods in the group with per-status counts under podCountsByStatus:
{ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull,
createContainerConfigError, containerCreating, oomKilled, completed, error,
containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError
} derived from each pod''s latest kubectl-style display status in the window).
Also reports whether the requested time range falls before the data retention
boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory,
podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no
data is available for that field.'
is one pod with its current phase) or ''grouped_list'' for custom groupBy
keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase:
{ pending, running, succeeded, failed, unknown } derived from each pod''s
latest phase in the window). Also reports whether the requested time range
falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest,
podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1
as a sentinel when no data is available for that field.'
operationId: ListPods
requestBody:
content:
@@ -16857,19 +16725,16 @@ paths:
statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each
row also reports the latest known desiredPods (k8s.statefulset.desired_pods)
and currentPods (k8s.statefulset.current_pods) replica counts and per-group
podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff,
imagePullBackOff, errImagePull, createContainerConfigError, containerCreating,
oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost,
shutdown, unexpectedAdmissionError } reflecting each pod''s latest kubectl-style
display status). Each statefulset includes metadata attributes (k8s.statefulset.name,
k8s.namespace.name, k8s.cluster.name). The response type is ''list'' for the
default k8s.statefulset.name grouping or ''grouped_list'' for custom groupBy
keys; in both modes every row aggregates pods owned by statefulsets in the
group. Supports filtering via a filter expression, custom groupBy, ordering
by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit
/ desired_pods / current_pods, and pagination via offset/limit. Also reports
whether the requested time range falls before the data retention boundary.
Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit,
podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each
pod''s latest k8s.pod.phase value). Each statefulset includes metadata attributes
(k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response
type is ''list'' for the default k8s.statefulset.name grouping or ''grouped_list''
for custom groupBy keys; in both modes every row aggregates pods owned by
statefulsets in the group. Supports filtering via a filter expression, custom
groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request
/ memory_limit / desired_pods / current_pods, and pagination via offset/limit.
Also reports whether the requested time range falls before the data retention
boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit,
statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods,
currentPods) return -1 as a sentinel when no data is available for that field.'
operationId: ListStatefulSets
@@ -24599,17 +24464,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- logs:read
- traces:read
- metrics:read
- audit-logs:read
- meter-metrics:read
- VIEWER
- tokenizer:
- logs:read
- traces:read
- metrics:read
- audit-logs:read
- meter-metrics:read
- VIEWER
summary: Query range
tags:
- querier
@@ -24676,17 +24533,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- logs:read
- traces:read
- metrics:read
- audit-logs:read
- meter-metrics:read
- VIEWER
- tokenizer:
- logs:read
- traces:read
- metrics:read
- audit-logs:read
- meter-metrics:read
- VIEWER
summary: Query range preview
tags:
- querier

View File

@@ -223,12 +223,17 @@ func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updated
return err
}
desiredTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, updatedRole.TransactionGroups)
existingGroups := authtypes.MustNewTransactionGroupsFromTuples(existingTuples)
additions, deletions := existingGroups.Diff(updatedRole.TransactionGroups)
additionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, additions)
if err != nil {
return err
}
additionTuples, deletionTuples := authtypes.DiffTuples(existingTuples, desiredTuples)
deletionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, deletions)
if err != nil {
return err
}
err = provider.Write(ctx, additionTuples, deletionTuples)
if err != nil {

View File

@@ -240,17 +240,17 @@ func TestManager_TestNotification_SendUnmatched_PromRule(t *testing.T) {
mock := mockStore.Mock()
// Mock the fingerprint query (for Prometheus label matching)
// args: $1=metric_name (the __name__ matcher maps onto the column)
mock.ExpectQuery("SELECT fingerprint, any").
WithArgs("test_metric").
WithArgs("test_metric", "__name__", "test_metric").
WillReturnRows(fingerprintRows)
// Mock the samples query (for Prometheus metric data)
// args: metric_name IN (discovered names), subquery metric_name, start, end
mock.ExpectQuery("SELECT metric_name, fingerprint, unix_milli").
WithArgs(
"test_metric",
"test_metric",
"__name__",
"test_metric",
queryStart,
queryEnd,
).

View File

@@ -64,7 +64,7 @@
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"antlr4": "4.13.2",
"axios": "1.18.0",
"axios": "1.16.0",
"babel-jest": "^29.6.4",
"chart.js": "3.9.1",
"chartjs-adapter-date-fns": "^2.0.0",
@@ -74,7 +74,7 @@
"crypto-js": "4.2.0",
"d3-hierarchy": "3.1.2",
"dayjs": "^1.10.7",
"dompurify": "3.4.12",
"dompurify": "3.4.11",
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
"history": "4.10.1",
@@ -89,7 +89,7 @@
"lodash-es": "^4.17.21",
"motion": "12.4.13",
"nuqs": "2.8.8",
"overlayscrollbars": "^2.16.0",
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.298.0",
@@ -199,9 +199,9 @@
"react-resizable": "3.0.4",
"redux-mock-store": "1.5.4",
"sass": "1.97.3",
"sharp": "0.35.0",
"sharp": "0.34.5",
"stylelint": "17.7.0",
"svgo": "4.0.2",
"svgo": "4.0.1",
"ts-jest": "29.4.9",
"typescript-plugin-css-modules": "5.2.0",
"use-sync-external-store": "1.6.0",
@@ -221,18 +221,5 @@
"*.(scss|css)": [
"stylelint"
]
},
"overrides": {
"@babel/core@<=7.29.0": ">=7.29.6 <8",
"@istanbuljs/load-nyc-config>js-yaml": ">=4.2.0 <5",
"cookie@<0.7.0": ">=0.7.1 <1",
"dompurify@<=3.4.10": ">=3.4.11 <4",
"esbuild@>=0.27.3 <0.28.1": ">=0.28.1 <0.29.0",
"js-cookie@<=3.0.5": ">=3.0.7 <4",
"js-yaml@>=4.0.0 <=4.1.1": ">=4.2.0 <5",
"prismjs@<1.30.0": ">=1.30.0 <2",
"react-router@>=6.7.0 <6.30.4": ">=6.30.4 <7",
"tmp@<0.2.6": ">=0.2.6 <0.3.0",
"yaml@>=1.0.0 <1.10.3": ">=1.10.3 <2"
}
}
}

461
frontend/pnpm-lock.yaml generated
View File

@@ -112,8 +112,8 @@ importers:
specifier: 4.13.2
version: 4.13.2
axios:
specifier: 1.18.0
version: 1.18.0
specifier: 1.16.0
version: 1.16.0
babel-jest:
specifier: ^29.6.4
version: 29.6.4(@babel/core@7.29.7)
@@ -142,8 +142,8 @@ importers:
specifier: ^1.10.7
version: 1.11.20
dompurify:
specifier: 3.4.12
version: 3.4.12
specifier: 3.4.11
version: 3.4.11
event-source-polyfill:
specifier: 1.0.31
version: 1.0.31
@@ -187,11 +187,11 @@ importers:
specifier: 2.8.8
version: 2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)
overlayscrollbars:
specifier: ^2.16.0
version: 2.16.0
specifier: ^2.8.1
version: 2.9.2
overlayscrollbars-react:
specifier: ^0.5.6
version: 0.5.6(overlayscrollbars@2.16.0)(react@18.2.0)
version: 0.5.6(overlayscrollbars@2.9.2)(react@18.2.0)
papaparse:
specifier: 5.4.1
version: 5.4.1
@@ -476,14 +476,14 @@ importers:
specifier: 1.97.3
version: 1.97.3
sharp:
specifier: 0.35.0
version: 0.35.0
specifier: 0.34.5
version: 0.34.5
stylelint:
specifier: 17.7.0
version: 17.7.0(typescript@5.9.3)
svgo:
specifier: 4.0.2
version: 4.0.2
specifier: 4.0.1
version: 4.0.1
ts-jest:
specifier: 29.4.9
version: 29.4.9(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.2.0)(babel-jest@29.6.4(@babel/core@7.29.7))(esbuild@0.28.1)(jest-util@30.4.1)(jest@30.2.0(@types/node@16.18.25)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.25)(typescript@5.9.3)))(typescript@5.9.3)
@@ -501,7 +501,7 @@ importers:
version: 0.5.1(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))
vite-plugin-image-optimizer:
specifier: 2.0.3
version: 2.0.3(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(sharp@0.35.0)(svgo@4.0.2)
version: 2.0.3(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(sharp@0.34.5)(svgo@4.0.1)
vite-tsconfig-paths:
specifier: 6.1.1
version: 6.1.1(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(typescript@5.9.3)
@@ -1460,9 +1460,6 @@ packages:
'@emnapi/core@1.8.1':
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
'@emnapi/runtime@1.11.2':
resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==}
'@emnapi/runtime@1.8.1':
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
@@ -1717,165 +1714,156 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
'@img/colour@1.0.0':
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.35.0':
resolution: {integrity: sha512-ZgaYEwaj+lx/5n4W8GmZ2IYz0PQHjN5eqRcfijWGB+2Aq7ZInZGa0qJyAn6DEtyLuWHRSrmWOqT9q3qqTBvmUQ==}
engines: {node: '>=20.9.0'}
'@img/sharp-darwin-arm64@0.34.5':
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.35.0':
resolution: {integrity: sha512-c1z9LFpKB0slQW3RchwBE8iSVzGp70TNjUUO9k4BZwwW4HH7JBGHeIy4b+kk4n/kcBASb9evKCE3/7Slmslgiw==}
engines: {node: '>=20.9.0'}
'@img/sharp-darwin-x64@0.34.5':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-freebsd-wasm32@0.35.0':
resolution: {integrity: sha512-Li2KTev0H90kEtnJHkI9xQojXt1AqWmFBMXiPw5kqd1jQgP7gi5HVK/qC5Rmh/59NuAwUuPzzPITmX22NomYYQ==}
engines: {node: '>=20.9.0'}
os: [freebsd]
'@img/sharp-libvips-darwin-arm64@1.3.0':
resolution: {integrity: sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==}
'@img/sharp-libvips-darwin-arm64@1.2.4':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.3.0':
resolution: {integrity: sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==}
'@img/sharp-libvips-darwin-x64@1.2.4':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.3.0':
resolution: {integrity: sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==}
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.3.0':
resolution: {integrity: sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==}
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.3.0':
resolution: {integrity: sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==}
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.3.0':
resolution: {integrity: sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==}
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.3.0':
resolution: {integrity: sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==}
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.3.0':
resolution: {integrity: sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==}
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.3.0':
resolution: {integrity: sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==}
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.3.0':
resolution: {integrity: sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==}
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.35.0':
resolution: {integrity: sha512-4+4XHLNT5wDT0roYlHTEmH9lDKt0acf9Tv+3hM3iceOirkxrR404/3WjAYZ9F9CkHrxeRcGLJXbi4vluMZ9O+A==}
engines: {node: '>=20.9.0'}
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.35.0':
resolution: {integrity: sha512-VVlpEWwizEFIOom0zdoeKuO5nuTswzVE5uHcBNvHzmeHUpNFajY3HFfbQ+zIH4E2kVaZ/yVxmsShW56TtEy4uA==}
engines: {node: '>=20.9.0'}
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.35.0':
resolution: {integrity: sha512-N3hzbEpUTJC8pWpPVJvgzGxM+so/MAXc8O2s/53B0LL9ZGpfXpME7Wizkc5d/8fRBlBtkDjzoZGDCqqNDHqLEw==}
engines: {node: '>=20.9.0'}
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.35.0':
resolution: {integrity: sha512-l6vmKVPnbS0RhVMbyxP5meAARsbhCnBN4fy31qz0+3a6Rv4jEqfzDrT89y6ZPkCi0AJGnwp2En528yXo401Hpw==}
engines: {node: '>=20.9.0'}
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.35.0':
resolution: {integrity: sha512-MYlMiPFiv/EKPAHnp3yNZ9AAWFsxga9c5Bkc6wkar6bqzHLlkGVJHRm0u1ei+VXnZxp3Mz9MG9ZIsI8vSOf3sQ==}
engines: {node: '>=20.9.0'}
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.35.0':
resolution: {integrity: sha512-TYaItB5oj1ioXjhyn2xrR208vf+YuIIcHptQWRRaBmFhvIvL9D72DXN8w75xup0KXA8UdEAhQ9Qb2S49FD/9Cw==}
engines: {node: '>=20.9.0'}
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.35.0':
resolution: {integrity: sha512-DSTb6ijQzqe6DdAaOBVqJ/SYf1vO8EW5bK6X6LRXufEBebf2722VCdvBUtZ3rtV0x2ApfPNDy/p7LrrjaWjiyQ==}
engines: {node: '>=20.9.0'}
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.35.0':
resolution: {integrity: sha512-K7ykQ+26Rt6+4BTU80AuGgTPIYX86UxiAKT4rcXX/WNTo7k1ZxpKz+TguHnwVpCqQK3B5PK0vZ0ZBe6nz/ib1w==}
engines: {node: '>=20.9.0'}
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.35.0':
resolution: {integrity: sha512-9woLIFORERCr+6cWu87dQ22J34EExkhc73U1kZW0c+RclQqWetoodByp4dWZ/hN8/KVmTRAx2HOnUwib8AwZdA==}
engines: {node: '>=20.9.0'}
'@img/sharp-webcontainers-wasm32@0.35.0':
resolution: {integrity: sha512-t+kie1TOyaDM6Dho+f+y0VqIUNhYQaKCUahuZVi0E0frgdiaOaPsDxDW3wfKacUdaNBCnK/ZDBMg33ydvHj8uA==}
engines: {node: '>=20.9.0'}
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.35.0':
resolution: {integrity: sha512-M5eKxug0dabbaWgFKvPa3odNs2OpaP+81NASfGKkt4GcYXpNhSu7CaeYxWkLNV6vHmUp4hnCxnxrUyhUJhXbKA==}
engines: {node: '>=20.9.0'}
'@img/sharp-win32-arm64@0.34.5':
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.35.0':
resolution: {integrity: sha512-z0+pZ03QCDvdVN0Ez9IX/yjWC19ikMlXrmdYMwYNLTh2BLPx3hXWPvyqWfquZ0BTO9O6GVOjIVoTcyyacMnWlQ==}
engines: {node: ^20.9.0}
'@img/sharp-win32-ia32@0.34.5':
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.35.0':
resolution: {integrity: sha512-feNnlz5ZHKr0MY1LPHvZQyJeBkbo4ctsn0D8FvA53VTw5TC63rfEL2UrWbkSBR19htSE7Mw78xYVwdJqoMWVHw==}
engines: {node: '>=20.9.0'}
'@img/sharp-win32-x64@0.34.5':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
@@ -4087,8 +4075,8 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
axios@1.18.0:
resolution: {integrity: sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==}
axios@1.16.0:
resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==}
babel-jest@29.6.4:
resolution: {integrity: sha512-meLj23UlSLddj6PC+YTOFRgDAtjnZom8w/ACsrx0gtPtv5cJZk0A5Unk5bV4wixD7XaPCN1fQvpww8czkZURmw==}
@@ -4893,8 +4881,8 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.4.12:
resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==}
dompurify@3.4.11:
resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==}
domutils@2.8.0:
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
@@ -5460,6 +5448,10 @@ packages:
resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==}
engines: {node: '>=20'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hasown@2.0.4:
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
engines: {node: '>= 0.4'}
@@ -6949,8 +6941,8 @@ packages:
overlayscrollbars: ^2.0.0
react: '>=16.8.0'
overlayscrollbars@2.16.0:
resolution: {integrity: sha512-N03oje/q7j93D0aLZtoCdsDSYLmhheSsv8H7oSLE7HhdV9P/bmCURtLV/KbPye7P/bpfyt/obSfDpGUYoJ0OWg==}
overlayscrollbars@2.9.2:
resolution: {integrity: sha512-iDT84r39i7oWP72diZN2mbJUsn/taCq568aQaIrc84S87PunBT7qtsVltAF2esk7ORTRjQDnfjVYoqqTzgs8QA==}
oxfmt@0.54.0:
resolution: {integrity: sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==}
@@ -8070,6 +8062,10 @@ packages:
sax@1.3.0:
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
sax@1.4.4:
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
engines: {node: '>=11.0.0'}
sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
engines: {node: '>=11.0.0'}
@@ -8126,9 +8122,9 @@ packages:
shallowequal@1.1.0:
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
sharp@0.35.0:
resolution: {integrity: sha512-BqvG5XbwPZ4NV0DK90d86leEECMsoa8bO0nqnKWlBDYxri4GJ7c4EDInaF6q20lTh/mATmnDIKWJFfXnoVfH5g==}
engines: {node: '>=20.9.0'}
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
@@ -8380,8 +8376,8 @@ packages:
svg-tags@1.0.0:
resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
svgo@4.0.2:
resolution: {integrity: sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==}
svgo@4.0.1:
resolution: {integrity: sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==}
engines: {node: '>=16'}
hasBin: true
@@ -10285,11 +10281,6 @@ snapshots:
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.11.2':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.8.1':
dependencies:
tslib: 2.8.1
@@ -10453,7 +10444,7 @@ snapshots:
'@types/string-hash': 1.1.3
d3-interpolate: 3.0.1
date-fns: 4.1.0
dompurify: 3.4.12
dompurify: 3.4.11
eventemitter3: 5.0.1
fast_array_intersect: 1.1.0
history: 4.10.1
@@ -10494,110 +10485,100 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
'@img/colour@1.1.0': {}
'@img/colour@1.0.0': {}
'@img/sharp-darwin-arm64@0.35.0':
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.3.0
'@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
'@img/sharp-darwin-x64@0.35.0':
'@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.3.0
'@img/sharp-libvips-darwin-x64': 1.2.4
optional: true
'@img/sharp-freebsd-wasm32@0.35.0':
'@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm@1.2.4':
optional: true
'@img/sharp-libvips-linux-ppc64@1.2.4':
optional: true
'@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true
'@img/sharp-libvips-linux-s390x@1.2.4':
optional: true
'@img/sharp-libvips-linux-x64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true
'@img/sharp-linux-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
'@img/sharp-linux-arm@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.4
optional: true
'@img/sharp-linux-ppc64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.2.4
optional: true
'@img/sharp-linux-riscv64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true
'@img/sharp-linux-s390x@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.2.4
optional: true
'@img/sharp-linux-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.4
optional: true
'@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true
'@img/sharp-linuxmusl-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true
'@img/sharp-wasm32@0.34.5':
dependencies:
'@img/sharp-wasm32': 0.35.0
'@emnapi/runtime': 1.8.1
optional: true
'@img/sharp-libvips-darwin-arm64@1.3.0':
'@img/sharp-win32-arm64@0.34.5':
optional: true
'@img/sharp-libvips-darwin-x64@1.3.0':
'@img/sharp-win32-ia32@0.34.5':
optional: true
'@img/sharp-libvips-linux-arm64@1.3.0':
optional: true
'@img/sharp-libvips-linux-arm@1.3.0':
optional: true
'@img/sharp-libvips-linux-ppc64@1.3.0':
optional: true
'@img/sharp-libvips-linux-riscv64@1.3.0':
optional: true
'@img/sharp-libvips-linux-s390x@1.3.0':
optional: true
'@img/sharp-libvips-linux-x64@1.3.0':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.3.0':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.3.0':
optional: true
'@img/sharp-linux-arm64@0.35.0':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.3.0
optional: true
'@img/sharp-linux-arm@0.35.0':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.3.0
optional: true
'@img/sharp-linux-ppc64@0.35.0':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.3.0
optional: true
'@img/sharp-linux-riscv64@0.35.0':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.3.0
optional: true
'@img/sharp-linux-s390x@0.35.0':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.3.0
optional: true
'@img/sharp-linux-x64@0.35.0':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.3.0
optional: true
'@img/sharp-linuxmusl-arm64@0.35.0':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.3.0
optional: true
'@img/sharp-linuxmusl-x64@0.35.0':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.3.0
optional: true
'@img/sharp-wasm32@0.35.0':
dependencies:
'@emnapi/runtime': 1.11.2
optional: true
'@img/sharp-webcontainers-wasm32@0.35.0':
dependencies:
'@img/sharp-wasm32': 0.35.0
optional: true
'@img/sharp-win32-arm64@0.35.0':
optional: true
'@img/sharp-win32-ia32@0.35.0':
optional: true
'@img/sharp-win32-x64@0.35.0':
'@img/sharp-win32-x64@0.34.5':
optional: true
'@isaacs/cliui@8.0.2':
@@ -13002,15 +12983,13 @@ snapshots:
dependencies:
possible-typed-array-names: 1.1.0
axios@1.18.0:
axios@1.16.0:
dependencies:
follow-redirects: 1.16.0
form-data: 4.0.6
https-proxy-agent: 5.0.1
proxy-from-env: 2.1.0
transitivePeerDependencies:
- debug
- supports-color
babel-jest@29.6.4(@babel/core@7.29.7):
dependencies:
@@ -13864,7 +13843,7 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.4.12:
dompurify@3.4.11:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -14519,6 +14498,10 @@ snapshots:
dependencies:
hookified: 1.15.1
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
hasown@2.0.4:
dependencies:
function-bind: 1.1.2
@@ -14755,7 +14738,7 @@ snapshots:
internal-slot@1.1.0:
dependencies:
es-errors: 1.3.0
hasown: 2.0.4
hasown: 2.0.2
side-channel: 1.1.0
internmap@2.0.3: {}
@@ -14809,7 +14792,7 @@ snapshots:
is-core-module@2.16.1:
dependencies:
hasown: 2.0.4
hasown: 2.0.2
is-date-object@1.1.0:
dependencies:
@@ -14878,7 +14861,7 @@ snapshots:
call-bound: 1.0.4
gopd: 1.2.0
has-tostringtag: 1.0.2
hasown: 2.0.4
hasown: 2.0.2
is-set@2.0.3: {}
@@ -16194,7 +16177,7 @@ snapshots:
monaco-editor@0.55.1:
dependencies:
dompurify: 3.4.12
dompurify: 3.4.11
marked: 14.0.0
motion-dom@11.18.1:
@@ -16289,7 +16272,7 @@ snapshots:
dependencies:
debug: 3.2.7
iconv-lite: 0.6.3
sax: 1.6.0
sax: 1.4.4
transitivePeerDependencies:
- supports-color
optional: true
@@ -16473,12 +16456,12 @@ snapshots:
outvariant@1.4.0: {}
overlayscrollbars-react@0.5.6(overlayscrollbars@2.16.0)(react@18.2.0):
overlayscrollbars-react@0.5.6(overlayscrollbars@2.9.2)(react@18.2.0):
dependencies:
overlayscrollbars: 2.16.0
overlayscrollbars: 2.9.2
react: 18.2.0
overlayscrollbars@2.16.0: {}
overlayscrollbars@2.9.2: {}
oxfmt@0.54.0:
dependencies:
@@ -17758,6 +17741,9 @@ snapshots:
sax@1.3.0:
optional: true
sax@1.4.4:
optional: true
sax@1.6.0: {}
saxes@6.0.0:
@@ -17811,37 +17797,36 @@ snapshots:
shallowequal@1.1.0: {}
sharp@0.35.0:
sharp@0.34.5:
dependencies:
'@img/colour': 1.1.0
'@img/colour': 1.0.0
detect-libc: 2.1.2
semver: 7.8.5
optionalDependencies:
'@img/sharp-darwin-arm64': 0.35.0
'@img/sharp-darwin-x64': 0.35.0
'@img/sharp-freebsd-wasm32': 0.35.0
'@img/sharp-libvips-darwin-arm64': 1.3.0
'@img/sharp-libvips-darwin-x64': 1.3.0
'@img/sharp-libvips-linux-arm': 1.3.0
'@img/sharp-libvips-linux-arm64': 1.3.0
'@img/sharp-libvips-linux-ppc64': 1.3.0
'@img/sharp-libvips-linux-riscv64': 1.3.0
'@img/sharp-libvips-linux-s390x': 1.3.0
'@img/sharp-libvips-linux-x64': 1.3.0
'@img/sharp-libvips-linuxmusl-arm64': 1.3.0
'@img/sharp-libvips-linuxmusl-x64': 1.3.0
'@img/sharp-linux-arm': 0.35.0
'@img/sharp-linux-arm64': 0.35.0
'@img/sharp-linux-ppc64': 0.35.0
'@img/sharp-linux-riscv64': 0.35.0
'@img/sharp-linux-s390x': 0.35.0
'@img/sharp-linux-x64': 0.35.0
'@img/sharp-linuxmusl-arm64': 0.35.0
'@img/sharp-linuxmusl-x64': 0.35.0
'@img/sharp-webcontainers-wasm32': 0.35.0
'@img/sharp-win32-arm64': 0.35.0
'@img/sharp-win32-ia32': 0.35.0
'@img/sharp-win32-x64': 0.35.0
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
'@img/sharp-libvips-darwin-arm64': 1.2.4
'@img/sharp-libvips-darwin-x64': 1.2.4
'@img/sharp-libvips-linux-arm': 1.2.4
'@img/sharp-libvips-linux-arm64': 1.2.4
'@img/sharp-libvips-linux-ppc64': 1.2.4
'@img/sharp-libvips-linux-riscv64': 1.2.4
'@img/sharp-libvips-linux-s390x': 1.2.4
'@img/sharp-libvips-linux-x64': 1.2.4
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
'@img/sharp-linux-arm': 0.34.5
'@img/sharp-linux-arm64': 0.34.5
'@img/sharp-linux-ppc64': 0.34.5
'@img/sharp-linux-riscv64': 0.34.5
'@img/sharp-linux-s390x': 0.34.5
'@img/sharp-linux-x64': 0.34.5
'@img/sharp-linuxmusl-arm64': 0.34.5
'@img/sharp-linuxmusl-x64': 0.34.5
'@img/sharp-wasm32': 0.34.5
'@img/sharp-win32-arm64': 0.34.5
'@img/sharp-win32-ia32': 0.34.5
'@img/sharp-win32-x64': 0.34.5
shebang-command@2.0.0:
dependencies:
@@ -18139,7 +18124,7 @@ snapshots:
svg-tags@1.0.0: {}
svgo@4.0.2:
svgo@4.0.1:
dependencies:
commander: 11.1.0
css-select: 5.2.2
@@ -18606,14 +18591,14 @@ snapshots:
pathe: 0.2.0
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
vite-plugin-image-optimizer@2.0.3(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(sharp@0.35.0)(svgo@4.0.2):
vite-plugin-image-optimizer@2.0.3(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(sharp@0.34.5)(svgo@4.0.1):
dependencies:
ansi-colors: 4.1.3
pathe: 2.0.3
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
optionalDependencies:
sharp: 0.35.0
svgo: 4.0.2
sharp: 0.34.5
svgo: 4.0.1
vite-tsconfig-paths@6.1.1(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(typescript@5.9.3):
dependencies:

View File

@@ -62,7 +62,6 @@
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"AI_OBSERVABILITY_OVERVIEW": "SigNoz | AI Observability Overview",
"AI_OBSERVABILITY_CONFIGURATION": "SigNoz | AI Observability Configuration",
"AI_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | AI Observability Attribute Mapping"
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration"
}

View File

@@ -87,7 +87,6 @@
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"AI_OBSERVABILITY_OVERVIEW": "SigNoz | AI Observability Overview",
"AI_OBSERVABILITY_CONFIGURATION": "SigNoz | AI Observability Configuration",
"AI_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | AI Observability Attribute Mapping"
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration"
}

View File

@@ -134,8 +134,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
}
if (
(pathname.startsWith(`${ROUTES.AI_OBSERVABILITY_BASE}/`) ||
pathname === ROUTES.AI_OBSERVABILITY_BASE) &&
(pathname.startsWith(`${ROUTES.LLM_OBSERVABILITY_BASE}/`) ||
pathname === ROUTES.LLM_OBSERVABILITY_BASE) &&
!isAIObservabilityEnabled
) {
return <Redirect to={ROUTES.HOME} />;

View File

@@ -514,24 +514,24 @@ const routes: AppRoutes[] = [
isPrivate: true,
},
{
path: ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING,
path: ROUTES.LLM_OBSERVABILITY_ATTRIBUTE_MAPPING,
exact: true,
component: LLMObservabilityPage,
key: 'AI_OBSERVABILITY_ATTRIBUTE_MAPPING',
key: 'LLM_OBSERVABILITY_ATTRIBUTE_MAPPING',
isPrivate: true,
},
{
path: ROUTES.AI_OBSERVABILITY_OVERVIEW,
path: ROUTES.LLM_OBSERVABILITY_OVERVIEW,
exact: true,
component: LLMObservabilityPage,
key: 'AI_OBSERVABILITY_OVERVIEW',
key: 'LLM_OBSERVABILITY_OVERVIEW',
isPrivate: true,
},
{
path: ROUTES.AI_OBSERVABILITY_CONFIGURATION,
path: ROUTES.LLM_OBSERVABILITY_CONFIGURATION,
exact: true,
component: LLMObservabilityPage,
key: 'AI_OBSERVABILITY_CONFIGURATION',
key: 'LLM_OBSERVABILITY_CONFIGURATION',
isPrivate: true,
},
];

View File

@@ -1,20 +0,0 @@
import { ENVIRONMENT } from 'constants/env';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
export function mockFieldsAPIsWithEmptyResponse(): void {
server.use(
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({ status: 'success', data: { keys: {}, complete: true } }),
),
),
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/values`, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({ status: 'success', data: { values: {}, complete: true } }),
),
),
);
}

View File

@@ -136,7 +136,7 @@ export const invalidateGetChecks = async (
};
/**
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* @summary List Clusters for Infra Monitoring
*/
export const listClusters = (
@@ -219,7 +219,7 @@ export const useListClusters = <
return useMutation(getListClustersMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on), currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on), readyNodes (k8s.daemonset.ready_nodes, the number of nodes running at least one ready daemon pod) and misscheduledNodes (k8s.daemonset.misscheduled_nodes, the number of nodes running the daemon pod but not supposed to) — note these are node counts, not pod counts. It also reports per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes / ready_nodes / misscheduled_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes, readyNodes, misscheduledNodes) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on), currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on), readyNodes (k8s.daemonset.ready_nodes, the number of nodes running at least one ready daemon pod) and misscheduledNodes (k8s.daemonset.misscheduled_nodes, the number of nodes running the daemon pod but not supposed to) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes / ready_nodes / misscheduled_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes, readyNodes, misscheduledNodes) return -1 as a sentinel when no data is available for that field.
* @summary List DaemonSets for Infra Monitoring
*/
export const listDaemonSets = (
@@ -302,7 +302,7 @@ export const useListDaemonSets = <
return useMutation(getListDaemonSetsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.
* @summary List Deployments for Infra Monitoring
*/
export const listDeployments = (
@@ -468,7 +468,7 @@ export const useListHosts = <
return useMutation(getListHostsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status); note podCountsByStatus.failed (current pod status) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.
* @summary List Jobs for Infra Monitoring
*/
export const listJobs = (
@@ -634,7 +634,7 @@ export const useListContainers = <
return useMutation(getListContainersMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
* @summary List Namespaces for Infra Monitoring
*/
export const listNamespaces = (
@@ -717,7 +717,7 @@ export const useListNamespaces = <
return useMutation(getListNamespacesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } for pods scheduled on the listed nodes, reflecting each pod's latest kubectl-style display status). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* @summary List Nodes for Infra Monitoring
*/
export const listNodes = (
@@ -800,7 +800,7 @@ export const useListNodes = <
return useMutation(getListNodesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod status (kubectl-style display status such as Running/Pending/CrashLoopBackOff, or no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current status) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-status counts under podCountsByStatus: { pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } derived from each pod's latest kubectl-style display status in the window). Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase: { pending, running, succeeded, failed, unknown } derived from each pod's latest phase in the window). Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.
* @summary List Pods for Infra Monitoring
*/
export const listPods = (
@@ -966,7 +966,7 @@ export const useListVolumes = <
return useMutation(getListVolumesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.
* @summary List StatefulSets for Infra Monitoring
*/
export const listStatefulSets = (

View File

@@ -2814,7 +2814,6 @@ export enum CloudintegrationtypesServiceIDDTO {
cassandradb = 'cassandradb',
redis = 'redis',
cloudsql_postgres = 'cloudsql_postgres',
memorystore_redis = 'memorystore_redis',
}
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**
@@ -3301,6 +3300,29 @@ export interface DashboardGridLayoutSpecDTO {
repeatVariable?: string;
}
export interface DashboardLinkDTO {
/**
* @type string
*/
name?: string;
/**
* @type boolean
*/
renderVariables?: boolean;
/**
* @type boolean
*/
targetBlank?: boolean;
/**
* @type string
*/
tooltip?: string;
/**
* @type string
*/
url?: string;
}
export interface DashboardtypesAxesDTO {
/**
* @type boolean
@@ -3467,9 +3489,9 @@ export enum Querybuildertypesv5FunctionNameDTO {
}
export interface Querybuildertypesv5FunctionDTO {
/**
* @type array,null
* @type array
*/
args?: Querybuildertypesv5FunctionArgDTO[] | null;
args?: Querybuildertypesv5FunctionArgDTO[];
name?: Querybuildertypesv5FunctionNameDTO;
}
@@ -3480,7 +3502,6 @@ export enum TelemetrytypesFieldContextDTO {
resource = 'resource',
attribute = 'attribute',
body = 'body',
'' = '',
}
export enum TelemetrytypesFieldDataTypeDTO {
string = 'string',
@@ -3488,13 +3509,11 @@ export enum TelemetrytypesFieldDataTypeDTO {
float64 = 'float64',
int64 = 'int64',
number = 'number',
'' = '',
}
export enum TelemetrytypesSignalDTO {
traces = 'traces',
logs = 'logs',
metrics = 'metrics',
'' = '',
}
export interface Querybuildertypesv5GroupByKeyDTO {
/**
@@ -3574,18 +3593,18 @@ export interface Querybuildertypesv5SecondaryAggregationDTO {
*/
expression?: string;
/**
* @type array,null
* @type array
*/
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
groupBy?: Querybuildertypesv5GroupByKeyDTO[];
/**
* @type integer
*/
limit?: number;
limitBy?: Querybuildertypesv5LimitByDTO;
/**
* @type array,null
* @type array
*/
order?: Querybuildertypesv5OrderByDTO[] | null;
order?: Querybuildertypesv5OrderByDTO[];
stepInterval?: Querybuildertypesv5StepDTO;
}
@@ -3612,13 +3631,12 @@ export enum Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQue
}
export enum TelemetrytypesSourceDTO {
meter = 'meter',
'' = '',
}
export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregationDTO {
/**
* @type array,null
* @type array
*/
aggregations?: Querybuildertypesv5LogAggregationDTO[] | null;
aggregations?: Querybuildertypesv5LogAggregationDTO[];
/**
* @type string
*/
@@ -3629,13 +3647,13 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
disabled?: boolean;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
* @type array
*/
functions?: Querybuildertypesv5FunctionDTO[] | null;
functions?: Querybuildertypesv5FunctionDTO[];
/**
* @type array,null
* @type array
*/
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
groupBy?: Querybuildertypesv5GroupByKeyDTO[];
having?: Querybuildertypesv5HavingDTO;
/**
* @type string
@@ -3655,17 +3673,17 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
*/
offset?: number;
/**
* @type array,null
* @type array
*/
order?: Querybuildertypesv5OrderByDTO[] | null;
order?: Querybuildertypesv5OrderByDTO[];
/**
* @type array,null
* @type array
*/
secondaryAggregations?: Querybuildertypesv5SecondaryAggregationDTO[] | null;
secondaryAggregations?: Querybuildertypesv5SecondaryAggregationDTO[];
/**
* @type array,null
* @type array
*/
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[] | null;
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[];
/**
* @enum logs
* @type string
@@ -3712,7 +3730,6 @@ export enum MetrictypesTemporalityDTO {
delta = 'delta',
cumulative = 'cumulative',
unspecified = 'unspecified',
'' = '',
}
export enum MetrictypesTimeAggregationDTO {
latest = 'latest',
@@ -3724,7 +3741,6 @@ export enum MetrictypesTimeAggregationDTO {
count_distinct = 'count_distinct',
rate = 'rate',
increase = 'increase',
'' = '',
}
export interface Querybuildertypesv5MetricAggregationDTO {
comparisonSpaceAggregationParam?: MetrictypesComparisonSpaceAggregationParamDTO;
@@ -3743,9 +3759,9 @@ export enum Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQue
}
export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregationDTO {
/**
* @type array,null
* @type array
*/
aggregations?: Querybuildertypesv5MetricAggregationDTO[] | null;
aggregations?: Querybuildertypesv5MetricAggregationDTO[];
/**
* @type string
*/
@@ -3756,13 +3772,13 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
disabled?: boolean;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
* @type array
*/
functions?: Querybuildertypesv5FunctionDTO[] | null;
functions?: Querybuildertypesv5FunctionDTO[];
/**
* @type array,null
* @type array
*/
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
groupBy?: Querybuildertypesv5GroupByKeyDTO[];
having?: Querybuildertypesv5HavingDTO;
/**
* @type string
@@ -3782,17 +3798,17 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
*/
offset?: number;
/**
* @type array,null
* @type array
*/
order?: Querybuildertypesv5OrderByDTO[] | null;
order?: Querybuildertypesv5OrderByDTO[];
/**
* @type array,null
* @type array
*/
secondaryAggregations?: Querybuildertypesv5SecondaryAggregationDTO[] | null;
secondaryAggregations?: Querybuildertypesv5SecondaryAggregationDTO[];
/**
* @type array,null
* @type array
*/
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[] | null;
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[];
/**
* @enum metrics
* @type string
@@ -3818,9 +3834,9 @@ export enum Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQue
}
export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregationDTO {
/**
* @type array,null
* @type array
*/
aggregations?: Querybuildertypesv5TraceAggregationDTO[] | null;
aggregations?: Querybuildertypesv5TraceAggregationDTO[];
/**
* @type string
*/
@@ -3831,13 +3847,13 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
disabled?: boolean;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
* @type array
*/
functions?: Querybuildertypesv5FunctionDTO[] | null;
functions?: Querybuildertypesv5FunctionDTO[];
/**
* @type array,null
* @type array
*/
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
groupBy?: Querybuildertypesv5GroupByKeyDTO[];
having?: Querybuildertypesv5HavingDTO;
/**
* @type string
@@ -3857,17 +3873,17 @@ export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTyp
*/
offset?: number;
/**
* @type array,null
* @type array
*/
order?: Querybuildertypesv5OrderByDTO[] | null;
order?: Querybuildertypesv5OrderByDTO[];
/**
* @type array,null
* @type array
*/
secondaryAggregations?: Querybuildertypesv5SecondaryAggregationDTO[] | null;
secondaryAggregations?: Querybuildertypesv5SecondaryAggregationDTO[];
/**
* @type array,null
* @type array
*/
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[] | null;
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[];
/**
* @enum traces
* @type string
@@ -4018,16 +4034,10 @@ export interface DashboardtypesDatasourceSpecDTO {
plugin?: DashboardtypesDatasourcePluginDTO;
}
export type DashboardtypesDashboardSpecDTODatasourcesAnyOf = {
export type DashboardtypesDashboardSpecDTODatasources = {
[key: string]: DashboardtypesDatasourceSpecDTO;
};
/**
* @nullable
*/
export type DashboardtypesDashboardSpecDTODatasources =
DashboardtypesDashboardSpecDTODatasourcesAnyOf | null;
export enum DashboardtypesPanelKindDTO {
Panel = 'Panel',
}
@@ -4042,29 +4052,6 @@ export interface DashboardtypesDisplayDTO {
name: string;
}
export interface DashboardtypesLinkDTO {
/**
* @type string
*/
name?: string;
/**
* @type boolean
*/
renderVariables?: boolean;
/**
* @type boolean
*/
targetBlank?: boolean;
/**
* @type string
*/
tooltip?: string;
/**
* @type string
*/
url?: string;
}
export enum DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpecDTOKind {
'signoz/TimeSeriesPanel' = 'signoz/TimeSeriesPanel',
}
@@ -4285,9 +4272,9 @@ export enum DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboa
}
export interface DashboardtypesListPanelSpecDTO {
/**
* @type array,null
* @type array
*/
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[] | null;
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[];
}
export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpecDTO {
@@ -4357,9 +4344,9 @@ export interface Querybuildertypesv5QueryBuilderFormulaDTO {
*/
expression?: string;
/**
* @type array,null
* @type array
*/
functions?: Querybuildertypesv5FunctionDTO[] | null;
functions?: Querybuildertypesv5FunctionDTO[];
having?: Querybuildertypesv5HavingDTO;
/**
* @type string
@@ -4374,9 +4361,9 @@ export interface Querybuildertypesv5QueryBuilderFormulaDTO {
*/
name?: string;
/**
* @type array,null
* @type array
*/
order?: Querybuildertypesv5OrderByDTO[] | null;
order?: Querybuildertypesv5OrderByDTO[];
}
export enum Querybuildertypesv5QueryEnvelopeFormulaDTOType {
@@ -4393,9 +4380,9 @@ export interface Querybuildertypesv5QueryEnvelopeFormulaDTO {
export interface Querybuildertypesv5QueryBuilderTraceOperatorDTO {
/**
* @type array,null
* @type array
*/
aggregations?: Querybuildertypesv5TraceAggregationDTO[] | null;
aggregations?: Querybuildertypesv5TraceAggregationDTO[];
/**
* @type string
*/
@@ -4410,13 +4397,13 @@ export interface Querybuildertypesv5QueryBuilderTraceOperatorDTO {
expression?: string;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
* @type array
*/
functions?: Querybuildertypesv5FunctionDTO[] | null;
functions?: Querybuildertypesv5FunctionDTO[];
/**
* @type array,null
* @type array
*/
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
groupBy?: Querybuildertypesv5GroupByKeyDTO[];
having?: Querybuildertypesv5HavingDTO;
/**
* @type string
@@ -4435,17 +4422,17 @@ export interface Querybuildertypesv5QueryBuilderTraceOperatorDTO {
*/
offset?: number;
/**
* @type array,null
* @type array
*/
order?: Querybuildertypesv5OrderByDTO[] | null;
order?: Querybuildertypesv5OrderByDTO[];
/**
* @type string
*/
returnSpansFrom?: string;
/**
* @type array,null
* @type array
*/
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[] | null;
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[];
stepInterval?: Querybuildertypesv5StepDTO;
}
@@ -4628,7 +4615,7 @@ export interface DashboardtypesPanelSpecDTO {
/**
* @type array
*/
links: DashboardtypesLinkDTO[];
links?: DashboardLinkDTO[];
plugin: DashboardtypesPanelPluginDTO;
/**
* @type array
@@ -4668,18 +4655,12 @@ export type DashboardtypesVariableDefaultValueDTO = string | string[];
export enum DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDynamicVariableSpecDTOKind {
'signoz/DynamicVariable' = 'signoz/DynamicVariable',
}
export enum DashboardtypesDynamicVariableSignalDTO {
traces = 'traces',
logs = 'logs',
metrics = 'metrics',
all = 'all',
}
export interface DashboardtypesDynamicVariableSpecDTO {
/**
* @type string
*/
name: string;
signal: DashboardtypesDynamicVariableSignalDTO;
signal?: TelemetrytypesSignalDTO;
}
export interface DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDynamicVariableSpecDTO {
@@ -4808,7 +4789,7 @@ export type DashboardtypesVariableDTO =
export interface DashboardtypesDashboardSpecDTO {
/**
* @type object,null
* @type object
*/
datasources?: DashboardtypesDashboardSpecDTODatasources;
display: DashboardtypesDisplayDTO;
@@ -4823,7 +4804,7 @@ export interface DashboardtypesDashboardSpecDTO {
/**
* @type array
*/
links: DashboardtypesLinkDTO[];
links?: DashboardLinkDTO[];
/**
* @type object
*/
@@ -5742,6 +5723,29 @@ export interface InframonitoringtypesNodeCountsByReadinessDTO {
ready: number;
}
export interface InframonitoringtypesPodCountsByPhaseDTO {
/**
* @type integer
*/
failed: number;
/**
* @type integer
*/
pending: number;
/**
* @type integer
*/
running: number;
/**
* @type integer
*/
succeeded: number;
/**
* @type integer
*/
unknown: number;
}
export interface InframonitoringtypesPodCountsByStatusDTO {
/**
* @type integer
@@ -5851,6 +5855,7 @@ export interface InframonitoringtypesClusterRecordDTO {
*/
meta: InframonitoringtypesClusterRecordDTOMeta;
nodeCountsByReadiness: InframonitoringtypesNodeCountsByReadinessDTO;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
}
@@ -6126,6 +6131,7 @@ export interface InframonitoringtypesDaemonSetRecordDTO {
* @type integer
*/
misscheduledNodes: number;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
/**
* @type integer
@@ -6207,6 +6213,7 @@ export interface InframonitoringtypesDeploymentRecordDTO {
* @type object,null
*/
meta: InframonitoringtypesDeploymentRecordDTOMeta;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
}
@@ -6373,6 +6380,7 @@ export interface InframonitoringtypesJobRecordDTO {
* @type object,null
*/
meta: InframonitoringtypesJobRecordDTOMeta;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
/**
* @type integer
@@ -6453,6 +6461,7 @@ export interface InframonitoringtypesNamespaceRecordDTO {
* @type string
*/
namespaceName: string;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
}
@@ -6519,6 +6528,7 @@ export interface InframonitoringtypesNodeRecordDTO {
* @type string
*/
nodeName: string;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
}
@@ -6539,6 +6549,14 @@ export interface InframonitoringtypesNodesDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export enum InframonitoringtypesPodPhaseDTO {
pending = 'pending',
running = 'running',
succeeded = 'succeeded',
failed = 'failed',
unknown = 'unknown',
no_data = 'no_data',
}
export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6595,6 +6613,7 @@ export interface InframonitoringtypesPodRecordDTO {
* @format double
*/
podCPURequest: number;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
/**
* @type number
@@ -6611,6 +6630,7 @@ export interface InframonitoringtypesPodRecordDTO {
* @format double
*/
podMemoryRequest: number;
podPhase: InframonitoringtypesPodPhaseDTO;
/**
* @type integer
* @format int64
@@ -6960,6 +6980,7 @@ export interface InframonitoringtypesStatefulSetRecordDTO {
* @type object,null
*/
meta: InframonitoringtypesStatefulSetRecordDTOMeta;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
/**
* @type number
@@ -8654,9 +8675,9 @@ export interface RuletypesGettableTestRuleDTO {
export interface RuletypesRenotifyDTO {
/**
* @type array,null
* @type array
*/
alertStates?: RuletypesAlertStateDTO[] | null;
alertStates?: RuletypesAlertStateDTO[];
/**
* @type boolean
*/
@@ -8669,9 +8690,9 @@ export interface RuletypesRenotifyDTO {
export interface RuletypesNotificationSettingsDTO {
/**
* @type array,null
* @type array
*/
groupBy?: string[] | null;
groupBy?: string[];
/**
* @type string
*/
@@ -9237,48 +9258,6 @@ export interface SpantypesGettableSpanMapperGroupsDTO {
items: SpantypesSpanMapperGroupDTO[];
}
export type SpantypesSpanMapperTestSpanDTOAttributesAnyOf = {
[key: string]: unknown;
};
/**
* @nullable
*/
export type SpantypesSpanMapperTestSpanDTOAttributes =
SpantypesSpanMapperTestSpanDTOAttributesAnyOf | null;
export type SpantypesSpanMapperTestSpanDTOResourceAnyOf = {
[key: string]: unknown;
};
/**
* @nullable
*/
export type SpantypesSpanMapperTestSpanDTOResource =
SpantypesSpanMapperTestSpanDTOResourceAnyOf | null;
export interface SpantypesSpanMapperTestSpanDTO {
/**
* @type object,null
*/
attributes?: SpantypesSpanMapperTestSpanDTOAttributes;
/**
* @type object,null
*/
resource?: SpantypesSpanMapperTestSpanDTOResource;
}
export interface SpantypesGettableSpanMapperTestDTO {
/**
* @type array,null
*/
collectorLogs?: string[] | null;
/**
* @type array,null
*/
spans?: SpantypesSpanMapperTestSpanDTO[] | null;
}
export enum SpantypesSpanMapperOperationDTO {
move = 'move',
copy = 'copy',
@@ -9620,33 +9599,6 @@ export interface SpantypesPostableSpanMapperGroupDTO {
name: string;
}
export interface SpantypesPostableSpanMapperTestGroupDTO {
condition: SpantypesSpanMapperGroupConditionDTO | null;
/**
* @type boolean
*/
enabled?: boolean;
/**
* @type array,null
*/
mappers?: SpantypesPostableSpanMapperDTO[] | null;
/**
* @type string
*/
name: string;
}
export interface SpantypesPostableSpanMapperTestDTO {
/**
* @type array,null
*/
groups: SpantypesPostableSpanMapperTestGroupDTO[] | null;
/**
* @type array,null
*/
spans: SpantypesSpanMapperTestSpanDTO[] | null;
}
export interface SpantypesSpanAggregationDTO {
aggregation: SpantypesSpanAggregationTypeDTO;
field: TelemetrytypesTelemetryFieldKeyDTO;
@@ -10997,14 +10949,6 @@ export type UpdateSpanMapperPathParameters = {
groupId: string;
mapperId: string;
};
export type TestSpanMappers200 = {
data: SpantypesGettableSpanMapperTestDTO;
/**
* @type string
*/
status: string;
};
export type GetStats200Data = { [key: string]: unknown };
export type GetStats200 = {

View File

@@ -30,10 +30,8 @@ import type {
RenderErrorResponseDTO,
SpantypesPostableSpanMapperDTO,
SpantypesPostableSpanMapperGroupDTO,
SpantypesPostableSpanMapperTestDTO,
SpantypesUpdatableSpanMapperDTO,
SpantypesUpdatableSpanMapperGroupDTO,
TestSpanMappers200,
UpdateSpanMapperGroupPathParameters,
UpdateSpanMapperPathParameters,
} from '../sigNoz.schemas';
@@ -782,86 +780,3 @@ export const useUpdateSpanMapper = <
> => {
return useMutation(getUpdateSpanMapperMutationOptions(options));
};
/**
* Tests how span mappers would transform sample spans
* @summary Test span mappers against sample spans
*/
export const testSpanMappers = (
spantypesPostableSpanMapperTestDTO?: BodyType<SpantypesPostableSpanMapperTestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<TestSpanMappers200>({
url: `/api/v1/span_mapper_groups/test`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: spantypesPostableSpanMapperTestDTO,
signal,
});
};
export const getTestSpanMappersMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
> => {
const mutationKey = ['testSpanMappers'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof testSpanMappers>>,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> }
> = (props) => {
const { data } = props ?? {};
return testSpanMappers(data);
};
return { mutationFn, ...mutationOptions };
};
export type TestSpanMappersMutationResult = NonNullable<
Awaited<ReturnType<typeof testSpanMappers>>
>;
export type TestSpanMappersMutationBody =
| BodyType<SpantypesPostableSpanMapperTestDTO>
| undefined;
export type TestSpanMappersMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Test span mappers against sample spans
*/
export const useTestSpanMappers = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
> => {
return useMutation(getTestSpanMappersMutationOptions(options));
};

View File

@@ -1,50 +1,26 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError, AxiosResponse } from 'axios';
import { AxiosError } from 'axios';
import { ErrorV2Resp } from 'types/api';
import { ExportRawDataProps } from 'types/api/exportRawData/getExportRawData';
export interface FetchExportDataProps extends ExportRawDataProps {
signal?: AbortSignal;
}
async function postExportRawData({
format,
body,
signal,
}: FetchExportDataProps): Promise<AxiosResponse<Blob>> {
return axios.post<Blob>(
`export_raw_data?format=${encodeURIComponent(format)}`,
body,
{
responseType: 'blob',
decompress: true,
headers: {
Accept: 'application/octet-stream',
'Content-Type': 'application/json',
},
timeout: 0,
signal,
},
);
}
/**
* Fetches a single export_raw_data page and returns it as a Blob.
* Callers own retry/cancel/error UX.
*/
export async function fetchExportData(
props: FetchExportDataProps,
): Promise<Blob> {
const response = await postExportRawData(props);
return response.data;
}
export const downloadExportData = async (
props: ExportRawDataProps,
): Promise<void> => {
try {
const response = await postExportRawData(props);
const response = await axios.post<Blob>(
`export_raw_data?format=${encodeURIComponent(props.format)}`,
props.body,
{
responseType: 'blob',
decompress: true,
headers: {
Accept: 'application/octet-stream',
'Content-Type': 'application/json',
},
timeout: 0,
},
);
if (response.status !== 200) {
throw new Error(

View File

@@ -20,7 +20,7 @@ import { Globe, Inbox, SquarePen } from '@signozhq/icons';
import AnnouncementsModal from './AnnouncementsModal';
import FeedbackModal from './FeedbackModal';
import ShareURLModal, { type ShareURLExtraOption } from './ShareURLModal';
import ShareURLModal from './ShareURLModal';
import './HeaderRightSection.styles.scss';
import { Typography } from '@signozhq/ui/typography';
@@ -29,15 +29,12 @@ interface HeaderRightSectionProps {
enableAnnouncements: boolean;
enableShare: boolean;
enableFeedback: boolean;
/** Optional page-specific toggle for the share dialog (e.g. "Include variables"). */
shareModalExtraOption?: ShareURLExtraOption;
}
function HeaderRightSection({
enableAnnouncements,
enableShare,
enableFeedback,
shareModalExtraOption,
}: HeaderRightSectionProps): JSX.Element | null {
const location = useLocation();
@@ -188,7 +185,7 @@ function HeaderRightSection({
rootClassName="header-section-popover-root"
className="shareable-link-popover"
placement="bottomRight"
content={<ShareURLModal extraOption={shareModalExtraOption} />}
content={<ShareURLModal />}
open={openShareURLModal}
destroyTooltipOnHide
arrow={false}

View File

@@ -24,22 +24,7 @@ const routesToBeSharedWithTime = [
ROUTES.METER_EXPLORER,
];
/**
* An optional, page-specific toggle in the share dialog (e.g. a dashboard's
* "Include variables"). When enabled, `apply` mutates the URL params that go into
* the shared link. Keeps this shared modal generic — the page owns what it adds.
*/
export interface ShareURLExtraOption {
label: string;
defaultEnabled?: boolean;
apply: (params: URLSearchParams) => void;
}
interface ShareURLModalProps {
extraOption?: ShareURLExtraOption;
}
function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
function ShareURLModal(): JSX.Element {
const urlQuery = useUrlQuery();
const location = useLocation();
const { selectedTime } = useSelector<AppState, GlobalReducer>(
@@ -49,9 +34,6 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
const [enableAbsoluteTime, setEnableAbsoluteTime] = useState(
selectedTime !== 'custom',
);
const [enableExtraOption, setEnableExtraOption] = useState(
extraOption?.defaultEnabled ?? false,
);
const startTime = urlQuery.get(QueryParams.startTime);
const endTime = urlQuery.get(QueryParams.endTime);
@@ -111,11 +93,6 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
}
}
if (extraOption && enableExtraOption) {
extraOption.apply(urlQuery);
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
}
return currentUrl;
};
@@ -166,20 +143,6 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
</>
)}
{extraOption && (
<div className="absolute-relative-time-toggler-container">
<Typography.Text className="absolute-relative-time-toggler-label">
{extraOption.label}
</Typography.Text>
<div className="absolute-relative-time-toggler">
<Switch
value={enableExtraOption}
onChange={(): void => setEnableExtraOption((prev) => !prev)}
/>
</div>
</div>
)}
<div className="share-link">
<div className="url-share-container">
<div className="url-share-container-header">

View File

@@ -247,6 +247,11 @@
var(--l2-background)
) !important;
&,
.ͼ1a {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
::-moz-selection {
background: var(
--query-builder-v2-selection-background-color,

View File

@@ -293,6 +293,11 @@ $max-recents-shown: 5;
var(--l2-background)
) !important;
&,
.ͼ1a {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
::-moz-selection {
background: var(
--query-builder-v2-selection-background-color,

View File

@@ -52,8 +52,6 @@ import type { SignalType } from 'types/api/v5/queryRange';
import { queryExamples, SUGGESTIONS_SECTION } from './constants';
import {
combineInitialAndUserExpression,
dedupeOptionsByLabel,
getFieldContextPrefix,
getRecentOptions,
renderRecentDeleteButton,
} from './utils';
@@ -224,12 +222,6 @@ function QuerySearch({
QueryKeyDataSuggestionsProps[] | null
>(null);
// dedupe keySuggestions by label/name
const dedupedKeySuggestions = useMemo(
() => dedupeOptionsByLabel(keySuggestions || []),
[keySuggestions],
);
const [showExamples] = useState(false);
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
@@ -254,11 +246,9 @@ function QuerySearch({
[key: string]: QueryKeyDataSuggestionsProps[];
}): any[] =>
Object.values(keys).flatMap((items: QueryKeyDataSuggestionsProps[]) =>
items.map(({ name, fieldDataType, fieldContext }) => ({
items.map(({ name, fieldDataType }) => ({
label: name,
type: fieldDataType === 'string' ? 'keyword' : fieldDataType,
fieldContext,
fieldDataType,
info: '',
details: '',
})),
@@ -317,17 +307,13 @@ function QuerySearch({
if (response.data.data) {
const { keys } = response.data.data;
const options = generateOptions(keys);
// Deduplicate by full variant identity (name + context + data type), NOT by
// label. deduping by label removes varient which is not expected. If we need
// to dedupe by label use dedupedKeySuggestions not dedupe the source itself
const variantId = (opt: QueryKeyDataSuggestionsProps): string =>
`${opt.label}|${opt.fieldContext ?? ''}|${opt.fieldDataType ?? ''}`;
// Use a Map to deduplicate by label and preserve order: new options take precedence
const merged = new Map<string, QueryKeyDataSuggestionsProps>();
options.forEach((opt) => merged.set(variantId(opt), opt));
options.forEach((opt) => merged.set(opt.label, opt));
if (searchText && lastKeyRef.current !== searchText) {
(keySuggestions || []).forEach((opt) => {
if (!merged.has(variantId(opt))) {
merged.set(variantId(opt), opt);
if (!merged.has(opt.label)) {
merged.set(opt.label, opt);
}
});
}
@@ -933,55 +919,8 @@ function QuerySearch({
if (queryContext.isInKey) {
const searchText = word?.text.toLowerCase().trim() ?? '';
const fieldContextMatch = getFieldContextPrefix(searchText);
if (fieldContextMatch) {
const { context: fieldContext, remainder } = fieldContextMatch;
// Fetch the context's page when the prefix is typed exactly eg.("attribute.")
if (remainder === '' && lastFetchedKeyRef.current !== searchText) {
debouncedFetchKeySuggestions(searchText);
}
//suggestions that actually do start with <fieldContext>.
const nameMatches = (keySuggestions || [])
.filter((option) => option.label.toLowerCase().includes(searchText))
.map((option) => ({ ...option, boost: 100 }));
//suggestions which do not start with the prefix but qualifies for suggestion
const contextQualified = (keySuggestions || [])
.filter(
(option) =>
option.fieldContext === fieldContext &&
option.label.toLowerCase().includes(remainder),
)
.map((option) => ({
...option,
label: `${fieldContext}.${option.label}`,
boost: 0,
}));
const contextOptions = dedupeOptionsByLabel([
...nameMatches,
...contextQualified,
]);
// If contextOptions is empty fetch again.
if (
contextOptions.length === 0 &&
lastFetchedKeyRef.current !== searchText
) {
debouncedFetchKeySuggestions(searchText);
}
return {
from: word?.from ?? 0,
to: word?.to ?? cursorPos.ch,
options: addSpaceToOptions(contextOptions),
};
}
options = dedupedKeySuggestions.filter((option) =>
options = (keySuggestions || []).filter((option) =>
option.label.toLowerCase().includes(searchText),
);
@@ -1030,26 +969,9 @@ function QuerySearch({
// If we have a key context, add that info to the operator suggestions
if (keyName) {
const keyContextMatch = getFieldContextPrefix(keyName);
// key-suggestion can contain multiple variants of a single key
// In variants we capture ones that match the label to typed keyName exactly or,
// if it has a prefix fieldContext remove it and then match.
const variants = (keySuggestions || []).filter(
(k) =>
k.label === keyName ||
(keyContextMatch !== null &&
k.fieldContext === keyContextMatch.context &&
k.label === keyContextMatch.remainder),
);
const variantTypes = new Set(
variants
.map((k) =>
k.type === 'keyword' ? QUERY_BUILDER_KEY_TYPES.STRING : k.type,
)
.filter(Boolean),
);
//if there are multi-variant, show all suggestions else just the one
const keyType = variantTypes.size === 1 ? [...variantTypes][0] : '';
// Find the key details from suggestions
const keyDetails = (keySuggestions || []).find((k) => k.label === keyName);
const keyType = keyDetails?.type || '';
// Filter operators based on key type
if (keyType) {
@@ -1292,7 +1214,7 @@ function QuerySearch({
if (curChar === '(') {
// In expression context, suggest keys, functions, or nested parentheses
options = [
...dedupedKeySuggestions,
...(keySuggestions || []),
{ label: '(', type: 'parenthesis', info: 'Open nested group' },
{ label: 'NOT', type: 'operator', info: 'Negate expression' },
...options.filter((opt) => opt.type === 'function'),

View File

@@ -1,120 +0,0 @@
import {
combineInitialAndUserExpression,
dedupeOptionsByLabel,
getFieldContextPrefix,
getUserExpressionFromCombined,
} from '../utils';
describe('entityLogsExpression', () => {
describe('combineInitialAndUserExpression', () => {
it('returns user when initial is empty', () => {
expect(combineInitialAndUserExpression('', 'body contains error')).toBe(
'body contains error',
);
});
it('returns initial when user is empty', () => {
expect(combineInitialAndUserExpression('k8s.pod.name = "x"', '')).toBe(
'k8s.pod.name = "x"',
);
});
it('wraps user in parentheses with AND', () => {
expect(
combineInitialAndUserExpression('k8s.pod.name = "x"', 'body = "a"'),
).toBe('k8s.pod.name = "x" AND (body = "a")');
});
});
describe('getUserExpressionFromCombined', () => {
it('returns empty when combined equals initial', () => {
expect(
getUserExpressionFromCombined('k8s.pod.name = "x"', 'k8s.pod.name = "x"'),
).toBe('');
});
it('extracts user from wrapped form', () => {
expect(
getUserExpressionFromCombined(
'k8s.pod.name = "x"',
'k8s.pod.name = "x" AND (body = "a")',
),
).toBe('body = "a"');
});
it('extracts user from legacy AND without parens', () => {
expect(
getUserExpressionFromCombined(
'k8s.pod.name = "x"',
'k8s.pod.name = "x" AND body = "a"',
),
).toBe('body = "a"');
});
it('returns full combined when initial is empty', () => {
expect(getUserExpressionFromCombined('', 'service.name = "a"')).toBe(
'service.name = "a"',
);
});
});
});
describe('getFieldContextPrefix', () => {
it('matches a complete context prefix with a remainder', () => {
expect(getFieldContextPrefix('attribute.status')).toStrictEqual({
context: 'attribute',
remainder: 'status',
});
});
it('matches a bare context prefix with empty remainder', () => {
expect(getFieldContextPrefix('resource.')).toStrictEqual({
context: 'resource',
remainder: '',
});
});
it('matches every backend field context', () => {
['attribute', 'resource', 'span', 'body', 'log', 'metric'].forEach((ctx) => {
expect(getFieldContextPrefix(`${ctx}.x`)).toStrictEqual({
context: ctx,
remainder: 'x',
});
});
});
it('does not match a partial context name', () => {
expect(getFieldContextPrefix('attr')).toBeNull();
expect(getFieldContextPrefix('attribute')).toBeNull();
});
it('does not match a non-context key with dots', () => {
expect(getFieldContextPrefix('status.code')).toBeNull();
});
it('matches context case-insensitively but keeps remainder casing', () => {
expect(getFieldContextPrefix('Attribute.Status')).toStrictEqual({
context: 'attribute',
remainder: 'Status',
});
});
});
describe('dedupeOptionsByLabel', () => {
it('keeps the first occurrence per label, preserving order', () => {
expect(
dedupeOptionsByLabel([
{ label: 'status.code', type: 'keyword' },
{ label: 'status.code', type: 'number' },
{ label: 'duration', type: 'number' },
]),
).toStrictEqual([
{ label: 'status.code', type: 'keyword' },
{ label: 'duration', type: 'number' },
]);
});
it('returns an empty array for empty input', () => {
expect(dedupeOptionsByLabel([])).toStrictEqual([]);
});
});

View File

@@ -0,0 +1,58 @@
import {
combineInitialAndUserExpression,
getUserExpressionFromCombined,
} from '../utils';
describe('entityLogsExpression', () => {
describe('combineInitialAndUserExpression', () => {
it('returns user when initial is empty', () => {
expect(combineInitialAndUserExpression('', 'body contains error')).toBe(
'body contains error',
);
});
it('returns initial when user is empty', () => {
expect(combineInitialAndUserExpression('k8s.pod.name = "x"', '')).toBe(
'k8s.pod.name = "x"',
);
});
it('wraps user in parentheses with AND', () => {
expect(
combineInitialAndUserExpression('k8s.pod.name = "x"', 'body = "a"'),
).toBe('k8s.pod.name = "x" AND (body = "a")');
});
});
describe('getUserExpressionFromCombined', () => {
it('returns empty when combined equals initial', () => {
expect(
getUserExpressionFromCombined('k8s.pod.name = "x"', 'k8s.pod.name = "x"'),
).toBe('');
});
it('extracts user from wrapped form', () => {
expect(
getUserExpressionFromCombined(
'k8s.pod.name = "x"',
'k8s.pod.name = "x" AND (body = "a")',
),
).toBe('body = "a"');
});
it('extracts user from legacy AND without parens', () => {
expect(
getUserExpressionFromCombined(
'k8s.pod.name = "x"',
'k8s.pod.name = "x" AND body = "a"',
),
).toBe('body = "a"');
});
it('returns full combined when initial is empty', () => {
expect(getUserExpressionFromCombined('', 'service.name = "a"')).toBe(
'service.name = "a"',
);
});
});
});

View File

@@ -1,16 +1,6 @@
export const RECENTS_SECTION = { name: 'Recent searches', rank: 1 } as const;
export const SUGGESTIONS_SECTION = { name: 'Suggestions', rank: 2 } as const;
// TODO: move to using TelemetrytypesFieldContextDTO when we migrate getKeySuggestions API (Part of https://github.com/SigNoz/engineering-pod/issues/5289)
export const FIELD_CONTEXTS = [
'attribute',
'resource',
'span',
'body',
'log',
'metric',
] as const;
// Custom CodeMirror Completion.type for recent-query entries. Used to discriminate
// recents from regular autocomplete completions in renderers and event handlers.
export const RECENT_COMPLETION_TYPE = 'recent';

View File

@@ -9,45 +9,11 @@ import type { SignalType } from 'types/api/v5/queryRange';
import 'utils/timeUtils';
import {
FIELD_CONTEXTS,
RECENT_COMPLETION_TYPE,
RECENTS_DISPLAY_CAP,
RECENTS_SECTION,
} from './constants';
export interface FieldContextPrefixMatch {
context: string;
remainder: string;
}
// This function checks if the text(query key) starts with a fieldContext
// This util strictly checks that and returns context and remainder back.
// This helps differentiate if the typed key was prefixed with a context or
// was it an actual queryKey like (attribute.abc)
export function getFieldContextPrefix(
text: string,
): FieldContextPrefixMatch | null {
const lower = text.toLowerCase();
const context = FIELD_CONTEXTS.find((ctx) => lower.startsWith(`${ctx}.`));
return context ? { context, remainder: text.slice(context.length + 1) } : null;
}
// Keeps the first occurrence per label, preserving order. Key suggestions hold
// one entry per (name, fieldContext, fieldDataType) variant; This means query builder
// could show multiple labels and this avoids that.
export function dedupeOptionsByLabel<T extends { label: string }>(
options: T[],
): T[] {
const seen = new Set<string>();
return options.filter((option) => {
if (seen.has(option.label)) {
return false;
}
seen.add(option.label);
return true;
});
}
export function combineInitialAndUserExpression(
initial: string,
user: string,

View File

@@ -1,217 +0,0 @@
import { initialQueriesMap } from 'constants/queryBuilder';
import { rest, server } from 'mocks-server/server';
import { render, userEvent, waitFor } from 'tests/test-utils';
import { DataSource } from 'types/common/queryBuilder';
import QuerySearch from '../QuerySearch/QuerySearch';
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
const CM_OPTION_SELECTOR = '.cm-tooltip-autocomplete .cm-completionLabel';
beforeAll(() => {
mockCodeMirrorDomApis();
});
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): { dashboardData: undefined } => ({
dashboardData: undefined,
}),
}));
jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
const handleRunQuery = jest.fn();
return {
__esModule: true,
useQueryBuilder: (): { handleRunQuery: () => void } => ({ handleRunQuery }),
handleRunQuery,
};
});
// Keys fixture: status.code exists as 3 variants (attribute string/number +
// resource string); attribute.a.b.c is a key literally named with the prefix.
const KEYS_FIXTURE = {
'status.code': [
{
name: 'status.code',
fieldContext: 'attribute',
fieldDataType: 'string',
signal: 'logs',
},
{
name: 'status.code',
fieldContext: 'attribute',
fieldDataType: 'number',
signal: 'logs',
},
{
name: 'status.code',
fieldContext: 'resource',
fieldDataType: 'string',
signal: 'logs',
},
],
'attribute.a.b.c': [
{
name: 'attribute.a.b.c',
fieldContext: 'attribute',
fieldDataType: 'string',
signal: 'logs',
},
],
'duration.nano': [
{
name: 'duration.nano',
fieldContext: 'span',
fieldDataType: 'number',
signal: 'logs',
},
],
};
const fetchedSearchTexts: string[] = [];
beforeEach(() => {
fetchedSearchTexts.length = 0;
server.use(
rest.get('http://localhost/api/v1/fields/keys', (req, res, ctx) => {
fetchedSearchTexts.push(req.url.searchParams.get('searchText') ?? '');
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: { complete: true, keys: KEYS_FIXTURE },
}),
);
}),
rest.get('http://localhost/api/v1/fields/values', (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: { values: { stringValues: [], numberValues: [] } },
}),
),
),
);
});
async function renderAndType(text: string): Promise<HTMLElement> {
render(
<QuerySearch
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
queryData={initialQueriesMap.logs.builder.queryData[0]}
dataSource={DataSource.LOGS}
/>,
);
await waitFor(() => {
expect(document.querySelector(CM_EDITOR_SELECTOR)).toBeInTheDocument();
});
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
await userEvent.click(editor);
await userEvent.type(editor, text);
return editor;
}
// Types a key, waits for its suggestion to render (proving the debounced key
// fetch resolved into state), then types a space to enter operator context.
async function typeKeyThenEnterOperatorContext(
key: string,
expectedKeyLabel: string,
): Promise<void> {
const editor = await renderAndType(key);
await waitFor(() => {
expect(visibleOptionLabels()).toContain(expectedKeyLabel);
});
// skipClick: a click would reset the caret to position 0 under the mocked
// DOM rects; we need the space appended at the end of the typed key.
await userEvent.type(editor, ' ', { skipClick: true });
await waitFor(() => {
expect(visibleOptionLabels()).toContain('=');
});
}
function visibleOptionLabels(): string[] {
return Array.from(document.querySelectorAll(CM_OPTION_SELECTOR)).map(
(el) => el.textContent ?? '',
);
}
describe('QuerySearch context-prefixed key suggestions', () => {
it('shows one deduped suggestion per key name in normal mode', async () => {
await renderAndType('statu');
await waitFor(() => {
expect(visibleOptionLabels()).toContain('status.code');
});
const statusOptions = visibleOptionLabels().filter(
(label) => label === 'status.code',
);
expect(statusOptions).toHaveLength(1);
});
it('shows context-scoped suggestions for a complete context prefix', async () => {
await renderAndType('attribute.');
await waitFor(() => {
expect(visibleOptionLabels()).toContain('attribute.status.code');
});
const labels = visibleOptionLabels();
// Literal key name match ranks first
expect(labels[0]).toBe('attribute.a.b.c');
// Context-qualified form of the literal key is also present
expect(labels).toContain('attribute.attribute.a.b.c');
// span-only key is not suggested under attribute.
expect(labels).not.toContain('attribute.duration.nano');
});
it('fetches the context page when the prefix is typed exactly', async () => {
await renderAndType('attribute.');
await waitFor(() => {
expect(fetchedSearchTexts).toContain('attribute.');
});
});
});
describe('QuerySearch operator suggestions by key type', () => {
it('shows all operators for a key with ambiguous types', async () => {
// status.code is string + number across variants → no type filtering
await typeKeyThenEnterOperatorContext('status.code', 'status.code');
const labels = visibleOptionLabels();
expect(labels).toContain('>');
expect(labels).toContain('LIKE');
});
it('shows numeric operators for a single-type number key', async () => {
await typeKeyThenEnterOperatorContext('duration.nano', 'duration.nano');
const labels = visibleOptionLabels();
expect(labels).toContain('>');
expect(labels).not.toContain('LIKE');
});
it('narrows operators when a context prefix disambiguates the type', async () => {
// status.code is ambiguous globally, but resource.status.code is string-only
await typeKeyThenEnterOperatorContext(
'resource.status.code',
'resource.status.code',
);
const labels = visibleOptionLabels();
expect(labels).toContain('LIKE');
expect(labels).not.toContain('>');
});
});

View File

@@ -8,13 +8,78 @@ import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/ty
import { DataSource } from 'types/common/queryBuilder';
import QuerySearch from '../QuerySearch/QuerySearch';
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
// Mock DOM APIs that CodeMirror needs
beforeAll(() => {
mockCodeMirrorDomApis();
// Mock getClientRects and getBoundingClientRect for Range objects
const mockRect: DOMRect = {
width: 100,
height: 20,
top: 0,
left: 0,
right: 100,
bottom: 20,
x: 0,
y: 0,
toJSON: (): DOMRect => mockRect,
} as DOMRect;
// Create a minimal Range mock with only what CodeMirror actually uses
const createMockRange = (): Range => {
let startContainer: Node = document.createTextNode('');
let endContainer: Node = document.createTextNode('');
let startOffset = 0;
let endOffset = 0;
const mockRange = {
// CodeMirror uses these for text measurement
getClientRects: (): DOMRectList =>
({
length: 1,
item: (index: number): DOMRect | null => (index === 0 ? mockRect : null),
0: mockRect,
*[Symbol.iterator](): Generator<DOMRect> {
yield mockRect;
},
}) as unknown as DOMRectList,
getBoundingClientRect: (): DOMRect => mockRect,
// CodeMirror calls these to set up text ranges
setStart: (node: Node, offset: number): void => {
startContainer = node;
startOffset = offset;
},
setEnd: (node: Node, offset: number): void => {
endContainer = node;
endOffset = offset;
},
// Minimal Range properties (TypeScript requires these)
get startContainer(): Node {
return startContainer;
},
get endContainer(): Node {
return endContainer;
},
get startOffset(): number {
return startOffset;
},
get endOffset(): number {
return endOffset;
},
get collapsed(): boolean {
return startContainer === endContainer && startOffset === endOffset;
},
commonAncestorContainer: document.body,
};
return mockRange as unknown as Range;
};
// Mock document.createRange to return a new Range instance each time
document.createRange = (): Range => createMockRange();
// Mock getBoundingClientRect for elements
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
});
jest.mock('hooks/useDarkMode', () => ({

View File

@@ -1,71 +0,0 @@
// Mocks the DOM measurement APIs CodeMirror needs to render in jsdom
// (Range client rects + element bounding rects). Call from a beforeAll in
// specs that render the real CodeMirror editor.
export function mockCodeMirrorDomApis(): void {
const mockRect: DOMRect = {
width: 100,
height: 20,
top: 0,
left: 0,
right: 100,
bottom: 20,
x: 0,
y: 0,
toJSON: (): DOMRect => mockRect,
} as DOMRect;
// Create a minimal Range mock with only what CodeMirror actually uses
const createMockRange = (): Range => {
let startContainer: Node = document.createTextNode('');
let endContainer: Node = document.createTextNode('');
let startOffset = 0;
let endOffset = 0;
const mockRange = {
// CodeMirror uses these for text measurement
getClientRects: (): DOMRectList =>
({
length: 1,
item: (index: number): DOMRect | null => (index === 0 ? mockRect : null),
0: mockRect,
*[Symbol.iterator](): Generator<DOMRect> {
yield mockRect;
},
}) as unknown as DOMRectList,
getBoundingClientRect: (): DOMRect => mockRect,
// CodeMirror calls these to set up text ranges
setStart: (node: Node, offset: number): void => {
startContainer = node;
startOffset = offset;
},
setEnd: (node: Node, offset: number): void => {
endContainer = node;
endOffset = offset;
},
// Minimal Range properties (TypeScript requires these)
get startContainer(): Node {
return startContainer;
},
get endContainer(): Node {
return endContainer;
},
get startOffset(): number {
return startOffset;
},
get endOffset(): number {
return endOffset;
},
get collapsed(): boolean {
return startContainer === endContainer && startOffset === endOffset;
},
commonAncestorContainer: document.body,
};
return mockRange as unknown as Range;
};
// Mock document.createRange to return a new Range instance each time
document.createRange = (): Range => createMockRange();
// Mock getBoundingClientRect for elements
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
}

View File

@@ -9,7 +9,6 @@ import { extractQueryPairs } from 'utils/queryContextUtils';
import {
convertAggregationToExpression,
convertExpressionToFilters,
convertFiltersToExpression,
convertFiltersToExpressionWithExistingQuery,
formatValueForExpression,
@@ -1599,36 +1598,4 @@ describe('formatValueForExpression', () => {
expect(formatValueForExpression([123] as any)).toBe('[123]');
});
});
// Regression: an escaped single quote must not gain a backslash on each
// expression <-> filters round-trip (broke Create Alert from a panel).
describe('escaped quote round-trip', () => {
const EXPR = "name = 'it\\'s a ribbon'"; // name = 'it\'s a ribbon' (single backslash)
it('stores the unescaped value in the filter item', () => {
const items = convertExpressionToFilters(EXPR);
expect(items).toHaveLength(1);
expect(items[0].value).toBe("it's a ribbon");
});
it('is lossless: expression -> filters -> expression', () => {
const items = convertExpressionToFilters(EXPR);
expect(convertFiltersToExpression({ items, op: 'AND' }).expression).toBe(
EXPR,
);
});
it('is idempotent across repeated existing-query conversions', () => {
const pass1 = convertFiltersToExpressionWithExistingQuery(
{ items: [], op: 'AND' },
EXPR,
);
expect(pass1.filter.expression).toBe(EXPR);
const pass2 = convertFiltersToExpressionWithExistingQuery(
pass1.filters,
pass1.filter.expression,
);
expect(pass2.filter.expression).toBe(EXPR);
});
});
});

View File

@@ -176,9 +176,7 @@ function formatSingleValueForFilter(
}
if (isQuoted(value)) {
// Unescape `\'` → `'` (inverse of formatSingleValue) so the round-trip doesn't
// double the backslash each pass.
return unquote(value).replace(/\\'/g, "'");
return unquote(value);
}
}

View File

@@ -11,8 +11,6 @@ import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
import { isKeyMatch } from './utils';
import { CheckedState } from '../../types';
import { SectionType } from './v2/itemRules';
export const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
export const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in', 'nin'];
@@ -150,8 +148,6 @@ export function applyCheckboxToggle({
value,
checked,
isOnlyOrAllClicked,
previousState,
sectionType,
}: {
currentQuery: Query;
activeQueryIndex: number;
@@ -161,8 +157,6 @@ export function applyCheckboxToggle({
value: string;
checked: boolean;
isOnlyOrAllClicked: boolean;
previousState?: CheckedState;
sectionType?: SectionType;
}): Query {
const activeItems =
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items;
@@ -222,7 +216,6 @@ export function applyCheckboxToggle({
);
if (currentFilter) {
const runningOperator = currentFilter?.op;
switch (runningOperator) {
case 'in':
if (checked) {
@@ -253,29 +246,9 @@ export function applyCheckboxToggle({
});
}
} else if (!checked) {
// Related section: clicking to exclude creates NOT_IN for just this value
if (sectionType === SectionType.RELATED) {
const newFilter: TagFilterItem = {
id: uuid(),
op: getNotInOperator(source),
key: filter.attributeKey,
value,
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
});
if (query.filter?.expression) {
query.filter.expression = removeKeysFromExpression(
query.filter.expression,
[filter.attributeKey.key],
);
}
} else if (isArray(currentFilter.value)) {
// if we are removing some value when the running operator is IN we filter.
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
// if we are removing some value when the running operator is IN we filter.
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
value: currentFilter.value.filter((val) => val !== value),
@@ -302,38 +275,11 @@ export function applyCheckboxToggle({
}
break;
case 'nin':
case 'not in': {
// NOT IN means "exclude these values"
// Check if value is currently in the exclusion list
const isValueInFilter = isArray(currentFilter.value)
? currentFilter.value.includes(value)
: currentFilter.value === value;
// When clicking unchecked "Other" item, user wants to SELECT it
// Replace NOT IN filter with IN [value]
if (previousState === 'unchecked' && checked) {
const newFilter: TagFilterItem = {
id: uuid(),
op: getOperatorValue(OPERATORS.IN),
key: filter.attributeKey,
value,
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
});
if (query.filter?.expression) {
query.filter.expression = removeKeysFromExpression(
query.filter.expression,
[filter.attributeKey.key],
);
}
} else if (!checked || !isValueInFilter) {
// Add to NOT IN when:
// - checked=false (user explicitly unchecked to exclude)
// - checked=true but value not in filter (clicking "other" value to exclude)
case 'not in':
// if the current running operator is NIN then when unchecking the value it gets
// added to the clause like key NIN [value1 , currentUnselectedValue]
if (!checked) {
// in case of array add the currentUnselectedValue to the list.
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
@@ -346,6 +292,7 @@ export function applyCheckboxToggle({
return item;
});
} else {
// in case of not an array make it one!
const newFilter = {
...currentFilter,
value: [currentFilter.value as string, value],
@@ -357,9 +304,8 @@ export function applyCheckboxToggle({
return item;
});
}
} else {
// Remove from NOT IN when value IS in filter and checked=true
// (user wants to include this value back)
} else if (checked) {
// opposite of above!
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
@@ -400,7 +346,6 @@ export function applyCheckboxToggle({
}
}
break;
}
case '=':
if (checked) {
const newFilter = {
@@ -444,11 +389,10 @@ export function applyCheckboxToggle({
}
}
} else {
// No filter for this key - all are visually selected.
// checked=true → user wants to select (IN), checked=false → exclude (NOT IN)
// case - when there is no filter for the current key that means all are selected right now.
const newFilterItem: TagFilterItem = {
id: uuid(),
op: checked ? getOperatorValue(OPERATORS.IN) : getNotInOperator(source),
op: getNotInOperator(source),
key: filter.attributeKey,
value,
};

View File

@@ -10,8 +10,6 @@ import {
applyCheckboxToggle,
clearFilterFromQuery,
} from './checkboxFilterQuery';
import { CheckedState } from '../../types';
import { SectionType } from './v2/itemRules';
interface UseCheckboxFilterActionsProps {
filter: IQuickFiltersConfig;
@@ -26,8 +24,6 @@ interface UseCheckboxFilterActionsReturn {
value: string,
checked: boolean,
isOnlyOrAllClicked: boolean,
previousState?: CheckedState,
sectionType?: SectionType,
) => void;
onClear: () => void;
}
@@ -57,8 +53,6 @@ function useCheckboxFilterActions({
value: string,
checked: boolean,
isOnlyOrAllClicked: boolean,
previousState?: CheckedState,
sectionType?: SectionType,
): void => {
dispatch(
applyCheckboxToggle({
@@ -70,8 +64,6 @@ function useCheckboxFilterActions({
value,
checked,
isOnlyOrAllClicked,
previousState,
sectionType,
}),
);
};

View File

@@ -1,103 +0,0 @@
.checkboxFilter {
display: flex;
flex-direction: column;
padding: var(--spacing-6);
gap: var(--spacing-6);
border-bottom: 1px solid var(--l1-border);
}
.search {
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
--input-border-color: var(--l2-border);
--input-focus-border-color: var(--l2-border);
}
.searchSpinner {
color: var(--l2-foreground);
animation: spin 0.8s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.values {
display: flex;
flex-direction: column;
}
.sectionValues {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
.loadingValues {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-6) 0;
}
.loadingMore {
align-self: center;
}
.noData {
align-self: center;
}
.showMore {
display: flex;
align-items: center;
justify-content: center;
}
.showMoreText {
color: var(--accent-primary);
cursor: pointer;
}
.goToDocs {
display: flex;
flex-direction: column;
gap: 44px;
}
.goToDocsContainer {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
margin-top: var(--spacing-2);
}
.goToDocsMessage {
color: var(--l2-foreground);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
.goToDocsButton {
display: flex;
align-items: center;
gap: var(--spacing-2);
cursor: pointer;
margin: 0 0 var(--spacing-2);
padding: 0;
}
.goToDocsButtonText {
color: var(--bg-robin-400);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}

View File

@@ -1,126 +0,0 @@
import { render, RenderResult } from 'tests/test-utils';
import { server, rest } from 'mocks-server/server';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import {
FiltersType,
IQuickFiltersConfig,
QuickFilterCheckboxUseFieldApis,
QuickFiltersSource,
} from '../../../types';
import CheckboxFilterV2 from './CheckboxFilterV2';
export const DEFAULT_FILTER: IQuickFiltersConfig = {
type: FiltersType.CHECKBOX,
title: 'Environment',
attributeKey: {
key: 'deployment.environment',
dataType: DataTypes.String,
type: 'tag',
},
dataSource: DataSource.TRACES,
defaultOpen: true,
};
export const DEFAULT_USE_FIELD_APIS: QuickFilterCheckboxUseFieldApis = {
startUnixMilli: 1700000000000,
endUnixMilli: 1700003600000,
existingQuery: null,
};
export function mockFieldsValuesAPI(response: {
relatedValues?: (string | null)[];
stringValues?: (string | null)[];
numberValues?: (number | null)[];
}): void {
server.use(
rest.get('http://localhost/api/v1/fields/values', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: response.relatedValues ?? [],
stringValues: response.stringValues ?? [],
numberValues: response.numberValues ?? [],
},
},
}),
),
),
);
}
export function mockFieldsValuesAPILoading(): void {
server.use(
rest.get('http://localhost/api/v1/fields/values', (_, res, ctx) =>
res(ctx.delay(10000)),
),
);
}
export function setupServer(): void {
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
}
export interface FilterItemConfig {
op: string;
value: string | string[];
}
export function renderWithFilter(
onFilterChange: jest.Mock,
filterItem?: FilterItemConfig,
): RenderResult {
const items: TagFilterItem[] = filterItem
? [
{
key: { key: 'deployment.environment' },
op: filterItem.op,
value: filterItem.value,
} as TagFilterItem,
]
: [];
return render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
onFilterChange={onFilterChange}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items, op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
}
export function getFilterFromCall(
onFilterChange: jest.Mock,
callIndex = 0,
): TagFilterItem | undefined {
const query = onFilterChange.mock.calls[callIndex]?.[0] as Query | undefined;
return query?.builder.queryData[0]?.filters?.items?.find(
(item) => item.key?.key === 'deployment.environment',
);
}

View File

@@ -1,241 +0,0 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { LoaderCircle } from '@signozhq/icons';
import {
IQuickFiltersConfig,
QuickFilterCheckboxUseFieldApis,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { NON_SELECTED_OPERATORS } from '../checkboxFilterQuery';
import useActiveQueryIndex from '../useActiveQueryIndex';
import useCheckboxDisclosure from '../useCheckboxDisclosure';
import useCheckboxFilterActions from '../useCheckboxFilterActions';
import useCheckboxFilterState from '../useCheckboxFilterState';
import { useFieldValues } from './useFieldValues';
import { useExistingQuery } from './useExistingQuery';
import { isKeyMatch } from '../utils';
import { CheckboxFilterV2Header } from './CheckboxFilterV2Header';
import { CheckboxFilterV2Section } from './CheckboxFilterV2Section';
import { buildSelectedSet, useSectionedValues } from './useSectionedValues';
import { useStaleRelatedExclusions } from './useStaleRelatedExclusions';
import styles from './CheckboxFilterV2.module.scss';
interface CheckboxFilterV2Props {
filter: IQuickFiltersConfig;
source: QuickFiltersSource;
onFilterChange?: (query: Query) => void;
useFieldApis: QuickFilterCheckboxUseFieldApis;
}
export default function CheckboxFilterV2(
props: CheckboxFilterV2Props,
): JSX.Element {
const { source, filter, onFilterChange, useFieldApis } = props;
const [searchText, setSearchText] = useState<string>('');
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
const { currentQuery } = useQueryBuilder();
const activeQueryIndex = useActiveQueryIndex(source);
const {
isOpen,
isSomeFilterPresentForCurrentAttribute,
visibleItemsCount,
onToggleOpen,
onShowMore,
} = useCheckboxDisclosure({ filter, activeQueryIndex });
// Auto-preserve open state when filter is present
useEffect(() => {
if (isSomeFilterPresentForCurrentAttribute && userToggleState === null) {
setUserToggleState(true);
}
}, [isSomeFilterPresentForCurrentAttribute, userToggleState]);
const { existingQuery, hasExistingQuery } = useExistingQuery({
useFieldApis,
activeQueryIndex,
});
const { relatedValues, allValues, isLoading, isFetching } = useFieldValues({
filter,
searchText,
existingQuery,
metricNamespace: useFieldApis.metricNamespace,
startUnixMilli: useFieldApis.startUnixMilli,
endUnixMilli: useFieldApis.endUnixMilli,
enabled: isOpen,
});
// Track if initial load completed (don't show skeleton after first load)
// Must track if loading ever started, otherwise hasLoadedOnce gets set
// immediately on first render when query is disabled (isLoading=false)
const hasLoadedOnce = useRef(false);
const wasLoading = useRef(false);
if (isLoading) {
wasLoading.current = true;
}
if (!isLoading && wasLoading.current && !hasLoadedOnce.current) {
hasLoadedOnce.current = true;
}
// Combine for state derivation
const attributeValues = useMemo(() => {
const combined = [...relatedValues, ...allValues];
return [...new Set(combined)];
}, [relatedValues, allValues]);
const { currentFilterState, isFilterDisabled, isMultipleValuesTrueForTheKey } =
useCheckboxFilterState({ filter, attributeValues, activeQueryIndex });
const { onChange, onClear } = useCheckboxFilterActions({
filter,
source,
attributeValues,
activeQueryIndex,
onFilterChange,
});
const setSearchTextDebounced = useDebouncedFn((...args) => {
setSearchText(args[0] as string);
}, DEBOUNCE_DELAY);
const currentFilterOp = useMemo(() => {
const filterSync = currentQuery?.builder.queryData?.[
activeQueryIndex
]?.filters?.items.find((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
);
return filterSync?.op;
}, [
currentQuery?.builder.queryData,
activeQueryIndex,
filter.attributeKey.key,
]);
const isNotInOperator = NON_SELECTED_OPERATORS.includes(currentFilterOp || '');
const selectedValues = useMemo(
() => [
...buildSelectedSet(
currentFilterState,
isSomeFilterPresentForCurrentAttribute,
isNotInOperator,
),
],
[currentFilterState, isSomeFilterPresentForCurrentAttribute, isNotInOperator],
);
const isRefreshing = isFetching && !isLoading && searchText === '';
const relatedExclusions = useStaleRelatedExclusions({
selectedValues,
isFetching,
isRefreshing,
});
const { sections, totalCount } = useSectionedValues({
relatedValues,
allValues,
currentFilterState,
isSomeFilterPresentForCurrentAttribute,
isNotInOperator,
hasExistingQuery,
visibleItemsCount,
relatedExclusions,
});
return (
<div className={styles.checkboxFilter} data-testid="checkbox-filter-v2">
<CheckboxFilterV2Header
title={filter.title}
isOpen={isOpen}
showClearAll={!!attributeValues.length}
onToggleOpen={onToggleOpen}
onClear={onClear}
isSomeFilterPresentForCurrentAttribute={
isSomeFilterPresentForCurrentAttribute
}
/>
{isOpen && isLoading && !hasLoadedOnce.current && (
<section>
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && (!isLoading || hasLoadedOnce.current) && (
<>
<section className={styles.search}>
<Input
placeholder="Filter values"
onChange={(e): void => setSearchTextDebounced(e.target.value)}
disabled={isFilterDisabled}
data-testid="checkbox-filter-search"
suffix={
isFetching ? (
<LoaderCircle
size={14}
className={styles.searchSpinner}
data-testid="checkbox-filter-search-loading"
/>
) : null
}
/>
</section>
{totalCount > 0 && (
<section className={styles.values}>
{sections.map((section, index) => (
<CheckboxFilterV2Section
key={section.type}
section={section}
index={index}
isFilterDisabled={isFilterDisabled}
filter={filter}
isSomeFilterPresentForCurrentAttribute={
isSomeFilterPresentForCurrentAttribute
}
isMultipleValuesTrueForTheKey={isMultipleValuesTrueForTheKey}
onChange={onChange}
/>
))}
</section>
)}
{totalCount === 0 && hasLoadedOnce.current && !isFetching && (
<section
className={styles.noData}
data-testid={
searchText
? 'checkbox-filter-no-search-results'
: 'checkbox-filter-empty'
}
>
<Typography.Text>No values found</Typography.Text>
</section>
)}
{visibleItemsCount < totalCount &&
!(searchText && (isLoading || isFetching)) && (
<section className={styles.showMore}>
<Typography.Text
className={styles.showMoreText}
onClick={onShowMore}
data-testid="checkbox-filter-show-more"
>
Show More...
</Typography.Text>
</section>
)}
</>
)}
</div>
);
}

View File

@@ -1,33 +0,0 @@
.header {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.leftAction {
display: flex;
align-items: center;
gap: var(--spacing-3);
}
.title {
color: var(--l2-foreground);
font-size: 14px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
text-transform: capitalize;
}
.rightAction {
display: flex;
align-items: center;
min-width: 48px;
}
.clearAll {
font-size: 12px;
color: var(--accent-primary);
cursor: pointer;
}

View File

@@ -1,62 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import { ChevronDown, ChevronRight } from '@signozhq/icons';
import styles from './CheckboxFilterV2Header.module.scss';
interface CheckboxFilterHeaderProps {
title: string;
isOpen: boolean;
showClearAll: boolean;
onToggleOpen: () => void;
onClear: () => void;
isSomeFilterPresentForCurrentAttribute: boolean;
}
export function CheckboxFilterV2Header({
title,
isOpen,
showClearAll,
onToggleOpen,
onClear,
isSomeFilterPresentForCurrentAttribute,
}: CheckboxFilterHeaderProps): JSX.Element {
return (
<section
role="button"
tabIndex={0}
className={styles.header}
onClick={onToggleOpen}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
onToggleOpen();
}
}}
data-testid="checkbox-filter-header"
data-state={isOpen ? 'open' : 'closed'}
>
<section className={styles.leftAction}>
{isOpen ? (
<ChevronDown size={13} cursor="pointer" />
) : (
<ChevronRight size={13} cursor="pointer" />
)}
<Typography.Text className={styles.title}>{title}</Typography.Text>
</section>
<section className={styles.rightAction}>
{isOpen && showClearAll && isSomeFilterPresentForCurrentAttribute && (
<Typography.Text
className={styles.clearAll}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
onClear();
}}
data-testid="checkbox-filter-clear-all"
>
Clear
</Typography.Text>
)}
</section>
</section>
);
}

View File

@@ -1,106 +0,0 @@
import {
IQuickFiltersConfig,
CheckedState,
} from 'components/QuickFilters/types';
import { CheckboxFilterV2ValueRow } from './CheckboxFilterV2ValueRow';
import { SectionDivider } from './SectionDivider';
import { Section } from './useSectionedValues';
import { SectionType } from './itemRules';
import styles from './CheckboxFilterV2.module.scss';
interface SectionConfig {
label: string;
tooltip?: string;
}
function getSectionConfig(type: SectionType): SectionConfig | null {
switch (type) {
case SectionType.SELECTED:
return { label: 'Selected' };
case SectionType.RELATED:
return {
label: 'Related',
tooltip: 'Values that are filtered by your current selection.',
};
case SectionType.ALL_VALUES:
return { label: 'All values' };
default:
return null;
}
}
interface CheckboxFilterV2SectionProps {
section: Section;
index: number;
isFilterDisabled: boolean;
filter: IQuickFiltersConfig;
isSomeFilterPresentForCurrentAttribute: boolean;
isMultipleValuesTrueForTheKey: boolean;
onChange: (
value: string,
checked: boolean,
isOnly: boolean,
previousState?: CheckedState,
sectionType?: SectionType,
) => void;
}
export function CheckboxFilterV2Section(
props: CheckboxFilterV2SectionProps,
): JSX.Element | null {
const {
section,
index,
isFilterDisabled,
filter,
isSomeFilterPresentForCurrentAttribute,
isMultipleValuesTrueForTheKey,
onChange,
} = props;
if (section.items.length === 0) {
return null;
}
const config = getSectionConfig(section.type);
// Show divider for all sections except first SELECTED section
const showDivider =
config !== null && (index > 0 || section.type !== SectionType.SELECTED);
return (
<div data-testid={`section-${section.type}`} className={styles.sectionValues}>
{showDivider && config && (
<SectionDivider label={config.label} tooltip={config.tooltip} />
)}
{section.items.map(({ value, badge, checkedState }) => {
const isChecked = checkedState === 'checked';
return (
<CheckboxFilterV2ValueRow
key={value}
value={value}
checkedState={checkedState}
disabled={isFilterDisabled}
title={filter.title}
badge={badge}
onlyButtonLabel={
isSomeFilterPresentForCurrentAttribute
? isChecked && !isMultipleValuesTrueForTheKey
? 'All'
: 'Only'
: 'Only'
}
customRendererForValue={filter.customRendererForValue}
onCheckboxChange={(checked, previousState): void =>
onChange(value, checked, false, previousState, section.type)
}
onOnlyOrAllClick={(): void => onChange(value, isChecked, true)}
/>
);
})}
</div>
);
}

View File

@@ -1,164 +0,0 @@
.valueRow {
display: flex;
align-items: center;
gap: var(--spacing-4);
min-height: 24px;
}
.checkbox {
display: inline-flex;
align-items: center;
}
.valueButton {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: var(--spacing-2);
width: calc(100% - 24px);
cursor: pointer;
}
.content {
display: flex;
align-items: center;
gap: var(--spacing-2);
min-width: 0;
}
.valueLabel {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.actions {
display: grid;
align-items: center;
justify-items: end;
// Stack badge / only / toggle in a single cell so the crossfade overlaps
// instead of laying them side-by-side mid-transition.
> * {
grid-area: 1 / 1;
}
}
.badge {
opacity: 1;
transition:
opacity 0.16s ease,
display 0.16s allow-discrete;
}
.onlyButton {
display: none;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateX(4px);
transition:
opacity 0.16s ease,
transform 0.16s ease,
display 0.16s allow-discrete;
--button-height: 21px;
--button-padding: var(--spacing-5);
&:hover {
background-color: unset;
}
}
.toggleButton {
display: none;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateX(4px);
transition:
opacity 0.16s ease,
transform 0.16s ease,
display 0.16s allow-discrete;
--button-height: 21px;
--button-padding: var(--spacing-5);
&:hover {
background-color: unset;
}
}
.isDisabled {
cursor: not-allowed;
.valueLabel {
color: var(--l3-foreground);
}
.onlyButton {
cursor: not-allowed;
color: var(--l3-foreground);
}
.toggleButton {
cursor: not-allowed;
color: var(--l3-foreground);
}
}
.valueButton:hover {
.onlyButton {
display: flex;
opacity: 1;
transform: translateX(0);
@starting-style {
opacity: 0;
transform: translateX(4px);
}
}
.badge {
display: none;
opacity: 0;
}
}
.checkbox:hover ~ .valueButton {
.toggleButton {
display: flex;
opacity: 1;
transform: translateX(0);
@starting-style {
opacity: 0;
transform: translateX(4px);
}
}
.badge {
display: none;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.badge,
.onlyButton,
.toggleButton {
transition: none;
}
}
.indicatorFalse {
width: 2px;
height: 11px;
border-radius: 2px;
background: var(--danger-background);
}
.indicatorTrue {
width: 2px;
height: 11px;
border-radius: 2px;
background: var(--bg-forest-500);
}

View File

@@ -1,118 +0,0 @@
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { BadgeConfig } from './itemRules';
import { CheckedState } from '../../../types';
import styles from './CheckboxFilterV2ValueRow.module.scss';
interface ValueRowProps {
value: string;
checkedState: CheckedState;
disabled: boolean;
title: string;
onlyButtonLabel: string;
customRendererForValue?: (value: string) => JSX.Element;
onCheckboxChange: (checked: boolean, previousState: CheckedState) => void;
onOnlyOrAllClick: () => void;
badge: BadgeConfig | null;
}
function toCheckboxValue(state: CheckedState): boolean | 'indeterminate' {
if (state === 'indeterminate') {
return 'indeterminate';
}
return state === 'checked';
}
const INDICATOR_CLASS_MAP = {
false: styles.indicatorFalse,
true: styles.indicatorTrue,
} as Record<string, string>;
export function CheckboxFilterV2ValueRow({
value,
checkedState,
disabled,
title,
onlyButtonLabel,
customRendererForValue,
onCheckboxChange,
onOnlyOrAllClick,
badge,
}: ValueRowProps): JSX.Element {
const indicatorClass = INDICATOR_CLASS_MAP[value];
return (
<div
className={styles.valueRow}
data-testid={`checkbox-value-row-${value}`}
data-state={checkedState}
data-disabled={disabled}
>
<div className={styles.checkbox}>
<Checkbox
onChange={(isChecked): void =>
onCheckboxChange(isChecked === true, checkedState)
}
value={toCheckboxValue(checkedState)}
disabled={disabled}
color="primary"
testId={`checkbox-${title}-${value}`}
/>
</div>
<div
role="button"
tabIndex={disabled ? -1 : 0}
className={cx(styles.valueButton, disabled && styles.isDisabled)}
onClick={(): void => {
if (disabled) {
return;
}
onOnlyOrAllClick();
}}
onKeyDown={(e): void => {
if (disabled) {
return;
}
if (e.key === 'Enter' || e.key === ' ') {
onOnlyOrAllClick();
}
}}
>
<div className={styles.content}>
{indicatorClass && <div className={indicatorClass} />}
{customRendererForValue ? (
customRendererForValue(value)
) : (
<Typography.Text title={value} className={styles.valueLabel}>
{value}
</Typography.Text>
)}
</div>
<div className={styles.actions}>
{badge && (
<Badge
variant="outline"
color={badge.color}
className={styles.badge}
testId={`badge-${badge.key}`}
>
{badge.label}
</Badge>
)}
<Button variant="ghost" color="secondary" className={styles.onlyButton}>
{onlyButtonLabel}
</Button>
<Button variant="ghost" color="secondary" className={styles.toggleButton}>
Toggle
</Button>
</div>
</div>
</div>
);
}

View File

@@ -1,29 +0,0 @@
.divider {
display: flex;
align-items: center;
gap: var(--spacing-4);
padding: var(--spacing-2) 0;
margin-bottom: calc(-1 * var(--spacing-4));
}
.line {
flex: 1;
height: 1px;
background: var(--l1-border);
}
.label {
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--l3-foreground);
white-space: nowrap;
}
.infoIcon {
color: var(--l3-foreground);
cursor: help;
flex-shrink: 0;
}

View File

@@ -1,33 +0,0 @@
import { Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Info } from '@signozhq/icons';
import styles from './SectionDivider.module.scss';
interface SectionDividerProps {
label: string;
tooltip?: string;
}
export function SectionDivider({
label,
tooltip,
}: SectionDividerProps): JSX.Element {
const testId = label.toLowerCase().replace(/\s+/g, '-');
return (
<div className={styles.divider} data-testid={`section-divider-${testId}`}>
<div className={styles.line} />
<Typography.Text className={styles.label}>{label}</Typography.Text>
{tooltip && (
<Tooltip title={tooltip}>
<Info
size={12}
className={styles.infoIcon}
data-testid="section-divider-info"
/>
</Tooltip>
)}
<div className={styles.line} />
</div>
);
}

View File

@@ -1,302 +0,0 @@
import { screen } from '@testing-library/react';
import { server, rest } from 'mocks-server/server';
import { render } from 'tests/test-utils';
import { QuickFiltersSource } from '../../../../types';
import CheckboxFilterV2 from '../CheckboxFilterV2';
import {
DEFAULT_FILTER,
DEFAULT_USE_FIELD_APIS,
setupServer,
} from '../CheckboxFilterV2.testUtils';
const USE_FIELD_APIS_AUTO_DERIVE = {
...DEFAULT_USE_FIELD_APIS,
existingQuery: undefined,
};
setupServer();
describe('CheckboxFilterV2 - existingQuery calculation', () => {
const captureExistingQuery = (): Promise<string | null> =>
new Promise((resolve) => {
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
const existingQuery = req.url.searchParams.get('existingQuery');
resolve(existingQuery);
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: ['test'],
numberValues: [],
},
},
}),
);
}),
);
});
describe('useFieldApis.existingQuery takes precedence', () => {
it('uses useFieldApis.existingQuery when provided', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'custom.query = "value"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'should.be.ignored = "yes"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe('custom.query = "value"');
});
it('returns undefined when useFieldApis.existingQuery is null', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: null,
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'should.be.ignored = "yes"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBeNull();
});
});
describe('V5 filter.expression preferred over V3 filters.items', () => {
it('uses V5 filter.expression when both exist', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'service.name', dataType: 'string', type: 'tag' },
op: '=',
value: 'from-v3-items',
},
],
op: 'AND',
},
filter: { expression: 'v5.expression = "preferred"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe('v5.expression = "preferred"');
});
it('uses V5 filter.expression when no V3 items exist', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'only.v5 = "expression"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe('only.v5 = "expression"');
});
});
describe('V3 filters.items fallback', () => {
it('converts V3 filters.items to expression when no V5 expression exists', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'service.name', dataType: 'string', type: 'tag' },
op: '=',
value: 'api-service',
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe("service.name = 'api-service'");
});
it('converts multiple V3 filters.items with AND operator', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'service.name', dataType: 'string', type: 'tag' },
op: '=',
value: 'api',
},
{
key: { key: 'env', dataType: 'string', type: 'tag' },
op: '=',
value: 'prod',
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe("service.name = 'api' AND env = 'prod'");
});
it('returns undefined when no filters exist', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBeNull();
});
});
});

View File

@@ -1,768 +0,0 @@
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { server, rest } from 'mocks-server/server';
import { render } from 'tests/test-utils';
import { QuickFiltersSource } from '../../../../types';
import CheckboxFilterV2 from '../CheckboxFilterV2';
import {
DEFAULT_FILTER,
DEFAULT_USE_FIELD_APIS,
getFilterFromCall,
mockFieldsValuesAPI,
renderWithFilter,
setupServer,
} from '../CheckboxFilterV2.testUtils';
setupServer();
describe('CheckboxFilterV2 - interactions', () => {
describe('search functionality', () => {
it('filters values based on search text', async () => {
const user = userEvent.setup();
let searchTextReceived = '';
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
searchTextReceived = req.url.searchParams.get('searchText') || '';
const values =
searchTextReceived === ''
? ['production', 'staging', 'development']
: ['production'];
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: values,
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
expect(screen.getByTestId('checkbox-value-row-staging')).toBeInTheDocument();
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'prod');
await waitFor(() => {
expect(searchTextReceived).toBe('prod');
});
await waitFor(() => {
expect(
screen.queryByTestId('checkbox-value-row-staging'),
).not.toBeInTheDocument();
});
});
it('filters values via search while preserving existingQuery context', async () => {
const user = userEvent.setup();
let requestCount = 0;
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
requestCount += 1;
const searchText = req.url.searchParams.get('searchText') || '';
if (requestCount === 1) {
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: ['production'],
stringValues: ['staging', 'development'],
numberValues: [],
},
},
}),
);
}
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: searchText === 'prod' ? ['production'] : [],
stringValues: searchText === 'prod' ? ['production'] : ['staging'],
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-production');
// Related values now appear in "Related" section (no badge, uses divider instead)
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'prod');
await waitFor(() => {
expect(
screen.queryByTestId('checkbox-value-row-staging'),
).not.toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-production'),
).toBeInTheDocument();
});
});
it('shows filtered results in all_values section when searching with existingQuery', async () => {
const user = userEvent.setup();
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
const searchText = req.url.searchParams.get('searchText') || '';
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: searchText === '' ? ['prod', 'staging'] : ['prod-match'],
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
);
await screen.findByTestId('checkbox-value-row-prod');
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'prod');
await waitFor(() => {
expect(screen.getByTestId('section-all_values')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-prod-match'),
).toBeInTheDocument();
});
});
it('shows empty search results message when no matches found', async () => {
const user = userEvent.setup();
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
const searchText = req.url.searchParams.get('searchText') || '';
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: searchText === '' ? ['prod', 'staging'] : [],
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-prod');
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'xyz-no-match');
await waitFor(() => {
expect(
screen.getByTestId('checkbox-filter-no-search-results'),
).toBeInTheDocument();
});
});
it('shows both RELATED and ALL_VALUES sections with non-overlapping values', async () => {
// This tests the bug fix where different pod names in relatedValues vs stringValues
// caused all items to go to ALL_VALUES instead of showing RELATED section
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
// Non-overlapping: different pod instances
relatedValues: ['pod-a-instance-1', 'pod-b-instance-1'],
stringValues: ['pod-a-instance-2', 'pod-b-instance-2'],
numberValues: [],
},
},
}),
),
),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
);
// Wait for values to load
await screen.findByTestId('checkbox-value-row-pod-a-instance-1');
// RELATED section should exist with relatedValues
expect(screen.getByTestId('section-related')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-a-instance-1'),
).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-b-instance-1'),
).toBeInTheDocument();
// ALL_VALUES section should exist with stringValues
expect(screen.getByTestId('section-all_values')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-a-instance-2'),
).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-b-instance-2'),
).toBeInTheDocument();
});
it('shows both sections during search with filtered non-overlapping values', async () => {
const user = userEvent.setup();
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
const searchText = req.url.searchParams.get('searchText') || '';
// Simulate API filtering - both arrays filtered but remain non-overlapping
const relatedValues =
searchText === '' ? ['pod-a-v1', 'pod-b-v1', 'pod-c-v1'] : ['pod-a-v1']; // filtered to match 'pod-a'
const stringValues =
searchText === '' ? ['pod-a-v2', 'pod-b-v2', 'pod-c-v2'] : ['pod-a-v2']; // filtered to match 'pod-a'
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues,
stringValues,
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
);
await screen.findByTestId('checkbox-value-row-pod-a-v1');
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'pod-a');
// After search, both sections should still appear with filtered results
await waitFor(() => {
// RELATED section with filtered relatedValues
expect(screen.getByTestId('section-related')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-a-v1'),
).toBeInTheDocument();
// ALL_VALUES section with filtered stringValues
expect(screen.getByTestId('section-all_values')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-a-v2'),
).toBeInTheDocument();
// Other values should be filtered out
expect(
screen.queryByTestId('checkbox-value-row-pod-b-v1'),
).not.toBeInTheDocument();
});
});
});
describe('header interactions', () => {
it('collapses when header clicked on open filter', async () => {
const user = userEvent.setup();
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
const header = screen.getByTestId('checkbox-filter-header');
expect(header).toHaveAttribute('data-state', 'open');
await user.click(header);
expect(header).toHaveAttribute('data-state', 'closed');
expect(
screen.queryByTestId('checkbox-value-row-production'),
).not.toBeInTheDocument();
});
it('expands when header clicked on closed filter', async () => {
const user = userEvent.setup();
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const header = screen.getByTestId('checkbox-filter-header');
expect(header).toHaveAttribute('data-state', 'closed');
await user.click(header);
expect(header).toHaveAttribute('data-state', 'open');
await screen.findByTestId('checkbox-value-row-production');
});
});
describe('show more functionality', () => {
it('shows "Show More..." when more than 10 values', async () => {
const values = Array.from(
{ length: 15 },
(_, i) => `value-${String(i).padStart(2, '0')}`,
);
mockFieldsValuesAPI({ stringValues: values });
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-value-00');
expect(screen.getByTestId('checkbox-filter-show-more')).toBeInTheDocument();
expect(
screen.queryByTestId('checkbox-value-row-value-10'),
).not.toBeInTheDocument();
});
it('loads more values when "Show More..." clicked', async () => {
const user = userEvent.setup();
const values = Array.from(
{ length: 15 },
(_, i) => `value-${String(i).padStart(2, '0')}`,
);
mockFieldsValuesAPI({ stringValues: values });
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-value-00');
await user.click(screen.getByTestId('checkbox-filter-show-more'));
await screen.findByTestId('checkbox-value-row-value-10');
expect(
screen.getByTestId('checkbox-value-row-value-14'),
).toBeInTheDocument();
});
});
describe('clear functionality', () => {
it('shows clear button when filter is open and has filter applied', async () => {
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['production'],
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-production');
expect(screen.getByTestId('checkbox-filter-clear-all')).toBeInTheDocument();
});
it('hides clear button when no filter applied for attribute', async () => {
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
expect(
screen.queryByTestId('checkbox-filter-clear-all'),
).not.toBeInTheDocument();
});
it('calls onFilterChange when clear clicked', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
onFilterChange={onFilterChange}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['production'],
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-production');
await user.click(screen.getByTestId('checkbox-filter-clear-all'));
expect(onFilterChange).toHaveBeenCalled();
});
});
describe('value row interactions', () => {
it('calls onFilterChange when checkbox value clicked', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
stringValues: ['production', 'staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
onFilterChange={onFilterChange}
/>,
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
await user.click(within(productionRow).getByText('production'));
expect(onFilterChange).toHaveBeenCalled();
});
it('creates NOT IN filter when unchecking related item with no existing filter', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['valueA'],
stringValues: ['valueB'],
});
// Start with no filter, related items show as checked
// Clicking unchecks them → creates NOT IN filter to exclude
renderWithFilter(onFilterChange);
const rowA = await screen.findByTestId('checkbox-value-row-valueA');
expect(rowA).toHaveAttribute('data-state', 'checked');
await user.click(within(rowA).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('not in');
expect(filter?.value).toBe('valueA');
});
it('converts NOT IN to IN when toggling unchecked (other) item', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['valueA'],
stringValues: ['valueB'],
});
// Clicking unchecked "Other" item with NOT IN filter should convert to IN [B]
renderWithFilter(onFilterChange, { op: 'not in', value: ['valueA'] });
const rowB = await screen.findByTestId('checkbox-value-row-valueB');
expect(rowB).toHaveAttribute('data-state', 'unchecked');
await user.click(within(rowB).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('in');
expect(filter?.value).toBe('valueB');
});
it('accumulates both values in IN when toggling checked (related) then unchecked (other)', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['valueA'],
stringValues: ['valueB'],
});
// Start with IN filter for valueA
renderWithFilter(onFilterChange, { op: 'in', value: ['valueA'] });
const rowA = await screen.findByTestId('checkbox-value-row-valueA');
expect(rowA).toHaveAttribute('data-state', 'checked');
const rowB = screen.getByTestId('checkbox-value-row-valueB');
expect(rowB).toHaveAttribute('data-state', 'unchecked');
// Toggle B (unchecked -> should add to IN)
await user.click(within(rowB).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('in');
expect(filter?.value).toStrictEqual(['valueA', 'valueB']);
});
it('adds to NOT IN when toggling checked (related) with existing NOT IN filter', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['valueA'],
stringValues: ['valueB'],
});
// Start with NOT IN filter for valueB
renderWithFilter(onFilterChange, { op: 'not in', value: ['valueB'] });
const rowA = await screen.findByTestId('checkbox-value-row-valueA');
// Related values show as checked
expect(rowA).toHaveAttribute('data-state', 'checked');
// Toggle A (checked -> should add to NOT IN)
await user.click(within(rowA).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('not in');
expect(filter?.value).toStrictEqual(['valueB', 'valueA']);
});
it('creates NOT IN for single value when toggling related item with existing IN filter', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['relatedValue'],
stringValues: ['otherValue'],
});
// Start with IN filter for otherValue (Selected section)
// relatedValue is in Related section (checked visually)
renderWithFilter(onFilterChange, { op: 'in', value: ['otherValue'] });
const selectedRow = await screen.findByTestId(
'checkbox-value-row-otherValue',
);
expect(selectedRow).toHaveAttribute('data-state', 'checked');
const relatedRow = screen.getByTestId('checkbox-value-row-relatedValue');
expect(relatedRow).toHaveAttribute('data-state', 'checked');
// Toggle related item (checked -> should create NOT IN with just this value)
await user.click(within(relatedRow).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('not in');
expect(filter?.value).toBe('relatedValue');
});
});
describe('custom renderer', () => {
it('uses customRendererForValue when provided', async () => {
mockFieldsValuesAPI({
stringValues: ['production'],
});
const customRenderer = (value: string): JSX.Element => (
<span data-testid="custom-rendered">{`ENV: ${value}`}</span>
);
render(
<CheckboxFilterV2
filter={{ ...DEFAULT_FILTER, customRendererForValue: customRenderer }}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('custom-rendered');
expect(screen.getByText('ENV: production')).toBeInTheDocument();
});
});
});

View File

@@ -1,499 +0,0 @@
import { screen, within } from '@testing-library/react';
import { render } from 'tests/test-utils';
import { QuickFiltersSource } from '../../../../types';
import CheckboxFilterV2 from '../CheckboxFilterV2';
import {
DEFAULT_FILTER,
DEFAULT_USE_FIELD_APIS,
mockFieldsValuesAPI,
setupServer,
} from '../CheckboxFilterV2.testUtils';
setupServer();
describe('CheckboxFilterV2 - item rules', () => {
describe('no existing query', () => {
it('all values show as checked with no badge when no query exists', async () => {
mockFieldsValuesAPI({
stringValues: ['production', 'staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(within(productionRow).getByText('production')).toBeInTheDocument();
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(productionRow).toHaveAttribute('data-state', 'checked');
expect(stagingRow).toHaveAttribute('data-state', 'checked');
// No section dividers when no existing query (all in "selected" section)
expect(
screen.queryByTestId('section-divider-related'),
).not.toBeInTheDocument();
expect(
screen.queryByTestId('section-divider-all-values'),
).not.toBeInTheDocument();
});
});
describe('with existing query (related values)', () => {
it('shows related values in "Related" section with checked state', async () => {
mockFieldsValuesAPI({
relatedValues: ['production'],
stringValues: ['staging', 'development'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(within(productionRow).getByText('production')).toBeInTheDocument();
// Related values show as checked in "Related" section
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
expect(productionRow).toHaveAttribute('data-state', 'checked');
// Other values show as unchecked in "All values" section
expect(screen.getByTestId('section-divider-all-values')).toBeInTheDocument();
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(stagingRow).toHaveAttribute('data-state', 'unchecked');
});
it('shows no badge for values not in relatedValues (unchecked state in All values section)', async () => {
mockFieldsValuesAPI({
relatedValues: ['production'],
stringValues: ['staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
const stagingRow = await screen.findByTestId('checkbox-value-row-staging');
expect(within(stagingRow).getByText('staging')).toBeInTheDocument();
expect(stagingRow).toHaveAttribute('data-state', 'unchecked');
expect(within(stagingRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
});
it('shows related values as checked when hasFilterForThisKey=true and isInRelatedValues=true', async () => {
mockFieldsValuesAPI({
relatedValues: ['production', 'staging'],
stringValues: ['development'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['production'],
},
],
op: 'AND',
},
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
// production is selected - in selected section
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(productionRow).toHaveAttribute('data-state', 'checked');
// staging is related - in related section with checked state
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(stagingRow).toHaveAttribute('data-state', 'checked');
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
});
});
describe('selected values with IN operator', () => {
it('shows checked state with no badge for IN-selected values', async () => {
mockFieldsValuesAPI({
stringValues: ['production', 'staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['production'],
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(productionRow).toHaveAttribute('data-state', 'checked');
expect(
within(productionRow).queryByTestId(/^badge-/),
).not.toBeInTheDocument();
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(stagingRow).toHaveAttribute('data-state', 'unchecked');
});
});
describe('selected values with NOT IN operator', () => {
it('shows unchecked state with no badge for NOT_IN-selected values', async () => {
mockFieldsValuesAPI({
stringValues: ['production', 'staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'not in',
value: ['production'],
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(productionRow).toHaveAttribute('data-state', 'unchecked');
expect(
within(productionRow).queryByTestId(/^badge-/),
).not.toBeInTheDocument();
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(stagingRow).toHaveAttribute('data-state', 'unchecked');
expect(within(stagingRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
});
});
describe('section ordering', () => {
it('orders selected section before related section before other section', async () => {
mockFieldsValuesAPI({
relatedValues: ['related-value'],
stringValues: ['other-value', 'selected-value'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['selected-value'],
},
],
op: 'AND',
},
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-selected-value');
const allRows = screen.getAllByTestId(/^checkbox-value-row-/);
const values = allRows.map((row) =>
row.getAttribute('data-testid')?.replace('checkbox-value-row-', ''),
);
expect(values[0]).toBe('selected-value');
expect(values[1]).toBe('related-value');
expect(values[2]).toBe('other-value');
});
it('sorts alphabetically within same section', async () => {
mockFieldsValuesAPI({
relatedValues: ['zebra', 'alpha', 'mike'],
stringValues: [],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-alpha');
const allRows = screen.getAllByTestId(/^checkbox-value-row-/);
const values = allRows.map((row) =>
row.getAttribute('data-testid')?.replace('checkbox-value-row-', ''),
);
expect(values).toStrictEqual(['alpha', 'mike', 'zebra']);
});
});
describe('mixed state scenarios', () => {
it('handles mixed state: IN-selected + related + other in same list', async () => {
mockFieldsValuesAPI({
relatedValues: ['related-env'],
stringValues: ['other-env', 'selected-env'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['selected-env'],
},
],
op: 'AND',
},
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
const selectedRow = await screen.findByTestId(
'checkbox-value-row-selected-env',
);
expect(selectedRow).toHaveAttribute('data-state', 'checked');
expect(within(selectedRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
// Related values now show as checked
const relatedRow = screen.getByTestId('checkbox-value-row-related-env');
expect(relatedRow).toHaveAttribute('data-state', 'checked');
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
const otherRow = screen.getByTestId('checkbox-value-row-other-env');
expect(otherRow).toHaveAttribute('data-state', 'unchecked');
expect(within(otherRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
});
it('handles NOT_IN-selected alongside related values', async () => {
mockFieldsValuesAPI({
relatedValues: ['related-env'],
stringValues: ['other-env', 'excluded-env'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'not in',
value: ['excluded-env'],
},
],
op: 'AND',
},
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
const excludedRow = await screen.findByTestId(
'checkbox-value-row-excluded-env',
);
expect(excludedRow).toHaveAttribute('data-state', 'unchecked');
expect(within(excludedRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
// Related values show as checked
const relatedRow = screen.getByTestId('checkbox-value-row-related-env');
expect(relatedRow).toHaveAttribute('data-state', 'checked');
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
});
});
});

View File

@@ -1,207 +0,0 @@
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { server, rest } from 'mocks-server/server';
import { render } from 'tests/test-utils';
import { QuickFiltersSource } from '../../../../types';
import CheckboxFilterV2 from '../CheckboxFilterV2';
import {
DEFAULT_FILTER,
DEFAULT_USE_FIELD_APIS,
mockFieldsValuesAPI,
mockFieldsValuesAPILoading,
setupServer,
} from '../CheckboxFilterV2.testUtils';
setupServer();
describe('CheckboxFilterV2 - states', () => {
describe('loading states', () => {
it('shows skeleton while loading initial data', async () => {
mockFieldsValuesAPILoading();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
expect(screen.getByTestId('checkbox-filter-v2')).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByTestId('checkbox-filter-v2').querySelector('.ant-skeleton'),
).toBeInTheDocument();
});
});
it('shows skeleton when initially closed filter is opened for the first time', async () => {
const user = userEvent.setup();
mockFieldsValuesAPILoading();
const closedFilter = { ...DEFAULT_FILTER, defaultOpen: false };
render(
<CheckboxFilterV2
filter={closedFilter}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
// Filter starts closed - no skeleton, no content
expect(
screen.getByTestId('checkbox-filter-v2').querySelector('.ant-skeleton'),
).not.toBeInTheDocument();
expect(
screen.queryByTestId('checkbox-filter-empty'),
).not.toBeInTheDocument();
// Click header to open
const header = screen.getByTestId('checkbox-filter-header');
await user.click(header);
// Should show skeleton while loading, NOT "No values found"
await waitFor(() => {
expect(
screen.getByTestId('checkbox-filter-v2').querySelector('.ant-skeleton'),
).toBeInTheDocument();
});
expect(
screen.queryByTestId('checkbox-filter-empty'),
).not.toBeInTheDocument();
});
it('shows search spinner when fetching after initial load', async () => {
const user = userEvent.setup();
let requestCount = 0;
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
requestCount += 1;
if (requestCount === 1) {
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: ['production', 'staging'],
numberValues: [],
},
},
}),
);
}
return res(ctx.delay(10000));
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'prod');
await waitFor(() => {
expect(
screen.getByTestId('checkbox-filter-search-loading'),
).toBeInTheDocument();
});
});
});
describe('empty states', () => {
it('shows "No values found" when API returns empty arrays', async () => {
mockFieldsValuesAPI({
relatedValues: [],
stringValues: [],
numberValues: [],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const emptySection = await screen.findByTestId('checkbox-filter-empty');
expect(emptySection).toBeInTheDocument();
});
});
describe('value rendering', () => {
it('renders values from API response', async () => {
mockFieldsValuesAPI({
stringValues: ['production', 'staging', 'development'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
expect(screen.getByTestId('checkbox-value-row-staging')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-development'),
).toBeInTheDocument();
});
it('renders number values converted to strings', async () => {
mockFieldsValuesAPI({
numberValues: [200, 404, 500],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const row200 = await screen.findByTestId('checkbox-value-row-200');
expect(within(row200).getByText('200')).toBeInTheDocument();
expect(
within(screen.getByTestId('checkbox-value-row-404')).getByText('404'),
).toBeInTheDocument();
expect(
within(screen.getByTestId('checkbox-value-row-500')).getByText('500'),
).toBeInTheDocument();
});
it('filters null/undefined values from response', async () => {
mockFieldsValuesAPI({
stringValues: ['valid', null, '', undefined as unknown as string],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const validRow = await screen.findByTestId('checkbox-value-row-valid');
expect(within(validRow).getByText('valid')).toBeInTheDocument();
expect(screen.queryAllByTestId(/^checkbox-value-row-/)).toHaveLength(1);
});
});
});

View File

@@ -1,156 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CheckboxFilterV2Header } from '../CheckboxFilterV2Header';
describe('CheckboxFilterV2Header', () => {
const defaultProps = {
title: 'Environment',
isOpen: false,
showClearAll: true,
isSomeFilterPresentForCurrentAttribute: true,
onToggleOpen: jest.fn(),
onClear: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('collapsed state', () => {
it('renders title', () => {
render(<CheckboxFilterV2Header {...defaultProps} isOpen={false} />);
expect(screen.getByText('Environment')).toBeInTheDocument();
});
it('sets data-state="closed" when collapsed', () => {
render(<CheckboxFilterV2Header {...defaultProps} isOpen={false} />);
const header = screen.getByTestId('checkbox-filter-header');
expect(header).toHaveAttribute('data-state', 'closed');
});
it('does not show clear button when collapsed', () => {
render(
<CheckboxFilterV2Header {...defaultProps} isOpen={false} showClearAll />,
);
expect(
screen.queryByTestId('checkbox-filter-clear-all'),
).not.toBeInTheDocument();
});
});
describe('expanded state', () => {
it('sets data-state="open" when expanded', () => {
render(<CheckboxFilterV2Header {...defaultProps} isOpen />);
const header = screen.getByTestId('checkbox-filter-header');
expect(header).toHaveAttribute('data-state', 'open');
});
it('shows clear button when expanded + showClearAll=true', () => {
render(<CheckboxFilterV2Header {...defaultProps} isOpen showClearAll />);
expect(screen.getByTestId('checkbox-filter-clear-all')).toBeInTheDocument();
expect(screen.getByText('Clear')).toBeInTheDocument();
});
it('hides clear button when showClearAll=false', () => {
render(
<CheckboxFilterV2Header {...defaultProps} isOpen showClearAll={false} />,
);
expect(
screen.queryByTestId('checkbox-filter-clear-all'),
).not.toBeInTheDocument();
});
it('hides clear button when no filter present for attribute', () => {
render(
<CheckboxFilterV2Header
{...defaultProps}
isOpen
showClearAll
isSomeFilterPresentForCurrentAttribute={false}
/>,
);
expect(
screen.queryByTestId('checkbox-filter-clear-all'),
).not.toBeInTheDocument();
});
});
describe('interactions', () => {
it('calls onToggleOpen on header click', async () => {
const user = userEvent.setup();
const onToggleOpen = jest.fn();
render(
<CheckboxFilterV2Header {...defaultProps} onToggleOpen={onToggleOpen} />,
);
await user.click(screen.getByTestId('checkbox-filter-header'));
expect(onToggleOpen).toHaveBeenCalledTimes(1);
});
it('calls onToggleOpen on Enter key', async () => {
const user = userEvent.setup();
const onToggleOpen = jest.fn();
render(
<CheckboxFilterV2Header {...defaultProps} onToggleOpen={onToggleOpen} />,
);
screen.getByTestId('checkbox-filter-header').focus();
await user.keyboard('{Enter}');
expect(onToggleOpen).toHaveBeenCalledTimes(1);
});
it('calls onToggleOpen on Space key', async () => {
const user = userEvent.setup();
const onToggleOpen = jest.fn();
render(
<CheckboxFilterV2Header {...defaultProps} onToggleOpen={onToggleOpen} />,
);
screen.getByTestId('checkbox-filter-header').focus();
await user.keyboard(' ');
expect(onToggleOpen).toHaveBeenCalledTimes(1);
});
it('calls onClear on clear button click', async () => {
const user = userEvent.setup();
const onClear = jest.fn();
render(
<CheckboxFilterV2Header {...defaultProps} isOpen onClear={onClear} />,
);
await user.click(screen.getByTestId('checkbox-filter-clear-all'));
expect(onClear).toHaveBeenCalledTimes(1);
});
it('clear button click does not trigger onToggleOpen', async () => {
const user = userEvent.setup();
const onToggleOpen = jest.fn();
const onClear = jest.fn();
render(
<CheckboxFilterV2Header
{...defaultProps}
isOpen
onToggleOpen={onToggleOpen}
onClear={onClear}
/>,
);
await user.click(screen.getByTestId('checkbox-filter-clear-all'));
expect(onClear).toHaveBeenCalledTimes(1);
expect(onToggleOpen).not.toHaveBeenCalled();
});
});
});

View File

@@ -1,297 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BadgeConfig } from '../itemRules';
import { CheckedState } from '../../../../types';
import { CheckboxFilterV2ValueRow } from '../CheckboxFilterV2ValueRow';
describe('CheckboxFilterV2ValueRow', () => {
const defaultProps = {
value: 'production',
checkedState: 'unchecked' as CheckedState,
disabled: false,
title: 'Environment',
onlyButtonLabel: 'Only',
onCheckboxChange: jest.fn(),
onOnlyOrAllClick: jest.fn(),
badge: null as BadgeConfig | null,
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('checked states', () => {
it('sets data-state="unchecked" for unchecked state', () => {
render(
<CheckboxFilterV2ValueRow {...defaultProps} checkedState="unchecked" />,
);
const row = screen.getByTestId('checkbox-value-row-production');
expect(row).toHaveAttribute('data-state', 'unchecked');
});
it('sets data-state="checked" for checked state', () => {
render(
<CheckboxFilterV2ValueRow {...defaultProps} checkedState="checked" />,
);
const row = screen.getByTestId('checkbox-value-row-production');
expect(row).toHaveAttribute('data-state', 'checked');
});
});
describe('badge variations', () => {
it('renders no badge when badge=null', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} badge={null} />);
expect(screen.queryByTestId(/^badge-/)).not.toBeInTheDocument();
});
it('renders "Not in" warning badge', () => {
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
badge={{ key: 'not_in', label: 'Not in', color: 'warning' }}
/>,
);
expect(screen.getByTestId('badge-not_in')).toBeInTheDocument();
expect(screen.getByText('Not in')).toBeInTheDocument();
});
it('renders "Related" robin badge', () => {
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
badge={{ key: 'related', label: 'Related', color: 'robin' }}
/>,
);
expect(screen.getByTestId('badge-related')).toBeInTheDocument();
expect(screen.getByText('Related')).toBeInTheDocument();
});
it('renders "Other" secondary badge', () => {
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
badge={{ key: 'other', label: 'Other', color: 'secondary' }}
/>,
);
expect(screen.getByTestId('badge-other')).toBeInTheDocument();
expect(screen.getByText('Other')).toBeInTheDocument();
});
});
describe('only/all button label', () => {
it('shows "Only" label by default', () => {
render(
<CheckboxFilterV2ValueRow {...defaultProps} onlyButtonLabel="Only" />,
);
expect(screen.getByText('Only')).toBeInTheDocument();
});
it('shows "All" label when appropriate', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} onlyButtonLabel="All" />);
expect(screen.getByText('All')).toBeInTheDocument();
});
});
describe('disabled state', () => {
it('sets data-disabled=true when disabled', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} disabled />);
const row = screen.getByTestId('checkbox-value-row-production');
expect(row).toHaveAttribute('data-disabled', 'true');
});
it('does not call onOnlyOrAllClick when disabled + clicked', async () => {
const user = userEvent.setup();
const onOnlyOrAllClick = jest.fn();
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
disabled
onOnlyOrAllClick={onOnlyOrAllClick}
/>,
);
await user.click(screen.getByText('production'));
expect(onOnlyOrAllClick).not.toHaveBeenCalled();
});
it('does not call onOnlyOrAllClick on keydown when disabled', async () => {
const user = userEvent.setup();
const onOnlyOrAllClick = jest.fn();
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
disabled
onOnlyOrAllClick={onOnlyOrAllClick}
/>,
);
screen.getByText('production').focus();
await user.keyboard('{Enter}');
expect(onOnlyOrAllClick).not.toHaveBeenCalled();
});
});
describe('special value indicators', () => {
it('renders row for "true" value', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} value="true" />);
expect(screen.getByTestId('checkbox-value-row-true')).toBeInTheDocument();
expect(screen.getByText('true')).toBeInTheDocument();
});
it('renders row for "false" value', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} value="false" />);
expect(screen.getByTestId('checkbox-value-row-false')).toBeInTheDocument();
expect(screen.getByText('false')).toBeInTheDocument();
});
it('renders row for regular values', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} value="production" />);
expect(
screen.getByTestId('checkbox-value-row-production'),
).toBeInTheDocument();
expect(screen.getByText('production')).toBeInTheDocument();
});
});
describe('interactions', () => {
it('renders checkbox with correct testId', () => {
render(
<CheckboxFilterV2ValueRow {...defaultProps} checkedState="unchecked" />,
);
expect(
screen.getByTestId('checkbox-Environment-production'),
).toBeInTheDocument();
});
it('calls onOnlyOrAllClick on value text click', async () => {
const user = userEvent.setup();
const onOnlyOrAllClick = jest.fn();
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
onOnlyOrAllClick={onOnlyOrAllClick}
/>,
);
await user.click(screen.getByText('production'));
expect(onOnlyOrAllClick).toHaveBeenCalledTimes(1);
});
it('calls onOnlyOrAllClick on Enter key', async () => {
const user = userEvent.setup();
const onOnlyOrAllClick = jest.fn();
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
onOnlyOrAllClick={onOnlyOrAllClick}
/>,
);
const valueButton = screen
.getByText('production')
.closest('[role="button"]');
await user.tab();
await user.tab();
if (valueButton && document.activeElement === valueButton) {
await user.keyboard('{Enter}');
}
expect(onOnlyOrAllClick).toHaveBeenCalled();
});
it('calls onOnlyOrAllClick on Space key', async () => {
const user = userEvent.setup();
const onOnlyOrAllClick = jest.fn();
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
onOnlyOrAllClick={onOnlyOrAllClick}
/>,
);
const valueButton = screen
.getByText('production')
.closest('[role="button"]');
await user.tab();
await user.tab();
if (valueButton && document.activeElement === valueButton) {
await user.keyboard(' ');
}
expect(onOnlyOrAllClick).toHaveBeenCalled();
});
it('shows Toggle button', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} />);
expect(screen.getByText('Toggle')).toBeInTheDocument();
});
});
describe('custom renderer', () => {
it('uses customRendererForValue when provided', () => {
const customRenderer = (value: string): JSX.Element => (
<span data-testid="custom-render">{`Custom: ${value}`}</span>
);
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
customRendererForValue={customRenderer}
/>,
);
expect(screen.getByTestId('custom-render')).toBeInTheDocument();
expect(screen.getByText('Custom: production')).toBeInTheDocument();
});
it('shows default value text when no custom renderer', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} />);
expect(screen.getByText('production')).toBeInTheDocument();
});
});
describe('state combinations', () => {
it('checked + not_in badge', () => {
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
checkedState="unchecked"
badge={{ key: 'not_in', label: 'Not in', color: 'warning' }}
/>,
);
expect(screen.getByTestId('badge-not_in')).toBeInTheDocument();
});
it('disabled + badge still shows badge', () => {
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
disabled
badge={{ key: 'other', label: 'Other', color: 'secondary' }}
/>,
);
expect(screen.getByTestId('badge-other')).toBeInTheDocument();
});
});
});

View File

@@ -1,131 +0,0 @@
import { deriveItemConfig, ItemContext, SectionType } from '../itemRules';
describe('itemRules', () => {
describe('deriveItemConfig', () => {
it('no query at all → section selected, no badge', () => {
const ctx: ItemContext = {
isSelectedOnFilter: false,
isInRelatedValues: true,
isNotInOperator: false,
hasExistingQuery: false,
hasFilterForThisKey: false,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.SELECTED);
expect(result.badge).toBeNull();
});
it('selected + IN operator → section selected, no badge', () => {
const ctx: ItemContext = {
isSelectedOnFilter: true,
isInRelatedValues: true,
isNotInOperator: false,
hasExistingQuery: true,
hasFilterForThisKey: true,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.SELECTED);
expect(result.badge).toBeNull();
});
it('selected + NOT IN operator → section selected, no badge, unchecked', () => {
const ctx: ItemContext = {
isSelectedOnFilter: true,
isInRelatedValues: false,
isNotInOperator: true,
hasExistingQuery: true,
hasFilterForThisKey: true,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.SELECTED);
expect(result.badge).toBeNull();
expect(result.checkedState).toBe('unchecked');
});
it('has query, not selected, in related → section related, checked', () => {
const ctx: ItemContext = {
isSelectedOnFilter: false,
isInRelatedValues: true,
isNotInOperator: false,
hasExistingQuery: true,
hasFilterForThisKey: false,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.RELATED);
expect(result.badge).toBeNull();
expect(result.checkedState).toBe('checked');
});
it('has query, has filter for this key, in related → section related, checked', () => {
const ctx: ItemContext = {
isSelectedOnFilter: false,
isInRelatedValues: true,
isNotInOperator: false,
hasExistingQuery: true,
hasFilterForThisKey: true,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.RELATED);
expect(result.badge).toBeNull();
expect(result.checkedState).toBe('checked');
});
it('has query, not in related → section all_values, unchecked', () => {
const ctx: ItemContext = {
isSelectedOnFilter: false,
isInRelatedValues: false,
isNotInOperator: false,
hasExistingQuery: true,
hasFilterForThisKey: false,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.ALL_VALUES);
expect(result.badge).toBeNull();
expect(result.checkedState).toBe('unchecked');
});
it('has query + filter for key, not selected, not in related → section all_values, unchecked', () => {
const ctx: ItemContext = {
isSelectedOnFilter: false,
isInRelatedValues: false,
isNotInOperator: false,
hasExistingQuery: true,
hasFilterForThisKey: true,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.ALL_VALUES);
expect(result.badge).toBeNull();
expect(result.checkedState).toBe('unchecked');
});
it('no query but has filter for key, not selected → fallback to checked (DEFAULT_CONFIG)', () => {
const ctx: ItemContext = {
isSelectedOnFilter: false,
isInRelatedValues: false,
isNotInOperator: false,
hasExistingQuery: false,
hasFilterForThisKey: true,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.SELECTED);
expect(result.badge).toBeNull();
expect(result.checkedState).toBe('checked');
});
});
});

View File

@@ -1,382 +0,0 @@
import { renderHook } from '@testing-library/react';
import { SectionType } from '../itemRules';
import { useSectionedValues, SectionedItem } from '../useSectionedValues';
function flattenSections(
sections: { type: SectionType; items: SectionedItem[] }[],
): SectionedItem[] {
return sections.flatMap((s) => s.items);
}
describe('useSectionedValues', () => {
const baseInput = {
relatedValues: ['val1', 'val2'],
allValues: ['val1', 'val2', 'val3'],
currentFilterState: {},
isSomeFilterPresentForCurrentAttribute: false,
isNotInOperator: false,
hasExistingQuery: false,
visibleItemsCount: 10,
relatedExclusions: [] as string[],
};
it('no query at all → all items in selected section, no badges', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
hasExistingQuery: false,
isSomeFilterPresentForCurrentAttribute: false,
}),
);
expect(result.current.sections).toHaveLength(1);
expect(result.current.sections[0].type).toBe(SectionType.SELECTED);
expect(result.current.sections[0].items).toHaveLength(3);
result.current.sections[0].items.forEach((item) => {
expect(item.badge).toBeNull();
});
});
it('has query, no filter for key → related and all_values sections', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: false,
}),
);
const allItems = flattenSections(result.current.sections);
const relatedItems = allItems.filter(
(item) => item.value === 'val1' || item.value === 'val2',
);
const otherItems = allItems.filter((item) => item.value === 'val3');
// Related values should be in related section, checked
relatedItems.forEach((item) => {
expect(item.section).toBe(SectionType.RELATED);
expect(item.checkedState).toBe('checked');
});
// Other values should be in all_values section, unchecked
otherItems.forEach((item) => {
expect(item.section).toBe(SectionType.ALL_VALUES);
expect(item.checkedState).toBe('unchecked');
});
});
it('has query + filter for key, selected value → in selected section', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: true,
currentFilterState: { val1: true, val2: false, val3: false },
}),
);
const allItems = flattenSections(result.current.sections);
const selectedItem = allItems.find((item) => item.value === 'val1');
expect(selectedItem?.section).toBe(SectionType.SELECTED);
expect(selectedItem?.badge).toBeNull();
});
it('has query + filter for key, NOT IN operator → excluded values in selected section, no badge', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: true,
isNotInOperator: true,
currentFilterState: { val1: false, val2: true, val3: true },
}),
);
const allItems = flattenSections(result.current.sections);
// val1 is unchecked + NOT IN = excluded
const excludedItem = allItems.find((item) => item.value === 'val1');
expect(excludedItem?.section).toBe(SectionType.SELECTED);
expect(excludedItem?.badge).toBeNull();
expect(excludedItem?.checkedState).toBe('unchecked');
});
it('items within same section sorted alphabetically', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
relatedValues: ['zebra', 'apple', 'mango'],
allValues: ['zebra', 'apple', 'mango'],
hasExistingQuery: false,
isSomeFilterPresentForCurrentAttribute: false,
}),
);
// All items have section selected, should be sorted alphabetically
const allItems = flattenSections(result.current.sections);
const values = allItems.map((item) => item.value);
expect(values).toStrictEqual(['apple', 'mango', 'zebra']);
});
describe('filtered results from API', () => {
it('keeps items in their natural sections with filtered API results', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: true,
currentFilterState: { val1: true },
}),
);
const sectionTypes = result.current.sections.map((s) => s.type);
expect(sectionTypes).toContain(SectionType.SELECTED);
expect(sectionTypes).toContain(SectionType.RELATED);
});
it('returns empty sections array when no values', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
relatedValues: [],
allValues: [],
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: false,
currentFilterState: {},
}),
);
expect(result.current.sections).toHaveLength(0);
expect(result.current.totalCount).toBe(0);
});
it('non-related items go to ALL_VALUES section', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
relatedValues: [],
allValues: ['other1', 'other2', 'other3'],
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: false,
}),
);
const allValuesSection = result.current.sections.find(
(s) => s.type === SectionType.ALL_VALUES,
);
expect(allValuesSection?.items).toHaveLength(3);
});
it('handles non-overlapping relatedValues and allValues correctly', () => {
// This tests the bug where different pod names in relatedValues vs allValues
// caused all items to go to ALL_VALUES instead of RELATED
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
relatedValues: ['pod-a-1', 'pod-b-1', 'pod-c-1'],
allValues: ['pod-a-2', 'pod-b-2', 'pod-c-2'],
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: false,
}),
);
const sectionTypes = result.current.sections.map((s) => s.type);
// RELATED section should exist with relatedValues items
expect(sectionTypes).toContain(SectionType.RELATED);
const relatedSection = result.current.sections.find(
(s) => s.type === SectionType.RELATED,
);
expect(relatedSection?.items).toHaveLength(3);
expect(relatedSection?.items.map((i) => i.value)).toStrictEqual(
expect.arrayContaining(['pod-a-1', 'pod-b-1', 'pod-c-1']),
);
// ALL_VALUES section should exist with allValues items
expect(sectionTypes).toContain(SectionType.ALL_VALUES);
const allValuesSection = result.current.sections.find(
(s) => s.type === SectionType.ALL_VALUES,
);
expect(allValuesSection?.items).toHaveLength(3);
expect(allValuesSection?.items.map((i) => i.value)).toStrictEqual(
expect.arrayContaining(['pod-a-2', 'pod-b-2', 'pod-c-2']),
);
});
});
describe('stale related values during refresh (relatedExclusions)', () => {
// Scenario: key A selected (existing query) + key B had value "oldSelected".
// User selects a different value on key B -> "oldSelected" is de-selected.
// keepPreviousData keeps the stale response where "oldSelected" is still in
// relatedValues, so it would wrongly render as "related". It is excluded.
it('subtracts only the excluded value from the RELATED section', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
currentFilterState: { newValue: true },
isSomeFilterPresentForCurrentAttribute: true,
hasExistingQuery: true,
// stale API data kept via keepPreviousData
relatedValues: ['oldSelected', 'otherRelated'],
allValues: ['newValue'],
relatedExclusions: ['oldSelected'],
}),
);
const allItems = flattenSections(result.current.sections);
// oldSelected must NOT render as related (dropped entirely - not in
// allValues nor selected)
expect(
allItems.find((item) => item.value === 'oldSelected'),
).toBeUndefined();
// other related values are kept
expect(allItems.find((item) => item.value === 'otherRelated')?.section).toBe(
SectionType.RELATED,
);
});
it('preserves every related value that is not the excluded (previously selected) one', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
currentFilterState: { newValue: true },
isSomeFilterPresentForCurrentAttribute: true,
hasExistingQuery: true,
// oldSelected was just de-selected; the rest are genuinely related
relatedValues: ['oldSelected', 'relatedA', 'relatedB', 'relatedC'],
allValues: ['newValue'],
relatedExclusions: ['oldSelected'],
}),
);
const relatedSection = result.current.sections.find(
(s) => s.type === SectionType.RELATED,
);
const relatedValues = relatedSection?.items.map((i) => i.value) ?? [];
// all non-excluded related values are kept
expect(relatedValues).toStrictEqual(['relatedA', 'relatedB', 'relatedC']);
// and they stay checked
relatedSection?.items.forEach((item) => {
expect(item.checkedState).toBe('checked');
});
// the excluded value is gone
expect(relatedValues).not.toContain('oldSelected');
});
it('keeps the RELATED section when other related values remain', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
currentFilterState: { newValue: true },
isSomeFilterPresentForCurrentAttribute: true,
hasExistingQuery: true,
relatedValues: ['oldSelected', 'otherRelated'],
allValues: ['newValue'],
relatedExclusions: ['oldSelected'],
}),
);
const sectionTypes = result.current.sections.map((s) => s.type);
expect(sectionTypes).toContain(SectionType.RELATED);
expect(sectionTypes).toContain(SectionType.SELECTED);
});
it('shows the excluded value normally when exclusions are empty', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
currentFilterState: { newValue: true },
isSomeFilterPresentForCurrentAttribute: true,
hasExistingQuery: true,
relatedValues: ['oldSelected', 'otherRelated'],
allValues: ['newValue'],
relatedExclusions: [],
}),
);
const allItems = flattenSections(result.current.sections);
expect(allItems.find((item) => item.value === 'oldSelected')?.section).toBe(
SectionType.RELATED,
);
});
});
describe('section ordering', () => {
it('sections appear in order: SELECTED → RELATED → ALL_VALUES', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
relatedValues: ['related1'],
allValues: ['all1'],
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: true,
currentFilterState: { selected1: true },
}),
);
const sectionTypes = result.current.sections.map((s) => s.type);
// Verify order
const selectedIdx = sectionTypes.indexOf(SectionType.SELECTED);
const relatedIdx = sectionTypes.indexOf(SectionType.RELATED);
const allValuesIdx = sectionTypes.indexOf(SectionType.ALL_VALUES);
expect(selectedIdx).toBeLessThan(relatedIdx);
expect(relatedIdx).toBeLessThan(allValuesIdx);
});
it('RELATED section appears before ALL_VALUES even with many items', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
relatedValues: ['r1', 'r2', 'r3', 'r4', 'r5'],
allValues: ['a1', 'a2', 'a3', 'a4', 'a5'],
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: false,
visibleItemsCount: 100,
}),
);
const sectionTypes = result.current.sections.map((s) => s.type);
expect(sectionTypes[0]).toBe(SectionType.RELATED);
expect(sectionTypes[1]).toBe(SectionType.ALL_VALUES);
});
it('visibleItemsCount limits total items across sections', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
relatedValues: ['r1', 'r2', 'r3'],
allValues: ['a1', 'a2', 'a3'],
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: false,
visibleItemsCount: 4,
}),
);
const totalItems = result.current.sections.reduce(
(sum, s) => sum + s.items.length,
0,
);
expect(totalItems).toBe(4);
// RELATED section should get priority (3 items)
const relatedSection = result.current.sections.find(
(s) => s.type === SectionType.RELATED,
);
expect(relatedSection?.items).toHaveLength(3);
// ALL_VALUES gets remaining (1 item)
const allValuesSection = result.current.sections.find(
(s) => s.type === SectionType.ALL_VALUES,
);
expect(allValuesSection?.items).toHaveLength(1);
});
});
});

View File

@@ -1,118 +0,0 @@
import { CheckedState } from '../../../types';
export enum SectionType {
SELECTED = 'selected',
RELATED = 'related',
ALL_VALUES = 'all_values',
}
export interface BadgeConfig {
key: string;
label: string;
color: 'robin' | 'warning' | 'secondary';
}
export interface ItemConfig {
section: SectionType;
badge: BadgeConfig | null;
checkedState: CheckedState;
}
export interface ItemContext {
isSelectedOnFilter: boolean;
isInRelatedValues: boolean;
isNotInOperator: boolean;
hasExistingQuery: boolean;
hasFilterForThisKey: boolean;
}
export interface DerivedItem extends ItemConfig {
value: string;
}
interface ItemRule {
condition: (ctx: ItemContext) => boolean;
config: ItemConfig;
}
const ITEM_RULES: ItemRule[] = [
// No existing query and no filter → all checked (selected section)
{
condition: (ctx): boolean =>
!ctx.hasExistingQuery && !ctx.hasFilterForThisKey,
config: {
section: SectionType.SELECTED,
badge: null,
checkedState: 'checked',
},
},
// Selected with NOT IN operator → unchecked, no badge
{
condition: (ctx): boolean => ctx.isSelectedOnFilter && ctx.isNotInOperator,
config: {
section: SectionType.SELECTED,
badge: null,
checkedState: 'unchecked',
},
},
// Selected with IN operator → checked
{
condition: (ctx): boolean => ctx.isSelectedOnFilter && !ctx.isNotInOperator,
config: {
section: SectionType.SELECTED,
badge: null,
checkedState: 'checked',
},
},
// Related values (from existing query) → checked
{
condition: (ctx): boolean => ctx.hasExistingQuery && ctx.isInRelatedValues,
config: {
section: SectionType.RELATED,
badge: null,
checkedState: 'checked',
},
},
// All values (has existing query but not related) → unchecked
{
condition: (ctx): boolean => ctx.hasExistingQuery,
config: {
section: SectionType.ALL_VALUES,
badge: null,
checkedState: 'unchecked',
},
},
];
// Fallback when no rule matches
const DEFAULT_CONFIG: ItemConfig = {
section: SectionType.SELECTED,
badge: null,
checkedState: 'checked',
};
export function deriveItemConfig(ctx: ItemContext): ItemConfig {
for (const rule of ITEM_RULES) {
if (rule.condition(ctx)) {
return rule.config;
}
}
return DEFAULT_CONFIG;
}
export function deriveItems(
values: string[],
relatedSet: Set<string>,
selectedOnFilterSet: Set<string>,
ctx: Omit<ItemContext, 'isSelectedOnFilter' | 'isInRelatedValues'>,
): DerivedItem[] {
return values.map((value) => {
const itemCtx: ItemContext = {
...ctx,
isSelectedOnFilter: selectedOnFilterSet.has(value),
isInRelatedValues: relatedSet.has(value),
};
const config = deriveItemConfig(itemCtx);
return { value, ...config };
});
}

View File

@@ -1,61 +0,0 @@
import { useMemo } from 'react';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
import { QuickFilterCheckboxUseFieldApis } from 'components/QuickFilters/types';
interface UseExistingQueryParams {
useFieldApis: QuickFilterCheckboxUseFieldApis;
activeQueryIndex: number;
}
interface UseExistingQueryResult {
existingQuery: string | undefined;
hasExistingQuery: boolean;
}
export function useExistingQuery({
useFieldApis,
activeQueryIndex,
}: UseExistingQueryParams): UseExistingQueryResult {
const { currentQuery } = useQueryBuilder();
const existingQuery = useMemo(() => {
if (useFieldApis.existingQuery === null) {
return undefined;
}
if (useFieldApis.existingQuery) {
return useFieldApis.existingQuery;
}
const queryData = currentQuery.builder.queryData?.[activeQueryIndex];
// Prefer V5 filter.expression
if (queryData?.filter?.expression) {
return queryData.filter.expression;
}
// Fall back to V3 filters.items
if (queryData?.filters?.items?.length) {
return convertFiltersToExpression(queryData.filters).expression;
}
return undefined;
}, [
useFieldApis.existingQuery,
currentQuery.builder.queryData,
activeQueryIndex,
]);
// Check if ANY filters exist in query (V3 items or V5 expression)
// This is separate from existingQuery because existingQuery can be explicitly
// disabled (null) while filters still exist in the query for UI purposes
const hasExistingQuery = useMemo(() => {
const queryData = currentQuery.builder.queryData?.[activeQueryIndex];
const hasV3Items = (queryData?.filters?.items?.length ?? 0) > 0;
const hasV5Expression = !!queryData?.filter?.expression;
return hasV3Items || hasV5Expression || !!existingQuery;
}, [currentQuery.builder.queryData, activeQueryIndex, existingQuery]);
return { existingQuery, hasExistingQuery };
}

View File

@@ -1,97 +0,0 @@
import { useMemo } from 'react';
import { useGetFieldsValues } from 'api/generated/services/fields';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
import { DataSource } from 'types/common/queryBuilder';
import { FIELD_API_CACHE_TIME } from 'constants/queryCacheTime';
interface UseFieldValuesProps {
filter: IQuickFiltersConfig;
searchText: string;
existingQuery?: string;
metricNamespace?: string;
startUnixMilli?: number;
endUnixMilli?: number;
enabled: boolean;
}
interface UseFieldValuesReturn {
relatedValues: string[];
allValues: string[];
isLoading: boolean;
isFetching: boolean;
}
const DATA_SOURCE_TO_SIGNAL: Record<DataSource, TelemetrytypesSignalDTO> = {
[DataSource.METRICS]: TelemetrytypesSignalDTO.metrics,
[DataSource.TRACES]: TelemetrytypesSignalDTO.traces,
[DataSource.LOGS]: TelemetrytypesSignalDTO.logs,
};
export function useFieldValues({
filter,
searchText,
existingQuery,
metricNamespace,
startUnixMilli,
endUnixMilli,
enabled,
}: UseFieldValuesProps): UseFieldValuesReturn {
const { data, isLoading, isFetching } = useGetFieldsValues(
{
signal: filter.dataSource
? DATA_SOURCE_TO_SIGNAL[filter.dataSource]
: undefined,
name: filter.attributeKey.key,
searchText,
existingQuery,
metricNamespace,
startUnixMilli,
// This field does not affect the backend but I wanted to keep it here
// in case we add the support in the future
endUnixMilli,
},
{
query: {
enabled,
cacheTime: FIELD_API_CACHE_TIME,
keepPreviousData: true,
},
},
);
const relatedValues: string[] = useMemo(() => {
const values = data?.data?.values;
if (!values) {
return [];
}
return (
values.relatedValues?.filter(
(value): value is string =>
value !== null && value !== undefined && value !== '',
) || []
);
}, [data]);
const allValues: string[] = useMemo(() => {
const values = data?.data?.values;
if (!values) {
return [];
}
const stringValues =
values.stringValues?.filter(
(value): value is string =>
value !== null && value !== undefined && value !== '',
) || [];
const numberValues =
values.numberValues
?.filter((value): value is number => value !== null && value !== undefined)
.map((value) => value.toString()) || [];
return [...stringValues, ...numberValues];
}, [data]);
return { relatedValues, allValues, isLoading, isFetching };
}

View File

@@ -1,152 +0,0 @@
import { useMemo } from 'react';
import { BadgeConfig, deriveItems, SectionType } from './itemRules';
import { CheckedState } from '../../../types';
interface SectionedValuesInput {
relatedValues: string[];
allValues: string[];
currentFilterState: Record<string, boolean>;
isSomeFilterPresentForCurrentAttribute: boolean;
isNotInOperator: boolean;
hasExistingQuery: boolean;
visibleItemsCount: number;
relatedExclusions: string[];
}
export interface SectionedItem {
value: string;
section: SectionType;
badge: BadgeConfig | null;
checkedState: CheckedState;
}
export interface Section {
type: SectionType;
items: SectionedItem[];
}
interface SectionedValuesOutput {
sections: Section[];
totalCount: number;
}
const SECTION_ORDER: SectionType[] = [
SectionType.SELECTED,
SectionType.RELATED,
SectionType.ALL_VALUES,
];
export function buildSelectedSet(
currentFilterState: Record<string, boolean>,
isSomeFilterPresentForCurrentAttribute: boolean,
isNotInOperator: boolean,
): Set<string> {
const selectedSet = new Set<string>();
if (!isSomeFilterPresentForCurrentAttribute) {
return selectedSet;
}
for (const [val, isChecked] of Object.entries(currentFilterState)) {
// NOT IN: unchecked = explicitly excluded
// IN: checked = explicitly selected
const shouldAdd = isNotInOperator ? !isChecked : isChecked;
if (shouldAdd) {
selectedSet.add(val);
}
}
return selectedSet;
}
export function useSectionedValues({
relatedValues,
allValues,
currentFilterState,
isSomeFilterPresentForCurrentAttribute,
isNotInOperator,
hasExistingQuery,
visibleItemsCount,
relatedExclusions,
}: SectionedValuesInput): SectionedValuesOutput {
const items = useMemo(() => {
const selectedSet = buildSelectedSet(
currentFilterState,
isSomeFilterPresentForCurrentAttribute,
isNotInOperator,
);
const exclusionSet = new Set(relatedExclusions);
const effectiveRelatedValues = relatedValues.filter(
(value) => !exclusionSet.has(value),
);
// Combine all values - API already filters both arrays by searchText
const allUniqueValues = Array.from(
new Set([...effectiveRelatedValues, ...allValues]),
);
// Include selected values at top - may not be in API response
const finalValues = [
...new Set([...Array.from(selectedSet), ...allUniqueValues]),
];
const relatedSet = new Set(effectiveRelatedValues);
return deriveItems(finalValues, relatedSet, selectedSet, {
isNotInOperator,
hasExistingQuery,
hasFilterForThisKey: isSomeFilterPresentForCurrentAttribute,
});
}, [
relatedValues,
allValues,
currentFilterState,
isSomeFilterPresentForCurrentAttribute,
isNotInOperator,
hasExistingQuery,
relatedExclusions,
]);
const sections = useMemo(() => {
// Group items by section
const sectionMap = new Map<SectionType, SectionedItem[]>();
for (const sectionType of SECTION_ORDER) {
sectionMap.set(sectionType, []);
}
for (const item of items) {
sectionMap.get(item.section)?.push(item);
}
// Sort items within each section alphabetically
for (const sectionItems of sectionMap.values()) {
sectionItems.sort((a, b) => a.value.localeCompare(b.value));
}
// Apply visibleItemsCount across all sections
let remaining = visibleItemsCount;
const result: Section[] = [];
for (const sectionType of SECTION_ORDER) {
const sectionItems = sectionMap.get(sectionType) || [];
if (sectionItems.length === 0) {
continue;
}
const itemsToTake = Math.min(sectionItems.length, remaining);
if (itemsToTake === 0) {
break;
}
result.push({
type: sectionType,
items: sectionItems.slice(0, itemsToTake),
});
remaining -= itemsToTake;
}
return result;
}, [items, visibleItemsCount]);
return { sections, totalCount: items.length };
}

View File

@@ -1,36 +0,0 @@
import { useEffect, useMemo, useRef } from 'react';
const EMPTY: string[] = [];
interface UseStaleRelatedExclusionsParams {
selectedValues: string[];
isFetching: boolean;
isRefreshing: boolean;
}
export function useStaleRelatedExclusions({
selectedValues,
isFetching,
isRefreshing,
}: UseStaleRelatedExclusionsParams): string[] {
const selectionAtLastFetchRef = useRef<string[]>(selectedValues);
useEffect(() => {
if (!isFetching) {
selectionAtLastFetchRef.current = selectedValues;
}
}, [isFetching, selectedValues]);
return useMemo(() => {
if (!isRefreshing) {
return EMPTY;
}
const current = new Set(selectedValues);
const removed = selectionAtLastFetchRef.current.filter(
(value) => !current.has(value),
);
return removed.length ? removed : EMPTY;
}, [isRefreshing, selectedValues]);
}

View File

@@ -1,6 +1,8 @@
.quick-filters-container {
display: flex;
flex-direction: row;
height: 100%;
min-height: 0;
position: relative;
.quick-filters-settings-container {

View File

@@ -32,7 +32,6 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { USER_ROLES } from 'types/roles';
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
import CheckboxV2 from './FilterRenderers/Checkbox/v2/CheckboxFilterV2';
import Duration from './FilterRenderers/Duration/Duration';
import Slider from './FilterRenderers/Slider/Slider';
import useFilterConfig from './hooks/useFilterConfig';
@@ -52,7 +51,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
signal,
showFilterCollapse = true,
showQueryName = true,
useFieldApis,
} = props;
const { user } = useAppContext();
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
@@ -299,45 +297,21 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
{filterConfig.map((filter) => {
switch (filter.type) {
case FiltersType.CHECKBOX:
return useFieldApis ? (
<CheckboxV2
key={filter.attributeKey.key}
source={source}
filter={filter}
onFilterChange={onFilterChange}
useFieldApis={useFieldApis}
/>
) : (
return (
<Checkbox
key={filter.attributeKey.key}
source={source}
filter={filter}
onFilterChange={onFilterChange}
/>
);
case FiltersType.DURATION:
return (
<Duration
key={filter.attributeKey.key}
filter={filter}
onFilterChange={onFilterChange}
/>
);
return <Duration filter={filter} onFilterChange={onFilterChange} />;
case FiltersType.SLIDER:
return <Slider key={filter.attributeKey.key} />;
return <Slider />;
// eslint-disable-next-line sonarjs/no-duplicated-branches
default:
return useFieldApis ? (
<CheckboxV2
key={filter.attributeKey.key}
source={source}
filter={filter}
onFilterChange={onFilterChange}
useFieldApis={useFieldApis}
/>
) : (
return (
<Checkbox
key={filter.attributeKey.key}
source={source}
filter={filter}
onFilterChange={onFilterChange}
@@ -407,5 +381,4 @@ QuickFilters.defaultProps = {
config: [],
showFilterCollapse: true,
showQueryName: true,
useFieldApis: undefined,
};

View File

@@ -26,11 +26,6 @@ export enum SignalType {
METER_EXPLORER = 'meter',
}
/**
* Missing export from signozhq/ui/checkbox, TODO(H4ad): Add and remove this type definition
*/
export type CheckedState = 'checked' | 'unchecked' | 'indeterminate';
export interface IQuickFiltersConfig {
type: FiltersType;
title: string;
@@ -51,7 +46,6 @@ export interface IQuickFiltersProps {
className?: string;
showFilterCollapse?: boolean;
showQueryName?: boolean;
useFieldApis?: QuickFilterCheckboxUseFieldApis;
}
export enum QuickFiltersSource {
@@ -62,19 +56,3 @@ export enum QuickFiltersSource {
EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
}
/**
* Opt-in: fetch values from the /v1/fields/values API instead of /v3/autocomplete/attribute_values
*/
export type QuickFilterCheckboxUseFieldApis = {
startUnixMilli: number;
endUnixMilli: number;
/**
* If you didn't specify a string, we automatically try to extract this from the currentQuery,
* from the filter.expression or filter.items.
*
* Use null to ignore/disable this behavior.
*/
existingQuery?: string | null;
metricNamespace?: string;
};

View File

@@ -1,112 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TagKeyValueInput from './TagKeyValueInput';
const TID = 'tag-key-value-input';
type User = ReturnType<typeof userEvent.setup>;
const startEditingFirstChip = async (user: User): Promise<HTMLElement> => {
await user.dblClick(screen.getAllByTestId(`${TID}-chip`)[0]);
return screen.getByTestId(`${TID}-edit`);
};
describe('TagKeyValueInput — inline chip edit', () => {
it('shows an error and stays in edit mode when Enter commits an invalid value', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'novalue{Enter}');
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
'key:value format',
);
// Still editing (input present), and no change committed.
expect(screen.getByTestId(`${TID}-edit`)).toBeInTheDocument();
expect(onTagsChange).not.toHaveBeenCalled();
});
it('shows a duplicate error when Enter commits an existing tag', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(
<TagKeyValueInput
tags={['env:prod', 'team:pulse']}
onTagsChange={onTagsChange}
/>,
);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'team:pulse{Enter}');
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
'already exists',
);
expect(onTagsChange).not.toHaveBeenCalled();
});
it('commits a valid edit on Enter', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'env:staging{Enter}');
expect(onTagsChange).toHaveBeenCalledWith(['env:staging']);
expect(screen.queryByTestId(`${TID}-error`)).not.toBeInTheDocument();
});
it('reverts silently (no error) when blurring an invalid edit', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'novalue');
await user.tab(); // blur off the edit input
expect(screen.queryByTestId(`${TID}-error`)).not.toBeInTheDocument();
expect(screen.queryByTestId(`${TID}-edit`)).not.toBeInTheDocument();
expect(onTagsChange).not.toHaveBeenCalled();
});
});
describe('TagKeyValueInput — backend-rule validation', () => {
it('rejects a key containing a space on inline edit', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'my key:prod{Enter}');
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
'spaces or special characters',
);
expect(screen.getByTestId(`${TID}-edit`)).toBeInTheDocument();
expect(onTagsChange).not.toHaveBeenCalled();
});
it('rejects a value containing a space in the new-tag input', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={[]} onTagsChange={onTagsChange} />);
const input = screen.getByTestId(TID);
await user.type(input, 'env:pro d{Enter}');
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
'spaces or special characters',
);
expect(onTagsChange).not.toHaveBeenCalled();
});
});

View File

@@ -5,7 +5,7 @@ import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import TagBadge from '../TagBadge/TagBadge';
import { validateTag } from './utils';
import { parseKeyValueTag } from './utils';
import styles from './TagKeyValueInput.module.scss';
@@ -43,12 +43,16 @@ function TagKeyValueInput({
if (!raw) {
return;
}
const result = validateTag(raw, tags);
if ('error' in result) {
setError(result.error);
const normalized = parseKeyValueTag(raw);
if (!normalized) {
setError('Tags must be in key:value format (both sides required).');
return;
}
onTagsChange([...tags, result.tag]);
if (tags.includes(normalized)) {
setError('This tag already exists.');
return;
}
onTagsChange([...tags, normalized]);
setInputValue('');
setError('');
};
@@ -78,20 +82,15 @@ function TagKeyValueInput({
const cancelEdit = (): void => {
setEditIndex(-1);
setEditValue('');
setError('');
};
const commitEdit = (revertOnInvalid = false): void => {
const result = validateTag(editValue, tags, editIndex);
if ('error' in result) {
if (revertOnInvalid) {
cancelEdit();
} else {
setError(result.error);
}
return;
const commitEdit = (): void => {
const normalized = parseKeyValueTag(editValue);
// Drop into a no-op (revert) on invalid or duplicate edits rather than
// stranding the user in an un-exitable edit box.
if (normalized && !tags.some((t, i) => t === normalized && i !== editIndex)) {
onTagsChange(tags.map((t, i) => (i === editIndex ? normalized : t)));
}
onTagsChange(tags.map((t, i) => (i === editIndex ? result.tag : t)));
cancelEdit();
};
@@ -122,14 +121,11 @@ function TagKeyValueInput({
value={editValue}
autoFocus
testId={`${testId}-edit`}
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
setEditValue(e.target.value);
if (error) {
setError('');
}
}}
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setEditValue(e.target.value)
}
onKeyDown={handleEditKeyDown}
onBlur={(): void => commitEdit(true)}
onBlur={commitEdit}
/>
) : (
<TagBadge

View File

@@ -15,42 +15,3 @@ export function parseKeyValueTag(raw: string): string | null {
}
return `${key}:${value}`;
}
const TAG_KEY_REGEX = new RegExp('^[a-zA-Z$_@{#][a-zA-Z0-9$_@#{}:/-]*$');
const TAG_VALUE_REGEX = new RegExp('^[a-zA-Z0-9$_@#{}:.+=/-]*$');
const MAX_TAG_LEN = 32;
export type TagValidation = { tag: string } | { error: string };
export function validateTag(
raw: string,
existingTags: string[],
excludeIndex = -1,
): TagValidation {
const normalized = parseKeyValueTag(raw);
if (!normalized) {
return { error: 'Tags must be in key:value format (both sides required).' };
}
const separator = normalized.indexOf(':');
const key = normalized.slice(0, separator);
const value = normalized.slice(separator + 1);
if (!TAG_KEY_REGEX.test(key)) {
return { error: 'Tag keys cannot contain spaces or special characters.' };
}
if (!TAG_VALUE_REGEX.test(value)) {
return { error: 'Tag values cannot contain spaces or special characters.' };
}
if (key.length > MAX_TAG_LEN || value.length > MAX_TAG_LEN) {
return {
error: `Tag key and value must each be ${MAX_TAG_LEN} characters or fewer.`,
};
}
if (
existingTags.some(
(tag, index) => tag === normalized && index !== excludeIndex,
)
) {
return { error: 'This tag already exists.' };
}
return { tag: normalized };
}

View File

@@ -1,11 +1,7 @@
import { ComponentProps, memo, useCallback, useLayoutEffect } from 'react';
import { ComponentProps, memo } from 'react';
import { TableComponents } from 'react-virtuoso';
import cx from 'classnames';
import {
chromePerformanceTanstackTableBeginHover,
chromePerformanceMeasureTanstackTable,
} from './perfDevtools';
import TanStackRowCells from './TanStackRow';
import {
useClearRowHovered,
@@ -14,7 +10,6 @@ import {
import { FlatItem, TableRowContext } from './types';
import tableStyles from './TanStackTable.module.scss';
import { chromePerformanceNow } from 'lib/chromePerformanceDevTools';
type VirtuosoTableRowProps<TData, TItemKey = string> = ComponentProps<
NonNullable<
@@ -27,7 +22,6 @@ function TanStackCustomTableRow<TData, TItemKey = string>({
context,
...props
}: VirtuosoTableRowProps<TData, TItemKey>): JSX.Element {
const renderStart = chromePerformanceNow();
const rowId = item.row.id;
const rowData = item.row.original;
@@ -35,23 +29,6 @@ function TanStackCustomTableRow<TData, TItemKey = string>({
const setHovered = useSetRowHovered(rowId);
const clearHovered = useClearRowHovered(rowId);
const handleMouseEnter = useCallback((): void => {
chromePerformanceTanstackTableBeginHover(rowId);
setHovered();
}, [rowId, setHovered]);
useLayoutEffect(() => {
chromePerformanceMeasureTanstackTable('Row render', renderStart, {
track: 'Row render',
color: 'secondary',
tooltipText: 'Row render + commit',
properties: [
['rowId', rowId],
['kind', item.kind],
],
});
});
if (item.kind === 'expansion') {
return (
<tr {...props} className={tableStyles.tableRowExpansion}>
@@ -88,7 +65,7 @@ function TanStackCustomTableRow<TData, TItemKey = string>({
{...props}
className={rowClassName}
style={rowStyle}
onMouseEnter={handleMouseEnter}
onMouseEnter={setHovered}
onMouseLeave={clearHovered}
>
<TanStackRowCells

View File

@@ -1,30 +0,0 @@
import { useLayoutEffect, type ReactNode } from 'react';
import { chromePerformanceTanstackTableEndHover } from './perfDevtools';
import { useIsRowHovered } from './TanStackTableStateContext';
import { TooltipSimple, TooltipSimpleProps } from '@signozhq/ui/tooltip';
export type HoverTooltipProps = Omit<TooltipSimpleProps, 'open'> & {
rowId: string;
children: ReactNode;
};
export function TanStackHoverTooltip({
rowId,
children,
...tooltipProps
}: HoverTooltipProps): JSX.Element {
const isHovered = useIsRowHovered(rowId);
useLayoutEffect(() => {
if (isHovered) {
chromePerformanceTanstackTableEndHover(rowId);
}
}, [isHovered, rowId]);
if (!isHovered) {
return <>{children}</>;
}
return <TooltipSimple {...tooltipProps}>{children}</TooltipSimple>;
}

View File

@@ -5,7 +5,6 @@ import {
useCallback,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
} from 'react';
@@ -45,7 +44,6 @@ import {
TanStackTableHandle,
TanStackTableProps,
} from './types';
import { chromePerformanceMeasureTanstackTable } from './perfDevtools';
import { useColumnDnd } from './useColumnDnd';
import { useColumnHandlers } from './useColumnHandlers';
import { useColumnState } from './useColumnState';
@@ -58,7 +56,6 @@ import { VirtuosoTableColGroup } from './VirtuosoTableColGroup';
import tableStyles from './TanStackTable.module.scss';
import viewStyles from './TanStackTableView.module.scss';
import { chromePerformanceNow } from 'lib/chromePerformanceDevTools';
const COLUMN_DND_AUTO_SCROLL = {
layoutShiftCompensation: false as const,
@@ -119,8 +116,6 @@ function TanStackTableInner<TData, TItemKey = string>(
);
}
const renderStart = chromePerformanceNow();
const virtuosoRef = useRef<TableVirtuosoHandle | null>(null);
const isDarkMode = useIsDarkMode();
@@ -349,19 +344,6 @@ function TanStackTableInner<TData, TItemKey = string>(
const visibleColumnsCount = table.getVisibleFlatColumns().length;
useLayoutEffect(() => {
chromePerformanceMeasureTanstackTable('Table render', renderStart, {
track: 'Table render',
color: 'primary',
tooltipText: 'TanStackTable render + commit',
properties: [
['rows', String(flatItems.length)],
['columns', String(visibleColumnsCount)],
['loading', String(isLoading)],
],
});
});
const columnOrderKey = useMemo(() => columnIds.join(','), [columnIds]);
const columnVisibilityKey = useMemo(
() =>

View File

@@ -1,8 +1,6 @@
import { TanStackHoverTooltip } from './TanStackHoverTooltip';
import { TanStackTableBase } from './TanStackTable';
import TanStackTableText from './TanStackTableText';
export * from './TanStackHoverTooltip';
export * from './TanStackTableStateContext';
export * from './types';
export * from './useCalculatedPageSize';
@@ -276,7 +274,6 @@ export * from './useTableParams';
*/
const TanStackTable = Object.assign(TanStackTableBase, {
Text: TanStackTableText,
HoverTooltip: TanStackHoverTooltip,
});
export default TanStackTable;

View File

@@ -1,35 +0,0 @@
import {
createChromePerformanceInteractionTracker,
chromePerformanceMeasure,
ChromePerformanceTrackEntryOptions,
} from 'lib/chromePerformanceDevTools';
const TABLE_TRACK_GROUP = 'SigNoz Table';
export function chromePerformanceMeasureTanstackTable(
name: string,
start: number,
{
track,
color,
tooltipText,
properties,
}: Omit<ChromePerformanceTrackEntryOptions, 'trackGroup'>,
): void {
chromePerformanceMeasure(name, start, {
trackGroup: TABLE_TRACK_GROUP,
track,
color,
tooltipText,
properties,
});
}
const hoverTracker = createChromePerformanceInteractionTracker(
TABLE_TRACK_GROUP,
'Hover',
'Row hover',
);
export const chromePerformanceTanstackTableBeginHover = hoverTracker.begin;
export const chromePerformanceTanstackTableEndHover = hoverTracker.end;

View File

@@ -21,7 +21,6 @@ export type TableCellContext<TData, TValue> = {
value: TValue;
isActive: boolean;
rowIndex: number;
rowId: string;
isExpanded: boolean;
canExpand: boolean;
toggleExpanded: () => void;

View File

@@ -135,7 +135,6 @@ export function buildTanstackColumnDef<TData, TItemKey = string>(
value: getValue() as TData[any],
isActive: isRowActive?.(rowData) ?? false,
rowIndex: row.index,
rowId: row.id,
isExpanded: row.getIsExpanded(),
canExpand: row.getCanExpand(),
toggleExpanded: (): void => {

View File

@@ -57,7 +57,4 @@ export enum QueryParams {
isTestAlert = 'isTestAlert',
yAxisUnit = 'yAxisUnit',
ruleName = 'ruleName',
matchType = 'matchType',
compareOp = 'compareOp',
evaluationWindowPreset = 'evaluationWindowPreset',
}

View File

@@ -1,5 +1,3 @@
export const DASHBOARD_CACHE_TIME = 30_000;
// keep it low or zero, otherwise, when enabled auto-refresh, this causes OOM due to accumulated queries in cache
export const DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED = 0;
export const FIELD_API_CACHE_TIME = 60_000;

View File

@@ -89,10 +89,10 @@ const ROUTES = {
AI_ASSISTANT_BASE: '/ai-assistant',
AI_ASSISTANT_ICON_PREVIEW: '/ai-assistant-icon-preview',
MCP_SERVER: '/settings/mcp-server',
AI_OBSERVABILITY_ATTRIBUTE_MAPPING: '/ai-observability/attribute-mapping',
AI_OBSERVABILITY_BASE: '/ai-observability',
AI_OBSERVABILITY_OVERVIEW: '/ai-observability/overview',
AI_OBSERVABILITY_CONFIGURATION: '/ai-observability/configuration',
LLM_OBSERVABILITY_ATTRIBUTE_MAPPING: '/llm-observability/attribute-mapping',
LLM_OBSERVABILITY_BASE: '/llm-observability',
LLM_OBSERVABILITY_OVERVIEW: '/llm-observability/overview',
LLM_OBSERVABILITY_CONFIGURATION: '/llm-observability/configuration',
} as const;
export default ROUTES;

View File

@@ -1,176 +0,0 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter, useHistory } from 'react-router-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { INITIAL_CREATE_ALERT_STATE } from '../constants';
import { CreateAlertProvider, useCreateAlertState } from '../index';
import { AlertThresholdMatchType } from '../types';
// The provider only needs a query with a unit + empty builder for these assertions.
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: (): unknown => ({
currentQuery: {
unit: 'bytes',
builder: { queryData: [], queryFormulas: [] },
},
redirectWithQueryBuilderData: jest.fn(),
}),
}));
const mutation = { mutate: jest.fn(), isLoading: false };
jest.mock('api/generated/services/rules', () => ({
useCreateRule: (): unknown => mutation,
useTestRule: (): unknown => mutation,
useUpdateRuleByID: (): unknown => mutation,
}));
function Probe(): JSX.Element {
const { thresholdState, evaluationWindow } = useCreateAlertState();
return (
<div>
<span data-testid="match-type">{thresholdState.matchType}</span>
<span data-testid="operator">{thresholdState.operator}</span>
<span data-testid="threshold-count">{thresholdState.thresholds.length}</span>
<span data-testid="threshold-value">
{thresholdState.thresholds[0]?.thresholdValue}
</span>
<span data-testid="window-type">{evaluationWindow.windowType}</span>
</div>
);
}
function renderWithSearch(search: string): void {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<MemoryRouter initialEntries={[`/alerts/new${search}`]}>
<QueryClientProvider client={queryClient}>
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
<Probe />
</CreateAlertProvider>
</QueryClientProvider>
</MemoryRouter>,
);
}
function serializeThreshold(thresholdValue: number): string {
return encodeURIComponent(
JSON.stringify([
{
id: 't1',
label: 'critical',
thresholdValue,
recoveryThresholdValue: null,
unit: 'bytes',
channels: [],
color: '#F1575F',
},
]),
);
}
describe('CreateAlertProvider — URL-declared prefill (issue #5291)', () => {
it('applies matchType, operator, and thresholds without the meter window', () => {
// Dashboard-style prefill: no evaluationWindowPreset → default window.
renderWithSearch(
`?matchType=on_average&compareOp=below&thresholds=${serializeThreshold(90)}`,
);
expect(screen.getByTestId('match-type')).toHaveTextContent('on_average');
expect(screen.getByTestId('operator')).toHaveTextContent('below');
expect(screen.getByTestId('threshold-count')).toHaveTextContent('1');
expect(screen.getByTestId('threshold-value')).toHaveTextContent('90');
// The meter evaluation-window preset must NOT fire.
expect(screen.getByTestId('window-type')).toHaveTextContent('rolling');
});
it('adjusts only the occurrence type when no thresholds are supplied', () => {
renderWithSearch(`?matchType=in_total`);
expect(screen.getByTestId('match-type')).toHaveTextContent('in_total');
// The default single critical threshold and operator are retained.
expect(screen.getByTestId('threshold-count')).toHaveTextContent('1');
expect(screen.getByTestId('threshold-value')).toHaveTextContent('0');
expect(screen.getByTestId('operator')).toHaveTextContent('above');
expect(screen.getByTestId('window-type')).toHaveTextContent('rolling');
});
it('applies the meter evaluation window when the preset is declared', () => {
// Ingestion-style prefill: explicit in_total + meter preset.
renderWithSearch(
`?matchType=in_total&evaluationWindowPreset=meter&thresholds=${serializeThreshold(
500,
)}`,
);
expect(screen.getByTestId('match-type')).toHaveTextContent('in_total');
expect(screen.getByTestId('threshold-value')).toHaveTextContent('500');
expect(screen.getByTestId('window-type')).toHaveTextContent('cumulative');
});
});
// Reproduces the #12137 regression: on the alert overview page the query builder
// rewrites location.search after the alert loads, which used to re-run the prefill
// effect and RESET the loaded threshold back to 0.
function SearchMutator(): JSX.Element {
const routerHistory = useHistory();
return (
<button
type="button"
data-testid="mutate-search"
onClick={(): void =>
routerHistory.replace(
'/alerts/overview?compositeQuery=normalized&ruleId=r1',
)
}
>
change search
</button>
);
}
describe('CreateAlertProvider — edit mode ignores URL prefill', () => {
it('keeps loaded thresholds when the query builder rewrites location.search', () => {
const initialAlertState = {
...INITIAL_CREATE_ALERT_STATE,
threshold: {
...INITIAL_CREATE_ALERT_STATE.threshold,
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
thresholds: [
{
...INITIAL_CREATE_ALERT_STATE.threshold.thresholds[0],
thresholdValue: 245,
},
],
},
};
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<MemoryRouter initialEntries={['/alerts/overview?ruleId=r1']}>
<QueryClientProvider client={queryClient}>
<CreateAlertProvider
initialAlertType={AlertTypes.METRICS_BASED_ALERT}
isEditMode
ruleId="r1"
initialAlertState={initialAlertState}
>
<Probe />
<SearchMutator />
</CreateAlertProvider>
</QueryClientProvider>
</MemoryRouter>,
);
expect(screen.getByTestId('threshold-value')).toHaveTextContent('245');
// Simulate the query builder normalizing the query into the URL post-load.
fireEvent.click(screen.getByTestId('mutate-search'));
expect(screen.getByTestId('threshold-value')).toHaveTextContent('245');
});
});

View File

@@ -1,71 +0,0 @@
import { AlertThresholdMatchType, AlertThresholdOperator } from '../types';
import {
EvaluationWindowPreset,
resolveUrlAlertPrefill,
} from '../resolveUrlAlertPrefill';
function resolve(search: string): ReturnType<typeof resolveUrlAlertPrefill> {
return resolveUrlAlertPrefill(new URLSearchParams(search));
}
describe('resolveUrlAlertPrefill', () => {
it('returns an empty plan when no params are present', () => {
expect(resolve('')).toStrictEqual({
thresholds: undefined,
matchType: undefined,
operator: undefined,
evaluationWindowPreset: undefined,
});
});
it('parses a thresholds array', () => {
const thresholds = [{ id: 't1', thresholdValue: 90 }];
const { thresholds: parsed } = resolve(
`thresholds=${encodeURIComponent(JSON.stringify(thresholds))}`,
);
expect(parsed).toStrictEqual(thresholds);
});
it('ignores malformed or non-array thresholds without throwing', () => {
const consoleError = jest
.spyOn(console, 'error')
.mockImplementation(() => undefined);
expect(resolve('thresholds=not-json').thresholds).toBeUndefined();
expect(
resolve(`thresholds=${encodeURIComponent('{"a":1}')}`).thresholds,
).toBeUndefined();
consoleError.mockRestore();
});
it('normalizes matchType aliases', () => {
expect(resolve('matchType=on_average').matchType).toBe(
AlertThresholdMatchType.ON_AVERAGE,
);
expect(resolve('matchType=sum').matchType).toBe(
AlertThresholdMatchType.IN_TOTAL,
);
expect(resolve('matchType=nonsense').matchType).toBeUndefined();
});
it('normalizes operator aliases', () => {
expect(resolve('compareOp=above').operator).toBe(
AlertThresholdOperator.IS_ABOVE,
);
expect(resolve('compareOp=%3E').operator).toBe(
AlertThresholdOperator.IS_ABOVE,
);
expect(resolve('compareOp=nonsense').operator).toBeUndefined();
});
it('recognizes the meter evaluation-window preset only', () => {
expect(resolve('evaluationWindowPreset=meter').evaluationWindowPreset).toBe(
EvaluationWindowPreset.METER,
);
expect(
resolve('evaluationWindowPreset=rolling').evaluationWindowPreset,
).toBeUndefined();
expect(resolve('').evaluationWindowPreset).toBeUndefined();
});
});

View File

@@ -1,63 +0,0 @@
import { AlertThresholdMatchType, AlertThresholdOperator } from './types';
// Mirrors the backend's CompareOperator.Normalize() in
// pkg/types/ruletypes/compare.go. Maps any accepted alias to the enum value
// the dropdown understands. Returns undefined for aliases the UI does not
// expose (e.g. above_or_equal, below_or_equal) so callers can keep the raw
// value on screen instead of silently rewriting it.
export function normalizeOperator(
raw: string | undefined,
): AlertThresholdOperator | undefined {
switch (raw) {
case '1':
case 'above':
case '>':
return AlertThresholdOperator.IS_ABOVE;
case '2':
case 'below':
case '<':
return AlertThresholdOperator.IS_BELOW;
case '3':
case 'equal':
case 'eq':
case '=':
return AlertThresholdOperator.IS_EQUAL_TO;
case '4':
case 'not_equal':
case 'not_eq':
case '!=':
return AlertThresholdOperator.IS_NOT_EQUAL_TO;
case '7':
case 'outside_bounds':
return AlertThresholdOperator.ABOVE_BELOW;
default:
return undefined;
}
}
// Mirrors the backend's MatchType.Normalize() in pkg/types/ruletypes/match.go.
export function normalizeMatchType(
raw: string | undefined,
): AlertThresholdMatchType | undefined {
switch (raw) {
case '1':
case 'at_least_once':
return AlertThresholdMatchType.AT_LEAST_ONCE;
case '2':
case 'all_the_times':
return AlertThresholdMatchType.ALL_THE_TIME;
case '3':
case 'on_average':
case 'avg':
return AlertThresholdMatchType.ON_AVERAGE;
case '4':
case 'in_total':
case 'sum':
return AlertThresholdMatchType.IN_TOTAL;
case '5':
case 'last':
return AlertThresholdMatchType.LAST;
default:
return undefined;
}
}

View File

@@ -23,13 +23,10 @@ import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/map
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { INITIAL_CREATE_ALERT_STATE } from './constants';
import {
EvaluationWindowPreset,
resolveUrlAlertPrefill,
} from './resolveUrlAlertPrefill';
import {
AdvancedOptionsAction,
AlertThresholdAction,
AlertThresholdMatchType,
CreateAlertAction,
CreateAlertSlice,
EvaluationWindowAction,
@@ -126,13 +123,9 @@ export function CreateAlertProvider(
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const thresholdsFromURL = queryParams.get(QueryParams.thresholds);
const ruleNameFromURL = queryParams.get(QueryParams.ruleName);
const yAxisUnitFromURL = queryParams.get(QueryParams.yAxisUnit);
// Prefill declared in the URL; applied verbatim, agnostic of the producer.
const urlPrefill = useMemo(
() => resolveUrlAlertPrefill(new URLSearchParams(location.search)),
[location.search],
);
const [alertType, setAlertType] = useState<AlertTypes>(() => {
if (isEditMode) {
@@ -168,14 +161,6 @@ export function CreateAlertProvider(
const yAxisUnitAppliedRef = useRef(false);
useEffect(() => {
// URL-declared prefill is a create-flow concern. In edit mode the alert
// state is owned by SET_INITIAL_STATE; running the RESET below would wipe
// the loaded thresholds each time the query builder rewrites location.search
// (which yields a fresh urlPrefill object and re-triggers this effect).
if (isEditMode) {
return;
}
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action: {
@@ -183,43 +168,34 @@ export function CreateAlertProvider(
},
});
if (urlPrefill.thresholds) {
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action: {
type: 'SET_THRESHOLDS',
payload: urlPrefill.thresholds,
},
});
}
if (thresholdsFromURL) {
try {
const thresholds = JSON.parse(thresholdsFromURL);
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action: {
type: 'SET_THRESHOLDS',
payload: thresholds,
},
});
} catch (error) {
console.error('Error parsing thresholds from URL:', error);
}
if (urlPrefill.matchType) {
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action: {
type: 'SET_MATCH_TYPE',
payload: urlPrefill.matchType,
},
});
}
if (urlPrefill.operator) {
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action: {
type: 'SET_OPERATOR',
payload: urlPrefill.operator,
},
});
}
if (urlPrefill.evaluationWindowPreset === EvaluationWindowPreset.METER) {
setCreateAlertState({
slice: CreateAlertSlice.EVALUATION_WINDOW,
action: {
type: 'SET_INITIAL_STATE_FOR_METER',
},
});
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action: {
type: 'SET_MATCH_TYPE',
payload: AlertThresholdMatchType.IN_TOTAL,
},
});
}
if (ruleNameFromURL && !ruleNameAppliedRef.current) {
@@ -243,7 +219,7 @@ export function CreateAlertProvider(
},
});
}
}, [alertType, urlPrefill, ruleNameFromURL, yAxisUnitFromURL, isEditMode]);
}, [alertType, thresholdsFromURL, ruleNameFromURL, yAxisUnitFromURL]);
useEffect(() => {
if (isEditMode && initialAlertState) {

View File

@@ -1,54 +0,0 @@
import { QueryParams } from 'constants/query';
import { normalizeMatchType, normalizeOperator } from './conditionNormalizers';
import {
AlertThresholdMatchType,
AlertThresholdOperator,
Threshold,
} from './types';
export enum EvaluationWindowPreset {
METER = 'meter',
}
export interface ResolvedAlertPrefill {
thresholds?: Threshold[];
matchType?: AlertThresholdMatchType;
operator?: AlertThresholdOperator;
evaluationWindowPreset?: EvaluationWindowPreset;
}
function parseThresholds(raw: string | null): Threshold[] | undefined {
if (!raw) {
return undefined;
}
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? (parsed as Threshold[]) : undefined;
} catch (error) {
console.error('Error parsing thresholds from URL:', error);
return undefined;
}
}
function parseEvaluationWindowPreset(
raw: string | null,
): EvaluationWindowPreset | undefined {
return raw === EvaluationWindowPreset.METER
? EvaluationWindowPreset.METER
: undefined;
}
/** URL → declarative prefill plan; the consumer applies it without knowing the producer. */
export function resolveUrlAlertPrefill(
params: URLSearchParams,
): ResolvedAlertPrefill {
return {
thresholds: parseThresholds(params.get(QueryParams.thresholds)),
matchType: normalizeMatchType(params.get(QueryParams.matchType) ?? undefined),
operator: normalizeOperator(params.get(QueryParams.compareOp) ?? undefined),
evaluationWindowPreset: parseEvaluationWindowPreset(
params.get(QueryParams.evaluationWindowPreset),
),
};
}

View File

@@ -238,12 +238,67 @@ export function getAdvancedOptionsStateFromAlertDef(
};
}
// Condition-alias normalizers live in a leaf module (they depend only on the
// enums) so `context/index` can consume them without an index↔utils cycle.
export {
normalizeOperator,
normalizeMatchType,
} from './context/conditionNormalizers';
// Mirrors the backend's CompareOperator.Normalize() in
// pkg/types/ruletypes/compare.go. Maps any accepted alias to the enum value
// the dropdown understands. Returns undefined for aliases the UI does not
// expose (e.g. above_or_equal, below_or_equal) so callers can keep the raw
// value on screen instead of silently rewriting it.
export function normalizeOperator(
raw: string | undefined,
): AlertThresholdOperator | undefined {
switch (raw) {
case '1':
case 'above':
case '>':
return AlertThresholdOperator.IS_ABOVE;
case '2':
case 'below':
case '<':
return AlertThresholdOperator.IS_BELOW;
case '3':
case 'equal':
case 'eq':
case '=':
return AlertThresholdOperator.IS_EQUAL_TO;
case '4':
case 'not_equal':
case 'not_eq':
case '!=':
return AlertThresholdOperator.IS_NOT_EQUAL_TO;
case '7':
case 'outside_bounds':
return AlertThresholdOperator.ABOVE_BELOW;
default:
return undefined;
}
}
// Mirrors the backend's MatchType.Normalize() in pkg/types/ruletypes/match.go.
export function normalizeMatchType(
raw: string | undefined,
): AlertThresholdMatchType | undefined {
switch (raw) {
case '1':
case 'at_least_once':
return AlertThresholdMatchType.AT_LEAST_ONCE;
case '2':
case 'all_the_times':
return AlertThresholdMatchType.ALL_THE_TIME;
case '3':
case 'on_average':
case 'avg':
return AlertThresholdMatchType.ON_AVERAGE;
case '4':
case 'in_total':
case 'sum':
return AlertThresholdMatchType.IN_TOTAL;
case '5':
case 'last':
return AlertThresholdMatchType.LAST;
default:
return undefined;
}
}
export function getThresholdStateFromAlertDef(
alertDef: PostableAlertRuleV2,

View File

@@ -65,5 +65,4 @@
min-width: 0;
padding-left: 12px;
padding-bottom: 12px;
padding-top: 8px;
}

View File

@@ -25,52 +25,20 @@ describe('calculateChartDimensions', () => {
});
});
it('RIGHT: reserves a side column capped at 320px / 40% of the width and keeps full height', () => {
it('RIGHT: reserves a side column capped at 30% of the width and keeps full height', () => {
const dims = calculateChartDimensions({
containerWidth: 1000,
containerHeight: 400,
legendConfig: { position: LegendPosition.RIGHT },
seriesLabels: labels(10, 40),
});
expect(dims.legendWidth).toBe(320);
expect(dims.width).toBe(680);
// 40-char labels approximate to 336px, capped at min(240, 30% of 1000).
expect(dims.legendWidth).toBe(240);
expect(dims.width).toBe(760);
expect(dims.height).toBe(400);
expect(dims.legendHeight).toBe(400);
});
it('RIGHT: sizes the column to the longest label when it fits under the cap', () => {
const dims = calculateChartDimensions({
containerWidth: 1000,
containerHeight: 400,
legendConfig: { position: LegendPosition.RIGHT },
seriesLabels: labels(5, 20),
});
expect(dims.legendWidth).toBe(216);
expect(dims.width).toBe(784);
});
it('RIGHT: never shrinks the column below the 150px floor', () => {
const dims = calculateChartDimensions({
containerWidth: 1000,
containerHeight: 400,
legendConfig: { position: LegendPosition.RIGHT },
seriesLabels: labels(3, 3),
});
expect(dims.legendWidth).toBe(150);
expect(dims.width).toBe(850);
});
it('RIGHT: on a narrow container the legend never takes more than 40% of the width', () => {
const dims = calculateChartDimensions({
containerWidth: 300,
containerHeight: 400,
legendConfig: { position: LegendPosition.RIGHT },
seriesLabels: labels(10, 40),
});
expect(dims.legendWidth).toBe(120);
expect(dims.width).toBe(180);
});
it('BOTTOM: a single row of items reserves one legend row', () => {
const dims = calculateChartDimensions({
containerWidth: 1000,

View File

@@ -15,12 +15,6 @@ const BASE_LEGEND_WIDTH = 16;
const LEGEND_PADDING = 12;
const LEGEND_LINE_HEIGHT = 28;
// RIGHT legend is a vertical column with its own width budget (cap protects the donut).
const MAX_RIGHT_LEGEND_WIDTH = 320;
const RIGHT_LEGEND_WIDTH_RATIO = 0.4;
// Column padding + copy button, not covered by the text-length estimate.
const RIGHT_LEGEND_RESERVED_WIDTH = 40;
/**
* Calculates the average width of the legend items based on the labels of the series.
* @param legends - The labels of the series.
@@ -48,7 +42,7 @@ export function calculateAverageLegendWidth(legends: string[]): number {
* Implementation details (high level):
* - Approximates legend item width from label text length, using a fixed average char width.
* - RIGHT legend:
* - `legendWidth` fits the longest label, clamped to [150px, min(MAX_RIGHT_LEGEND_WIDTH, 40% width)].
* - `legendWidth` is clamped between 150px and min(MAX_LEGEND_WIDTH, 30% of container width).
* - Chart width is `containerWidth - legendWidth`.
* - BOTTOM legend:
* - Computes how many items fit per row, then uses at most 2 rows.
@@ -86,22 +80,9 @@ export function calculateChartDimensions({
const legendItemCount = seriesLabels.length;
if (legendConfig.position === LegendPosition.RIGHT) {
// Size the column to the longest name (up to the cap) so it doesn't ellipsize.
const longestLabelLength = seriesLabels.reduce(
(max, label) => Math.max(max, label.length),
0,
);
const desiredLegendWidth =
BASE_LEGEND_WIDTH +
longestLabelLength * AVG_CHAR_WIDTH +
RIGHT_LEGEND_RESERVED_WIDTH;
const maxRightLegendWidth = Math.min(
MAX_RIGHT_LEGEND_WIDTH,
containerWidth * RIGHT_LEGEND_WIDTH_RATIO,
);
const maxRightLegendWidth = Math.min(MAX_LEGEND_WIDTH, containerWidth * 0.3);
const rightLegendWidth = Math.min(
Math.max(150, desiredLegendWidth),
Math.max(150, approxLegendItemWidth),
maxRightLegendWidth,
);

View File

@@ -15,10 +15,6 @@
&--legend-right {
flex-direction: row;
.chart-layout__legend-wrapper {
padding-top: 8px;
}
}
&__legend-wrapper {

View File

@@ -53,7 +53,6 @@ import {
defaultTraceSelectedColumns,
} from 'container/OptionsMenu/constants';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
@@ -67,6 +66,7 @@ import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMapp
import { cloneDeep, isEqual, omit } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { FormattingOptions } from 'providers/preferences/types';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { ViewProps } from 'types/api/saveViews/types';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
@@ -1031,19 +1031,26 @@ function ExplorerOptions({
/>
</div>
</Modal>
<ExportPanelContainer
<Modal
footer={null}
onOk={onCancel(false)}
onCancel={onCancel(false)}
open={isExport}
onClose={onCancel(false)}
query={isOneChartPerQuery ? queryToExport : query}
isLoading={isLoading}
onExport={(dashboard, isNewDashboard): void => {
if (isOneChartPerQuery && queryToExport) {
onExport(dashboard, isNewDashboard, queryToExport);
} else {
onExport(dashboard, isNewDashboard);
}
}}
/>
centered
destroyOnClose
>
<ExportPanelContainer
query={isOneChartPerQuery ? queryToExport : query}
isLoading={isLoading}
onExport={(dashboard, isNewDashboard): void => {
if (isOneChartPerQuery && queryToExport) {
onExport(dashboard, isNewDashboard, queryToExport);
} else {
onExport(dashboard, isNewDashboard);
}
}}
/>
</Modal>
</div>
);
}
@@ -1051,7 +1058,7 @@ function ExplorerOptions({
export interface ExplorerOptionsProps {
isLoading?: boolean;
onExport: (
dashboard: ExportDashboard | null,
dashboard: Dashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void;

View File

@@ -1,7 +1,6 @@
import { useHistory } from 'react-router-dom';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { MOCK_QUERY } from 'container/QueryTable/Drilldown/__tests__/mockTableData';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { rest, server } from 'mocks-server/server';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
@@ -81,7 +80,7 @@ const renderExplorerOptionWrapper = (
isLoading: false,
onExport: jest.fn() as jest.MockedFunction<
(
dashboard: ExportDashboard | null,
dashboard: Dashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void
@@ -151,7 +150,7 @@ describe('ExplorerOptionWrapper', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const testOnExport = jest.fn() as jest.MockedFunction<
(
dashboard: ExportDashboard | null,
dashboard: Dashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void
@@ -180,16 +179,15 @@ describe('ExplorerOptionWrapper', () => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
// Click the "New dashboard" button
const newDashboardButton = screen.getByTestId('export-panel-new-dashboard');
// Click the "New Dashboard" button
const newDashboardButton = screen.getByRole('button', {
name: /new dashboard/i,
});
await user.click(newDashboardButton);
// Wait for the API call to complete and onExport to be called
await waitFor(() => {
expect(testOnExport).toHaveBeenCalledWith(
{ id: NEW_DASHBOARD_ID, title: TEST_DASHBOARD_TITLE },
true,
);
expect(testOnExport).toHaveBeenCalledWith(mockNewDashboard, true);
});
});
@@ -197,7 +195,7 @@ describe('ExplorerOptionWrapper', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const testOnExport = jest.fn() as jest.MockedFunction<
(
dashboard: ExportDashboard | null,
dashboard: Dashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void
@@ -231,12 +229,13 @@ describe('ExplorerOptionWrapper', () => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
// Wait for the dashboard select dropdown to render inside the dialog
const modal = screen.getByRole('dialog');
// Wait for dashboards to load and then click on the dashboard select dropdown
await waitFor(() => {
expect(modal.querySelector('[role="combobox"]')).toBeTruthy();
expect(screen.getByText('Select Dashboard')).toBeInTheDocument();
});
// Get the modal and find the dashboard select dropdown within it
const modal = screen.getByRole('dialog');
const dashboardSelect = modal.querySelector(
'[role="combobox"]',
) as HTMLElement;
@@ -252,21 +251,19 @@ describe('ExplorerOptionWrapper', () => {
const dashboardOption = screen.getByText(mockDashboard1.data.title);
await user.click(dashboardOption);
// Wait for the selection to be made and the export button to be enabled
// Wait for the selection to be made and the Export button to be enabled
await waitFor(() => {
expect(screen.getByTestId('export-panel-export')).not.toBeDisabled();
const exportButton = screen.getByRole('button', { name: /export/i });
expect(exportButton).not.toBeDisabled();
});
// Click the export button
const exportButton = screen.getByTestId('export-panel-export');
// Click the Export button
const exportButton = screen.getByRole('button', { name: /export/i });
await user.click(exportButton);
// Wait for onExport to be called with the selected dashboard
await waitFor(() => {
expect(testOnExport).toHaveBeenCalledWith(
{ id: 'dashboard-1', title: 'Dashboard 1' },
false,
);
expect(testOnExport).toHaveBeenCalledWith(mockDashboard1, false);
});
});
@@ -287,7 +284,7 @@ describe('ExplorerOptionWrapper', () => {
// Create a real handleExport function similar to LogsExplorerViews
// This should NOT call useUpdateDashboard (as per PR #8029)
const handleExport = (dashboard: ExportDashboard | null): void => {
const handleExport = (dashboard: Dashboard | null): void => {
if (!dashboard) {
return;
}
@@ -329,12 +326,13 @@ describe('ExplorerOptionWrapper', () => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
// Wait for the dashboard select dropdown to render inside the dialog
const modal = screen.getByRole('dialog');
// Wait for dashboards to load and then click on the dashboard select dropdown
await waitFor(() => {
expect(modal.querySelector('[role="combobox"]')).toBeTruthy();
expect(screen.getByText('Select Dashboard')).toBeInTheDocument();
});
// Get the modal and find the dashboard select dropdown within it
const modal = screen.getByRole('dialog');
const dashboardSelect = modal.querySelector(
'[role="combobox"]',
) as HTMLElement;
@@ -350,13 +348,14 @@ describe('ExplorerOptionWrapper', () => {
const dashboardOption = screen.getByText(mockDashboard.data.title);
await user.click(dashboardOption);
// Wait for the selection to be made and the export button to be enabled
// Wait for the selection to be made and the Export button to be enabled
await waitFor(() => {
expect(screen.getByTestId('export-panel-export')).not.toBeDisabled();
const exportButton = screen.getByRole('button', { name: /export/i });
expect(exportButton).not.toBeDisabled();
});
// Click the export button
const exportButton = screen.getByTestId('export-panel-export');
// Click the Export button
const exportButton = screen.getByRole('button', { name: /export/i });
await user.click(exportButton);
// Wait for the handleExport function to be called and navigation to occur
@@ -376,7 +375,7 @@ describe('ExplorerOptionWrapper', () => {
it('should not show export buttons when component is disabled', () => {
const testOnExport = jest.fn() as jest.MockedFunction<
(
dashboard: ExportDashboard | null,
dashboard: Dashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void

View File

@@ -1,74 +0,0 @@
import { useMemo } from 'react';
// eslint-disable-next-line signoz/no-antd-components
import { Select, SelectProps } from 'antd';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { getSelectOptions } from './utils';
import styles from './ExportPanel.module.scss';
interface ExportDashboardSelectProps {
dashboards: ExportDashboard[];
value: string | null;
/** The picked dashboard, pinned as an option so its label survives a later search. */
selectedDashboard: ExportDashboard | null;
loading?: boolean;
disabled?: boolean;
onChange: (dashboardId: string) => void;
onSearch: (search: string) => void;
}
/**
* Dashboard picker for the "Add to dashboard" dialog. Server-side search (`filterOption`
* off, typing via `onSearch`); the selected dashboard is pinned as an option so its label
* survives a narrowing search, and `getPopupContainer` keeps the overlay from clipping the
* dropdown.
*/
function ExportDashboardSelect({
dashboards,
value,
selectedDashboard,
loading,
disabled,
onChange,
onSearch,
}: ExportDashboardSelectProps): JSX.Element {
const options = useMemo<SelectProps['options']>(() => {
const base = getSelectOptions(dashboards) ?? [];
if (
selectedDashboard &&
!base.some((option) => option.value === selectedDashboard.id)
) {
return [
{ label: selectedDashboard.title, value: selectedDashboard.id },
...base,
];
}
return base;
}, [dashboards, selectedDashboard]);
return (
<Select
className={styles.dashboardSelect}
placeholder="Select a dashboard"
showSearch
filterOption={false}
loading={loading}
disabled={disabled}
value={value}
onChange={onChange}
onSearch={onSearch}
data-testid="export-dashboard-select"
options={options}
getPopupContainer={(trigger): HTMLElement =>
trigger.parentElement ?? document.body
}
/>
);
}
ExportDashboardSelect.defaultProps = {
loading: false,
disabled: false,
};
export default ExportDashboardSelect;

View File

@@ -1,42 +0,0 @@
.body {
display: flex;
flex-direction: column;
gap: 20px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.label {
font-size: 12px;
color: var(--l2-foreground);
}
.dashboardSelect {
width: 100%;
}
.newDashboard {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding-top: 16px;
border-top: 1px solid var(--l2-border);
}
.hint {
font-size: 13px;
color: var(--l2-foreground);
}
.footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
width: 100%;
}

View File

@@ -1,159 +1,127 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { useMutation } from 'react-query';
import { Button } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { useCreateExportDashboard } from 'hooks/dashboard/useCreateExportDashboard';
import createDashboard from 'api/v1/dashboards/create';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { ExportPanelProps } from '.';
import {
ExportDashboard,
useExportDashboards,
} from 'hooks/dashboard/useExportDashboards';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
DashboardSelect,
NewDashboardButton,
SelectWrapper,
Title,
Wrapper,
} from './styles';
import { filterOptions, getSelectOptions } from './utils';
import ExportDashboardSelect from './ExportDashboardSelect';
import styles from './ExportPanel.module.scss';
export interface ExportPanelProps {
isLoading?: boolean;
onExport: (
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
) => void;
query: Query | null;
/** Controlled open state of the dialog. */
open: boolean;
/** Called when the dialog requests to close (Cancel / overlay / Esc). */
onClose: () => void;
}
/**
* "Add to dashboard" dialog: export the panel into an existing dashboard or a newly
* created one. Navigation is the caller's job via `onExport` (flag-aware V1/V2 editor).
*/
function ExportPanelContainer({
isLoading,
onExport,
open,
onClose,
}: ExportPanelProps): JSX.Element {
const { t } = useTranslation(['dashboard']);
// Track the object, not just the id, so export survives a search that narrows it out.
const [selectedDashboard, setSelectedDashboard] =
useState<ExportDashboard | null>(null);
const [searchText, setSearchText] = useState('');
const [dashboardId, setDashboardId] = useState<string | null>(null);
const {
dashboards,
data,
isLoading: isAllDashboardsLoading,
isFetching: isDashboardsFetching,
} = useExportDashboards(searchText);
refetch,
} = useGetAllDashboard();
const { create: createNewDashboard, isLoading: createDashboardLoading } =
useCreateExportDashboard({
title: t('new_dashboard_title', { ns: 'dashboard' }),
onCreated: (dashboard) => onExport(dashboard, true),
const { showErrorModal } = useErrorModal();
const { mutate: createNewDashboard, isLoading: createDashboardLoading } =
useMutation(createDashboard, {
onSuccess: (data) => {
if (data.data) {
onExport(data?.data, true);
}
refetch();
},
onError: (error) => {
showErrorModal(error as APIError);
},
});
// Reset on close so each open starts fresh (the dialog stays mounted).
useEffect(() => {
if (!open) {
setSelectedDashboard(null);
setSearchText('');
}
}, [open]);
const handleSelect = useCallback(
(dashboardId: string): void => {
setSelectedDashboard(
dashboards.find(({ id }) => id === dashboardId) ?? null,
);
},
[dashboards],
);
const options = useMemo(() => getSelectOptions(data?.data || []), [data]);
const handleExportClick = useCallback((): void => {
onExport(selectedDashboard, false);
}, [selectedDashboard, onExport]);
const currentSelectedDashboard = data?.data?.find(
({ id }) => id === dashboardId,
);
const isExportDisabled =
isAllDashboardsLoading || !selectedDashboard || isLoading;
onExport(currentSelectedDashboard || null, false);
}, [data, dashboardId, onExport]);
const handleSelect = useCallback(
(selectedDashboardId: string): void => {
setDashboardId(selectedDashboardId);
},
[setDashboardId],
);
const handleNewDashboard = useCallback(async () => {
try {
await createNewDashboard({
title: t('new_dashboard_title', {
ns: 'dashboard',
}),
uploadedGrafana: false,
version: ENTITY_VERSION_V5,
});
} catch (error) {
showErrorModal(error as APIError);
}
}, [createNewDashboard, t, showErrorModal]);
const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading;
const isDisabled =
isAllDashboardsLoading || !options?.length || !dashboardId || isLoading;
return (
<DialogWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
onClose();
}
}}
title="Add to dashboard"
testId="export-panel-dialog"
footer={
<div className={styles.footer}>
<Button
variant="outlined"
color="secondary"
size="md"
onClick={onClose}
testId="export-panel-cancel"
>
Cancel
</Button>
<Button
color="primary"
size="md"
loading={isLoading}
disabled={isExportDisabled}
onClick={handleExportClick}
testId="export-panel-export"
>
Add to dashboard
</Button>
</div>
}
>
<div className={styles.body}>
<div className={styles.field}>
<Typography.Text className={styles.label}>
Select a dashboard
</Typography.Text>
<ExportDashboardSelect
dashboards={dashboards}
value={selectedDashboard?.id ?? null}
selectedDashboard={selectedDashboard}
loading={isDashboardsFetching}
disabled={isAllDashboardsLoading || createDashboardLoading}
onChange={handleSelect}
onSearch={setSearchText}
/>
</div>
<Wrapper direction="vertical">
<Title>Export Panel</Title>
<div className={styles.newDashboard}>
<Typography.Text className={styles.hint}>
Or create a new dashboard with this panel
</Typography.Text>
<Button
variant="outlined"
color="secondary"
size="md"
prefix={<Plus size={14} />}
loading={createDashboardLoading}
disabled={createDashboardLoading}
onClick={createNewDashboard}
testId="export-panel-new-dashboard"
>
New dashboard
</Button>
</div>
</div>
</DialogWrapper>
<SelectWrapper direction="horizontal">
<DashboardSelect
placeholder="Select Dashboard"
options={options}
showSearch
loading={isDashboardLoading}
disabled={isDashboardLoading}
value={dashboardId}
onSelect={handleSelect}
filterOption={filterOptions}
/>
<Button
type="primary"
loading={isLoading}
disabled={isDisabled}
onClick={handleExportClick}
>
Export
</Button>
</SelectWrapper>
<Typography>
Or create dashboard with this panel -
<NewDashboardButton
disabled={createDashboardLoading}
loading={createDashboardLoading}
type="link"
onClick={handleNewDashboard}
>
New Dashboard
</NewDashboardButton>
</Typography>
</Wrapper>
);
}
ExportPanelContainer.defaultProps = {
isLoading: false,
};
export default ExportPanelContainer;

View File

@@ -0,0 +1,49 @@
import { useCallback, useState } from 'react';
import { Modal } from 'antd';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import ExportPanelContainer from './ExportPanelContainer';
function ExportPanel({
isLoading,
onExport,
query,
}: ExportPanelProps): JSX.Element {
const [isExport, setIsExport] = useState<boolean>(false);
const onModalToggle = useCallback((value: boolean) => {
setIsExport(value);
}, []);
const onCancel = (value: boolean) => (): void => {
onModalToggle(value);
};
return (
<Modal
footer={null}
onOk={onCancel(false)}
onCancel={onCancel(false)}
open={isExport}
centered
destroyOnClose
>
<ExportPanelContainer
query={query}
isLoading={isLoading}
onExport={onExport}
/>
</Modal>
);
}
export interface ExportPanelProps {
isLoading?: boolean;
onExport: (dashboard: Dashboard | null, isNewDashboard?: boolean) => void;
query: Query | null;
}
ExportPanel.defaultProps = { isLoading: false };
export default ExportPanel;

View File

@@ -0,0 +1,34 @@
import { FunctionComponent } from 'react';
import { Button, Select, SelectProps, Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import styled from 'styled-components';
export const DashboardSelect: FunctionComponent<SelectProps> = styled(
Select,
)<SelectProps>`
width: 100%;
`;
export const SelectWrapper = styled(Space)`
width: 100%;
margin-bottom: 1rem;
.ant-space-item:first-child {
width: 100%;
max-width: 20rem;
}
`;
export const Wrapper = styled(Space)`
width: 100%;
`;
export const NewDashboardButton = styled(Button)`
&&& {
padding: 0 0.125rem;
}
`;
export const Title = styled(Typography.Text)`
font-size: 1rem;
`;

View File

@@ -1,10 +1,16 @@
import { SelectProps } from 'antd';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { Dashboard } from 'types/api/dashboard/getAll';
export const getSelectOptions = (
data: ExportDashboard[],
): SelectProps['options'] =>
data.map(({ id, title }) => ({
label: title,
export const getSelectOptions = (data: Dashboard[]): SelectProps['options'] =>
data.map(({ id, data }) => ({
label: data.title,
value: id,
}));
export const filterOptions: SelectProps['filterOption'] = (
input,
options,
): boolean =>
(options?.label?.toString() ?? '')
?.toLowerCase()
.includes(input.toLowerCase());

View File

@@ -1,20 +1,17 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listHosts } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesHostRecordDTO,
InframonitoringtypesHostStatusDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import APIError from 'types/api/error';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { initialQueriesMap } from 'constants/queryBuilder';
import K8sBaseDetails, {
K8sDetailsFilters,
@@ -22,12 +19,10 @@ import K8sBaseDetails, {
import { K8sBaseList } from 'container/InfraMonitoringK8sV2/Base/K8sBaseList';
import StatusFilter from 'container/InfraMonitoringHostsV2/StatusFilter';
import { K8sBaseFilters } from 'container/InfraMonitoringK8sV2/Base/types';
import {
InfraMonitoringEntity,
METRIC_NAMESPACE_BY_ENTITY,
} from 'container/InfraMonitoringK8sV2/constants';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useAppContext } from 'providers/App/App';
import { DataSource } from 'types/common/queryBuilder';
import {
@@ -48,8 +43,6 @@ import { getHostsQuickFiltersConfig } from './utils';
import styles from './InfraMonitoringHosts.module.scss';
import { ArrowUpToLine, Filter } from '@signozhq/icons';
import { NANO_SECOND_MULTIPLIER, useGlobalTimeStore } from 'store/globalTime';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
function Hosts(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
@@ -58,17 +51,6 @@ function Hosts(): JSX.Element {
const { redirectWithQueryBuilderData } = useQueryBuilder();
const isInitialized = useRef(false);
const selectedTime = useGlobalTimeStore((state) => state.selectedTime);
const getMinMaxTime = useGlobalTimeStore((state) => state.getMinMaxTime);
const { startUnixMilli, endUnixMilli } = useMemo(() => {
const { minTime, maxTime } = getMinMaxTime();
return {
startUnixMilli: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
endUnixMilli: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedTime, getMinMaxTime]);
useEffect(() => {
if (isInitialized.current) {
return;
@@ -91,6 +73,11 @@ function Hosts(): JSX.Element {
}
}, [compositeQuery, redirectWithQueryBuilderData]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const handleFilterVisibilityChange = (): void => {
setShowFilters(!showFilters);
};
@@ -135,12 +122,13 @@ function Hosts(): JSX.Element {
endTimeBeforeRetention: data.endTimeBeforeRetention,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch hosts';
return {
type: 'list' as const,
records: [] as InframonitoringtypesHostRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
@@ -153,7 +141,7 @@ function Hosts(): JSX.Element {
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesHostRecordDTO | null;
error?: APIError | null;
error?: string | null;
}> => {
try {
const response = await listHosts(
@@ -171,10 +159,11 @@ function Hosts(): JSX.Element {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch host';
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
@@ -183,8 +172,8 @@ function Hosts(): JSX.Element {
const getInitialLogTracesExpression = useCallback(
(host: InframonitoringtypesHostRecordDTO) =>
hostInitialLogTracesExpression(host),
[],
hostInitialLogTracesExpression(host, dotMetricsEnabled),
[dotMetricsEnabled],
);
const controlListPrefix = !showFilters ? (
<div className={styles.quickFiltersToggleContainer}>
@@ -205,31 +194,21 @@ function Hosts(): JSX.Element {
<div className={styles.infraContentRow}>
{showFilters && (
<div className={styles.quickFiltersContainer}>
<OverlayScrollbar>
<>
<div className={styles.quickFiltersContainerHeader}>
<Typography.Text>Filters</Typography.Text>
<Tooltip title="Collapse Filters">
<ArrowUpToLine
style={{ rotate: '270deg', cursor: 'pointer' }}
onClick={handleFilterVisibilityChange}
size="md"
/>
</Tooltip>
</div>
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={getHostsQuickFiltersConfig()}
handleFilterVisibilityChange={handleFilterVisibilityChange}
useFieldApis={{
metricNamespace:
METRIC_NAMESPACE_BY_ENTITY[InfraMonitoringEntity.HOSTS],
startUnixMilli,
endUnixMilli,
}}
<div className={styles.quickFiltersContainerHeader}>
<Typography.Text>Filters</Typography.Text>
<Tooltip title="Collapse Filters">
<ArrowUpToLine
style={{ rotate: '270deg', cursor: 'pointer' }}
onClick={handleFilterVisibilityChange}
size="md"
/>
</>
</OverlayScrollbar>
</Tooltip>
</div>
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={getHostsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
/>
</div>
)}
<div
@@ -246,7 +225,6 @@ function Hosts(): JSX.Element {
getRowKey={getHostRowKey}
getItemKey={getHostItemKey}
eventCategory={InfraMonitoringEvents.HostEntity}
detailsQueryKeyPrefix="hosts"
/>
</div>
</div>

View File

@@ -48,6 +48,24 @@
width: 280px;
min-width: 280px;
border-right: 1px solid var(--l1-border);
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
:global(.ant-collapse-header) {
border-bottom: 1px solid var(--l1-border);
@@ -62,6 +80,28 @@
padding-left: 18px;
}
}
:global(.quick-filters) {
overflow-y: auto;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--accent-primary);
}
}
}
.quickFiltersContainerHeader {

View File

@@ -8,8 +8,8 @@ import {
InframonitoringtypesHostStatusDTO,
} from 'api/generated/services/sigNoz.schemas';
import { K8sDetailsMetadataConfig } from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
import { SelectedItemParams } from 'container/InfraMonitoringK8sV2/hooks';
import {
getHostQueryPayload,
@@ -63,7 +63,7 @@ export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
},
{
label: 'OPERATING SYSTEM',
getValue: (h): string => h.meta?.[INFRA_MONITORING_ATTR_KEYS.OS_TYPE] || '-',
getValue: (h): string => h.meta?.['os.type'] || '-',
render: (value): React.ReactNode =>
value !== '-' ? (
<Badge variant="outline" className={infraHostsStyles.infraMonitoringTags}>
@@ -101,8 +101,9 @@ export function getHostMetricsQueryPayload(
host: InframonitoringtypesHostRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
): ReturnType<typeof getHostQueryPayload> {
return getHostQueryPayload(host.hostName, start, end, true);
return getHostQueryPayload(host.hostName, start, end, dotMetricsEnabled);
}
export { hostWidgetInfo };
@@ -110,13 +111,17 @@ export { hostWidgetInfo };
export const hostGetSelectedItemExpression = (
params: SelectedItemParams,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.HOST_NAME} = ${formatValueForExpression(params.selectedItem ?? '')}`;
`host.name = ${formatValueForExpression(params.selectedItem ?? '')}`;
export function hostInitialLogTracesExpression(
host: InframonitoringtypesHostRecordDTO,
dotMetricsEnabled: boolean,
): string {
const hostKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.HOST_NAME
: 'host_name';
const hostName = formatValueForExpression(host.hostName || '');
return `${INFRA_MONITORING_ATTR_KEYS.HOST_NAME} = ${hostName}`;
return `${hostKey} = ${hostName}`;
}
export function hostInitialEventsExpression(

View File

@@ -130,13 +130,12 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
accessorFn: (row): string => row.status,
width: { min: 140 },
enableSort: false,
cell: ({ value, groupMeta, row, rowId }): React.ReactNode => {
cell: ({ value, groupMeta, row }): React.ReactNode => {
const status = value as InframonitoringtypesHostStatusDTO;
if (groupMeta) {
return (
<GroupedStatusCounts
rowId={rowId}
items={[
{
value: row.activeHostCount,
@@ -196,13 +195,13 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
<ColumnHeader
tooltip="Excluding cache memory."
docPath="/infrastructure-monitoring/host-monitoring#memory-usage"
className={styles.memoryUsageHeader}
>
Memory Usage
<br /> (WSS)
Memory Usage (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.memory,
width: { min: 200 },
width: { min: 240 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
@@ -249,12 +248,11 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
id: 'load15',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#load-avg">
Load Avg
<br /> (15min)
Load Avg (15min)
</ColumnHeader>
),
accessorFn: (row): number => row.load15,
width: { min: 160 },
width: { min: 200 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const load15 = Number(value);
@@ -278,7 +276,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
</ColumnHeader>
),
accessorFn: (row): number => row.diskUsage,
width: { min: 200 },
width: { min: 160 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const diskUsage = value as number;

View File

@@ -13,6 +13,14 @@
text-align: right;
}
.memoryUsageHeader {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: var(--spacing-2);
width: 100%;
}
.statusTag {
width: fit-content;
font-family: Inter, sans-serif;

View File

@@ -7,10 +7,12 @@ import {
IQuickFiltersConfig,
} from 'components/QuickFilters/types';
import { TriangleAlert } from '@signozhq/icons';
import { CellValueTooltip } from 'container/InfraMonitoringK8sV2/components';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
import { CellValueTooltip } from 'container/InfraMonitoringK8sV2/components';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import TanStackTable from 'components/TanStackTableView';
const HOSTNAME_DOCS_URL =
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty';
@@ -21,7 +23,11 @@ export function HostnameCell({
}): React.ReactElement {
const isEmpty = !hostName || !hostName.trim();
if (!isEmpty) {
return <CellValueTooltip value={hostName} />;
return (
<CellValueTooltip value={hostName}>
<TanStackTable.Text>{hostName}</TanStackTable.Text>
</CellValueTooltip>
);
}
return (
<>
@@ -62,18 +68,32 @@ export function HostnameCell({
);
}
export function getHostsQuickFiltersConfig(): IQuickFiltersConfig[] {
export function getHostsQuickFiltersConfig(
dotMetricsEnabled: boolean,
): IQuickFiltersConfig[] {
const hostNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.HOST_NAME
: 'host_name';
const osTypeKey = dotMetricsEnabled ? 'os.type' : 'os_type';
const metricName = dotMetricsEnabled
? 'system.cpu.load_average.15m'
: 'system_cpu_load_average_15m';
const environmentKey = dotMetricsEnabled
? 'deployment.environment'
: 'deployment_environment';
return [
{
type: FiltersType.CHECKBOX,
title: 'Host Name',
attributeKey: {
key: INFRA_MONITORING_ATTR_KEYS.HOST_NAME,
key: hostNameKey,
dataType: DataTypes.String,
type: 'resource',
},
aggregateOperator: 'noop',
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.SYSTEM_CPU_LOAD_AVERAGE_15M,
aggregateAttribute: metricName,
dataSource: DataSource.METRICS,
defaultOpen: true,
},
@@ -81,12 +101,12 @@ export function getHostsQuickFiltersConfig(): IQuickFiltersConfig[] {
type: FiltersType.CHECKBOX,
title: 'OS Type',
attributeKey: {
key: INFRA_MONITORING_ATTR_KEYS.OS_TYPE,
key: osTypeKey,
dataType: DataTypes.String,
type: 'resource',
},
aggregateOperator: 'noop',
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.SYSTEM_CPU_LOAD_AVERAGE_15M,
aggregateAttribute: metricName,
dataSource: DataSource.METRICS,
defaultOpen: true,
},
@@ -94,7 +114,7 @@ export function getHostsQuickFiltersConfig(): IQuickFiltersConfig[] {
type: FiltersType.CHECKBOX,
title: 'Environment',
attributeKey: {
key: INFRA_MONITORING_ATTR_KEYS.DEPLOYMENT_ENVIRONMENT,
key: environmentKey,
dataType: DataTypes.String,
type: 'resource',
},

View File

@@ -11,6 +11,6 @@
}
.columnHeaderLabel {
text-align: left;
text-align: center;
padding: var(--spacing-2) var(--spacing-2) var(--spacing-2) 0px;
}

View File

@@ -1,39 +1,142 @@
import { useCallback, useEffect, useMemo } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useQuery } from 'react-query';
import { Color, Spacing } from '@signozhq/design-tokens';
import { X } from '@signozhq/icons';
import { Button, Drawer, Tooltip } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import { Drawer } from 'antd';
import logEvent from 'api/common/logEvent';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import APIError from 'types/api/error';
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { InfraMonitoringEvents } from 'constants/events';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { QueryParams } from 'constants/query';
import {
GlobalTimeProvider,
NANO_SECOND_MULTIPLIER,
useGlobalTimeStore,
} from 'store/globalTime';
initialQueryBuilderFormValuesMap,
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import GetMinMax from 'lib/getMinMax';
import {
BarChart,
ChevronsLeftRight,
Compass,
DraftingCompass,
ScrollText,
X,
} from '@signozhq/icons';
import { isCustomTimeRange, useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import {
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from '../constants';
import { useInfraMonitoringSelectedItemParams } from '../hooks';
import { InfraMonitoringEntity, VIEW_TYPES } from '../constants';
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks';
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
import {
SelectedItemParams,
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringSelectedItemParams,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
import LoadingContainer from '../LoadingContainer';
import K8sBaseDetailsContent from './K8sBaseDetailsContent';
import { K8sBaseDetailsProps } from './types';
import '../EntityDetailsUtils/entityDetails.styles.scss';
import { parseAsString, useQueryState } from 'nuqs';
import {
EntityCountConfig,
EntityCountsSection,
} from './components/EntityCountsSection/EntityCountsSection';
export type {
CustomTab,
CustomTabRenderProps,
K8sBaseDetailsProps,
K8sDetailsCountConfig,
K8sDetailsFilters,
K8sDetailsMetadataConfig,
} from './types';
const TimeRangeOffset = 1000000000;
export interface K8sDetailsMetadataConfig<T> {
label: string;
getValue: (entity: T) => string | number;
render?: (value: string | number, entity: T) => React.ReactNode;
}
export type K8sDetailsCountConfig<T> = EntityCountConfig<T>;
export interface K8sDetailsFilters {
filter: { expression: string };
start: number;
end: number;
}
export interface K8sBaseDetailsProps<T> {
category: InfraMonitoringEntity;
eventCategory: string;
// Data fetching configuration
getSelectedItemExpression: (params: SelectedItemParams) => string;
fetchEntityData: (
filters: K8sDetailsFilters,
signal?: AbortSignal,
) => Promise<{ data: T | null; error?: string | null }>;
// Entity configuration
getEntityName: (entity: T) => string;
getInitialLogTracesExpression: (entity: T) => string;
getInitialEventsExpression: (entity: T) => string;
metadataConfig: K8sDetailsMetadataConfig<T>[];
countsConfig?: K8sDetailsCountConfig<T>[];
getCountsFilterExpression?: (entity: T) => string;
entityWidgetInfo: {
title: string;
yAxisUnit: string;
docPath?: string;
}[];
getEntityQueryPayload: (
entity: T,
start: number,
end: number,
dotMetricsEnabled: boolean,
) => GetQueryResultsProps[];
queryKeyPrefix: string;
/** When true, only metrics are shown and the Metrics/Logs/Traces/Events tab bar is hidden. */
hideDetailViewTabs?: boolean;
tabsConfig?: {
showMetrics?: boolean;
showLogs?: boolean;
showTraces?: boolean;
showEvents?: boolean;
};
customTabs?: Array<{
key: string;
label: string;
icon: React.ReactNode;
render: (props: {
entity: T;
timeRange: { startTime: number; endTime: number };
selectedInterval: Time;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}) => React.ReactNode;
}>;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function K8sBaseDetails<T>({
category,
eventCategory,
@@ -54,6 +157,7 @@ export default function K8sBaseDetails<T>({
}: K8sBaseDetailsProps<T>): JSX.Element {
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
const lastComputedMinMax = useGlobalTimeStore((s) => s.lastComputedMinMax);
const getAutoRefreshQueryKey = useGlobalTimeStore(
(s) => s.getAutoRefreshQueryKey,
);
@@ -127,9 +231,59 @@ export default function K8sBaseDetails<T>({
const entityName = entity ? getEntityName(entity) : '';
// Content state (previously in K8sBaseDetailsContent)
const tabVisibility = useMemo(
() => ({
showMetrics: true,
showLogs: true,
showTraces: true,
showEvents: true,
...tabsConfig,
}),
[tabsConfig],
);
const { startMs, endMs } = useMemo(
() => ({
startMs: Math.floor(lastComputedMinMax.minTime / NANO_SECOND_MULTIPLIER),
endMs: Math.floor(lastComputedMinMax.maxTime / NANO_SECOND_MULTIPLIER),
}),
[lastComputedMinMax],
);
const [modalTimeRange, setModalTimeRange] = useState(() => ({
startTime: startMs,
endTime: endMs,
}));
// TODO(h4ad): Remove this and use context/zustand
const lastSelectedInterval = useRef<Time | null>(null);
const [selectedInterval, setSelectedInterval] = useState<Time>(
lastSelectedInterval.current
? lastSelectedInterval.current
: isCustomTimeRange(selectedTime)
? DEFAULT_TIME_RANGE
: selectedTime,
);
const [selectedView, setSelectedView] = useInfraMonitoringView();
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
const [userLogsExpression] = useQueryState(
K8S_ENTITY_LOGS_EXPRESSION_KEY,
parseAsString,
);
const [userTracesExpression] = useQueryState(
K8S_ENTITY_TRACES_EXPRESSION_KEY,
parseAsString,
);
useEffect(() => {
if (entity) {
void logEvent(InfraMonitoringEvents.PageVisited, {
logEvent(InfraMonitoringEvents.PageVisited, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
@@ -137,6 +291,133 @@ export default function K8sBaseDetails<T>({
}
}, [entity, eventCategory]);
useEffect(() => {
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
if (!isCustomTimeRange(currentSelectedInterval)) {
setSelectedInterval(currentSelectedInterval);
const { minTime, maxTime } = getMinMaxTime();
setModalTimeRange({
startTime: Math.floor(minTime / TimeRangeOffset),
endTime: Math.floor(maxTime / TimeRangeOffset),
});
}
}, [getMinMaxTime, selectedTime]);
const handleTabChange = (value: string): void => {
setSelectedView(value);
setLogFiltersParam(null);
setTracesFiltersParam(null);
setEventsFiltersParam(null);
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
view: value,
});
};
const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
lastSelectedInterval.current = interval as Time;
setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) {
setModalTimeRange({
startTime: Math.floor(dateTimeRange[0] / 1000),
endTime: Math.floor(dateTimeRange[1] / 1000),
});
} else {
const { maxTime, minTime } = GetMinMax(interval);
setModalTimeRange({
startTime: Math.floor(minTime / TimeRangeOffset),
endTime: Math.floor(maxTime / TimeRangeOffset),
});
}
logEvent(InfraMonitoringEvents.TimeUpdated, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
interval,
view: effectiveView,
});
},
[eventCategory, effectiveView],
);
const handleExplorePagesRedirect = (): void => {
const urlQuery = new URLSearchParams();
if (selectedInterval !== 'custom') {
urlQuery.set(QueryParams.relativeTime, selectedInterval);
} else {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.startTime, modalTimeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
}
logEvent(InfraMonitoringEvents.ExploreClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
view: selectedView,
});
if (selectedView === VIEW_TYPES.LOGS) {
const fullExpression = combineInitialAndUserExpression(
logsAndTracesInitialExpression,
userLogsExpression || '',
);
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.logs,
aggregateOperator: LogsAggregatorOperator.NOOP,
expression: fullExpression,
filter: { expression: fullExpression },
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const fullExpression = combineInitialAndUserExpression(
logsAndTracesInitialExpression,
userTracesExpression || '',
);
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
expression: fullExpression,
filter: { expression: fullExpression },
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};
return (
<Drawer
width="70%"
@@ -163,52 +444,219 @@ export default function K8sBaseDetails<T>({
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
{(isEntityLoading || !selectedItem) && <LoadingContainer />}
{selectedItem && (isEntityError || hasResponseError) && (
{isEntityLoading && <LoadingContainer />}
{(isEntityError || hasResponseError) && (
<div className="entity-error-container">
<ErrorContent
error={
entityResponse?.error ??
(entityError instanceof APIError ? entityError : null) ?? {
code: 500,
message:
entityError instanceof Error
? entityError.message
: 'Failed to load entity details',
}
}
/>
<Typography.Text color="danger">
{entityResponse?.error ||
(entityError instanceof Error
? entityError.message
: 'Failed to load entity details')}
</Typography.Text>
</div>
)}
{selectedItem && entity && !isEntityLoading && !hasResponseError && (
<GlobalTimeProvider
inheritGlobalTime
enableUrlParams={{
relativeTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_RELATIVE_TIME,
startTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_START_TIME,
endTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_END_TIME,
}}
>
<K8sBaseDetailsContent<T>
entity={entity}
category={category}
eventCategory={eventCategory}
metadataConfig={metadataConfig}
countsConfig={countsConfig}
getCountsFilterExpression={getCountsFilterExpression}
selectedItem={selectedItem}
handleClose={handleClose}
entityWidgetInfo={entityWidgetInfo}
getEntityQueryPayload={getEntityQueryPayload}
queryKeyPrefix={queryKeyPrefix}
hideDetailViewTabs={hideDetailViewTabs}
tabsConfig={tabsConfig}
customTabs={customTabs}
logsAndTracesInitialExpression={logsAndTracesInitialExpression}
eventsInitialExpression={eventsInitialExpression}
/>
</GlobalTimeProvider>
{entity && !isEntityLoading && !hasResponseError && (
<>
<div className="entity-detail-drawer__entity">
<div className="entity-details-grid">
<div className="labels-row">
{metadataConfig.map((config) => (
<Typography.Text
key={config.label}
color="muted"
className="entity-details-metadata-label"
>
{config.label}
</Typography.Text>
))}
</div>
<div className="values-row">
{metadataConfig.map((config) => {
const value = config.getValue(entity);
const displayValue = String(value);
return (
<Typography.Text
key={config.label}
className="entity-details-metadata-value"
>
{config.render ? (
config.render(value, entity)
) : (
<Tooltip title={displayValue}>{displayValue}</Tooltip>
)}
</Typography.Text>
);
})}
</div>
</div>
{countsConfig &&
countsConfig.length > 0 &&
selectedItem &&
getCountsFilterExpression && (
<EntityCountsSection
entity={entity}
countsConfig={countsConfig}
selectedItem={selectedItem}
filterExpression={getCountsFilterExpression(entity)}
closeDrawer={handleClose}
/>
)}
</div>
{!hideDetailViewTabs && (
<div className="views-tabs-container">
<ToggleGroupSimple
type="single"
className="views-tabs"
onChange={handleTabChange}
value={selectedView}
items={[
...(tabVisibility.showMetrics
? [
{
value: VIEW_TYPES.METRICS,
label: (
<div className="view-title">
<BarChart size={14} />
Metrics
</div>
),
},
]
: []),
...(tabVisibility.showLogs
? [
{
value: VIEW_TYPES.LOGS,
label: (
<div className="view-title">
<ScrollText size={14} />
Logs
</div>
),
},
]
: []),
...(tabVisibility.showTraces
? [
{
value: VIEW_TYPES.TRACES,
label: (
<div className="view-title">
<DraftingCompass size={14} />
Traces
</div>
),
},
]
: []),
...(tabVisibility.showEvents
? [
{
value: VIEW_TYPES.EVENTS,
label: (
<div className="view-title">
<ChevronsLeftRight size={14} />
Events
</div>
),
},
]
: []),
...(customTabs?.map((tab) => ({
value: tab.key,
label: (
<div className="view-title">
{tab.icon}
{tab.label}
</div>
),
})) ?? []),
]}
/>
{selectedView === VIEW_TYPES.LOGS && (
<Tooltip title="Go to Logs Explorer" placement="left">
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
</Tooltip>
)}
{selectedView === VIEW_TYPES.TRACES && (
<Tooltip title="Go to Traces Explorer" placement="left">
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
</Tooltip>
)}
</div>
)}
{effectiveView === VIEW_TYPES.METRICS && (
<EntityMetrics<T>
entity={entity}
selectedInterval={selectedInterval}
timeRange={modalTimeRange}
handleTimeChange={handleTimeChange}
isModalTimeSelection
entityWidgetInfo={entityWidgetInfo}
getEntityQueryPayload={getEntityQueryPayload}
category={category}
queryKey={`${queryKeyPrefix}Metrics`}
/>
)}
{effectiveView === VIEW_TYPES.LOGS && (
<EntityLogs
timeRange={modalTimeRange}
isModalTimeSelection
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
queryKey={`${queryKeyPrefix}Logs`}
category={category}
initialExpression={logsAndTracesInitialExpression}
/>
)}
{effectiveView === VIEW_TYPES.TRACES && (
<EntityTraces
timeRange={modalTimeRange}
isModalTimeSelection
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
queryKey={`${queryKeyPrefix}Traces`}
category={category}
initialExpression={logsAndTracesInitialExpression}
/>
)}
{effectiveView === VIEW_TYPES.EVENTS && tabVisibility.showEvents && (
<EntityEvents
timeRange={modalTimeRange}
isModalTimeSelection
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
category={category}
queryKey={`${queryKeyPrefix}Events`}
initialExpression={eventsInitialExpression}
/>
)}
{customTabs?.map((tab) =>
selectedView === tab.key ? (
<React.Fragment key={tab.key}>
{tab.render({
entity,
timeRange: modalTimeRange,
selectedInterval,
handleTimeChange,
})}
</React.Fragment>
) : null,
)}
</>
)}
</Drawer>
);

View File

@@ -1,401 +0,0 @@
import { Fragment, useEffect, useMemo } from 'react';
import {
BarChart,
ChevronsLeftRight,
Compass,
DraftingCompass,
ScrollText,
} from '@signozhq/icons';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Typography } from '@signozhq/ui/typography';
import { Button, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { parseAsString, useQueryState } from 'nuqs';
import {
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { VIEW_TYPES } from '../constants';
import { useEntityDetailsTime } from '../EntityDetailsUtils/EntityDateTimeSelector/useEntityDetailsTime';
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks';
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
import {
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
import { EntityCountsSection } from './components/EntityCountsSection/EntityCountsSection';
import { K8sBaseDetailsContentProps } from './types';
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function K8sBaseDetailsContent<T>({
entity,
category,
eventCategory,
metadataConfig,
countsConfig,
getCountsFilterExpression,
selectedItem,
handleClose,
entityWidgetInfo,
getEntityQueryPayload,
queryKeyPrefix,
hideDetailViewTabs,
tabsConfig,
customTabs,
logsAndTracesInitialExpression,
eventsInitialExpression,
}: K8sBaseDetailsContentProps<T>): JSX.Element {
const { timeRange, selectedInterval, handleTimeChange } =
useEntityDetailsTime();
const tabVisibility = useMemo(
() => ({
showMetrics: true,
showLogs: true,
showTraces: true,
showEvents: true,
...tabsConfig,
}),
[tabsConfig],
);
const [selectedView, setSelectedView] = useInfraMonitoringView();
const validTabs = useMemo(() => {
const tabs: string[] = [];
if (tabVisibility.showMetrics) {
tabs.push(VIEW_TYPES.METRICS);
}
if (tabVisibility.showLogs) {
tabs.push(VIEW_TYPES.LOGS);
}
if (tabVisibility.showTraces) {
tabs.push(VIEW_TYPES.TRACES);
}
if (tabVisibility.showEvents) {
tabs.push(VIEW_TYPES.EVENTS);
}
if (customTabs) {
tabs.push(...customTabs.map((t) => t.key));
}
return tabs;
}, [tabVisibility, customTabs]);
useEffect(() => {
if (!hideDetailViewTabs && !validTabs.includes(selectedView)) {
const firstValid = validTabs[0] || VIEW_TYPES.METRICS;
void setSelectedView(firstValid);
}
}, [hideDetailViewTabs, selectedView, validTabs, setSelectedView]);
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
const [userLogsExpression] = useQueryState(
K8S_ENTITY_LOGS_EXPRESSION_KEY,
parseAsString,
);
const [userTracesExpression] = useQueryState(
K8S_ENTITY_TRACES_EXPRESSION_KEY,
parseAsString,
);
const handleTabChange = (value: string | null): void => {
if (!value) {
return;
}
void setSelectedView(value);
void setLogFiltersParam(null);
void setTracesFiltersParam(null);
void setEventsFiltersParam(null);
void logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
view: value,
});
};
const handleExplorePagesRedirect = (): void => {
const urlQuery = new URLSearchParams();
if (selectedInterval !== 'custom') {
urlQuery.set(QueryParams.relativeTime, selectedInterval);
} else {
urlQuery.delete(QueryParams.relativeTime);
// Explorer URL params are in milliseconds, timeRange is in seconds
urlQuery.set(QueryParams.startTime, (timeRange.startTime * 1000).toString());
urlQuery.set(QueryParams.endTime, (timeRange.endTime * 1000).toString());
}
void logEvent(InfraMonitoringEvents.ExploreClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
view: selectedView,
});
if (selectedView === VIEW_TYPES.LOGS) {
const fullExpression = combineInitialAndUserExpression(
logsAndTracesInitialExpression,
userLogsExpression || '',
);
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.logs,
aggregateOperator: LogsAggregatorOperator.NOOP,
expression: fullExpression,
filter: { expression: fullExpression },
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const fullExpression = combineInitialAndUserExpression(
logsAndTracesInitialExpression,
userTracesExpression || '',
);
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
expression: fullExpression,
filter: { expression: fullExpression },
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};
return (
<>
<div className="entity-detail-drawer__entity">
<div className="entity-details-grid">
<div className="labels-row">
{metadataConfig.map((config) => (
<Typography.Text
key={config.label}
color="muted"
className="entity-details-metadata-label"
>
{config.label}
</Typography.Text>
))}
</div>
<div className="values-row">
{metadataConfig.map((config) => {
const value = config.getValue(entity);
const displayValue = String(value);
return (
<Typography.Text
key={config.label}
className="entity-details-metadata-value"
>
{config.render ? (
config.render(value, entity)
) : (
<Tooltip title={displayValue}>{displayValue}</Tooltip>
)}
</Typography.Text>
);
})}
</div>
</div>
{countsConfig &&
countsConfig.length > 0 &&
selectedItem &&
getCountsFilterExpression && (
<EntityCountsSection
entity={entity}
countsConfig={countsConfig}
selectedItem={selectedItem}
filterExpression={getCountsFilterExpression(entity)}
closeDrawer={handleClose}
/>
)}
</div>
{!hideDetailViewTabs && (
<div className="views-tabs-container">
<ToggleGroupSimple
type="single"
className="views-tabs"
onChange={handleTabChange}
value={selectedView}
items={[
...(tabVisibility.showMetrics
? [
{
value: VIEW_TYPES.METRICS,
label: (
<div className="view-title">
<BarChart size={14} />
Metrics
</div>
),
},
]
: []),
...(tabVisibility.showLogs
? [
{
value: VIEW_TYPES.LOGS,
label: (
<div className="view-title">
<ScrollText size={14} />
Logs
</div>
),
},
]
: []),
...(tabVisibility.showTraces
? [
{
value: VIEW_TYPES.TRACES,
label: (
<div className="view-title">
<DraftingCompass size={14} />
Traces
</div>
),
},
]
: []),
...(tabVisibility.showEvents
? [
{
value: VIEW_TYPES.EVENTS,
label: (
<div className="view-title">
<ChevronsLeftRight size={14} />
Events
</div>
),
},
]
: []),
...(customTabs?.map((tab) => ({
value: tab.key,
label: (
<div className="view-title">
{tab.icon}
{tab.label}
</div>
),
})) ?? []),
]}
/>
{selectedView === VIEW_TYPES.LOGS && (
<Tooltip title="Go to Logs Explorer" placement="left">
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
</Tooltip>
)}
{selectedView === VIEW_TYPES.TRACES && (
<Tooltip title="Go to Traces Explorer" placement="left">
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
</Tooltip>
)}
</div>
)}
{effectiveView === VIEW_TYPES.METRICS && (
<EntityMetrics<T>
entity={entity}
eventEntity={eventCategory}
entityWidgetInfo={entityWidgetInfo}
getEntityQueryPayload={getEntityQueryPayload}
category={category}
queryKey={`${queryKeyPrefix}Metrics`}
/>
)}
{effectiveView === VIEW_TYPES.LOGS && (
<EntityLogs
eventEntity={eventCategory}
queryKey={`${queryKeyPrefix}Logs`}
category={category}
initialExpression={logsAndTracesInitialExpression}
/>
)}
{effectiveView === VIEW_TYPES.TRACES && (
<EntityTraces
eventEntity={eventCategory}
queryKey={`${queryKeyPrefix}Traces`}
category={category}
initialExpression={logsAndTracesInitialExpression}
/>
)}
{effectiveView === VIEW_TYPES.EVENTS && tabVisibility.showEvents && (
<EntityEvents
eventEntity={eventCategory}
queryKey={`${queryKeyPrefix}Events`}
initialExpression={eventsInitialExpression}
category={category}
/>
)}
{customTabs?.map((tab) =>
selectedView === tab.key ? (
<Fragment key={tab.key}>
{tab.render({
entity,
timeRange,
selectedInterval,
handleTimeChange,
})}
</Fragment>
) : null,
)}
</>
);
}

View File

@@ -14,7 +14,6 @@ import { useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
import { openInNewTab } from 'utils/navigation';
import APIError from 'types/api/error';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -34,14 +33,13 @@ import K8sHeader from './K8sHeader';
import { K8sPaginationWarning } from './K8sPaginationWarning';
import { K8sBaseFilters } from './types';
import { getGroupedByMeta } from './utils';
import { K8sInstrumentationChecksCallout } from './components/K8sInstrumentationChecksCallout/K8sInstrumentationChecksCallout';
import styles from './K8sBaseList.module.scss';
import cx from 'classnames';
export type K8sBaseListEmptyStateContext = {
isError: boolean;
error?: APIError | null;
error?: string | null;
totalCount: number;
hasFilters: boolean;
isLoading: boolean;
@@ -68,21 +66,20 @@ export type K8sBaseListProps<
records?: T[];
data?: T[];
total: number;
error?: APIError | null;
error?: string | null;
rawData?: unknown;
endTimeBeforeRetention?: boolean;
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
}>;
/** Function to get the unique key for a row. */
getRowKey: (record: T) => string;
getRowKey?: (record: T) => string;
/** Function to get the item key used for selection. Can return string or SelectedItemParams. */
getItemKey: (record: T) => TItemKey;
getItemKey?: (record: T) => TItemKey;
eventCategory: InfraMonitoringEvents;
renderEmptyState?: (
context: K8sBaseListEmptyStateContext,
) => React.ReactNode | null;
extraQueryKeyParts?: string[];
detailsQueryKeyPrefix: string;
};
export function K8sBaseList<
@@ -99,7 +96,6 @@ export function K8sBaseList<
eventCategory,
renderEmptyState,
extraQueryKeyParts = [],
detailsQueryKeyPrefix,
}: K8sBaseListProps<T, TItemKey>): JSX.Element {
const { currentQuery } = useQueryBuilder();
const expression = currentQuery.builder.queryData[0]?.filter?.expression || '';
@@ -236,29 +232,17 @@ export function K8sBaseList<
}, [eventCategory, totalCount]);
const handleRowClick = useCallback(
(record: T, itemKey: TItemKey): void => {
(_record: T, itemKey: TItemKey): void => {
if (groupBy.length === 0) {
const params: SelectedItemParams =
typeof itemKey === 'object'
? itemKey
: {
selectedItem: itemKey,
clusterName: null,
namespaceName: null,
};
if (detailsQueryKeyPrefix) {
const detailQueryKey = getAutoRefreshQueryKey(
selectedTime,
`${detailsQueryKeyPrefix}EntityDetails`,
params.selectedItem,
params.clusterName,
params.namespaceName,
);
queryClient.setQueryData(detailQueryKey, { data: record });
if (typeof itemKey === 'object' && itemKey !== null) {
setSelectedItemParams(itemKey);
} else {
setSelectedItemParams({
selectedItem: itemKey,
clusterName: null,
namespaceName: null,
});
}
setSelectedItemParams(params);
}
void logEvent(InfraMonitoringEvents.ItemClicked, {
@@ -267,15 +251,7 @@ export function K8sBaseList<
category: eventCategory,
});
},
[
eventCategory,
groupBy.length,
setSelectedItemParams,
detailsQueryKeyPrefix,
getAutoRefreshQueryKey,
selectedTime,
queryClient,
],
[eventCategory, groupBy.length, setSelectedItemParams],
);
const handleRowClickNewTab = useCallback(
@@ -401,8 +377,6 @@ export function K8sBaseList<
cancelQuery={cancelQuery}
/>
<div ref={containerRef} className={styles.tableContainer}>
<K8sInstrumentationChecksCallout entity={entity} />
{isError && (
<Typography>
{data?.error?.toString() || 'Something went wrong'}
@@ -425,7 +399,7 @@ export function K8sBaseList<
onRowClickNewTab={handleRowClickNewTab}
renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined}
getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined}
className={cx(styles.k8SListTable)}
className={cx(styles.k8SListTable, expandedRowColumns)}
enableQueryParams={{
page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,

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