Compare commits

..

133 Commits

Author SHA1 Message Date
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
693 changed files with 6173 additions and 6028 deletions

View File

@@ -39,6 +39,7 @@ jobs:
matrix:
suite:
- alerts
- alertmanager
- callbackauthn
- cloudintegrations
- dashboard

View File

@@ -408,16 +408,6 @@ cloudintegration:
# The version of the cloud integration agent.
version: v0.0.8
##################### Trace Detail #####################
tracedetail:
waterfall:
# Number of spans returned per request when the trace is too large to show all at once.
span_page_size: 500
# Maximum depth of descendents to auto-expand for the selected span.
max_depth_to_auto_expand: 5
# Threshold below which all spans are returned without windowing.
max_limit_to_select_all_spans: 10000
##################### Authz #################################
authz:
# Specifies the authz provider to use.

View File

@@ -4581,140 +4581,6 @@ components:
TimeDuration:
format: int64
type: integer
TracedetailtypesEvent:
properties:
attributeMap:
additionalProperties: {}
type: object
isError:
type: boolean
name:
type: string
timeUnixNano:
minimum: 0
type: integer
type: object
TracedetailtypesGettableWaterfallTrace:
properties:
endTimestampMillis:
minimum: 0
type: integer
hasMissingSpans:
type: boolean
hasMore:
type: boolean
rootServiceEntryPoint:
type: string
rootServiceName:
type: string
serviceNameToTotalDurationMap:
additionalProperties:
minimum: 0
type: integer
nullable: true
type: object
spans:
items:
$ref: '#/components/schemas/TracedetailtypesWaterfallSpan'
nullable: true
type: array
startTimestampMillis:
minimum: 0
type: integer
totalErrorSpansCount:
minimum: 0
type: integer
totalSpansCount:
minimum: 0
type: integer
uncollapsedSpans:
items:
type: string
nullable: true
type: array
type: object
TracedetailtypesPostableWaterfall:
properties:
limit:
minimum: 0
type: integer
selectedSpanId:
type: string
uncollapsedSpans:
items:
type: string
nullable: true
type: array
type: object
TracedetailtypesWaterfallSpan:
properties:
attributes:
additionalProperties: {}
nullable: true
type: object
db_name:
type: string
db_operation:
type: string
duration_nano:
minimum: 0
type: integer
events:
items:
$ref: '#/components/schemas/TracedetailtypesEvent'
nullable: true
type: array
external_http_method:
type: string
external_http_url:
type: string
flags:
minimum: 0
type: integer
has_children:
type: boolean
has_error:
type: boolean
http_host:
type: string
http_method:
type: string
http_url:
type: string
is_remote:
type: string
kind_string:
type: string
level:
minimum: 0
type: integer
name:
type: string
parent_span_id:
type: string
resource:
additionalProperties:
type: string
nullable: true
type: object
response_status_code:
type: string
span_id:
type: string
status_code:
type: integer
status_code_string:
type: string
status_message:
type: string
sub_tree_node_count:
minimum: 0
type: integer
trace_id:
type: string
trace_state:
type: string
type: object
TypesAlertStatus:
properties:
inhibitedBy:
@@ -16129,76 +15995,6 @@ paths:
summary: Put profile in Zeus for a deployment.
tags:
- zeus
/api/v3/traces/{traceID}/waterfall:
post:
deprecated: false
description: Returns the waterfall view of spans for a given trace ID with tree
structure, metadata, and windowed pagination
operationId: GetWaterfall
parameters:
- in: path
name: traceID
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TracedetailtypesPostableWaterfall'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TracedetailtypesGettableWaterfallTrace'
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: Get waterfall view for a trace
tags:
- tracedetail
/api/v5/query_range:
post:
deprecated: false

View File

@@ -86,28 +86,6 @@ func (provider *provider) BatchCheck(ctx context.Context, tupleReq map[string]*o
return provider.openfgaServer.BatchCheck(ctx, tupleReq)
}
func (provider *provider) CheckTransactions(ctx context.Context, subject string, orgID valuer.UUID, transactions []*authtypes.Transaction) ([]*authtypes.TransactionWithAuthorization, error) {
tuples, err := authtypes.NewTuplesFromTransactions(transactions, subject, orgID)
if err != nil {
return nil, err
}
batchResults, err := provider.openfgaServer.BatchCheck(ctx, tuples)
if err != nil {
return nil, err
}
results := make([]*authtypes.TransactionWithAuthorization, len(transactions))
for i, txn := range transactions {
result := batchResults[txn.ID.StringValue()]
results[i] = &authtypes.TransactionWithAuthorization{
Transaction: txn,
Authorized: result.Authorized,
}
}
return results, nil
}
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType authtypes.Type) ([]*authtypes.Object, error) {
return provider.openfgaServer.ListObjects(ctx, subject, relation, objectType)
}

View File

@@ -277,8 +277,23 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
annotations := make(ruletypes.Labels, 0, len(r.Annotations().Map()))
for name, value := range r.Annotations().Map() {
// no need to expand custom templating annotations — they get expanded in the notifier layer
if ruletypes.IsCustomTemplatingAnnotation(name) {
annotations = append(annotations, ruletypes.Label{Name: name, Value: value})
continue
}
annotations = append(annotations, ruletypes.Label{Name: name, Value: expand(value)})
}
// Add values to be used in notifier layer for notification templates
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationValue, Value: value})
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationThresholdValue, Value: threshold})
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationCompareOp, Value: smpl.CompareOperator.Literal()})
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationMatchType, Value: smpl.MatchType.Literal()})
if smpl.IsRecovering {
lb.Set(ruletypes.LabelIsRecovering, "true")
}
if smpl.IsMissing {
lb.Set(ruletypes.AlertNameLabel, "[No data] "+r.Name())
lb.Set(ruletypes.NoDataLabel, "true")

View File

@@ -286,20 +286,6 @@
// Prevents useStore.getState() - export standalone actions instead
"signoz/no-navigator-clipboard": "error",
// Prevents navigator.clipboard - use useCopyToClipboard hook instead (disabled in tests via override)
"signoz/no-raw-absolute-path": "error",
// Prevents window.open(path), window.location.origin + path, window.location.href = path
"no-restricted-globals": [
"error",
{
"name": "localStorage",
"message": "Use scoped wrappers from api/browser/localstorage/ instead (ensures keys are prefixed when served under a URL base path)."
},
{
"name": "sessionStorage",
"message": "Use scoped wrappers from api/browser/sessionstorage/ instead (ensures keys are prefixed when served under a URL base path)."
}
],
// Prevents direct localStorage/sessionStorage access — use scoped wrappers
"no-restricted-imports": [
"error",
{
@@ -611,11 +597,8 @@
"rules": {
"import/first": "off",
// Should ignore due to mocks
"signoz/no-navigator-clipboard": "off",
// Tests can use navigator.clipboard directly,
"signoz/no-raw-absolute-path":"off",
"no-restricted-globals": "off"
// Tests need raw localStorage/sessionStorage to seed DOM state for isolation
"signoz/no-navigator-clipboard": "off"
// Tests can use navigator.clipboard directly
}
},
{

View File

@@ -1,4 +1,3 @@
// oxlint-disable-next-line typescript/no-require-imports
const path = require('path');
module.exports = {

View File

@@ -2,7 +2,6 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<base href="[[.BaseHref]]" />
<meta
http-equiv="Cache-Control"
content="no-cache, no-store, must-revalidate, max-age: 0"
@@ -40,12 +39,12 @@
<meta
data-react-helmet="true"
property="og:image"
content="[[.BaseHref]]images/signoz-hero-image.webp"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image"
content="[[.BaseHref]]images/signoz-hero-image.webp"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
@@ -60,7 +59,7 @@
<meta data-react-helmet="true" name="docusaurus_locale" content="en" />
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
<meta name="robots" content="noindex" />
<link data-react-helmet="true" rel="shortcut icon" href="favicon.ico" />
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
</head>
<body data-theme="default">
<script>
@@ -68,14 +67,8 @@
// Mirrors the logic in ThemeProvider (hooks/useDarkMode/index.tsx).
(function () {
try {
// When served under a URL prefix (e.g. /signoz/), storage keys are scoped
// to that prefix by the React app (see utils/storage.ts getScopedKey).
// Read the <base> tag — already populated by the Go template — to derive
// the same prefix here, before any JS module has loaded.
var basePath = (document.querySelector('base') || {}).getAttribute('href') || '/';
var prefix = basePath === '/' ? '' : basePath;
var theme = localStorage.getItem(prefix + 'THEME');
var autoSwitch = localStorage.getItem(prefix + 'THEME_AUTO_SWITCH') === 'true';
var theme = localStorage.getItem('THEME');
var autoSwitch = localStorage.getItem('THEME_AUTO_SWITCH') === 'true';
if (autoSwitch) {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
@@ -143,7 +136,7 @@
})(document, 'script');
}
</script>
<link rel="stylesheet" href="css/uPlot.min.css" />
<link rel="stylesheet" href="/css/uPlot.min.css" />
<script type="module" src="./src/index.tsx"></script>
</body>
</html>

View File

@@ -1,152 +0,0 @@
/**
* Rule: no-raw-absolute-path
*
* Catches patterns that break at runtime when the app is served from a
* sub-path (e.g. /signoz/):
*
* 1. window.open(path, '_blank')
* → use openInNewTab(path) which calls withBasePath internally
*
* 2. window.location.origin + path / `${window.location.origin}${path}`
* → use getAbsoluteUrl(path)
*
* 3. frontendBaseUrl: window.location.origin (bare origin usage)
* → use getBaseUrl() to include the base path
*
* 4. window.location.href = path
* → use withBasePath(path) or navigate() for internal navigation
*
* External URLs (first arg starts with "http") are explicitly allowed.
*/
function isOriginAccess(node) {
return (
node.type === 'MemberExpression' &&
!node.computed &&
node.property.name === 'origin' &&
node.object.type === 'MemberExpression' &&
!node.object.computed &&
node.object.property.name === 'location' &&
node.object.object.type === 'Identifier' &&
node.object.object.name === 'window'
);
}
function isHrefAccess(node) {
return (
node.type === 'MemberExpression' &&
!node.computed &&
node.property.name === 'href' &&
node.object.type === 'MemberExpression' &&
!node.object.computed &&
node.object.property.name === 'location' &&
node.object.object.type === 'Identifier' &&
node.object.object.name === 'window'
);
}
function isExternalUrl(node) {
if (node.type === 'Literal' && typeof node.value === 'string') {
return node.value.startsWith('http://') || node.value.startsWith('https://');
}
if (node.type === 'TemplateLiteral' && node.quasis.length > 0) {
const raw = node.quasis[0].value.raw;
return raw.startsWith('http://') || raw.startsWith('https://');
}
return false;
}
// window.open(withBasePath(x)) and window.open(getAbsoluteUrl(x)) are already safe.
function isSafeHelperCall(node) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
(node.callee.name === 'withBasePath' || node.callee.name === 'getAbsoluteUrl')
);
}
export default {
meta: {
type: 'suggestion',
docs: {
description:
'Disallow raw window.open and origin-concatenation patterns that miss the runtime base path',
category: 'Base Path Safety',
},
schema: [],
messages: {
windowOpen:
'Use openInNewTab(path) instead of window.open(path, "_blank") — openInNewTab prepends the base path automatically.',
originConcat:
'Use getAbsoluteUrl(path) instead of window.location.origin + path — getAbsoluteUrl prepends the base path automatically.',
originDirect:
'Use getBaseUrl() instead of window.location.origin — getBaseUrl includes the base path.',
hrefAssign:
'Use withBasePath(path) or navigate() instead of window.location.href = path — ensures the base path is included.',
},
},
create(context) {
return {
// window.open(path, ...) — allow only external first-arg URLs
CallExpression(node) {
const { callee, arguments: args } = node;
if (
callee.type !== 'MemberExpression' ||
callee.object.type !== 'Identifier' ||
callee.object.name !== 'window' ||
callee.property.name !== 'open'
)
{return;}
if (args.length === 0) {return;}
if (isExternalUrl(args[0])) {return;}
if (isSafeHelperCall(args[0])) {return;}
context.report({ node, messageId: 'windowOpen' });
},
// window.location.origin + path
BinaryExpression(node) {
if (node.operator !== '+') {return;}
if (isOriginAccess(node.left) || isOriginAccess(node.right)) {
context.report({ node, messageId: 'originConcat' });
}
},
// `${window.location.origin}${path}`
TemplateLiteral(node) {
if (node.expressions.some(isOriginAccess)) {
context.report({ node, messageId: 'originConcat' });
}
},
// window.location.origin used directly (not in concatenation)
// Catches: frontendBaseUrl: window.location.origin
MemberExpression(node) {
if (!isOriginAccess(node)) {return;}
const parent = node.parent;
// Skip if parent is BinaryExpression with + (handled by BinaryExpression visitor)
if (parent.type === 'BinaryExpression' && parent.operator === '+') {return;}
// Skip if inside TemplateLiteral (handled by TemplateLiteral visitor)
if (parent.type === 'TemplateLiteral') {return;}
context.report({ node, messageId: 'originDirect' });
},
// window.location.href = path
AssignmentExpression(node) {
if (node.operator !== '=') {return;}
if (!isHrefAccess(node.left)) {return;}
// Allow external URLs
if (isExternalUrl(node.right)) {return;}
// Allow safe helper calls
if (isSafeHelperCall(node.right)) {return;}
context.report({ node, messageId: 'hrefAssign' });
},
};
},
};

View File

@@ -8,7 +8,6 @@
import noZustandGetStateInHooks from './rules/no-zustand-getstate-in-hooks.mjs';
import noNavigatorClipboard from './rules/no-navigator-clipboard.mjs';
import noUnsupportedAssetPattern from './rules/no-unsupported-asset-pattern.mjs';
import noRawAbsolutePath from './rules/no-raw-absolute-path.mjs';
export default {
meta: {
@@ -18,6 +17,5 @@ export default {
'no-zustand-getstate-in-hooks': noZustandGetStateInHooks,
'no-navigator-clipboard': noNavigatorClipboard,
'no-unsupported-asset-pattern': noUnsupportedAssetPattern,
'no-raw-absolute-path': noRawAbsolutePath,
},
};

View File

@@ -60,7 +60,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
if (org && org.length > 0 && org[0].id !== undefined) {
return org[0];
}
return;
return undefined;
}, [org]);
const { data: usersData, isFetching: isFetchingUsers } = useListUsers({
@@ -192,7 +192,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
if (isPrivate) {
if (isLoggedInState) {
const route = routePermission[key];
if (route && route.some((e) => e === user.role) === undefined) {
if (route && route.find((e) => e === user.role) === undefined) {
return <Redirect to={ROUTES.UN_AUTHORIZED} />;
}
} else {

View File

@@ -320,12 +320,12 @@ function App(): JSX.Element {
}),
],
// Performance Monitoring
tracesSampleRate: 1, // Capture 100% of the transactions
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
tracePropagationTargets: [],
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
beforeSend(event) {
const sessionReplayUrl = posthog.get_session_replay_url?.({
withTimestamp: true,

View File

@@ -2,7 +2,6 @@ import { initReactI18next } from 'react-i18next';
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { getBasePath } from 'utils/basePath';
import cacheBursting from '../../i18n-translations-hash.json';
@@ -25,7 +24,7 @@ i18n
const ns = namespace[0];
const pathkey = `/${language}/${ns}`;
const hash = cacheBursting[pathkey as keyof typeof cacheBursting] || '';
return `${getBasePath()}locales/${language}/${namespace}.json?h=${hash}`;
return `/locales/${language}/${namespace}.json?h=${hash}`;
},
},
react: {

View File

@@ -24,7 +24,7 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
const { errors, error } = data;
const errorMessage =
Array.isArray(errors) && errors.length > 0 ? errors[0].msg : error;
Array.isArray(errors) && errors.length >= 1 ? errors[0].msg : error;
return {
statusCode,

View File

@@ -1,130 +0,0 @@
/**
* localstorage/get — lazy migration tests.
*
* basePath is memoized at module init, so each describe block re-imports the
* module with a fresh DOM state via jest.isolateModules.
*/
type GetModule = typeof import('../get');
function loadGetModule(href: string): GetModule {
const base = document.createElement('base');
base.setAttribute('href', href);
document.head.append(base);
let mod!: GetModule;
jest.isolateModules(() => {
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
mod = require('../get');
});
return mod;
}
afterEach(() => {
for (const el of document.head.querySelectorAll('base')) {
el.remove();
}
localStorage.clear();
});
describe('get — root path "/"', () => {
it('reads the bare key', () => {
const { default: get } = loadGetModule('/');
localStorage.setItem('AUTH_TOKEN', 'tok');
expect(get('AUTH_TOKEN')).toBe('tok');
});
it('returns null when key is absent', () => {
const { default: get } = loadGetModule('/');
expect(get('MISSING')).toBeNull();
});
it('does NOT promote bare keys (no-op at root)', () => {
const { default: get } = loadGetModule('/');
localStorage.setItem('THEME', 'light');
get('THEME');
// bare key must still be present — no migration at root
expect(localStorage.getItem('THEME')).toBe('light');
});
});
describe('get — prefixed path "/signoz/"', () => {
it('reads an already-scoped key directly', () => {
const { default: get } = loadGetModule('/signoz/');
localStorage.setItem('/signoz/AUTH_TOKEN', 'scoped-tok');
expect(get('AUTH_TOKEN')).toBe('scoped-tok');
});
it('returns null when neither scoped nor bare key exists', () => {
const { default: get } = loadGetModule('/signoz/');
expect(get('MISSING')).toBeNull();
});
it('lazy-migrates bare key to scoped key on first read', () => {
const { default: get } = loadGetModule('/signoz/');
localStorage.setItem('AUTH_TOKEN', 'old-tok');
const result = get('AUTH_TOKEN');
expect(result).toBe('old-tok');
expect(localStorage.getItem('/signoz/AUTH_TOKEN')).toBe('old-tok');
expect(localStorage.getItem('AUTH_TOKEN')).toBeNull();
});
it('scoped key takes precedence over bare key', () => {
const { default: get } = loadGetModule('/signoz/');
localStorage.setItem('AUTH_TOKEN', 'bare-tok');
localStorage.setItem('/signoz/AUTH_TOKEN', 'scoped-tok');
expect(get('AUTH_TOKEN')).toBe('scoped-tok');
// bare key left untouched — scoped already existed
expect(localStorage.getItem('AUTH_TOKEN')).toBe('bare-tok');
});
it('subsequent reads after migration use scoped key (no double-write)', () => {
const { default: get } = loadGetModule('/signoz/');
localStorage.setItem('THEME', 'dark');
get('THEME'); // triggers migration
localStorage.removeItem('THEME'); // simulate bare key gone
// second read still finds the scoped key
expect(get('THEME')).toBe('dark');
});
});
describe('get — two-prefix isolation', () => {
it('/signoz/ and /testing/ do not share migrated values', () => {
localStorage.setItem('THEME', 'light');
const base1 = document.createElement('base');
base1.setAttribute('href', '/signoz/');
document.head.append(base1);
let getSignoz!: GetModule['default'];
jest.isolateModules(() => {
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
getSignoz = require('../get').default;
});
base1.remove();
// migrate bare → /signoz/THEME
getSignoz('THEME');
const base2 = document.createElement('base');
base2.setAttribute('href', '/testing/');
document.head.append(base2);
let getTesting!: GetModule['default'];
jest.isolateModules(() => {
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
getTesting = require('../get').default;
});
base2.remove();
// /testing/ prefix: bare key already gone, scoped key does not exist
expect(getTesting('THEME')).toBeNull();
expect(localStorage.getItem('/signoz/THEME')).toBe('light');
expect(localStorage.getItem('/testing/THEME')).toBeNull();
});
});
export {};

View File

@@ -1,26 +1,7 @@
/* oxlint-disable no-restricted-globals */
import { getBasePath } from 'utils/basePath';
import { getScopedKey } from 'utils/storage';
const get = (key: string): string | null => {
try {
const scopedKey = getScopedKey(key);
const value = localStorage.getItem(scopedKey);
// Lazy migration: if running under a URL prefix and the scoped key doesn't
// exist yet, fall back to the bare key (written by a previous root deployment).
// Promote it to the scoped key and remove the bare key so future reads are fast.
if (value === null && getBasePath() !== '/') {
const bare = localStorage.getItem(key);
if (bare !== null) {
localStorage.setItem(scopedKey, bare);
localStorage.removeItem(key);
return bare;
}
}
return value;
} catch {
return localStorage.getItem(key);
} catch (e) {
return '';
}
};

View File

@@ -1,11 +1,8 @@
/* oxlint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const remove = (key: string): boolean => {
try {
localStorage.removeItem(getScopedKey(key));
window.localStorage.removeItem(key);
return true;
} catch {
} catch (e) {
return false;
}
};

View File

@@ -1,11 +1,8 @@
/* oxlint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const set = (key: string, value: string): boolean => {
try {
localStorage.setItem(getScopedKey(key), value);
localStorage.setItem(key, value);
return true;
} catch {
} catch (e) {
return false;
}
};

View File

@@ -1,81 +0,0 @@
/**
* sessionstorage/get — lazy migration tests.
* Mirrors the localStorage get tests; same logic, different storage.
*/
type GetModule = typeof import('../get');
function loadGetModule(href: string): GetModule {
const base = document.createElement('base');
base.setAttribute('href', href);
document.head.append(base);
let mod!: GetModule;
jest.isolateModules(() => {
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
mod = require('../get');
});
return mod;
}
afterEach(() => {
for (const el of document.head.querySelectorAll('base')) {
el.remove();
}
sessionStorage.clear();
});
describe('get — root path "/"', () => {
it('reads the bare key', () => {
const { default: get } = loadGetModule('/');
sessionStorage.setItem('retry-lazy-refreshed', 'true');
expect(get('retry-lazy-refreshed')).toBe('true');
});
it('returns null when key is absent', () => {
const { default: get } = loadGetModule('/');
expect(get('MISSING')).toBeNull();
});
it('does NOT promote bare keys at root', () => {
const { default: get } = loadGetModule('/');
sessionStorage.setItem('retry-lazy-refreshed', 'true');
get('retry-lazy-refreshed');
expect(sessionStorage.getItem('retry-lazy-refreshed')).toBe('true');
});
});
describe('get — prefixed path "/signoz/"', () => {
it('reads an already-scoped key directly', () => {
const { default: get } = loadGetModule('/signoz/');
sessionStorage.setItem('/signoz/retry-lazy-refreshed', 'true');
expect(get('retry-lazy-refreshed')).toBe('true');
});
it('returns null when neither scoped nor bare key exists', () => {
const { default: get } = loadGetModule('/signoz/');
expect(get('MISSING')).toBeNull();
});
it('lazy-migrates bare key to scoped key on first read', () => {
const { default: get } = loadGetModule('/signoz/');
sessionStorage.setItem('retry-lazy-refreshed', 'true');
const result = get('retry-lazy-refreshed');
expect(result).toBe('true');
expect(sessionStorage.getItem('/signoz/retry-lazy-refreshed')).toBe('true');
expect(sessionStorage.getItem('retry-lazy-refreshed')).toBeNull();
});
it('scoped key takes precedence over bare key', () => {
const { default: get } = loadGetModule('/signoz/');
sessionStorage.setItem('retry-lazy-refreshed', 'bare');
sessionStorage.setItem('/signoz/retry-lazy-refreshed', 'scoped');
expect(get('retry-lazy-refreshed')).toBe('scoped');
expect(sessionStorage.getItem('retry-lazy-refreshed')).toBe('bare');
});
});
export {};

View File

@@ -1,27 +0,0 @@
/* oxlint-disable no-restricted-globals */
import { getBasePath } from 'utils/basePath';
import { getScopedKey } from 'utils/storage';
const get = (key: string): string | null => {
try {
const scopedKey = getScopedKey(key);
const value = sessionStorage.getItem(scopedKey);
// Lazy migration: same pattern as localStorage — promote bare keys written
// by a previous root deployment to the scoped key on first read.
if (value === null && getBasePath() !== '/') {
const bare = sessionStorage.getItem(key);
if (bare !== null) {
sessionStorage.setItem(scopedKey, bare);
sessionStorage.removeItem(key);
return bare;
}
}
return value;
} catch {
return '';
}
};
export default get;

View File

@@ -1,13 +0,0 @@
/* oxlint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const remove = (key: string): boolean => {
try {
sessionStorage.removeItem(getScopedKey(key));
return true;
} catch {
return false;
}
};
export default remove;

View File

@@ -1,13 +0,0 @@
/* oxlint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const set = (key: string, value: string): boolean => {
try {
sessionStorage.setItem(getScopedKey(key), value);
return true;
} catch {
return false;
}
};
export default set;

View File

@@ -21,12 +21,12 @@ const dashboardVariablesQuery = async (
});
const timeVariables: Record<string, number> = {
start_timestamp_ms: Number.parseInt(start, 10) * 1e3,
end_timestamp_ms: Number.parseInt(end, 10) * 1e3,
start_timestamp_nano: Number.parseInt(start, 10) * 1e9,
end_timestamp_nano: Number.parseInt(end, 10) * 1e9,
start_timestamp: Number.parseInt(start, 10),
end_timestamp: Number.parseInt(end, 10),
start_timestamp_ms: parseInt(start, 10) * 1e3,
end_timestamp_ms: parseInt(end, 10) * 1e3,
start_timestamp_nano: parseInt(start, 10) * 1e9,
end_timestamp_nano: parseInt(end, 10) * 1e9,
start_timestamp: parseInt(start, 10),
end_timestamp: parseInt(end, 10),
};
const payload = { ...props };

View File

@@ -104,7 +104,7 @@ describe('getFieldKeys API', () => {
const result = await getFieldKeys('traces');
// Verify the returned structure matches SuccessResponseV2 format
expect(result).toStrictEqual({
expect(result).toEqual({
httpStatusCode: 200,
data: mockSuccessResponse.data.data,
});

View File

@@ -199,7 +199,7 @@ describe('getFieldValues API', () => {
const result = await getFieldValues('traces', 'service.name');
// Verify the returned structure matches SuccessResponseV2 format
expect(result).toStrictEqual({
expect(result).toEqual({
httpStatusCode: 200,
data: expect.objectContaining({
values: expect.any(Object),

View File

@@ -5559,237 +5559,6 @@ export interface TelemetrytypesTelemetryFieldValuesDTO {
export type TimeDurationDTO = number;
export type TracedetailtypesEventDTOAttributeMap = { [key: string]: unknown };
export interface TracedetailtypesEventDTO {
/**
* @type object
*/
attributeMap?: TracedetailtypesEventDTOAttributeMap;
/**
* @type boolean
*/
isError?: boolean;
/**
* @type string
*/
name?: string;
/**
* @type integer
* @minimum 0
*/
timeUnixNano?: number;
}
/**
* @nullable
*/
export type TracedetailtypesGettableWaterfallTraceDTOServiceNameToTotalDurationMap =
{ [key: string]: number } | null;
export interface TracedetailtypesGettableWaterfallTraceDTO {
/**
* @type integer
* @minimum 0
*/
endTimestampMillis?: number;
/**
* @type boolean
*/
hasMissingSpans?: boolean;
/**
* @type boolean
*/
hasMore?: boolean;
/**
* @type string
*/
rootServiceEntryPoint?: string;
/**
* @type string
*/
rootServiceName?: string;
/**
* @type object
* @nullable true
*/
serviceNameToTotalDurationMap?: TracedetailtypesGettableWaterfallTraceDTOServiceNameToTotalDurationMap;
/**
* @type array
* @nullable true
*/
spans?: TracedetailtypesWaterfallSpanDTO[] | null;
/**
* @type integer
* @minimum 0
*/
startTimestampMillis?: number;
/**
* @type integer
* @minimum 0
*/
totalErrorSpansCount?: number;
/**
* @type integer
* @minimum 0
*/
totalSpansCount?: number;
/**
* @type array
* @nullable true
*/
uncollapsedSpans?: string[] | null;
}
export interface TracedetailtypesPostableWaterfallDTO {
/**
* @type integer
* @minimum 0
*/
limit?: number;
/**
* @type string
*/
selectedSpanId?: string;
/**
* @type array
* @nullable true
*/
uncollapsedSpans?: string[] | null;
}
/**
* @nullable
*/
export type TracedetailtypesWaterfallSpanDTOAttributes = {
[key: string]: unknown;
} | null;
/**
* @nullable
*/
export type TracedetailtypesWaterfallSpanDTOResource = {
[key: string]: string;
} | null;
export interface TracedetailtypesWaterfallSpanDTO {
/**
* @type object
* @nullable true
*/
attributes?: TracedetailtypesWaterfallSpanDTOAttributes;
/**
* @type string
*/
db_name?: string;
/**
* @type string
*/
db_operation?: string;
/**
* @type integer
* @minimum 0
*/
duration_nano?: number;
/**
* @type array
* @nullable true
*/
events?: TracedetailtypesEventDTO[] | null;
/**
* @type string
*/
external_http_method?: string;
/**
* @type string
*/
external_http_url?: string;
/**
* @type integer
* @minimum 0
*/
flags?: number;
/**
* @type boolean
*/
has_children?: boolean;
/**
* @type boolean
*/
has_error?: boolean;
/**
* @type string
*/
http_host?: string;
/**
* @type string
*/
http_method?: string;
/**
* @type string
*/
http_url?: string;
/**
* @type string
*/
is_remote?: string;
/**
* @type string
*/
kind_string?: string;
/**
* @type integer
* @minimum 0
*/
level?: number;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
parent_span_id?: string;
/**
* @type object
* @nullable true
*/
resource?: TracedetailtypesWaterfallSpanDTOResource;
/**
* @type string
*/
response_status_code?: string;
/**
* @type string
*/
span_id?: string;
/**
* @type integer
*/
status_code?: number;
/**
* @type string
*/
status_code_string?: string;
/**
* @type string
*/
status_message?: string;
/**
* @type integer
* @minimum 0
*/
sub_tree_node_count?: number;
/**
* @type string
*/
trace_id?: string;
/**
* @type string
*/
trace_state?: string;
}
export interface TypesAlertStatusDTO {
/**
* @type array
@@ -7724,17 +7493,6 @@ export type GetHosts200 = {
status: string;
};
export type GetWaterfallPathParameters = {
traceID: string;
};
export type GetWaterfall200 = {
data: TracedetailtypesGettableWaterfallTraceDTO;
/**
* @type string
*/
status: string;
};
export type QueryRangeV5200 = {
data: Querybuildertypesv5QueryRangeResponseDTO;
/**

View File

@@ -1,123 +0,0 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import { useMutation } from 'react-query';
import type {
MutationFunction,
UseMutationOptions,
UseMutationResult,
} from 'react-query';
import type {
GetWaterfall200,
GetWaterfallPathParameters,
RenderErrorResponseDTO,
TracedetailtypesPostableWaterfallDTO,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns the waterfall view of spans for a given trace ID with tree structure, metadata, and windowed pagination
* @summary Get waterfall view for a trace
*/
export const getWaterfall = (
{ traceID }: GetWaterfallPathParameters,
tracedetailtypesPostableWaterfallDTO: BodyType<TracedetailtypesPostableWaterfallDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetWaterfall200>({
url: `/api/v3/traces/${traceID}/waterfall`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: tracedetailtypesPostableWaterfallDTO,
signal,
});
};
export const getGetWaterfallMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getWaterfall>>,
TError,
{
pathParams: GetWaterfallPathParameters;
data: BodyType<TracedetailtypesPostableWaterfallDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof getWaterfall>>,
TError,
{
pathParams: GetWaterfallPathParameters;
data: BodyType<TracedetailtypesPostableWaterfallDTO>;
},
TContext
> => {
const mutationKey = ['getWaterfall'];
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 getWaterfall>>,
{
pathParams: GetWaterfallPathParameters;
data: BodyType<TracedetailtypesPostableWaterfallDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return getWaterfall(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type GetWaterfallMutationResult = NonNullable<
Awaited<ReturnType<typeof getWaterfall>>
>;
export type GetWaterfallMutationBody =
BodyType<TracedetailtypesPostableWaterfallDTO>;
export type GetWaterfallMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get waterfall view for a trace
*/
export const useGetWaterfall = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getWaterfall>>,
TError,
{
pathParams: GetWaterfallPathParameters;
data: BodyType<TracedetailtypesPostableWaterfallDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof getWaterfall>>,
TError,
{
pathParams: GetWaterfallPathParameters;
data: BodyType<TracedetailtypesPostableWaterfallDTO>;
},
TContext
> => {
const mutationOptions = getGetWaterfallMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -1,6 +1,5 @@
import {
interceptorRejected,
interceptorsRequestBasePath,
interceptorsRequestResponse,
interceptorsResponse,
} from 'api';
@@ -18,9 +17,7 @@ export const GeneratedAPIInstance = <T>(
return generatedAPIAxiosInstance({ ...config }).then(({ data }) => data);
};
generatedAPIAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
generatedAPIAxiosInstance.interceptors.request.use(interceptorsRequestResponse);
generatedAPIAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
generatedAPIAxiosInstance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,

View File

@@ -11,7 +11,6 @@ import axios, {
import { ENVIRONMENT } from 'constants/env';
import { Events } from 'constants/events';
import { LOCALSTORAGE } from 'constants/localStorage';
import { getBasePath } from 'utils/basePath';
import { eventEmitter } from 'utils/getEventEmitter';
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4, apiV5 } from './apiV1';
@@ -32,7 +31,7 @@ export const interceptorsResponse = (
): Promise<AxiosResponse<any>> => {
if ((value.config as any)?.metadata) {
const duration =
Date.now() - (value.config as any).metadata.startTime;
new Date().getTime() - (value.config as any).metadata.startTime;
if (duration > RESPONSE_TIMEOUT_THRESHOLD && value.config.url !== '/event') {
eventEmitter.emit(Events.SLOW_API_WARNING, true, {
@@ -55,7 +54,7 @@ export const interceptorsRequestResponse = (
): InternalAxiosRequestConfig => {
// Attach metadata safely (not sent with the request)
Object.defineProperty(value, 'metadata', {
value: { startTime: Date.now() },
value: { startTime: new Date().getTime() },
enumerable: false, // Prevents it from being included in the request
});
@@ -68,39 +67,6 @@ export const interceptorsRequestResponse = (
return value;
};
// Strips the leading '/' from path and joins with base — idempotent if already prefixed.
// e.g. prependBase('/signoz/', '/api/v1/') → '/signoz/api/v1/'
function prependBase(base: string, path: string): string {
return path.startsWith(base) ? path : base + path.slice(1);
}
// Prepends the runtime base path to outgoing requests so API calls work under
// a URL prefix (e.g. /signoz/api/v1/…). No-op for root deployments and dev
// (dev baseURL is a full http:// URL, not an absolute path).
export const interceptorsRequestBasePath = (
value: InternalAxiosRequestConfig,
): InternalAxiosRequestConfig => {
const basePath = getBasePath();
if (basePath === '/') {
return value;
}
if (value.baseURL?.startsWith('/')) {
// Production relative baseURL: '/api/v1/' → '/signoz/api/v1/'
value.baseURL = prependBase(basePath, value.baseURL);
} else if (value.baseURL?.startsWith('http')) {
// Dev absolute baseURL (VITE_FRONTEND_API_ENDPOINT): 'https://host/api/v1/' → 'https://host/signoz/api/v1/'
const url = new URL(value.baseURL);
url.pathname = prependBase(basePath, url.pathname);
value.baseURL = url.toString();
} else if (!value.baseURL && value.url?.startsWith('/')) {
// Orval-generated client (empty baseURL, path in url): '/api/signoz/v1/rules' → '/signoz/api/signoz/v1/rules'
value.url = prependBase(basePath, value.url);
}
return value;
};
export const interceptorRejected = async (
value: AxiosResponse<any>,
): Promise<AxiosResponse<any>> => {
@@ -143,7 +109,7 @@ export const interceptorRejected = async (
Logout();
}
}
} catch {
} catch (error) {
Logout();
}
}
@@ -160,14 +126,13 @@ export const interceptorRejected = async (
const interceptorRejectedBase = async (
value: AxiosResponse<any>,
): Promise<AxiosResponse<any>> => { throw value; };
): Promise<AxiosResponse<any>> => Promise.reject(value);
const instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
});
instance.interceptors.request.use(interceptorsRequestResponse);
instance.interceptors.request.use(interceptorsRequestBasePath);
instance.interceptors.response.use(interceptorsResponse, interceptorRejected);
export const AxiosAlertManagerInstance = axios.create({
@@ -182,7 +147,6 @@ ApiV2Instance.interceptors.response.use(
interceptorRejected,
);
ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV2Instance.interceptors.request.use(interceptorsRequestBasePath);
// axios V3
export const ApiV3Instance = axios.create({
@@ -194,7 +158,6 @@ ApiV3Instance.interceptors.response.use(
interceptorRejected,
);
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV3Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios V4
@@ -207,7 +170,6 @@ ApiV4Instance.interceptors.response.use(
interceptorRejected,
);
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV4Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios V5
@@ -220,7 +182,6 @@ ApiV5Instance.interceptors.response.use(
interceptorRejected,
);
ApiV5Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV5Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios Base
@@ -233,7 +194,6 @@ LogEventAxiosInstance.interceptors.response.use(
interceptorRejectedBase,
);
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestResponse);
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
//
AxiosAlertManagerInstance.interceptors.response.use(
@@ -241,7 +201,6 @@ AxiosAlertManagerInstance.interceptors.response.use(
interceptorRejected,
);
AxiosAlertManagerInstance.interceptors.request.use(interceptorsRequestResponse);
AxiosAlertManagerInstance.interceptors.request.use(interceptorsRequestBasePath);
export { apiV1 };
export default instance;

View File

@@ -76,10 +76,10 @@ describe('interceptorRejected', () => {
}
const mockAxiosFn = (axios as unknown) as jest.Mock;
expect(mockAxiosFn.mock.calls).toHaveLength(1);
expect(mockAxiosFn.mock.calls.length).toBe(1);
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
expect(Array.isArray(JSON.parse(retryCallConfig.data))).toBe(true);
expect(JSON.parse(retryCallConfig.data)).toStrictEqual(arrayPayload);
expect(JSON.parse(retryCallConfig.data)).toEqual(arrayPayload);
});
it('should preserve object payload structure when retrying a 401 request', async () => {
@@ -112,9 +112,9 @@ describe('interceptorRejected', () => {
}
const mockAxiosFn = (axios as unknown) as jest.Mock;
expect(mockAxiosFn.mock.calls).toHaveLength(1);
expect(mockAxiosFn.mock.calls.length).toBe(1);
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
expect(JSON.parse(retryCallConfig.data)).toStrictEqual(objectPayload);
expect(JSON.parse(retryCallConfig.data)).toEqual(objectPayload);
});
it('should handle undefined data gracefully when retrying', async () => {
@@ -145,7 +145,7 @@ describe('interceptorRejected', () => {
}
const mockAxiosFn = (axios as unknown) as jest.Mock;
expect(mockAxiosFn.mock.calls).toHaveLength(1);
expect(mockAxiosFn.mock.calls.length).toBe(1);
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
expect(retryCallConfig.data).toBeUndefined();
});

View File

@@ -3,16 +3,13 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { withBasePath } from 'utils/basePath';
// 10 min in ms
const TIMEOUT_IN_MS = 10 * 60 * 1000;
export const LiveTail = (queryParams: string): EventSourcePolyfill =>
new EventSourcePolyfill(
ENVIRONMENT.baseURL
? `${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`
: withBasePath(`${apiV1}logs/tail?${queryParams}`),
`${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`,
{
headers: {
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,

View File

@@ -37,11 +37,11 @@ export const downloadExportData = async (
const filename =
response.headers['content-disposition']
?.split('filename=')[1]
?.replaceAll(/["']/g, '') || `exported_data.${props.format || 'txt'}`;
?.replace(/["']/g, '') || `exported_data.${props.format || 'txt'}`;
link.setAttribute('download', filename);
document.body.append(link);
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);

View File

@@ -101,7 +101,7 @@ describe('convertV5ResponseToLegacy', () => {
const q = result.payload.data.result[0];
expect(q.queryName).toBe('A');
expect(q.legend).toBe('{{service.name}}');
expect(q.series?.[0]).toStrictEqual(
expect(q.series?.[0]).toEqual(
expect.objectContaining({
labels: { 'service.name': 'adservice' },
values: [
@@ -190,7 +190,7 @@ describe('convertV5ResponseToLegacy', () => {
expect(result.payload.data.resultType).toBe('scalar');
const [tableEntry] = result.payload.data.result;
expect(tableEntry.table?.columns).toStrictEqual([
expect(tableEntry.table?.columns).toEqual([
{
name: 'service.name',
queryName: 'A',
@@ -206,7 +206,7 @@ describe('convertV5ResponseToLegacy', () => {
},
{ name: 'F1', queryName: 'F1', isValueColumn: true, id: 'F1' },
]);
expect(tableEntry.table?.rows?.[0]).toStrictEqual({
expect(tableEntry.table?.rows?.[0]).toEqual({
data: {
'service.name': 'adservice',
'A.count()': 606,
@@ -263,7 +263,7 @@ describe('convertV5ResponseToLegacy', () => {
expect(result.payload.data.resultType).toBe('scalar');
const [tableEntry] = result.payload.data.result;
expect(tableEntry.table?.columns).toStrictEqual([
expect(tableEntry.table?.columns).toEqual([
{
name: 'service.name',
queryName: 'A',
@@ -273,7 +273,7 @@ describe('convertV5ResponseToLegacy', () => {
// Single aggregation: name resolves to legend, id resolves to queryName
{ name: '{{service.name}}', queryName: 'A', isValueColumn: true, id: 'A' },
]);
expect(tableEntry.table?.rows?.[0]).toStrictEqual({
expect(tableEntry.table?.rows?.[0]).toEqual({
data: {
'service.name': 'adservice',
A: 580,

View File

@@ -85,7 +85,7 @@ function convertTimeSeriesData(
const { index, alias } = aggregation;
const seriesData = aggregation[seriesKey];
if (!seriesData || seriesData.length === 0) {
if (!seriesData || !seriesData.length) {
return [];
}

View File

@@ -104,7 +104,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result).toStrictEqual(
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'Legend A', F1: 'Formula Legend' },
queryPayload: expect.objectContaining({
@@ -154,7 +154,7 @@ describe('prepareQueryRangePayloadV5', () => {
);
// Legend map combines builder and formulas
expect(result.legendMap).toStrictEqual({ A: 'Legend A', F1: 'Formula Legend' });
expect(result.legendMap).toEqual({ A: 'Legend A', F1: 'Formula Legend' });
const payload: QueryRangePayloadV5 = result.queryPayload;
@@ -166,7 +166,7 @@ describe('prepareQueryRangePayloadV5', () => {
expect(payload.formatOptions?.fillGaps).toBe(true);
// Variables mapped as { key: { value } }
expect(payload.variables).toStrictEqual({
expect(payload.variables).toEqual({
svc: { value: 'api' },
count: { value: 5 },
flag: { value: true },
@@ -226,7 +226,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result).toStrictEqual(
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'LP' },
queryPayload: expect.objectContaining({
@@ -255,7 +255,7 @@ describe('prepareQueryRangePayloadV5', () => {
}),
);
expect(result.legendMap).toStrictEqual({ A: 'LP' });
expect(result.legendMap).toEqual({ A: 'LP' });
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.requestType).toBe('time_series');
@@ -296,7 +296,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result).toStrictEqual(
expect(result).toEqual(
expect.objectContaining({
legendMap: { Q: 'LC' },
queryPayload: expect.objectContaining({
@@ -324,7 +324,7 @@ describe('prepareQueryRangePayloadV5', () => {
}),
);
expect(result.legendMap).toStrictEqual({ Q: 'LC' });
expect(result.legendMap).toEqual({ Q: 'LC' });
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.requestType).toBe('scalar');
@@ -353,7 +353,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result).toStrictEqual(
expect(result).toEqual(
expect.objectContaining({
legendMap: {},
queryPayload: expect.objectContaining({
@@ -397,7 +397,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result).toStrictEqual(
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'Legend A' },
queryPayload: expect.objectContaining({
@@ -471,7 +471,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result).toStrictEqual(
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'Legend A' },
queryPayload: expect.objectContaining({
@@ -585,7 +585,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result).toStrictEqual(
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: '{{service.name}}' },
queryPayload: expect.objectContaining({
@@ -684,7 +684,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result.legendMap).toStrictEqual({ A: 'Legend A' });
expect(result.legendMap).toEqual({ A: 'Legend A' });
expect(result.queryPayload.compositeQuery.queries).toHaveLength(1);
const builderQuery = result.queryPayload.compositeQuery.queries.find(
@@ -694,7 +694,7 @@ describe('prepareQueryRangePayloadV5', () => {
expect(logSpec.name).toBe('A');
expect(logSpec.signal).toBe('logs');
expect(logSpec.filter).toStrictEqual({
expect(logSpec.filter).toEqual({
expression:
"service.name = 'payment-service' AND http.status_code >= 400 AND message contains 'error'",
});
@@ -731,7 +731,7 @@ describe('prepareQueryRangePayloadV5', () => {
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const logSpec = builderQuery.spec as LogBuilderQuery;
expect(logSpec.filter).toStrictEqual({ expression: 'http.status_code >= 500' });
expect(logSpec.filter).toEqual({ expression: 'http.status_code >= 500' });
});
it('derives expression from filters when filter is undefined', () => {
@@ -775,7 +775,7 @@ describe('prepareQueryRangePayloadV5', () => {
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const logSpec = builderQuery.spec as LogBuilderQuery;
expect(logSpec.filter).toStrictEqual({ expression: "service.name = 'checkout'" });
expect(logSpec.filter).toEqual({ expression: "service.name = 'checkout'" });
});
it('prefers filter.expression over filters when both are present', () => {
@@ -819,7 +819,7 @@ describe('prepareQueryRangePayloadV5', () => {
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const logSpec = builderQuery.spec as LogBuilderQuery;
expect(logSpec.filter).toStrictEqual({ expression: "service.name = 'frontend'" });
expect(logSpec.filter).toEqual({ expression: "service.name = 'frontend'" });
});
it('returns empty expression when neither filter nor filters provided', () => {
@@ -853,7 +853,7 @@ describe('prepareQueryRangePayloadV5', () => {
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const logSpec = builderQuery.spec as LogBuilderQuery;
expect(logSpec.filter).toStrictEqual({ expression: '' });
expect(logSpec.filter).toEqual({ expression: '' });
});
it('returns empty expression when filters provided with empty items', () => {
@@ -887,6 +887,6 @@ describe('prepareQueryRangePayloadV5', () => {
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const logSpec = builderQuery.spec as LogBuilderQuery;
expect(logSpec.filter).toStrictEqual({ expression: '' });
expect(logSpec.filter).toEqual({ expression: '' });
});
});

View File

@@ -243,7 +243,7 @@ export function parseAggregations(
let alias = match[2] || availableAlias; // Use provided alias or availableAlias if not matched
if (alias) {
// Remove quotes if present
alias = alias.replaceAll(/^['"]|['"]$/g, '');
alias = alias.replace(/^['"]|['"]$/g, '');
result.push({ expression: expr, alias });
} else {
result.push({ expression: expr });
@@ -634,8 +634,8 @@ export const prepareQueryRangePayloadV5 = ({
// Create V5 payload
const queryPayload: QueryRangePayloadV5 = {
schemaVersion: 'v1',
start: startTime ? startTime * 1e3 : Number.parseInt(start, 10) * 1e3,
end: endTime ? endTime * 1e3 : Number.parseInt(end, 10) * 1e3,
start: startTime ? startTime * 1e3 : parseInt(start, 10) * 1e3,
end: endTime ? endTime * 1e3 : parseInt(end, 10) * 1e3,
requestType,
compositeQuery: {
queries,

View File

@@ -13,7 +13,7 @@ function AppLoading(): JSX.Element {
try {
const theme = get(LOCALSTORAGE.THEME);
return theme !== THEME_MODE.LIGHT; // Return true for dark, false for light
} catch {
} catch (error) {
// If localStorage is not available, default to dark theme
return true;
}

View File

@@ -23,7 +23,7 @@ describe('AppLoading', () => {
it('should render loading screen with dark theme by default', () => {
// Mock localStorage to return dark theme (or undefined for default)
mockGet.mockReturnValue();
mockGet.mockReturnValue(undefined);
render(<AppLoading />);
@@ -40,7 +40,7 @@ describe('AppLoading', () => {
it('should have proper structure and content', () => {
// Mock localStorage to return dark theme
mockGet.mockReturnValue();
mockGet.mockReturnValue(undefined);
render(<AppLoading />);

View File

@@ -51,7 +51,7 @@ export function FilterSelect({
// Memoize options to include the typed value if not present
const mergedOptions = useMemo(() => {
if (
searchValue.trim().length > 0 &&
!!searchValue.trim().length &&
!options.some((opt) => opt.value === searchValue)
) {
return [{ value: searchValue, label: searchValue }, ...options];

View File

@@ -57,7 +57,7 @@ export const useGetValueFromWidget = (
return 'Error';
}
const value = Number.parseFloat(
const value = parseFloat(
query.data?.payload?.data?.newResult?.data?.result?.[0]?.series?.[0]
?.values?.[0]?.value || 'NaN',
);

View File

@@ -104,11 +104,11 @@ export const createFiltersFromData = (
op: string;
value: string;
}> => {
const excludeKeys = new Set(['A', 'A_without_unit']);
const excludeKeys = ['A', 'A_without_unit'];
return (
Object.entries(data)
.filter(([key]) => !excludeKeys.has(key))
.filter(([key]) => !excludeKeys.includes(key))
// eslint-disable-next-line sonarjs/no-identical-functions
.map(([key, value]) => ({
id: uuidv4(),

View File

@@ -12,7 +12,6 @@ import { AppState } from 'store/reducers';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { withBasePath } from 'utils/basePath';
export interface NavigateToExplorerProps {
filters: TagFilterItem[];
@@ -134,7 +133,7 @@ export function useNavigateToExplorer(): (
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
window.open(withBasePath(newExplorerPath), sameTab ? '_self' : '_blank');
window.open(newExplorerPath, sameTab ? '_self' : '_blank');
},
[
prepareQuery,

View File

@@ -9,7 +9,6 @@ import { CreditCard, MessageSquareText, X } from 'lucide-react';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
export default function ChatSupportGateway(): JSX.Element {
const { notifications } = useNotifications();
@@ -55,7 +54,7 @@ export default function ChatSupportGateway(): JSX.Element {
});
updateCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
};

View File

@@ -440,8 +440,8 @@ function ClientSideQBSearch(
const values: Array<string | number | boolean> = [];
const { tagValue } = getTagToken(searchValue);
if (isArray(tagValue)) {
if (!isEmpty(tagValue.at(-1))) {
values.push(tagValue.at(-1));
if (!isEmpty(tagValue[tagValue.length - 1])) {
values.push(tagValue[tagValue.length - 1]);
}
} else if (!isEmpty(tagValue)) {
values.push(tagValue);
@@ -488,7 +488,7 @@ function ClientSideQBSearch(
const computedTagValue =
tag.value &&
Array.isArray(tag.value) &&
tag.value.at(-1) === ''
tag.value[tag.value.length - 1] === ''
? tag.value?.slice(0, -1)
: tag.value ?? '';
filterTags.items.push({
@@ -610,7 +610,7 @@ function ClientSideQBSearch(
searchValue={searchValue}
className={className}
rootClassName="query-builder-search client-side-qb-search"
disabled={attributeKeys.length === 0}
disabled={!attributeKeys.length}
style={selectStyle}
onSearch={handleSearch}
onSelect={handleDropdownSelect}

View File

@@ -147,7 +147,7 @@ function CustomTimePicker({
return `Last ${selectedTime}`;
}
const value = Number.parseInt(match[1], 10);
const value = parseInt(match[1], 10);
const unit = match[2];
// Map unit abbreviations to full words
@@ -312,7 +312,7 @@ function CustomTimePicker({
const match = inputValue.match(/^(\d+)([mhdw])$/) as RegExpMatchArray;
const value = Number.parseInt(match[1], 10);
const value = parseInt(match[1], 10);
const unit = match[2];
const currentTime = dayjs();

View File

@@ -21,7 +21,7 @@ interface RangePickerModalProps {
setIsOpen: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler: (
dateTimeRange: DateTimeRangeType,
lexicalContext?: LexicalContext ,
lexicalContext?: LexicalContext | undefined,
) => void;
selectedTime: string;
onTimeChange?: (

View File

@@ -82,7 +82,7 @@ const createTimezoneEntry = (
name: displayName,
value,
offset,
searchIndex: offset.replaceAll(/ /g, ''),
searchIndex: offset.replace(/ /g, ''),
...(hasDivider && { hasDivider }),
};
};

View File

@@ -13,7 +13,7 @@ import '@testing-library/jest-dom';
import { DownloadFormats, DownloadRowCounts } from './constants';
import DownloadOptionsMenu from './DownloadOptionsMenu';
const mockDownloadExportData = jest.fn().mockResolvedValue();
const mockDownloadExportData = jest.fn().mockResolvedValue(undefined);
jest.mock('api/v1/download/downloadExportData', () => ({
downloadExportData: (...args: any[]): any => mockDownloadExportData(...args),
default: (...args: any[]): any => mockDownloadExportData(...args),
@@ -94,7 +94,7 @@ describe.each([
const testId = `periscope-btn-download-${dataSource}`;
beforeEach(() => {
mockDownloadExportData.mockReset().mockResolvedValue();
mockDownloadExportData.mockReset().mockResolvedValue(undefined);
(message.success as jest.Mock).mockReset();
(message.error as jest.Mock).mockReset();
mockUseQueryBuilder.mockReturnValue({
@@ -213,7 +213,7 @@ describe.each([
const callArgs = mockDownloadExportData.mock.calls[0][0];
const query = callArgs.body.compositeQuery.queries[0];
expect(query.spec.groupBy).toBeUndefined();
expect(query.spec.having).toStrictEqual({ expression: '' });
expect(query.spec.having).toEqual({ expression: '' });
});
});
@@ -238,7 +238,7 @@ describe.each([
expect(mockDownloadExportData).toHaveBeenCalledTimes(1);
const callArgs = mockDownloadExportData.mock.calls[0][0];
const query = callArgs.body.compositeQuery.queries[0];
expect(query.spec.selectFields).toStrictEqual([
expect(query.spec.selectFields).toEqual([
expect.objectContaining({
name: 'http.status',
fieldDataType: 'int64',
@@ -322,7 +322,7 @@ describe('DownloadOptionsMenu for traces with queryTraceOperator', () => {
const testId = `periscope-btn-download-${dataSource}`;
beforeEach(() => {
mockDownloadExportData.mockReset().mockResolvedValue();
mockDownloadExportData.mockReset().mockResolvedValue(undefined);
(message.success as jest.Mock).mockReset();
});

View File

@@ -6,39 +6,39 @@ jest.mock('react-dnd', () => ({
}));
describe('Utils testing of DraggableTableRow component', () => {
it('Should dropHandler return true', () => {
test('Should dropHandler return true', () => {
const monitor = {
isOver: jest.fn().mockReturnValueOnce(true),
} as never;
const dropDataTruthy = dropHandler(monitor);
expect(dropDataTruthy).toStrictEqual({ isOver: true });
expect(dropDataTruthy).toEqual({ isOver: true });
});
it('Should dropHandler return false', () => {
test('Should dropHandler return false', () => {
const monitor = {
isOver: jest.fn().mockReturnValueOnce(false),
} as never;
const dropDataFalsy = dropHandler(monitor);
expect(dropDataFalsy).toStrictEqual({ isOver: false });
expect(dropDataFalsy).toEqual({ isOver: false });
});
it('Should dragHandler return true', () => {
test('Should dragHandler return true', () => {
const monitor = {
isDragging: jest.fn().mockReturnValueOnce(true),
} as never;
const dragDataTruthy = dragHandler(monitor);
expect(dragDataTruthy).toStrictEqual({ isDragging: true });
expect(dragDataTruthy).toEqual({ isDragging: true });
});
it('Should dragHandler return false', () => {
test('Should dragHandler return false', () => {
const monitor = {
isDragging: jest.fn().mockReturnValueOnce(false),
} as never;
const dragDataFalsy = dragHandler(monitor);
expect(dragDataFalsy).toStrictEqual({ isDragging: false });
expect(dragDataFalsy).toEqual({ isDragging: false });
});
});

View File

@@ -28,7 +28,6 @@ import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useTimezone } from 'providers/Timezone';
import APIError from 'types/api/error';
import { getAbsoluteUrl } from 'utils/basePath';
import { toAPIError } from 'utils/errorUtils';
import DeleteMemberDialog from './DeleteMemberDialog';
@@ -382,7 +381,7 @@ function EditMemberDrawer({
pathParams: { id: member.id },
});
if (response?.data?.token) {
const link = getAbsoluteUrl(`/password-reset?token=${response.data.token}`);
const link = `${window.location.origin}/password-reset?token=${response.data.token}`;
setResetLink(link);
setResetLinkExpiresAt(
response.data.expiresAt

View File

@@ -361,10 +361,12 @@ describe('EditMemberDrawer', () => {
await user.click(screen.getByRole('button', { name: /delete member/i }));
await expect(screen.findByText(/are you sure you want to delete/i)).resolves.toBeInTheDocument();
expect(
await screen.findByText(/are you sure you want to delete/i),
).toBeInTheDocument();
const confirmBtns = screen.getAllByRole('button', { name: /delete member/i });
await user.click(confirmBtns.at(-1));
await user.click(confirmBtns[confirmBtns.length - 1]);
await waitFor(() => {
expect(mockDeleteMutate).toHaveBeenCalledWith({
@@ -439,10 +441,12 @@ describe('EditMemberDrawer', () => {
await user.click(screen.getByRole('button', { name: /revoke invite/i }));
await expect(screen.findByText(/Are you sure you want to revoke the invite/i)).resolves.toBeInTheDocument();
expect(
await screen.findByText(/Are you sure you want to revoke the invite/i),
).toBeInTheDocument();
const confirmBtns = screen.getAllByRole('button', { name: /revoke invite/i });
await user.click(confirmBtns.at(-1));
await user.click(confirmBtns[confirmBtns.length - 1]);
await waitFor(() => {
expect(mockDeleteMutate).toHaveBeenCalledWith({
@@ -549,7 +553,7 @@ describe('EditMemberDrawer', () => {
const confirmBtns = screen.getAllByRole('button', {
name: /delete member/i,
});
await user.click(confirmBtns.at(-1));
await user.click(confirmBtns[confirmBtns.length - 1]);
await waitFor(() => {
expect(showErrorModal).toHaveBeenCalledWith(
@@ -580,7 +584,7 @@ describe('EditMemberDrawer', () => {
const confirmBtns = screen.getAllByRole('button', {
name: /revoke invite/i,
});
await user.click(confirmBtns.at(-1));
await user.click(confirmBtns[confirmBtns.length - 1]);
await waitFor(() => {
expect(showErrorModal).toHaveBeenCalledWith(

View File

@@ -180,7 +180,7 @@ it('should close the modal when the onCancel event is triggered', async () => {
await waitFor(() => {
// check if the modal is not visible
const modal = document.querySelectorAll('.ant-modal');
const modal = document.getElementsByClassName('ant-modal');
const style = window.getComputedStyle(modal[0]);
expect(style.display).toBe('none');
});

View File

@@ -34,7 +34,7 @@ export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
const query = mapQueryDataFromApi(compositeQuery);
return { query, name, id, panelType: compositeQuery.panelType, extraData };
}
return;
return undefined;
};
export const omitIdFromQuery = (query: Query | null): any => ({
@@ -198,7 +198,7 @@ export const deleteViewHandler = ({
export const trimViewName = (viewName: string): string => {
if (viewName.length > 20) {
return `${viewName.slice(0, 20)}...`;
return `${viewName.substring(0, 20)}...`;
}
return viewName;
};

View File

@@ -9,7 +9,7 @@ const getOrCreateLegendList = (
id: string,
isLonger: boolean,
): HTMLUListElement => {
const legendContainer = document.querySelector(`#${id}`);
const legendContainer = document.getElementById(id);
let listContainer = legendContainer?.querySelector('ul');
if (!listContainer) {
@@ -26,7 +26,7 @@ const getOrCreateLegendList = (
listContainer.style.flexWrap = 'wrap';
listContainer.style.justifyContent = 'center';
listContainer.style.fontSize = '0.75rem';
legendContainer?.append(listContainer);
legendContainer?.appendChild(listContainer);
}
return listContainer;
@@ -64,7 +64,7 @@ export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => ({
// li.style.marginTop = '5px';
li.onclick = (): void => {
// @ts-expect-error
// @ts-ignore
const { type } = chart.config;
if (type === 'pie' || type === 'doughnut') {
// Pie and doughnut charts only have a single dataset and visibility is per item
@@ -101,11 +101,11 @@ export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => ({
textContainer.style.textDecoration = item.hidden ? 'line-through' : '';
const text = document.createTextNode(item.text);
textContainer.append(text);
textContainer.appendChild(text);
li.append(boxSpan);
li.append(textContainer);
ul.append(li);
li.appendChild(boxSpan);
li.appendChild(textContainer);
ul.appendChild(li);
}
});
},

View File

@@ -9,7 +9,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'millisecond');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
TIME_UNITS.millisecond,
);
}
@@ -17,7 +17,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'second');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
TIME_UNITS.second,
);
}
@@ -25,7 +25,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'minute');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
TIME_UNITS.minute,
);
}
@@ -33,7 +33,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'hour');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
TIME_UNITS.hour,
);
}
@@ -41,7 +41,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'day');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
TIME_UNITS.day,
);
}
@@ -49,7 +49,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'week');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
TIME_UNITS.week,
);
}
@@ -57,7 +57,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'month');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
TIME_UNITS.month,
);
}
@@ -65,7 +65,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'year');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
TIME_UNITS.year,
);
}

View File

@@ -7,7 +7,7 @@ const testFullPrecisionGetYAxisFormattedValue = (
): string => getYAxisFormattedValue(value, format, PrecisionOptionsEnum.FULL);
describe('getYAxisFormattedValue - none (full precision legacy assertions)', () => {
it('large integers and decimals', () => {
test('large integers and decimals', () => {
expect(testFullPrecisionGetYAxisFormattedValue('250034', 'none')).toBe(
'250034',
);
@@ -22,7 +22,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
);
});
it('preserves leading zeros after decimal until first non-zero', () => {
test('preserves leading zeros after decimal until first non-zero', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1.0000234', 'none')).toBe(
'1.0000234',
);
@@ -31,7 +31,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
);
});
it('trims to three significant decimals and removes trailing zeros', () => {
test('trims to three significant decimals and removes trailing zeros', () => {
expect(
testFullPrecisionGetYAxisFormattedValue('0.000000250034', 'none'),
).toBe('0.000000250034');
@@ -55,7 +55,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
).toBe('0.00000025');
});
it('whole numbers normalize', () => {
test('whole numbers normalize', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1000', 'none')).toBe('1000');
expect(testFullPrecisionGetYAxisFormattedValue('99.5458', 'none')).toBe(
'99.5458',
@@ -68,7 +68,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
);
});
it('strip redundant decimal zeros', () => {
test('strip redundant decimal zeros', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1000.000', 'none')).toBe(
'1000',
);
@@ -78,7 +78,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
expect(testFullPrecisionGetYAxisFormattedValue('1.000', 'none')).toBe('1');
});
it('edge values', () => {
test('edge values', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0', 'none')).toBe('0');
expect(testFullPrecisionGetYAxisFormattedValue('-0', 'none')).toBe('0');
expect(testFullPrecisionGetYAxisFormattedValue('Infinity', 'none')).toBe('∞');
@@ -92,7 +92,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
expect(testFullPrecisionGetYAxisFormattedValue('abc123', 'none')).toBe('NaN');
});
it('small decimals keep precision as-is', () => {
test('small decimals keep precision as-is', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.0001', 'none')).toBe(
'0.0001',
);
@@ -104,7 +104,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
);
});
it('simple decimals preserved', () => {
test('simple decimals preserved', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.1', 'none')).toBe('0.1');
expect(testFullPrecisionGetYAxisFormattedValue('0.2', 'none')).toBe('0.2');
expect(testFullPrecisionGetYAxisFormattedValue('0.3', 'none')).toBe('0.3');
@@ -115,7 +115,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
});
describe('getYAxisFormattedValue - units (full precision legacy assertions)', () => {
it('ms', () => {
test('ms', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1500', 'ms')).toBe('1.5 s');
expect(testFullPrecisionGetYAxisFormattedValue('500', 'ms')).toBe('500 ms');
expect(testFullPrecisionGetYAxisFormattedValue('60000', 'ms')).toBe('1 min');
@@ -127,19 +127,19 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
);
});
it('s', () => {
test('s', () => {
expect(testFullPrecisionGetYAxisFormattedValue('90', 's')).toBe('1.5 mins');
expect(testFullPrecisionGetYAxisFormattedValue('30', 's')).toBe('30 s');
expect(testFullPrecisionGetYAxisFormattedValue('3600', 's')).toBe('1 hour');
});
it('m', () => {
test('m', () => {
expect(testFullPrecisionGetYAxisFormattedValue('90', 'm')).toBe('1.5 hours');
expect(testFullPrecisionGetYAxisFormattedValue('30', 'm')).toBe('30 min');
expect(testFullPrecisionGetYAxisFormattedValue('1440', 'm')).toBe('1 day');
});
it('bytes', () => {
test('bytes', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1024', 'bytes')).toBe(
'1 KiB',
);
@@ -149,7 +149,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
);
});
it('mbytes', () => {
test('mbytes', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1024', 'mbytes')).toBe(
'1 GiB',
);
@@ -161,7 +161,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
);
});
it('kbytes', () => {
test('kbytes', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1024', 'kbytes')).toBe(
'1 MiB',
);
@@ -173,7 +173,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
);
});
it('short', () => {
test('short', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1000', 'short')).toBe('1 K');
expect(testFullPrecisionGetYAxisFormattedValue('1500', 'short')).toBe(
'1.5 K',
@@ -201,7 +201,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
);
});
it('percent', () => {
test('percent', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.15', 'percent')).toBe(
'0.15%',
);
@@ -235,7 +235,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
).toBe('1.005555555595959%');
});
it('ratio', () => {
test('ratio', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.5', 'ratio')).toBe(
'0.5 ratio',
);
@@ -247,7 +247,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
);
});
it('temperature units', () => {
test('temperature units', () => {
expect(testFullPrecisionGetYAxisFormattedValue('25', 'celsius')).toBe(
'25 °C',
);
@@ -267,13 +267,13 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
);
});
it('ms edge cases', () => {
test('ms edge cases', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0', 'ms')).toBe('0 ms');
expect(testFullPrecisionGetYAxisFormattedValue('-1500', 'ms')).toBe('-1.5 s');
expect(testFullPrecisionGetYAxisFormattedValue('Infinity', 'ms')).toBe('∞');
});
it('bytes edge cases', () => {
test('bytes edge cases', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0', 'bytes')).toBe('0 B');
expect(testFullPrecisionGetYAxisFormattedValue('-1024', 'bytes')).toBe(
'-1 KiB',
@@ -282,7 +282,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
});
describe('getYAxisFormattedValue - precision option tests', () => {
it('precision 0 drops decimal part', () => {
test('precision 0 drops decimal part', () => {
expect(getYAxisFormattedValue('1.2345', 'none', 0)).toBe('1');
expect(getYAxisFormattedValue('0.9999', 'none', 0)).toBe('0');
expect(getYAxisFormattedValue('12345.6789', 'none', 0)).toBe('12345');
@@ -294,7 +294,7 @@ describe('getYAxisFormattedValue - precision option tests', () => {
// with unit
expect(getYAxisFormattedValue('4353.81', 'ms', 0)).toBe('4 s');
});
it('precision 1,2,3,4 decimals', () => {
test('precision 1,2,3,4 decimals', () => {
expect(getYAxisFormattedValue('1.2345', 'none', 1)).toBe('1.2');
expect(getYAxisFormattedValue('1.2345', 'none', 2)).toBe('1.23');
expect(getYAxisFormattedValue('1.2345', 'none', 3)).toBe('1.234');
@@ -345,7 +345,7 @@ describe('getYAxisFormattedValue - precision option tests', () => {
expect(getYAxisFormattedValue('0.123456', 'percent', 4)).toBe('0.1235%'); // approximation
});
it('precision full uses up to DEFAULT_SIGNIFICANT_DIGITS significant digits', () => {
test('precision full uses up to DEFAULT_SIGNIFICANT_DIGITS significant digits', () => {
expect(
getYAxisFormattedValue(
'0.00002625429914148441',

View File

@@ -109,8 +109,8 @@ export const useXAxisTimeUnit = (
};
const time = getTimeStamp(timeStamp as Date | number);
minTimeLocal = Math.min(Number.parseInt(time.toString(), 10), minTimeLocal);
maxTimeLocal = Math.max(Number.parseInt(time.toString(), 10), maxTimeLocal);
minTimeLocal = Math.min(parseInt(time.toString(), 10), minTimeLocal);
maxTimeLocal = Math.max(parseInt(time.toString(), 10), maxTimeLocal);
});
localTime = {

View File

@@ -25,7 +25,7 @@ export const getYAxisFormattedValue = (
format: string,
precision: PrecisionOption = 2, // default precision requested
): string => {
const numValue = Number.parseFloat(value);
const numValue = parseFloat(value);
// Handle non-numeric or special values first.
if (isNaN(numValue)) {
@@ -79,10 +79,10 @@ export const getYAxisFormattedValue = (
}
const formatter = getValueFormat(format);
const formattedValue = formatter(numValue, computeDecimals());
const formattedValue = formatter(numValue, computeDecimals(), undefined);
if (formattedValue.text && formattedValue.text.includes('.')) {
formattedValue.text = formatDecimalWithLeadingZeros(
Number.parseFloat(formattedValue.text),
parseFloat(formattedValue.text),
precision,
);
}

View File

@@ -284,7 +284,7 @@ describe('GuardAuthZ', () => {
expect(
screen.getAllByText(
new RegExp(permission.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&')),
new RegExp(permission.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
).length,
).toBeGreaterThan(0);
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();

View File

@@ -13,7 +13,6 @@ import GetMinMax from 'lib/getMinMax';
import { Check, Info, Link2 } from 'lucide-react';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getAbsoluteUrl } from 'utils/basePath';
const routesToBeSharedWithTime = [
ROUTES.LOGS_EXPLORER,
@@ -81,13 +80,17 @@ function ShareURLModal(): JSX.Element {
urlQuery.delete(QueryParams.relativeTime);
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
currentUrl = `${window.location.origin}${
location.pathname
}?${urlQuery.toString()}`;
} else {
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
urlQuery.set(QueryParams.relativeTime, selectedTime);
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
currentUrl = `${window.location.origin}${
location.pathname
}?${urlQuery.toString()}`;
}
}

View File

@@ -17,7 +17,6 @@ import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { ROLES } from 'types/roles';
import { EMAIL_REGEX } from 'utils/app';
import { getBaseUrl } from 'utils/basePath';
import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
@@ -192,7 +191,7 @@ function InviteMembersModal({
email: row.email.trim(),
name: '',
role: row.role as ROLES,
frontendBaseUrl: getBaseUrl(),
frontendBaseUrl: window.location.origin,
});
} else {
await inviteUsers({
@@ -200,7 +199,7 @@ function InviteMembersModal({
email: row.email.trim(),
name: '',
role: row.role,
frontendBaseUrl: getBaseUrl(),
frontendBaseUrl: window.location.origin,
})),
});
}

View File

@@ -90,9 +90,11 @@ describe('InviteMembersModal', () => {
screen.getByRole('button', { name: /invite team members/i }),
);
await expect(screen.findByText(
expect(
await screen.findByText(
'Please enter valid emails and select roles for team members',
)).resolves.toBeInTheDocument();
),
).toBeInTheDocument();
});
it('shows email-only message when email is invalid but role is selected', async () => {
@@ -110,7 +112,9 @@ describe('InviteMembersModal', () => {
screen.getByRole('button', { name: /invite team members/i }),
);
await expect(screen.findByText('Please enter valid emails for team members')).resolves.toBeInTheDocument();
expect(
await screen.findByText('Please enter valid emails for team members'),
).toBeInTheDocument();
});
it('shows role-only message when email is valid but role is missing', async () => {
@@ -126,7 +130,9 @@ describe('InviteMembersModal', () => {
screen.getByRole('button', { name: /invite team members/i }),
);
await expect(screen.findByText('Please select roles for team members')).resolves.toBeInTheDocument();
expect(
await screen.findByText('Please select roles for team members'),
).toBeInTheDocument();
});
});
@@ -198,7 +204,7 @@ describe('InviteMembersModal', () => {
await user.type(emailInputs[1], 'bob@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
const editorOptions = await screen.findAllByText('Editor');
await user.click(editorOptions.at(-1));
await user.click(editorOptions[editorOptions.length - 1]);
await user.click(
screen.getByRole('button', { name: /invite team members/i }),
@@ -250,7 +256,7 @@ describe('InviteMembersModal', () => {
await user.type(emailInputs[1], 'bob@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
const editorOptions = await screen.findAllByText('Editor');
await user.click(editorOptions.at(-1));
await user.click(editorOptions[editorOptions.length - 1]);
await user.click(
screen.getByRole('button', { name: /invite team members/i }),

View File

@@ -14,7 +14,6 @@ import { useAppContext } from 'providers/App/App';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import './LaunchChatSupport.styles.scss';
@@ -155,7 +154,7 @@ function LaunchChatSupport({
});
updateCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
};

View File

@@ -1,7 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { ArrowUpRight } from 'lucide-react';
import { openInNewTab } from 'utils/navigation';
import './LearnMore.styles.scss';
@@ -15,7 +14,7 @@ function LearnMore({ text, url, onClick }: LearnMoreProps): JSX.Element {
const handleClick = (): void => {
onClick?.();
if (url) {
openInNewTab(url);
window.open(url, '_blank');
}
};
return (

View File

@@ -26,7 +26,7 @@ function QueryBuilderSearchWrapper({
const tagFiltersLength = tagFilters.items.length;
if (
(!tagFiltersLength && (!filters || filters.items.length === 0)) ||
(!tagFiltersLength && (!filters || !filters.items.length)) ||
tagFiltersLength === filters?.items.length ||
!contextQuery
) {

View File

@@ -163,7 +163,7 @@ function LogDetailInner({
}, [log.id, logs, onNavigateLog, onScrollToLog, selectedView]);
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length === 0) {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) {
return null;
}

View File

@@ -27,7 +27,7 @@ describe('getLogIndicatorType', () => {
expect(getLogIndicatorType(log)).toBe('TRACE');
});
it('severity_text should be used when severity_number is absent', () => {
it('severity_text should be used when severity_number is absent ', () => {
const log = {
date: '2024-02-29T12:34:46Z',
timestamp: 1646115296,
@@ -157,7 +157,7 @@ describe('logIndicatorBySeverityNumber', () => {
];
logLevelExpectations.forEach((e) => {
for (let sevNum = e.minSevNumber; sevNum <= e.maxSevNumber; sevNum++) {
const sevText = (Math.random() + 1).toString(36).slice(2);
const sevText = (Math.random() + 1).toString(36).substring(2);
const log = {
date: '2024-02-29T12:34:46Z',

View File

@@ -55,7 +55,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
title: name,
dataIndex: name,
accessorKey: name,
id: name.toLowerCase().replaceAll(/\./g, '_'),
id: name.toLowerCase().replace(/\./g, '_'),
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {

View File

@@ -77,7 +77,7 @@ function OptionsMenu({
};
const handleSearchValueChange = useDebouncedFn((event): void => {
// @ts-expect-error
// @ts-ignore
const value = event?.target?.value || '';
if (addColumn && addColumn?.onSearch) {

View File

@@ -72,7 +72,7 @@ describe('LogsFormatOptionsMenu (unit)', () => {
fireEvent.click(formatButton);
const getMenuItems = (): Element[] =>
[...document.querySelectorAll('.menu-items .item')];
Array.from(document.querySelectorAll('.menu-items .item'));
const findItemByLabel = (label: string): Element | undefined =>
getMenuItems().find((el) => (el.textContent || '').includes(label));
@@ -136,7 +136,9 @@ describe('LogsFormatOptionsMenu (unit)', () => {
fireEvent.click(fontButton);
// Choose MEDIUM
const optionButtons = [...document.querySelectorAll('.font-size-dropdown .option-btn')];
const optionButtons = Array.from(
document.querySelectorAll('.font-size-dropdown .option-btn'),
);
const mediumBtn = optionButtons[1] as HTMLElement;
fireEvent.click(mediumBtn);

View File

@@ -50,7 +50,7 @@ function Code({
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
// @ts-expect-error
// @ts-ignore
style={a11yDark}
language={match[1]}
PreTag="div"
@@ -115,7 +115,7 @@ function MarkdownRenderer({
className={className}
rehypePlugins={[rehypeRaw as any]}
components={{
// @ts-expect-error
// @ts-ignore
a: Link,
pre: ({ children }) =>
Pre({

View File

@@ -20,7 +20,6 @@ import {
KAFKA_SETUP_DOC_LINK,
MessagingQueueHealthCheckService,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuid } from 'uuid';
import './MessagingQueueHealthCheck.styles.scss';
@@ -77,7 +76,7 @@ function ErrorTitleAndKey({
if (isCloudUserVal && !!link) {
history.push(link);
} else {
openInNewTab(KAFKA_SETUP_DOC_LINK);
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
}
};
return {

View File

@@ -26,9 +26,9 @@ function MessagingQueueHealthCheck({
isFetching: consumerLoading,
} = useOnboardingStatus(
{
enabled: serviceToInclude.filter(
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Consumers,
).length > 0,
).length,
},
MessagingQueueHealthCheckService.Consumers,
);
@@ -40,9 +40,9 @@ function MessagingQueueHealthCheck({
isFetching: producerLoading,
} = useOnboardingStatus(
{
enabled: serviceToInclude.filter(
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Producers,
).length > 0,
).length,
},
MessagingQueueHealthCheckService.Producers,
);
@@ -54,9 +54,9 @@ function MessagingQueueHealthCheck({
isFetching: kafkaLoading,
} = useOnboardingStatus(
{
enabled: serviceToInclude.filter(
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Kafka,
).length > 0,
).length,
},
MessagingQueueHealthCheckService.Kafka,
);

View File

@@ -607,7 +607,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
try {
const parts = text.split(
new RegExp(
`(${searchQuery.replaceAll(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
'gi',
),
);
@@ -615,7 +615,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
<>
{parts.map((part, i) => {
// Create a unique key that doesn't rely on array index
const uniqueKey = `${text.slice(0, 3)}-${part.slice(0, 3)}-${i}`;
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">
@@ -819,7 +819,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const getLastVisibleChipIndex = useCallback((): number => {
const visibleIndices = getVisibleChipIndices();
return visibleIndices.length > 0
? visibleIndices.at(-1)
? visibleIndices[visibleIndices.length - 1]
: -1;
}, [getVisibleChipIndices]);

View File

@@ -152,7 +152,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
try {
const parts = text.split(
new RegExp(
`(${searchQuery.replaceAll(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
'gi',
),
);
@@ -160,7 +160,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
<>
{parts.map((part, i) => {
// Create a deterministic but unique key
const uniqueKey = `${text.slice(0, 3)}-${part.slice(0, 3)}-${i}`;
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">

View File

@@ -61,7 +61,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 1. CUSTOM VALUES SUPPORT =====
describe('Custom Values Support (CS)', () => {
it('CS-01: Custom values persist in selected state', async () => {
test('CS-01: Custom values persist in selected state', async () => {
const { rerender } = renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -87,7 +87,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(screen.getByText('another-custom')).toBeInTheDocument();
});
it('CS-02: Partial matches create custom values', async () => {
test('CS-02: Partial matches create custom values', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -129,7 +129,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(combobox).toBeInTheDocument();
});
it('CS-03: Exact match filtering behavior', async () => {
test('CS-03: Exact match filtering behavior', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -154,14 +154,14 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Check for highlighted "frontend" text
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = [...highlightedElements].map(
const highlightTexts = Array.from(highlightedElements).map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('Frontend');
// Frontend option should be visible in dropdown - use a simpler approach
const optionLabels = document.querySelectorAll('.option-label-text');
const hasFrontendOption = [...optionLabels].some((label) =>
const hasFrontendOption = Array.from(optionLabels).some((label) =>
label.textContent?.includes('Frontend'),
);
expect(hasFrontendOption).toBe(true);
@@ -176,7 +176,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('CS-04: Search filtering with "end" pattern', async () => {
test('CS-04: Search filtering with "end" pattern', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -201,19 +201,19 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Check for highlighted "end" text in the options
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = [...highlightedElements].map(
const highlightTexts = Array.from(highlightedElements).map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('end');
// Check that Frontend and Backend options are present with highlighted text
const optionLabels = document.querySelectorAll('.option-label-text');
const hasFrontendOption = [...optionLabels].some(
const hasFrontendOption = Array.from(optionLabels).some(
(label) =>
label.textContent?.includes('Front') &&
label.textContent?.includes('end'),
);
const hasBackendOption = [...optionLabels].some(
const hasBackendOption = Array.from(optionLabels).some(
(label) =>
label.textContent?.includes('Back') && label.textContent?.includes('end'),
);
@@ -222,10 +222,10 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(hasBackendOption).toBe(true);
// Other options should be filtered out
const hasDatabaseOption = [...optionLabels].some((label) =>
const hasDatabaseOption = Array.from(optionLabels).some((label) =>
label.textContent?.includes('Database'),
);
const hasApiGatewayOption = [...optionLabels].some((label) =>
const hasApiGatewayOption = Array.from(optionLabels).some((label) =>
label.textContent?.includes('API Gateway'),
);
@@ -234,7 +234,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('CS-05: Comma-separated values behavior', async () => {
test('CS-05: Comma-separated values behavior', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -281,7 +281,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 2. SEARCH AND FILTERING =====
describe('Search and Filtering (SF)', () => {
it('SF-01: Selected values pushed to top', async () => {
test('SF-01: Selected values pushed to top', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -298,14 +298,14 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(dropdown).toBeInTheDocument();
const options = dropdown?.querySelectorAll('.option-label-text') || [];
const optionTexts = [...options].map((el) => el.textContent);
const optionTexts = Array.from(options).map((el) => el.textContent);
// Database should be at the top (after ALL option if present)
expect(optionTexts[0]).toBe('Database');
});
});
it('SF-02: Filtering with search text', async () => {
test('SF-02: Filtering with search text', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -332,14 +332,14 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Check for highlighted text within the Frontend option
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = [...highlightedElements].map(
const highlightTexts = Array.from(highlightedElements).map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('Front');
// Should show Frontend option (highlighted) - use a simpler approach
const optionLabels = document.querySelectorAll('.option-label-text');
const hasFrontendOption = [...optionLabels].some((label) =>
const hasFrontendOption = Array.from(optionLabels).some((label) =>
label.textContent?.includes('Frontend'),
);
expect(hasFrontendOption).toBe(true);
@@ -350,7 +350,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('SF-03: Highlighting search matches', async () => {
test('SF-03: Highlighting search matches', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -374,14 +374,14 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// Should highlight matching text in options
await waitFor(() => {
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = [...highlightedElements].map(
const highlightTexts = Array.from(highlightedElements).map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('end');
});
});
it('SF-04: Search with no results', async () => {
test('SF-04: Search with no results', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -424,7 +424,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 3. KEYBOARD NAVIGATION =====
describe('Keyboard Navigation (KN)', () => {
it('KN-01: Arrow key navigation in dropdown', async () => {
test('KN-01: Arrow key navigation in dropdown', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -465,7 +465,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('KN-02: Tab navigation to dropdown', async () => {
test('KN-02: Tab navigation to dropdown', async () => {
renderWithVirtuoso(
<div>
<input data-testid="prev-input" />
@@ -515,7 +515,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('KN-03: Enter selection in dropdown', async () => {
test('KN-03: Enter selection in dropdown', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -540,7 +540,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(mockOnChange).toHaveBeenCalledWith(['frontend'], ['frontend']);
});
it('KN-04: Chip deletion with keyboard', async () => {
test('KN-04: Chip deletion with keyboard', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -586,7 +586,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 5. UI/UX BEHAVIORS =====
describe('UI/UX Behaviors (UI)', () => {
it('UI-01: Loading state does not block interaction', async () => {
test('UI-01: Loading state does not block interaction', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -603,7 +603,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('UI-02: Component remains editable in all states', async () => {
test('UI-02: Component remains editable in all states', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -634,7 +634,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(combobox).not.toBeDisabled();
});
it('UI-03: Toggle/Only labels in dropdown', async () => {
test('UI-03: Toggle/Only labels in dropdown', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -656,7 +656,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('UI-04: Should display values with loading info at bottom', async () => {
test('UI-04: Should display values with loading info at bottom', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -677,7 +677,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('UI-05: Error state display in footer', async () => {
test('UI-05: Error state display in footer', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -696,7 +696,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('UI-06: No data state display', async () => {
test('UI-06: No data state display', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={[]}
@@ -716,7 +716,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 6. CLEAR ACTIONS =====
describe('Clear Actions (CA)', () => {
it('CA-01: Ctrl+A selects all chips', async () => {
test('CA-01: Ctrl+A selects all chips', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -760,7 +760,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('CA-02: Clear icon removes all selections', async () => {
test('CA-02: Clear icon removes all selections', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -777,7 +777,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
}
});
it('CA-03: Individual chip removal', async () => {
test('CA-03: Individual chip removal', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -790,7 +790,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
const removeButtons = document.querySelectorAll(
'.ant-select-selection-item-remove',
);
expect(removeButtons).toHaveLength(2);
expect(removeButtons.length).toBe(2);
await user.click(removeButtons[1] as Element);
@@ -804,7 +804,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 7. SAVE AND SELECTION TRIGGERS =====
describe('Save and Selection Triggers (ST)', () => {
it('ST-01: ESC triggers save action', async () => {
test('ST-01: ESC triggers save action', async () => {
const mockDropdownChange = jest.fn();
renderWithVirtuoso(
@@ -837,7 +837,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('ST-02: Mouse selection works', async () => {
test('ST-02: Mouse selection works', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -859,7 +859,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
);
});
it('ST-03: ENTER in input field creates custom value', async () => {
test('ST-03: ENTER in input field creates custom value', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -892,7 +892,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('ST-04: Search text persistence', async () => {
test('ST-04: Search text persistence', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -932,7 +932,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 8. SPECIAL OPTIONS AND STATES =====
describe('Special Options and States (SO)', () => {
it('SO-01: ALL option appears first and separated', async () => {
test('SO-01: ALL option appears first and separated', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -954,7 +954,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('SO-02: ALL selection behavior', async () => {
test('SO-02: ALL selection behavior', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -981,7 +981,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
);
});
it('SO-03: ALL tag display when all selected', () => {
test('SO-03: ALL tag display when all selected', () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -996,7 +996,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(screen.queryByText('frontend')).not.toBeInTheDocument();
});
it('SO-04: Footer information display', async () => {
test('SO-04: Footer information display', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -1017,7 +1017,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== GROUPED OPTIONS SUPPORT =====
describe('Grouped Options Support', () => {
it('handles grouped options correctly', async () => {
test('handles grouped options correctly', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockGroupedOptions} onChange={mockOnChange} />,
);
@@ -1041,7 +1041,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== ACCESSIBILITY TESTS =====
describe('Accessibility', () => {
it('has proper ARIA attributes', async () => {
test('has proper ARIA attributes', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -1058,7 +1058,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('supports screen reader navigation', async () => {
test('supports screen reader navigation', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -1079,7 +1079,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 9. ADVANCED KEYBOARD NAVIGATION =====
describe('Advanced Keyboard Navigation (AKN)', () => {
it('AKN-01: Shift + Arrow + Del chip deletion', async () => {
test('AKN-01: Shift + Arrow + Del chip deletion', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -1137,7 +1137,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(combobox).toHaveFocus();
});
it('AKN-03: Mouse out closes dropdown', async () => {
test('AKN-03: Mouse out closes dropdown', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -1164,7 +1164,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 10. ADVANCED FILTERING AND HIGHLIGHTING =====
describe('Advanced Filtering and Highlighting (AFH)', () => {
it('AFH-01: Highlighted values pushed to top', async () => {
test('AFH-01: Highlighted values pushed to top', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -1189,14 +1189,14 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Check for highlighted text
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = [...highlightedElements].map(
const highlightTexts = Array.from(highlightedElements).map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('front');
// Get all option items to check the order
const optionItems = document.querySelectorAll('.option-item');
const optionTexts = [...optionItems]
const optionTexts = Array.from(optionItems)
.map((item) => {
const labelElement = item.querySelector('.option-label-text');
return labelElement?.textContent?.trim();
@@ -1220,7 +1220,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('AFH-02: Distinction between selection Enter and save Enter', async () => {
test('AFH-02: Distinction between selection Enter and save Enter', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -1267,7 +1267,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 11. ADVANCED CLEAR ACTIONS =====
describe('Advanced Clear Actions (ACA)', () => {
it('ACA-01: Clear action waiting behavior', async () => {
test('ACA-01: Clear action waiting behavior', async () => {
const mockOnChangeWithDelay = jest.fn().mockImplementation(
() =>
new Promise<void>((resolve) => {
@@ -1300,7 +1300,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 12. ADVANCED UI STATES =====
describe('Advanced UI States (AUS)', () => {
it('AUS-01: No data with previous value selected', async () => {
test('AUS-01: No data with previous value selected', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={[]}
@@ -1322,7 +1322,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(screen.getByText('previous-value')).toBeInTheDocument();
});
it('AUS-02: Always editable accessibility', async () => {
test('AUS-02: Always editable accessibility', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -1338,7 +1338,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(combobox).not.toBeDisabled();
});
it('AUS-03: Sufficient space for search value', async () => {
test('AUS-03: Sufficient space for search value', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -1372,7 +1372,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 13. REGEX AND CUSTOM VALUES =====
describe('Regex and Custom Values (RCV)', () => {
it('RCV-01: Regex pattern support', async () => {
test('RCV-01: Regex pattern support', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -1418,7 +1418,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('RCV-02: Custom values treated as normal dropdown values', async () => {
test('RCV-02: Custom values treated as normal dropdown values', async () => {
const customOptions = [
...mockOptions,
{ label: 'custom-value', value: 'custom-value', type: 'custom' as const },
@@ -1456,7 +1456,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 14. DROPDOWN PERSISTENCE =====
describe('Dropdown Persistence (DP)', () => {
it('DP-01: Dropdown stays open for non-save actions', async () => {
test('DP-01: Dropdown stays open for non-save actions', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);

View File

@@ -50,7 +50,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 1. CUSTOM VALUES SUPPORT =====
describe('Custom Values Support (CS)', () => {
it('CS-02: Partial matches create custom values', async () => {
test('CS-02: Partial matches create custom values', async () => {
render(
<CustomSelect
options={mockOptions}
@@ -110,7 +110,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('CS-03: Exact match behavior', async () => {
test('CS-03: Exact match behavior', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -133,14 +133,14 @@ describe('CustomSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Check for highlighted "frontend" text
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = [...highlightedElements].map(
const highlightTexts = Array.from(highlightedElements).map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('Frontend');
// Frontend option should be visible in dropdown - use a simpler approach
const optionContents = document.querySelectorAll('.option-content');
const hasFrontendOption = [...optionContents].some((content) =>
const hasFrontendOption = Array.from(optionContents).some((content) =>
content.textContent?.includes('Frontend'),
);
expect(hasFrontendOption).toBe(true);
@@ -161,7 +161,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 2. SEARCH AND FILTERING =====
describe('Search and Filtering (SF)', () => {
it('SF-01: Selected values pushed to top', async () => {
test('SF-01: Selected values pushed to top', async () => {
render(
<CustomSelect
options={mockOptions}
@@ -178,14 +178,14 @@ describe('CustomSelect - Comprehensive Tests', () => {
expect(dropdown).toBeInTheDocument();
const options = dropdown?.querySelectorAll('.option-content') || [];
const optionTexts = [...options].map((el) => el.textContent);
const optionTexts = Array.from(options).map((el) => el.textContent);
// Database should be at the top
expect(optionTexts[0]).toContain('Database');
});
});
it('SF-02: Real-time search filtering', async () => {
test('SF-02: Real-time search filtering', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -210,14 +210,14 @@ describe('CustomSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Check for highlighted text within the Frontend option
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = [...highlightedElements].map(
const highlightTexts = Array.from(highlightedElements).map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('Front');
// Should show Frontend option (highlighted) - use a simpler approach
const optionContents = document.querySelectorAll('.option-content');
const hasFrontendOption = [...optionContents].some((content) =>
const hasFrontendOption = Array.from(optionContents).some((content) =>
content.textContent?.includes('Frontend'),
);
expect(hasFrontendOption).toBe(true);
@@ -228,7 +228,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('SF-03: Search highlighting', async () => {
test('SF-03: Search highlighting', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -250,14 +250,14 @@ describe('CustomSelect - Comprehensive Tests', () => {
// Should highlight matching text in options
await waitFor(() => {
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = [...highlightedElements].map(
const highlightTexts = Array.from(highlightedElements).map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('end');
});
});
it('SF-04: Search with partial matches', async () => {
test('SF-04: Search with partial matches', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -298,7 +298,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 3. KEYBOARD NAVIGATION =====
describe('Keyboard Navigation (KN)', () => {
it('KN-01: Arrow key navigation in dropdown', async () => {
test('KN-01: Arrow key navigation in dropdown', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -329,7 +329,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('KN-02: Tab navigation to dropdown', async () => {
test('KN-02: Tab navigation to dropdown', async () => {
render(
<div>
<input data-testid="prev-input" />
@@ -355,7 +355,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('KN-03: Enter selection in dropdown', async () => {
test('KN-03: Enter selection in dropdown', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -376,7 +376,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('KN-04: Space key selection', async () => {
test('KN-04: Space key selection', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -396,7 +396,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('KN-05: Tab navigation within dropdown', async () => {
test('KN-05: Tab navigation within dropdown', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -417,7 +417,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 4. UI/UX BEHAVIORS =====
describe('UI/UX Behaviors (UI)', () => {
it('UI-01: Loading state does not block interaction', async () => {
test('UI-01: Loading state does not block interaction', async () => {
render(
<CustomSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -429,7 +429,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
expect(combobox).toHaveFocus();
});
it('UI-02: Component remains editable in all states', () => {
test('UI-02: Component remains editable in all states', () => {
render(
<CustomSelect
options={mockOptions}
@@ -444,7 +444,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
expect(combobox).not.toBeDisabled();
});
it('UI-03: Loading state display in footer', async () => {
test('UI-03: Loading state display in footer', async () => {
render(
<CustomSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -458,7 +458,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('UI-04: Error state display in footer', async () => {
test('UI-04: Error state display in footer', async () => {
render(
<CustomSelect
options={mockOptions}
@@ -477,7 +477,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('UI-05: No data state display', async () => {
test('UI-05: No data state display', async () => {
render(
<CustomSelect
options={[]}
@@ -497,7 +497,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 6. SAVE AND SELECTION TRIGGERS =====
describe('Save and Selection Triggers (ST)', () => {
it('ST-01: Mouse selection works', async () => {
test('ST-01: Mouse selection works', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -520,7 +520,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 7. GROUPED OPTIONS SUPPORT =====
describe('Grouped Options Support', () => {
it('handles grouped options correctly', async () => {
test('handles grouped options correctly', async () => {
render(
<CustomSelect options={mockGroupedOptions} onChange={mockOnChange} />,
);
@@ -541,7 +541,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('grouped option selection works', async () => {
test('grouped option selection works', async () => {
render(
<CustomSelect options={mockGroupedOptions} onChange={mockOnChange} />,
);
@@ -566,7 +566,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 8. ACCESSIBILITY =====
describe('Accessibility', () => {
it('has proper ARIA attributes', async () => {
test('has proper ARIA attributes', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -580,7 +580,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('supports screen reader navigation', async () => {
test('supports screen reader navigation', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -596,7 +596,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('has proper focus management', async () => {
test('has proper focus management', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -617,7 +617,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 10. EDGE CASES =====
describe('Edge Cases', () => {
it('handles special characters in options', async () => {
test('handles special characters in options', async () => {
const specialOptions = [
{ label: 'Option with spaces', value: 'option-with-spaces' },
{ label: 'Option-with-dashes', value: 'option-with-dashes' },
@@ -638,7 +638,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('handles extremely long option labels', async () => {
test('handles extremely long option labels', async () => {
const longLabelOptions = [
{
label:
@@ -663,7 +663,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 11. ADVANCED KEYBOARD NAVIGATION =====
describe('Advanced Keyboard Navigation (AKN)', () => {
it('AKN-01: Mouse out closes dropdown', async () => {
test('AKN-01: Mouse out closes dropdown', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -684,7 +684,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('AKN-02: TAB navigation from input to dropdown', async () => {
test('AKN-02: TAB navigation from input to dropdown', async () => {
render(
<div>
<input data-testid="prev-input" />
@@ -722,7 +722,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 12. ADVANCED FILTERING AND HIGHLIGHTING =====
describe('Advanced Filtering and Highlighting (AFH)', () => {
it('AFH-01: Highlighted values pushed to top', async () => {
test('AFH-01: Highlighted values pushed to top', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -745,14 +745,14 @@ describe('CustomSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Check for highlighted text
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = [...highlightedElements].map(
const highlightTexts = Array.from(highlightedElements).map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('front');
// Get all option items to check the order
const optionItems = document.querySelectorAll('.option-item');
const optionTexts = [...optionItems]
const optionTexts = Array.from(optionItems)
.map((item) => {
const contentElement = item.querySelector('.option-content');
return contentElement?.textContent?.trim();
@@ -776,7 +776,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('AFH-02: Distinction between selection Enter and save Enter', async () => {
test('AFH-02: Distinction between selection Enter and save Enter', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -830,7 +830,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 13. ADVANCED CLEAR ACTIONS =====
describe('Advanced Clear Actions (ACA)', () => {
it('ACA-01: Clear action waiting behavior', async () => {
test('ACA-01: Clear action waiting behavior', async () => {
const mockOnChangeWithDelay = jest.fn().mockImplementation(
() =>
new Promise((resolve) => {
@@ -860,7 +860,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
expect(mockOnChangeWithDelay).toHaveBeenCalled();
});
it('ACA-02: Single select clear behavior like text input', async () => {
test('ACA-02: Single select clear behavior like text input', async () => {
render(
<CustomSelect
options={mockOptions}
@@ -883,7 +883,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 14. ADVANCED UI STATES =====
describe('Advanced UI States (AUS)', () => {
it('AUS-01: No data with previous value selected', async () => {
test('AUS-01: No data with previous value selected', async () => {
render(
<CustomSelect
options={[]}
@@ -905,7 +905,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
expect(screen.getAllByText('previous-value')).toHaveLength(2);
});
it('AUS-02: Always editable accessibility', async () => {
test('AUS-02: Always editable accessibility', async () => {
render(
<CustomSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -921,7 +921,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
expect(combobox).not.toBeDisabled();
});
it('AUS-03: Sufficient space for search value', async () => {
test('AUS-03: Sufficient space for search value', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -950,7 +950,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('AUS-04: No spinners blocking user interaction', async () => {
test('AUS-04: No spinners blocking user interaction', async () => {
render(
<CustomSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -976,7 +976,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 15. REGEX AND CUSTOM VALUES =====
describe('Regex and Custom Values (RCV)', () => {
it('RCV-01: Regex pattern support', async () => {
test('RCV-01: Regex pattern support', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -1019,7 +1019,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
it('RCV-02: Custom values treated as normal dropdown values', async () => {
test('RCV-02: Custom values treated as normal dropdown values', async () => {
const customOptions = [
...mockOptions,
{ label: 'custom-value', value: 'custom-value', type: 'custom' as const },
@@ -1051,7 +1051,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 16. DROPDOWN PERSISTENCE =====
describe('Dropdown Persistence (DP)', () => {
it('DP-01: Dropdown closes only on save actions', async () => {
test('DP-01: Dropdown closes only on save actions', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');

View File

@@ -86,7 +86,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 1. INTEGRATION WITH CUSTOMSELECT =====
describe('CustomSelect Integration (VI)', () => {
it('VI-01: Single select variable integration', async () => {
test('VI-01: Single select variable integration', async () => {
const variable = createMockVariable({
multiSelect: false,
type: 'CUSTOM',
@@ -130,7 +130,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 2. INTEGRATION WITH CUSTOMMULTISELECT =====
describe('CustomMultiSelect Integration (VI)', () => {
it('VI-02: Multi select variable integration', async () => {
test('VI-02: Multi select variable integration', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
@@ -174,7 +174,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 3. TEXTBOX VARIABLE TYPE =====
describe('Textbox Variable Integration', () => {
it('VI-03: Textbox variable handling', async () => {
test('VI-03: Textbox variable handling', async () => {
const variable = createMockVariable({
type: 'TEXTBOX',
selectedValue: 'initial-value',
@@ -219,7 +219,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 4. VALUE PERSISTENCE AND STATE MANAGEMENT =====
describe('Value Persistence and State Management', () => {
it('VI-04: All selected state handling', () => {
test('VI-04: All selected state handling', () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
@@ -243,7 +243,7 @@ describe('VariableItem Integration Tests', () => {
expect(screen.getByText('ALL')).toBeInTheDocument();
});
it('VI-05: Dropdown behavior with temporary selections', async () => {
test('VI-05: Dropdown behavior with temporary selections', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
@@ -277,7 +277,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 6. ACCESSIBILITY AND USER EXPERIENCE =====
describe('Accessibility and User Experience', () => {
it('VI-06: Variable description tooltip', async () => {
test('VI-06: Variable description tooltip', async () => {
const variable = createMockVariable({
description: 'This variable controls the service selection',
type: 'CUSTOM',
@@ -310,7 +310,7 @@ describe('VariableItem Integration Tests', () => {
});
});
it('VI-07: Variable name display', () => {
test('VI-07: Variable name display', () => {
const variable = createMockVariable({
name: 'service_name',
type: 'CUSTOM',
@@ -331,7 +331,7 @@ describe('VariableItem Integration Tests', () => {
expect(screen.getByText('$service_name')).toBeInTheDocument();
});
it('VI-08: Max tag count behavior', async () => {
test('VI-08: Max tag count behavior', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
@@ -365,7 +365,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 8. SEARCH INTERACTION TESTS =====
describe('Search Interaction Tests', () => {
it('VI-14: Search persistence across dropdown open/close', async () => {
test('VI-14: Search persistence across dropdown open/close', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',
@@ -417,7 +417,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 9. ADVANCED KEYBOARD NAVIGATION =====
describe('Advanced Keyboard Navigation (VI)', () => {
it('VI-15: Shift + Arrow + Del chip deletion in multiselect', async () => {
test('VI-15: Shift + Arrow + Del chip deletion in multiselect', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',
@@ -461,7 +461,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 11. ADVANCED UI STATES =====
describe('Advanced UI States (VI)', () => {
it('VI-19: No data with previous value selected in variable', async () => {
test('VI-19: No data with previous value selected in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: '',
@@ -499,7 +499,7 @@ describe('VariableItem Integration Tests', () => {
expect(combobox).toBeInTheDocument();
});
it('VI-20: Always editable accessibility in variable', async () => {
test('VI-20: Always editable accessibility in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2',
@@ -530,7 +530,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 13. DROPDOWN PERSISTENCE =====
describe('Dropdown Persistence (VI)', () => {
it('VI-24: Dropdown stays open for non-save actions in variable', async () => {
test('VI-24: Dropdown stays open for non-save actions in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',

View File

@@ -44,7 +44,7 @@ describe('OverflowInputToolTip', () => {
jest.restoreAllMocks();
});
it('shows tooltip when content overflows and input is clamped at maxAutoWidth', async () => {
test('shows tooltip when content overflows and input is clamped at maxAutoWidth', async () => {
mockOverflow(150, 250); // clientWidth >= maxAutoWidth (150), scrollWidth > clientWidth
render(<OverflowInputToolTip value="Very long overflowing text" />);
@@ -64,7 +64,7 @@ describe('OverflowInputToolTip', () => {
).toBeInTheDocument();
});
it('does NOT show tooltip when content does not overflow', async () => {
test('does NOT show tooltip when content does not overflow', async () => {
mockOverflow(150, 100); // content fits (scrollWidth <= clientWidth)
render(<OverflowInputToolTip value="Short text" />);
@@ -76,7 +76,7 @@ describe('OverflowInputToolTip', () => {
});
});
it('does NOT show tooltip when content overflows but input is NOT at maxAutoWidth', async () => {
test('does NOT show tooltip when content overflows but input is NOT at maxAutoWidth', async () => {
mockOverflow(100, 250); // clientWidth < maxAutoWidth (150), scrollWidth > clientWidth
render(<OverflowInputToolTip value="Long but input not clamped" />);
@@ -88,7 +88,7 @@ describe('OverflowInputToolTip', () => {
});
});
it('uncontrolled input allows typing', async () => {
test('uncontrolled input allows typing', async () => {
render(<OverflowInputToolTip defaultValue="Init" />);
const input = screen.getByRole('textbox') as HTMLInputElement;
@@ -97,7 +97,7 @@ describe('OverflowInputToolTip', () => {
expect(input).toHaveValue('InitABC');
});
it('disabled input never shows tooltip even if overflowing', async () => {
test('disabled input never shows tooltip even if overflowing', async () => {
mockOverflow(150, 300);
render(<OverflowInputToolTip value="Overflowing!" disabled />);
@@ -109,7 +109,7 @@ describe('OverflowInputToolTip', () => {
});
});
it('renders mirror span and input correctly (structural assertions instead of snapshot)', () => {
test('renders mirror span and input correctly (structural assertions instead of snapshot)', () => {
const { container } = render(<OverflowInputToolTip value="Snapshot" />);
const mirror = container.querySelector('.overflow-input-mirror');
const input = container.querySelector('input') as HTMLInputElement | null;

View File

@@ -25,7 +25,7 @@ function OverlayScrollbar({
autoHide: 'scroll',
theme: isDarkMode ? 'os-theme-light' : 'os-theme-dark',
},
...customOptions,
...(customOptions || {}),
} as PartialOptions),
[customOptions, isDarkMode],
);

View File

@@ -159,7 +159,7 @@ function HavingFilter({
if (tokens.length === 0) {
return false;
}
const lastToken = tokens.at(-1);
const lastToken = tokens[tokens.length - 1];
// Check if the last token is exactly an operator or ends with an operator and space
return havingOperators.some((op) => {
const opWithSpace = `${op.value} `;
@@ -253,7 +253,7 @@ function HavingFilter({
if (
!text.endsWith(' ') &&
tokens.length >= 2 &&
havingOperators.some((op) => op.value === tokens.at(-2))
havingOperators.some((op) => op.value === tokens[tokens.length - 2])
) {
return null;
}
@@ -261,8 +261,8 @@ function HavingFilter({
// Suggest key/operator pairs and ( for grouping
if (
tokens.length === 0 ||
conjunctions.some((c) => tokens.at(-1) === c.value.trim()) ||
tokens.at(-1) === '('
conjunctions.some((c) => tokens[tokens.length - 1] === c.value.trim()) ||
tokens[tokens.length - 1] === '('
) {
return {
from: context.pos,
@@ -275,7 +275,7 @@ function HavingFilter({
// Show suggestions when typing
if (tokens.length > 0) {
const lastToken = tokens.at(-1);
const lastToken = tokens[tokens.length - 1];
const filteredOptions = options.filter((opt) =>
opt.label.toLowerCase().includes(lastToken.toLowerCase()),
);
@@ -293,8 +293,8 @@ function HavingFilter({
// Suggest conjunctions after a value and a space
if (
tokens.length > 0 &&
(isNumber(tokens.at(-1)) ||
tokens.at(-1) === ')') &&
(isNumber(tokens[tokens.length - 1]) ||
tokens[tokens.length - 1] === ')') &&
text.endsWith(' ')
) {
return {

View File

@@ -535,7 +535,7 @@ function QueryAddOns({
>
<Radio.Button
className={
selectedViews.some((view) => view.key === addOn.key)
selectedViews.find((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
}

View File

@@ -23,7 +23,7 @@ function QueryAggregationOptions({
panelType?: string;
onAggregationIntervalChange: (value: number) => void;
onChange?: (value: string) => void;
queryData: IBuilderQuery ;
queryData: IBuilderQuery | IBuilderTraceOperator;
}): JSX.Element {
const showAggregationInterval = useMemo(() => {
if (panelType === PANEL_TYPES.VALUE) {

View File

@@ -286,7 +286,7 @@ function QuerySearch({
}
});
}
setKeySuggestions([...merged.values()]);
setKeySuggestions(Array.from(merged.values()));
// Force reopen the completion if editor is available and focused
if (editorRef.current) {
@@ -339,7 +339,7 @@ function QuerySearch({
// If value contains single quotes, escape them and wrap in single quotes
if (value.includes("'")) {
// Replace single quotes with escaped single quotes
const escapedValue = value.replaceAll(/'/g, "\\'");
const escapedValue = value.replace(/'/g, "\\'");
return `'${escapedValue}'`;
}
@@ -899,12 +899,12 @@ function QuerySearch({
// If we have previous pairs, we can prioritize keys that haven't been used yet
if (queryContext.queryPairs && queryContext.queryPairs.length > 0) {
const usedKeys = new Set(queryContext.queryPairs.map((pair) => pair.key));
const usedKeys = queryContext.queryPairs.map((pair) => pair.key);
// Add boost to unused keys to prioritize them
options = options.map((option) => ({
...option,
boost: usedKeys.has(option.label) ? -10 : 10,
boost: usedKeys.includes(option.label) ? -10 : 10,
}));
}

View File

@@ -29,7 +29,7 @@ describe('traceOperatorContextUtils', () => {
null,
);
expect(context).toStrictEqual({
expect(context).toEqual({
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
@@ -62,7 +62,7 @@ describe('traceOperatorContextUtils', () => {
false,
);
expect(context).toStrictEqual({
expect(context).toEqual({
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
@@ -193,7 +193,7 @@ describe('traceOperatorContextUtils', () => {
it('should return default context for empty query', () => {
const result = getTraceOperatorContextAtCursor('', 0);
expect(result).toStrictEqual({
expect(result).toEqual({
tokenType: -1,
text: '',
start: 0,
@@ -211,7 +211,7 @@ describe('traceOperatorContextUtils', () => {
it('should return default context for null query', () => {
const result = getTraceOperatorContextAtCursor(null as any, 0);
expect(result).toStrictEqual({
expect(result).toEqual({
tokenType: -1,
text: '',
start: 0,
@@ -229,7 +229,7 @@ describe('traceOperatorContextUtils', () => {
it('should return default context for undefined query', () => {
const result = getTraceOperatorContextAtCursor(undefined as any, 0);
expect(result).toStrictEqual({
expect(result).toEqual({
tokenType: -1,
text: '',
start: 0,

View File

@@ -8,21 +8,21 @@ const makeTraceOperator = (expression: string): IBuilderTraceOperator =>
describe('getInvolvedQueriesInTraceOperator', () => {
it('returns empty array for empty input', () => {
const result = getInvolvedQueriesInTraceOperator([]);
expect(result).toStrictEqual([]);
expect(result).toEqual([]);
});
it('extracts identifiers from expression', () => {
const result = getInvolvedQueriesInTraceOperator([
makeTraceOperator('A => B'),
]);
expect(result).toStrictEqual(['A', 'B']);
expect(result).toEqual(['A', 'B']);
});
it('extracts identifiers from complex expression', () => {
const result = getInvolvedQueriesInTraceOperator([
makeTraceOperator('A => (NOT B || C)'),
]);
expect(result).toStrictEqual(['A', 'B', 'C']);
expect(result).toEqual(['A', 'B', 'C']);
});
it('filters out querynames from complex expression', () => {
@@ -31,7 +31,7 @@ describe('getInvolvedQueriesInTraceOperator', () => {
'(A1 && (NOT B2 || (C3 -> (D4 && E5)))) => ((F6 || G7) && (NOT (H8 -> I9)))',
),
]);
expect(result).toStrictEqual([
expect(result).toEqual([
'A1',
'B2',
'C3',

View File

@@ -222,7 +222,9 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
timeout: 2000,
});
const lastArgs = mockedGetKeysOnMount.mock.calls.at(-1)?.[0] as { signal: unknown; searchText: string };
const lastArgs = mockedGetKeysOnMount.mock.calls[
mockedGetKeysOnMount.mock.calls.length - 1
]?.[0] as { signal: unknown; searchText: string };
expect(lastArgs).toMatchObject({ signal: DataSource.LOGS, searchText: '' });
});

View File

@@ -1,13 +1,10 @@
import getSessionStorageApi from 'api/browser/sessionstorage/get';
import removeSessionStorageApi from 'api/browser/sessionstorage/remove';
import setSessionStorageApi from 'api/browser/sessionstorage/set';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export const PREVIOUS_QUERY_KEY = 'previousQuery';
function getPreviousQueryFromStore(): Record<string, IBuilderQuery> {
try {
const raw = getSessionStorageApi(PREVIOUS_QUERY_KEY);
const raw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
if (!raw) {
return {};
}
@@ -20,7 +17,7 @@ function getPreviousQueryFromStore(): Record<string, IBuilderQuery> {
function writePreviousQueryToStore(store: Record<string, IBuilderQuery>): void {
try {
setSessionStorageApi(PREVIOUS_QUERY_KEY, JSON.stringify(store));
sessionStorage.setItem(PREVIOUS_QUERY_KEY, JSON.stringify(store));
} catch {
// ignore quota or serialization errors
}
@@ -66,7 +63,7 @@ export const removeKeyFromPreviousQuery = (key: string): void => {
export const clearPreviousQuery = (): void => {
try {
removeSessionStorageApi(PREVIOUS_QUERY_KEY);
sessionStorage.removeItem(PREVIOUS_QUERY_KEY);
} catch {
// no-op
}

View File

@@ -85,7 +85,7 @@ describe('previousQuery.utils', () => {
saveAsPreviousQuery(key, sampleQuery);
const fromStore = getPreviousQueryFromKey(key);
expect(fromStore).toStrictEqual(sampleQuery);
expect(fromStore).toEqual(sampleQuery);
});
it('saveAsPreviousQuery merges multiple entries and removeKeyFromPreviousQuery deletes one', () => {

View File

@@ -22,18 +22,18 @@ describe('convertFiltersToExpression', () => {
it('should handle empty, null, and undefined inputs', () => {
// Test null and undefined
expect(convertFiltersToExpression(null as any)).toStrictEqual({ expression: '' });
expect(convertFiltersToExpression(undefined as any)).toStrictEqual({
expect(convertFiltersToExpression(null as any)).toEqual({ expression: '' });
expect(convertFiltersToExpression(undefined as any)).toEqual({
expression: '',
});
// Test empty filters
expect(convertFiltersToExpression({ items: [], op: 'AND' })).toStrictEqual({
expect(convertFiltersToExpression({ items: [], op: 'AND' })).toEqual({
expression: '',
});
expect(
convertFiltersToExpression({ items: undefined, op: 'AND' } as any),
).toStrictEqual({ expression: '' });
).toEqual({ expression: '' });
});
it('should convert basic comparison operators with proper value formatting', () => {
@@ -92,7 +92,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expect(result).toEqual({
expression:
"service = 'api-gateway' AND status != 'error' AND duration > 100 AND count <= 50 AND is_active = true AND enabled = false AND count = 0 AND regex REGEXP '.*'",
});
@@ -124,7 +124,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expect(result).toEqual({
expression:
"message = 'user\\'s data' AND description = '' AND path = '/api/v1/users'",
});
@@ -162,7 +162,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expect(result).toEqual({
expression:
"service in ['api-gateway', 'user-service', 'auth-service'] AND status in ['success'] AND tags in [] AND name in ['John\\'s', 'Mary\\'s', 'Bob']",
});
@@ -224,7 +224,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expect(result).toEqual({
expression:
"service NOT IN ['api-gateway', 'user-service'] AND message NOT LIKE 'error' AND path NOT REGEXP '/api/.*' AND service NOT IN ['api-gateway'] AND user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
});
@@ -268,7 +268,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expect(result).toEqual({
expression:
"user_id exists AND user_id exists AND has(tags, 'production') AND hasAny(tags, ['production', 'staging']) AND hasAll(tags, ['production', 'monitoring'])",
});
@@ -312,7 +312,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expect(result).toEqual({
expression:
"service = 'api-gateway' AND status = 'success' AND service in ['api-gateway']",
});
@@ -362,7 +362,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expect(result).toEqual({
expression:
"service in ['api-gateway', 'user-service'] AND user_id exists AND has(tags, 'production') AND duration > 100 AND status NOT IN ['error', 'timeout'] AND method = 'POST'",
});
@@ -412,7 +412,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expect(result).toEqual({
expression:
"count = 0 AND score > 100 AND limit >= 50 AND threshold < 1000 AND max_value <= 999 AND values in ['1', '2', '3', '4', '5']",
});
@@ -456,7 +456,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expect(result).toEqual({
expression:
"is_active = true AND is_deleted = false AND email = 'user@example.com' AND description = 'Contains \"quotes\" and \\'apostrophes\\'' AND path = '/api/v1/users/123?filter=true'",
});
@@ -506,7 +506,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expect(result).toEqual({
expression:
"has(tags, 'production') AND hasAny(labels, ['env:prod', 'service:api']) AND hasAll(metadata, ['version:1.0', 'team:backend']) AND services in ['api-gateway', 'user-service', 'auth-service', 'payment-service'] AND excluded_services NOT IN ['legacy-service', 'deprecated-service'] AND status_codes in ['200', '201', '400', '500']",
});
@@ -544,7 +544,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expect(result).toEqual({
expression:
"user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
});
@@ -565,9 +565,10 @@ describe('convertFiltersToExpression', () => {
const result = convertFiltersToExpressionWithExistingQuery(
filters,
undefined,
);
expect(result.filters).toStrictEqual(filters);
expect(result.filters).toEqual(filters);
expect(result.filter.expression).toBe("service.name = 'test-service'");
});
@@ -579,9 +580,10 @@ describe('convertFiltersToExpression', () => {
const result = convertFiltersToExpressionWithExistingQuery(
filters,
undefined,
);
expect(result.filters).toStrictEqual(filters);
expect(result.filters).toEqual(filters);
expect(result.filter.expression).toBe('');
});
@@ -609,7 +611,7 @@ describe('convertFiltersToExpression', () => {
expect(result.filter).toBeDefined();
expect(result.filter.expression).toBe("service.name = 'updated-service'");
// Ensure parser can parse the existing query
expect(extractQueryPairs(existingQuery)).toStrictEqual(
expect(extractQueryPairs(existingQuery)).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: 'service.name',
@@ -803,7 +805,7 @@ describe('convertAggregationToExpression', () => {
temporality: 'delta',
});
expect(result).toStrictEqual([
expect(result).toEqual([
{
metricName: 'test_metric',
timeAggregation: 'avg',
@@ -823,7 +825,7 @@ describe('convertAggregationToExpression', () => {
spaceAggregation: 'noop',
});
expect(result).toStrictEqual([
expect(result).toEqual([
{
metricName: 'test_metric',
timeAggregation: 'count',
@@ -839,7 +841,7 @@ describe('convertAggregationToExpression', () => {
dataSource: DataSource.METRICS,
});
expect(result).toStrictEqual([
expect(result).toEqual([
{
metricName: '',
timeAggregation: 'sum',
@@ -856,7 +858,7 @@ describe('convertAggregationToExpression', () => {
alias: 'trace_alias',
});
expect(result).toStrictEqual([
expect(result).toEqual([
{
expression: 'count(test_metric)',
alias: 'trace_alias',
@@ -872,7 +874,7 @@ describe('convertAggregationToExpression', () => {
alias: 'log_alias',
});
expect(result).toStrictEqual([
expect(result).toEqual([
{
expression: 'avg(test_metric)',
alias: 'log_alias',
@@ -887,7 +889,7 @@ describe('convertAggregationToExpression', () => {
dataSource: DataSource.TRACES,
});
expect(result).toStrictEqual([
expect(result).toEqual([
{
expression: 'count()',
},
@@ -901,7 +903,7 @@ describe('convertAggregationToExpression', () => {
dataSource: DataSource.LOGS,
});
expect(result).toStrictEqual([
expect(result).toEqual([
{
expression: 'sum(test_metric)',
},
@@ -915,7 +917,7 @@ describe('convertAggregationToExpression', () => {
dataSource: DataSource.METRICS,
});
expect(result).toStrictEqual([
expect(result).toEqual([
{
metricName: 'test_metric',
timeAggregation: 'max',
@@ -931,7 +933,7 @@ describe('convertAggregationToExpression', () => {
dataSource: DataSource.METRICS,
});
expect(result).toStrictEqual([
expect(result).toEqual([
{
metricName: 'test_metric',
timeAggregation: 'sum',
@@ -949,7 +951,7 @@ describe('convertAggregationToExpression', () => {
dataSource: DataSource.TRACES,
});
expect(result).toStrictEqual([
expect(result).toEqual([
{
expression: 'count()',
},
@@ -963,7 +965,7 @@ describe('convertAggregationToExpression', () => {
dataSource: DataSource.LOGS,
});
expect(result).toStrictEqual([
expect(result).toEqual([
{
expression: 'count()',
},

View File

@@ -58,7 +58,7 @@ const formatSingleValue = (v: string | number | boolean): string => {
return v;
}
// Quote and escape single quotes in strings
return `'${v.replaceAll(/'/g, "\\'")}'`;
return `'${v.replace(/'/g, "\\'")}'`;
}
// Convert numbers and booleans to strings without quotes
return String(v);

View File

@@ -471,6 +471,6 @@ describe('CheckboxFilter - User Flows', () => {
expect(filterForServiceName.key.key).toBe(SERVICE_NAME_KEY);
expect(filterForServiceName.op).toBe('in');
expect(filterForServiceName.value).toStrictEqual(['mq-kafka', 'otel-demo']);
expect(filterForServiceName.value).toEqual(['mq-kafka', 'otel-demo']);
});
});

View File

@@ -30,7 +30,7 @@ import { isKeyMatch } from './utils';
import './Checkbox.styles.scss';
const SELECTED_OPERATORS = new Set([OPERATORS['='], 'in']);
const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in'];
const SOURCES_WITH_EMPTY_STATE_ENABLED = [QuickFiltersSource.LOGS_EXPLORER];
@@ -194,7 +194,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
);
if (filterSync) {
if (SELECTED_OPERATORS.has(filterSync.op)) {
if (SELECTED_OPERATORS.includes(filterSync.op)) {
if (isArray(filterSync.value)) {
filterSync.value.forEach((val) => {
filterState[String(val)] = true;
@@ -532,12 +532,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((q, idx) => {
queryData: [
...currentQuery.builder.queryData.map((q, idx) => {
if (idx === activeQueryIndex) {
return query;
}
return q;
}),
],
},
};
@@ -551,7 +553,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const isEmptyStateWithDocsEnabled =
SOURCES_WITH_EMPTY_STATE_ENABLED.includes(source) &&
!searchText &&
attributeValues.length === 0;
!attributeValues.length;
return (
<div className="checkbox-filter">
@@ -575,7 +577,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
<Typography.Text className="title">{filter.title}</Typography.Text>
</section>
<section className="right-action">
{isOpen && attributeValues.length > 0 && (
{isOpen && !!attributeValues.length && (
<Typography.Text
className="clear-all"
onClick={(e): void => {
@@ -591,7 +593,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
</section>
{isOpen &&
(isLoading || isLoadingKeyValueSuggestions) &&
attributeValues.length === 0 && (
!attributeValues.length && (
<section className="loading">
<Skeleton paragraph={{ rows: 4 }} />
</section>

View File

@@ -139,7 +139,7 @@ function Duration({
attribute as AllTraceFilterKeys,
)
) {
if (!values || values.length === 0) {
if (!values || !values.length) {
return [];
}
let minValue = '';

View File

@@ -92,7 +92,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
}
return currentQuery.builder.queryData.map((query, index) => ({
label: query.queryName || String.fromCodePoint(65 + index),
label: query.queryName || String.fromCharCode(65 + index),
value: index,
}));
}, [currentQuery?.builder?.queryData]);

View File

@@ -323,7 +323,7 @@ describe('Quick Filters with custom filters', () => {
const settingsButton = icon.closest('button') ?? icon;
await user.click(settingsButton);
await expect(screen.findByText('Edit quick filters')).resolves.toBeInTheDocument();
expect(await screen.findByText('Edit quick filters')).toBeInTheDocument();
const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!;
expect(addedSection).toContainElement(
@@ -454,7 +454,7 @@ describe('Quick Filters with custom filters', () => {
});
const requestBody = putHandler.mock.calls[0][0];
expect(requestBody.filters).toStrictEqual(
expect(requestBody.filters).toEqual(
expect.arrayContaining([
expect.not.objectContaining({ key: FILTER_OS_DESCRIPTION }),
]),
@@ -535,12 +535,12 @@ describe('Quick Filters refetch behavior', () => {
);
const { unmount } = render(<TestQuickFilters signal={SIGNAL} />);
await expect(screen.findByText(FILTER_SERVICE_NAME)).resolves.toBeInTheDocument();
expect(await screen.findByText(FILTER_SERVICE_NAME)).toBeInTheDocument();
unmount();
render(<TestQuickFilters signal={SIGNAL} />);
await expect(screen.findByText(FILTER_SERVICE_NAME)).resolves.toBeInTheDocument();
expect(await screen.findByText(FILTER_SERVICE_NAME)).toBeInTheDocument();
expect(getCalls).toBe(2);
});
@@ -578,7 +578,7 @@ describe('Quick Filters refetch behavior', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<TestQuickFilters signal={SIGNAL} />);
await expect(screen.findByText(FILTER_SERVICE_NAME)).resolves.toBeInTheDocument();
expect(await screen.findByText(FILTER_SERVICE_NAME)).toBeInTheDocument();
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
const settingsButton = icon.closest('button') ?? icon;
@@ -628,7 +628,7 @@ describe('Quick Filters refetch behavior', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<TestQuickFilters signal={SIGNAL} />);
await expect(screen.findByText(FILTER_SERVICE_NAME)).resolves.toBeInTheDocument();
expect(await screen.findByText(FILTER_SERVICE_NAME)).toBeInTheDocument();
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
const settingsButton = icon.closest('button') ?? icon;
@@ -657,6 +657,6 @@ describe('Quick Filters refetch behavior', () => {
render(<TestQuickFilters signal={SIGNAL} config={[]} />);
await expect(screen.findByText('No filters found')).resolves.toBeInTheDocument();
expect(await screen.findByText('No filters found')).toBeInTheDocument();
});
});

View File

@@ -19,8 +19,8 @@ const getFilterName = (str: string): string => {
// replace . and _ with space
// capitalize the first letter of each word
return str
.replaceAll(/\./g, ' ')
.replaceAll(/_/g, ' ')
.replace(/\./g, ' ')
.replace(/_/g, ' ')
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');

View File

@@ -24,7 +24,7 @@ function RefreshPaymentStatus({
try {
await refreshPaymentStatus();
[await activeLicenseRefetch()];
await Promise.all([activeLicenseRefetch()]);
} catch (e) {
console.error(e);
}

View File

@@ -49,9 +49,9 @@ function DynamicColumnTable({
setColumnsData((prevColumns) =>
prevColumns
? [
...prevColumns.slice(0, - 1),
...prevColumns.slice(0, prevColumns.length - 1),
...visibleColumns,
prevColumns.at(-1),
prevColumns[prevColumns.length - 1],
]
: undefined,
);
@@ -108,7 +108,8 @@ function DynamicColumnTable({
// Update URL with new page number while preserving other params
urlQuery.set('page', page.toString());
safeNavigate({ search: `?${urlQuery.toString()}` });
const newUrl = `${window.location.pathname}?${urlQuery.toString()}`;
safeNavigate(newUrl);
// Call original pagination handler if provided
if (pagination?.onChange && !!pageSize) {

View File

@@ -1,6 +1,3 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { DynamicColumnsKey } from './contants';
import {
GetNewColumnDataFunction,
@@ -15,7 +12,7 @@ export const getVisibleColumns: GetVisibleColumnsFunction = ({
}) => {
let columnVisibilityData: { [key: string]: boolean };
try {
const storedData = getLocalStorageKey(tablesource);
const storedData = localStorage.getItem(tablesource);
if (typeof storedData === 'string' && dynamicColumns) {
columnVisibilityData = JSON.parse(storedData);
return dynamicColumns.filter((column) => {
@@ -31,7 +28,7 @@ export const getVisibleColumns: GetVisibleColumnsFunction = ({
initialColumnVisibility[key] = false;
});
setLocalStorageKey(tablesource, JSON.stringify(initialColumnVisibility));
localStorage.setItem(tablesource, JSON.stringify(initialColumnVisibility));
} catch (error) {
console.error(error);
}
@@ -45,14 +42,14 @@ export const setVisibleColumns = ({
dynamicColumns,
}: SetVisibleColumnsProps): void => {
try {
const storedData = getLocalStorageKey(tablesource);
const storedData = localStorage.getItem(tablesource);
if (typeof storedData === 'string' && dynamicColumns) {
const columnVisibilityData = JSON.parse(storedData);
const { key } = dynamicColumns[index];
if (key) {
columnVisibilityData[key] = checked;
}
setLocalStorageKey(tablesource, JSON.stringify(columnVisibilityData));
localStorage.setItem(tablesource, JSON.stringify(columnVisibilityData));
}
} catch (error) {
console.error(error);
@@ -68,9 +65,9 @@ export const getNewColumnData: GetNewColumnDataFunction = ({
if (checked && dynamicColumns) {
return prevColumns
? [
...prevColumns.slice(0, - 1),
...prevColumns.slice(0, prevColumns.length - 1),
dynamicColumns[index],
prevColumns.at(-1),
prevColumns[prevColumns.length - 1],
]
: undefined;
}

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