Compare commits

...

30 Commits

Author SHA1 Message Date
Gaurav Tewari
c1e81a57a3 chore: remove ox lint 2026-05-22 20:54:16 +05:30
Gaurav Tewari
c30994f966 fix: lint 2026-05-22 20:51:38 +05:30
Gaurav Tewari
9aef69a186 fix: lint 2026-05-22 20:48:05 +05:30
Gaurav Tewari
66bc4b4398 Merge remote-tracking branch 'origin/main' into refactor/dropdown-migration
# Conflicts:
#	frontend/plugins/rules/no-antd-components.mjs
#	frontend/src/components/ResizeTable/DynamicColumnTable.tsx
#	frontend/src/pages/AlertDetails/AlertHeader/ActionButtons/ActionButtons.tsx
#	frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelStep.tsx
2026-05-22 19:03:05 +05:30
Vinicius Lourenço
04a4d3fe32 fix(date-time-selection-v2): out of sync query params (#11399)
* fix(date-time-selection-v2): out of sync query params

* chore(get-current-search-params): explain why we have that file

* fix(pr): address comments
2026-05-22 11:58:53 +00:00
swapnil-signoz
27dc996fd8 chore(integrations): make dot-metrics dashboards canonical, remove IsDotMetricsEnabled flag (#11406)
IsDotMetricsEnabled always returns true so the _dot.json variants were
always served. Replace each non-dot dashboard JSON with the dot content,
delete the _dot.json files, and remove the dead flag-check logic from
HydrateFileUris.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 10:14:48 +00:00
Karan Balani
83b25f3e9a fix(authdomain): sso enabled toggle value from nested config (#11402)
* fix(authdomain): read ssoEnabled from nested config path in enforce SSO column

* test(authdomain): assert enforce SSO toggle reflects nested config.ssoEnabled

* test(authdomain): drop unnecessary Switch mock

---------

Co-authored-by: Karan Balani <29383381+balanikaran@users.noreply.github.com>
2026-05-22 09:24:09 +00:00
Yunus M
67e4c4611c refactor: replace Ant Design Switch with Signoz UI Switch across mult… (#11223)
* refactor: replace Ant Design Switch with Signoz UI Switch across multiple components

* fix: update snapshot of failing test

* feat: update snapshot

* refactor: update imports to use Signoz UI Switch from the new path across multiple components

* refactor: update banned components to use Signoz UI imports for Typography and Switch

* refactor: replace Ant Design Switch with Signoz UI Switch
2026-05-22 08:50:41 +00:00
SagarRajput-7
7274421895 chore: fga ui feedbacks (#11403)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: updated the signozhq version and removed ts-expect-error from button

* chore: renamed authz test with authz.test.tsx

* chore: remove error from useAuthZ public API and fallbackOnError from GuardAuthZ

* chore: updated test cases

* chore: updated test cases

* chore: restore error to useAuthZ API with fail-open default in GuardAuthZ

* chore: updated test cases
2026-05-21 23:49:44 +00:00
Gaurav Tewari
04c58a1572 chore: save antd file 2026-05-22 01:58:11 +05:30
Gaurav Tewari
9bbdd00858 chore: sync with main 2026-05-22 01:56:59 +05:30
Gaurav Tewari
316e9c7361 chore: remove internal docs 2026-05-22 01:50:54 +05:30
Gaurav Tewari
634166860b fix: add dropdown to list 2026-05-22 01:40:15 +05:30
Gaurav Tewari
af8f2fa95a chore: self review comments 2026-05-22 01:32:54 +05:30
SagarRajput-7
9c6656d6b9 fix(user-info): surfaced errors for reset password and fixed issues (#11389)
* fix(user-info): surfaced errors for reset password and fixed issues

* fix(user-info): removed notification from atnd and used toast and showerrormodal in userinfo

* fix(user-info): refactor and added tests

* fix(user-info): code refactor
2026-05-21 17:24:31 +00:00
Nikhil Mantri
5c54a2537c chore: arrays non-nullable (#11388) 2026-05-21 17:22:25 +00:00
Gaurav Tewari
f81fd78ff6 fix: lint 2026-05-21 22:09:24 +05:30
Nikhil Mantri
bf201710a7 feat(infra-monitoring): allow order by primary name column in v2 apis (#11264)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
* chore: baseline setup

* chore: endpoint detail update

* chore: added logic for hosts v3 api

* fix: bug fix

* chore: disk usage

* chore: added validate function

* chore: added some unit tests

* chore: return status as a string

* chore: yarn generate api

* chore: removed isSendingK8sAgentsMetricsCode

* chore: moved funcs

* chore: added validation on order by

* chore: added pods list logic

* chore: updated openapi yml

* chore: updated spec

* chore: pods api meta start time

* chore: nil pointer check

* chore: nil pointer dereference fix in req.Filter

* chore: added temporalities of metrics

* chore: added pods metrics temporality

* chore: unified composite key function

* chore: code improvements

* chore: added pods list api updates

* chore: hostStatusNone added for clarity that this field can be left empty as well in payload

* chore: yarn generate api

* chore: return errors from getMetadata and lint fix

* chore: return errors from getMetadata and lint fix

* chore: added hostName logic

* chore: modified getMetadata query

* chore: add type for response and files rearrange

* chore: warnings added passing from queryResponse warning to host lists response struct

* chore: added better metrics existence check

* chore: added a TODO remark

* chore: added required metrics check

* chore: distributed samples table to local table change for get metadata

* chore: frontend fix

* chore: endpoint correction

* chore: endpoint modification openapi

* chore: escape backtick to prevent sql injection

* chore: rearrage

* chore: improvements

* chore: validate order by to validate function

* chore: improved description

* chore: added TODOs and made filterByStatus a part of filter struct

* chore: ignore empty string hosts in get active hosts

* feat(infra-monitoring): v2 hosts list - return counts of active & inactive hosts for custom group by attributes (#10956)

* chore: add functionality for showing active and inactive counts in custom group by

* chore: bug fix

* chore: added subquery for active and total count

* chore: ignore empty string hosts in get active hosts

* fix: sinceUnixMilli for determining active hosts compute once per request

* chore: refactor code

* chore: rename HostsList -> ListHosts

* chore: rearrangement

* chore: inframonitoring types renaming

* chore: added types package

* chore: file structure further breakdown for clarity

* chore: comments correction

* chore: removed temporalities

* chore: pods code restructuring

* chore: comments resolve

* chore: added json tag required: true

* chore: removed pod metric temporalities

* chore: removed internal server error

* chore: added status unauthorized

* chore: remove a defensive nil map check, the function ensure non-nil map when err nil

* chore: cleanup and rename

* chore: make sort stable in case of tiebreaker by comparing composite group by keys

* chore: regen api client for inframonitoring

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: added phase counts feature

* chore: added queries for pod phase counts in custom group by

* chore: added required tags

* chore: added support for pod phase unknown

* chore: removed pods - order by phase

* chore: improved api description to document -1 as no data in numeric fields

* fix: rebase fixes

* chore: added unknown phase count

* fix: isPodUIDInGroupBy in buildPodRecords

* chore: 3 cte --> 2 cte

* chore: pod phase with local table of time series as counts

* chore: comment correction

* chore: corrected comment

* chore: value column for samples table added

* chore: removed query G for phase counts

* chore: rename variable

* chore: added PodPhaseNum constants to types

* feat(infra-monitoring): v2 pods list apis - phase counts when custom grouping (#11088)

* chore: added phase counts feature

* chore: added queries for pod phase counts in custom group by

* chore: added unknown phase count

* fix: isPodUIDInGroupBy in buildPodRecords

* chore: 3 cte --> 2 cte

* chore: pod phase with local table of time series as counts

* chore: comment correction

* chore: corrected comment

* chore: value column for samples table added

* chore: removed query G for phase counts

* chore: rename variable

* chore: added PodPhaseNum constants to types

* chore: nodes list v2 full blown

* chore: metadata fix

* chore: updated comment

* chore: namespaces code

* chore: v2 nodes api

* chore: rename

* chore: v2 clusters list api

* chore: namespaces code

* chore: rename

* chore: review clusters PR

* chore: pvcs code added

* chore: updated endpoint and spec

* chore: pvcs todo

* chore: added condition

* chore: added filter

* chore: added code for deployments

* chore: query nit

* chore: statefulsets code added

* chore: base filter added

* chore: added base deployments change

* chore: added base condition

* chore: v2 jobs list api added

* chore: added daemonsets api

* chore: added pod phase counts

* chore: for pods and nodes, replace none with no_data

* chore: node and pod counts structs added

* chore: namespace record uses PodCountsByPhase

* chore: cluster record uses PodCountsByPhase, NodeCountsByReadiness

* chore: deployment record uses PodCountsByPhase

* chore: statefulset record uses PodCountsByPhase

* chore: job record uses PodCountsByPhase

* chore: daemonset record uses PodCountsByPhase

* chore: added remaining metrics to check

* chore: metrics existence check

* chore: statefulset metrics added

* chore: added jobs metrics

* chore: added metrics

* chore: feature added

* chore: cosmetic changes

* chore: replaced common order by key with entity specific attr key

* chore: moved paginateByName to types and added unit tests

* chore: added pageGroups

* chore: assert added instead of require

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
2026-05-21 14:55:14 +00:00
Gaurav Tewari
b589a7b2e9 fix: failing test 2026-05-21 20:24:03 +05:30
Gaurav Tewari
716dbc7847 chore: migration from antd 2026-05-21 18:05:21 +05:30
Aditya Singh
a5adc52276 feat(trace-details): promote ExpandableValue to periscope + dropdown z-index fix (#11393)
* feat: add expandable value component around status message

* feat: add minor change

* feat: style fix

* feat: remove comment
2026-05-21 12:19:51 +00:00
Aditya Singh
5ddcf33811 Fix drilldown on service details page (#11338)
* feat: fix drilldown on service details page

* feat: hide edit btn if no dashboard
2026-05-21 10:51:36 +00:00
Gaurav Tewari
3a92c7577f fix: another bug 2026-05-21 16:00:12 +05:30
Gaurav Tewari
ba043a5741 fix: side nav issue 2026-05-21 14:59:23 +05:30
Gaurav Tewari
6d2b99eb8d fix: self review changes 2026-05-21 14:11:13 +05:30
SagarRajput-7
c0fe996e7a feat(no-auth): added no auth frontend setup (#11218)
* feat(no-auth): wire preflight global-config check and gate AppRoutes render & cleanAuthStorage util

* feat(no-auth): setup interceptor and ui hiding for no auth mode

* feat(no-auth): replace hide pattern with disable+tooltip via NoAuthGuard

* feat(no-auth): replace localstorage approach with module-level singleton

* feat(no-auth): added no-auth announcement banner and added authguard on sa page

* feat(no-auth): added more authguard

* feat(no-auth): fixes and refactor after rebase

* feat(no-auth): added noauth guard at more places and added tests

* feat(no-auth): refactor and feedback fix

* feat(no-auth): added noauth guard at more places and refactor

* feat(no-auth): changed banner text and code refactor

* feat(no-auth): added doc link under learn more text

* feat(no-auth): removed ui guards and special handling for the no auth mode'

* feat(no-auth): updated test case
2026-05-21 08:38:51 +00:00
Shivam Gupta
1b6bb78ca4 feat(onboarding): add Cert Manager, GraphQL, Railway, ASP.NET, Istio, slog, Scala, Apache Druid, Azure CDN datasources (#11384)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
* feat(onboarding): add Cert Manager, GraphQL, Railway, ASP.NET Core Metrics, Istio, log/slog, Scala, Apache Druid, Azure CDN FrontDoor datasources and update Fly.io, Azure Blob Storage

- Add new onboarding entries for Cert Manager, GraphQL, Railway, ASP.NET Core Metrics,
  Istio Metrics, log/slog, Scala, Apache Druid, and Azure CDN / Front Door
- Add SVG logos for all new datasources
- Update Fly.io entry with logs support and new docs link
- Add One Click Azure option to Azure Blob Storage entry
- Azure CDN FrontDoor links directly to integrations page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: format onboarding config with oxfmt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 06:59:44 +00:00
Ashwin Bhatkal
0583f30e35 test: dashboards details spec with new e2e framework (#11279)
* test: dashboards details spec with new e2e framework

* test: skip delete tests because of flaky hover
2026-05-21 05:52:17 +00:00
primus-bot[bot]
fb3e316ce9 chore(release): bump to v0.125.1 (#11381)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-05-20 17:46:41 +00:00
Gaurav Tewari
3765ca3d42 chore: migrate dropdown 2026-05-20 17:39:33 +05:30
235 changed files with 9324 additions and 20838 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -2689,7 +2689,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesClusterRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -2759,7 +2758,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesDaemonSetRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -2829,7 +2827,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesDeploymentRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -2908,7 +2905,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesHostRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -2984,7 +2980,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesJobRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3032,7 +3027,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesNamespaceRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3110,7 +3104,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesNodeRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3209,7 +3202,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesPodRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3554,7 +3546,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesStatefulSetRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3615,7 +3606,6 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesVolumeRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'

View File

@@ -49,7 +49,7 @@
"@signozhq/design-tokens": "2.1.4",
"@signozhq/icons": "0.4.0",
"@signozhq/resizable": "0.0.2",
"@signozhq/ui": "0.0.19",
"@signozhq/ui": "0.0.21",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
"@uiw/codemirror-theme-copilot": "4.23.11",

View File

@@ -14,7 +14,11 @@
*/
const BANNED_COMPONENTS = {
Typography: 'Use @signozhq/ui Typography instead of antd Typography.',
Typography:
'Use @signozhq/ui/typography Typography instead of antd Typography.',
Switch: 'Use @signozhq/ui/switch Switch instead of antd Switch.',
Dropdown:
'Use @signozhq/ui DropdownMenuSimple (or the composable DropdownMenu primitives) from @signozhq/ui/dropdown-menu instead of antd Dropdown.',
Badge: 'Use @signozhq/ui/badge instead of antd Badge.',
};

View File

@@ -77,8 +77,8 @@ importers:
specifier: 0.0.2
version: 0.0.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@signozhq/ui':
specifier: 0.0.19
version: 0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
specifier: 0.0.21
version: 0.0.21(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
'@tanstack/react-table':
specifier: 8.21.3
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -3269,8 +3269,8 @@ packages:
peerDependencies:
react: ^18.2.0
'@signozhq/ui@0.0.19':
resolution: {integrity: sha512-2q6aRxN/PR4PlR2xJZAREEuvLPiDFggfFKzCW2Z5vHVVbrgnvZHWD1jPUuwszfEg0ceH3UvkwqceO7wN4uRJAA==}
'@signozhq/ui@0.0.21':
resolution: {integrity: sha512-uLM3Vqwxlk2USXbwtb3qRLpjZR9b9QSHFQq/jtcfYNMDmIE/sNjSj0nRkEhX4RqqRgsLRt2PVA33aeWxDOLO3g==}
peerDependencies:
'@signozhq/icons': 0.3.0
react: ^18.2.0
@@ -3851,27 +3851,6 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
'@webassemblyjs/ast@1.14.1':
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
'@webassemblyjs/floating-point-hex-parser@1.13.2':
resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==}
'@webassemblyjs/helper-api-error@1.13.2':
resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==}
'@webassemblyjs/helper-buffer@1.14.1':
resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==}
'@webassemblyjs/helper-numbers@1.13.2':
resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==}
'@webassemblyjs/helper-wasm-bytecode@1.13.2':
resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==}
'@webassemblyjs/helper-wasm-section@1.14.1':
resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==}
'@xmldom/xmldom@0.8.13':
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
engines: {node: '>=10.0.0'}
@@ -12034,7 +12013,7 @@ snapshots:
- react-dom
- tailwindcss
'@signozhq/ui@0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
'@signozhq/ui@0.0.21(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
dependencies:
'@chenglou/pretext': 0.0.5
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)

View File

@@ -166,6 +166,7 @@ function createMockAppContext(
userPreferences: [],
hostsData: null,
isLoggedIn: true,
isPreflightLoading: false,
org: [{ createdAt: 0, id: 'org-id', displayName: 'Test Org' }],
isFetchingUser: false,
isFetchingActiveLicense: false,

View File

@@ -59,6 +59,7 @@ function App(): JSX.Element {
isLoggedIn: isLoggedInState,
featureFlags,
org,
isPreflightLoading,
} = useAppContext();
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
const isAIAssistantEnabled = useIsAIAssistantEnabled();
@@ -386,6 +387,10 @@ function App(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCloudUser, isEnterpriseSelfHostedUser]);
if (isPreflightLoading) {
return <Spinner tip="Loading..." />;
}
// if the user is in logged in state
if (isLoggedInState) {
// if the setup calls are loading then return a spinner

View File

@@ -0,0 +1,72 @@
import axios from 'axios';
import { getIsNoAuthMode } from 'utils/noAuthMode';
import { interceptorRejected } from '../index';
jest.mock('utils/noAuthMode', () => ({
getIsNoAuthMode: jest.fn(),
}));
jest.mock('api/v2/sessions/rotate/post', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('AppRoutes/utils', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('../utils', () => ({
Logout: jest.fn(),
}));
// oxlint-disable-next-line typescript/no-require-imports typescript/no-var-requires
const post = require('api/v2/sessions/rotate/post').default;
// oxlint-disable-next-line typescript/no-require-imports typescript/no-var-requires
const { Logout } = require('../utils');
describe('interceptorRejected — no-auth mode', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(axios, 'isAxiosError').mockReturnValue(true);
});
it('does NOT call rotate or Logout when no-auth mode is enabled on 401', async () => {
(getIsNoAuthMode as jest.Mock).mockReturnValue(true);
const error = {
isAxiosError: true,
response: {
status: 401,
config: { url: '/dashboards', method: 'get' },
},
config: { url: '/dashboards', headers: {} },
};
await interceptorRejected(error as any).catch(() => {});
expect(post).not.toHaveBeenCalled();
expect(Logout).not.toHaveBeenCalled();
});
it('DOES attempt rotate when no-auth mode is disabled on 401', async () => {
(getIsNoAuthMode as jest.Mock).mockReturnValue(false);
(post as jest.Mock).mockResolvedValue({
data: { accessToken: 'a', refreshToken: 'b' },
});
const error = {
isAxiosError: true,
response: {
status: 401,
config: { url: '/dashboards', method: 'get' },
},
config: { url: '/dashboards', headers: {} },
};
await interceptorRejected(error as any).catch(() => {});
expect(post).toHaveBeenCalled();
});
});

View File

@@ -3488,9 +3488,9 @@ export interface InframonitoringtypesClustersDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesClusterRecordDTO[] | null;
records: InframonitoringtypesClusterRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3566,9 +3566,9 @@ export interface InframonitoringtypesDaemonSetsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesDaemonSetRecordDTO[] | null;
records: InframonitoringtypesDaemonSetRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3644,9 +3644,9 @@ export interface InframonitoringtypesDeploymentsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesDeploymentRecordDTO[] | null;
records: InframonitoringtypesDeploymentRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3730,9 +3730,9 @@ export interface InframonitoringtypesHostsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesHostRecordDTO[] | null;
records: InframonitoringtypesHostRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3816,9 +3816,9 @@ export interface InframonitoringtypesJobsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesJobRecordDTO[] | null;
records: InframonitoringtypesJobRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3866,9 +3866,9 @@ export interface InframonitoringtypesNamespacesDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesNamespaceRecordDTO[] | null;
records: InframonitoringtypesNamespaceRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3933,9 +3933,9 @@ export interface InframonitoringtypesNodesDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesNodeRecordDTO[] | null;
records: InframonitoringtypesNodeRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -4017,9 +4017,9 @@ export interface InframonitoringtypesPodsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesPodRecordDTO[] | null;
records: InframonitoringtypesPodRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -4437,9 +4437,9 @@ export interface InframonitoringtypesStatefulSetsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesStatefulSetRecordDTO[] | null;
records: InframonitoringtypesStatefulSetRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -4506,9 +4506,9 @@ export interface InframonitoringtypesVolumesDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array,null
* @type array
*/
records: InframonitoringtypesVolumeRecordDTO[] | null;
records: InframonitoringtypesVolumeRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer

View File

@@ -13,6 +13,7 @@ import { Events } from 'constants/events';
import { LOCALSTORAGE } from 'constants/localStorage';
import { getBasePath } from 'utils/basePath';
import { eventEmitter } from 'utils/getEventEmitter';
import { getIsNoAuthMode } from 'utils/noAuthMode';
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4, apiV5 } from './apiV1';
import { Logout } from './utils';
@@ -108,7 +109,10 @@ export const interceptorRejected = async (
if (axios.isAxiosError(value) && value.response) {
const { response } = value;
const isNoAuthMode = getIsNoAuthMode();
if (
!isNoAuthMode &&
response.status === 401 &&
// if the session rotate call or the create session errors out with 401 or the delete sessions call returns 401 then we do not retry!
response.config.url !== '/sessions/rotate' &&
@@ -140,16 +144,20 @@ export const interceptorRejected = async (
return await Promise.resolve(reResponse);
} catch (error) {
if ((error as AxiosError)?.response?.status === 401) {
Logout();
void Logout();
}
}
} catch (error) {
Logout();
void Logout();
}
}
if (response.status === 401 && response.config.url === '/sessions/rotate') {
Logout();
if (
!isNoAuthMode &&
response.status === 401 &&
response.config.url === '/sessions/rotate'
) {
void Logout();
}
}
return await Promise.reject(value);

View File

@@ -0,0 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M8.932 20.806c-.369 0-.738.007-1.109 0-.35-.007-.587-.206-.623-.5a.587.587 0 0 1 .53-.636c.79-.062 1.582-.063 2.372-.003a.548.548 0 0 1 .522.602c-.024.326-.253.526-.616.54zM1.792 8.345c-.392 0-.782.008-1.173.002-.327-.006-.577-.22-.614-.512-.037-.293.146-.544.499-.615.192-.032.388-.045.583-.039a81.515 81.515 0 0 1 1.597 0c.163 0 .325.019.483.056.288.073.445.318.411.617-.034.298-.214.477-.515.487-.424.014-.848.004-1.272.004zm7.588 8.417H4.292a2.464 2.464 0 0 1-.326-.007c-.294-.04-.48-.209-.508-.506-.029-.298.11-.501.391-.606.179-.065.365-.051.549-.051 3.347 0 6.695.005 10.042-.006 1.174-.004 2.187-.439 2.993-1.3.69-.738 1.053-1.63 1.16-2.635.085-.788-.027-1.513-.516-2.156-.544-.718-1.28-1.078-2.163-1.082-3.163-.013-6.328-.005-9.487-.01-.336 0-.673-.027-1.007-.058-.29-.027-.45-.201-.469-.492-.021-.317.141-.545.429-.6a1.55 1.55 0 0 1 .29-.015h10.177c1.71.004 3.187 1.038 3.726 2.654.383 1.147.246 2.304-.182 3.416-.824 2.135-2.762 3.448-5.055 3.454-1.652.005-3.304 0-4.956 0zm2.906-13.568c1.533 0 3.066-.008 4.598 0 2.935.018 5.629 1.892 6.653 4.626.442 1.181.538 2.403.412 3.657-.185 1.842-.735 3.552-1.776 5.084-1.608 2.365-3.873 3.68-6.679 4.118-.95.148-1.905.13-2.86.13-.397 0-.61-.181-.633-.51-.025-.351.196-.621.587-.645.434-.026.87-.004 1.305-.016 2.641-.072 4.928-.982 6.74-2.935 1.269-1.37 1.912-3.039 2.13-4.878.151-1.275.135-2.544-.37-3.752-.773-1.85-2.159-2.983-4.068-3.509-.74-.204-1.5-.243-2.26-.247-2.837-.017-5.675-.007-8.511-.007-.12 0-.24.004-.359-.006a.57.57 0 0 1-.517-.536.557.557 0 0 1 .456-.557c.13-.018.261-.024.392-.019h4.762Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,7 @@
<svg width="456" height="456" viewBox="0 0 456 456" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="456" height="456" rx="50" fill="#512BD4"/>
<path d="M81.2738 291.333C78.0496 291.333 75.309 290.259 73.052 288.11C70.795 285.906 69.6665 283.289 69.6665 280.259C69.6665 277.173 70.795 274.529 73.052 272.325C75.309 270.121 78.0496 269.019 81.2738 269.019C84.5518 269.019 87.3193 270.121 89.5763 272.325C91.887 274.529 93.0424 277.173 93.0424 280.259C93.0424 283.289 91.887 285.906 89.5763 288.11C87.3193 290.259 84.5518 291.333 81.2738 291.333Z" fill="white"/>
<path d="M210.167 289.515H189.209L133.994 202.406C132.597 200.202 131.441 197.915 130.528 195.546H130.044C130.474 198.081 130.689 203.508 130.689 211.827V289.515H112.149V171H134.477L187.839 256.043C190.096 259.57 191.547 261.994 192.192 263.316H192.514C191.977 260.176 191.708 254.859 191.708 247.365V171H210.167V289.515Z" fill="white"/>
<path d="M300.449 289.515H235.561V171H297.87V187.695H254.746V221.249H294.485V237.861H254.746V272.903H300.449V289.515Z" fill="white"/>
<path d="M392.667 187.695H359.457V289.515H340.272V187.695H307.143V171H392.667V187.695Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<defs>
<linearGradient id="a" x1="9" y1="17" x2="9" y2="1" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#0078d4"/>
<stop offset="1" stop-color="#5ea0ef"/>
</linearGradient>
</defs>
<circle cx="9" cy="9" r="8" fill="url(#a)"/>
<ellipse cx="9" cy="9" rx="3.2" ry="8" fill="none" stroke="#fff" stroke-width=".7"/>
<line x1="1" y1="9" x2="17" y2="9" stroke="#fff" stroke-width=".7"/>
<line x1="2" y1="5.5" x2="16" y2="5.5" stroke="#fff" stroke-width=".5"/>
<line x1="2" y1="12.5" x2="16" y2="12.5" stroke="#fff" stroke-width=".5"/>
<circle cx="9" cy="9" r="8" fill="none" stroke="#fff" stroke-width=".7"/>
<path d="M13.5 10.5l1.5-1.5-1.5-1.5M4.5 10.5L3 9l1.5-1.5" stroke="#50e6ff" stroke-width="1" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 877 B

View File

@@ -0,0 +1,15 @@
<svg width="142" height="142" viewBox="0 0 142 142" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_0_812)" transform="matrix(1.002103,0,0,1.0377318,6.9399999e-7,-2.5317276e-4)">
<path d="m 141.702,68.418 c 0,7.4632 -4.567,14.1123 -6.748,20.8385 -2.263,6.9789 -2.552,15.0285 -6.776,20.8385 -4.267,5.868 -11.856,8.611 -17.719,12.881 -5.805,4.228 -10.7345,10.628 -17.7061,12.895 -6.7286,2.186 -14.4463,-0.021 -21.9018,-0.021 -7.4555,0 -15.1731,2.207 -21.8998,0.021 C 41.9778,133.604 37.048,127.204 31.2428,122.976 25.3799,118.706 17.7913,115.963 13.5247,110.095 9.30055,104.287 9.01135,96.2374 6.74791,89.2565 4.56351,82.5225 0,75.8735 0,68.418 0,60.9624 4.56737,54.3057 6.74791,47.5795 9.01135,40.6005 9.30055,32.5507 13.5247,26.741 17.7913,20.8753 25.3799,18.1297 31.2428,13.8617 37.048,9.63414 41.9778,3.23209 48.9513,0.966872 55.678,-1.21924 63.3956,0.986167 70.8511,0.986167 c 7.4555,0 15.1732,-2.205407 21.8999,-0.019295 6.9735,2.265218 11.903,8.667268 17.708,12.894828 5.863,4.268 13.452,7.0136 17.719,12.8793 4.224,5.8097 4.513,13.8595 6.776,20.8385 2.181,6.7262 6.748,13.3771 6.748,20.8385 z" fill="#326ce5"/>
<path d="m 70.8473,8.18683 c -33.2383,0 -60.1837,26.96657 -60.1837,60.23097 0,33.2642 26.9454,60.2312 60.1837,60.2312 33.2387,0 60.1837,-26.959 60.1837,-60.2312 0,-33.2721 -26.945,-60.23097 -60.1837,-60.23097 z M 70.8319,123.408 C 40.4778,123.408 15.9058,98.8053 15.9,68.4274 15.9,38.0167 40.5357,13.3791 70.9109,13.437 c 30.3751,0.0579 54.9111,24.6589 54.8841,55.0329 -0.027,30.374 -24.609,54.9481 -54.9631,54.9381 z" fill="#ffffff"/>
<path d="m 13.5883,60.53 c 8.1804,0 8.1804,3.859 16.3589,3.859 8.1784,0 8.1804,-3.859 16.3607,-3.859 8.1804,0 8.1785,3.859 16.3589,3.859 8.1804,0 8.1785,-3.859 16.3589,-3.859 8.1804,0 8.1803,3.859 16.3588,3.859 8.1785,0 8.1805,-3.859 16.3605,-3.859 8.181,0 8.181,3.859 16.361,3.859" stroke="#ffffff" strokeMiterlimit="10"/>
<path d="m 13.5883,68.248 c 8.1804,0 8.1804,3.859 16.3589,3.859 8.1784,0 8.1804,-3.859 16.3607,-3.859 8.1804,0 8.1785,3.859 16.3589,3.859 8.1804,0 8.1785,-3.859 16.3589,-3.859 8.1804,0 8.1803,3.859 16.3588,3.859 8.1785,0 8.1805,-3.859 16.3605,-3.859 8.181,0 8.181,3.859 16.361,3.859" stroke="#ffffff" strokeMiterlimit="10"/>
<path d="m 13.5883,77.5095 c 8.1804,0 8.1804,3.859 16.3589,3.859 8.1784,0 8.1804,-3.859 16.3607,-3.859 8.1804,0 8.1785,3.859 16.3589,3.859 8.1804,0 8.1785,-3.859 16.3589,-3.859 8.1804,0 8.1803,3.859 16.3588,3.859 8.1785,0 8.1805,-3.859 16.3605,-3.859 8.181,0 8.181,3.859 16.361,3.859" stroke="#ffffff" strokeMiterlimit="10"/>
<path d="m 70.8473,8.18683 c -33.2383,0 -60.1837,26.96657 -60.1837,60.23097 0,33.2642 26.9454,60.2312 60.1837,60.2312 33.2387,0 60.1837,-26.959 60.1837,-60.2312 0,-33.2721 -26.945,-60.23097 -60.1837,-60.23097 z M 70.8319,123.408 C 40.4778,123.408 15.9058,98.8053 15.9,68.4274 15.9,38.0167 40.5357,13.3791 70.9109,13.437 c 30.3751,0.0579 54.9111,24.6589 54.8841,55.0329 -0.027,30.374 -24.609,54.9481 -54.9631,54.9381 z" fill="#ffffff"/>
</g>
<defs>
<clipPath id="clip0_0_812">
<rect width="141.702" height="136.837" fill="#ffffff"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,55 @@
<svg viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
<g>
<g>
<g>
<rect x="122" y="-0.4" transform="matrix(-0.866 -0.5 0.5 -0.866 163.3196 363.3136)" fill="#E535AB" width="16.6" height="320.3"/>
</g>
</g>
<g>
<g>
<rect x="39.8" y="272.2" fill="#E535AB" width="320.3" height="16.6"/>
</g>
</g>
<g>
<g>
<rect x="37.9" y="312.2" transform="matrix(-0.866 -0.5 0.5 -0.866 83.0693 663.3409)" fill="#E535AB" width="185" height="16.6"/>
</g>
</g>
<g>
<g>
<rect x="177.1" y="71.1" transform="matrix(-0.866 -0.5 0.5 -0.866 463.3409 283.0693)" fill="#E535AB" width="185" height="16.6"/>
</g>
</g>
<g>
<g>
<rect x="122.1" y="-13" transform="matrix(-0.5 -0.866 0.866 -0.5 126.7903 232.1221)" fill="#E535AB" width="16.6" height="185"/>
</g>
</g>
<g>
<g>
<rect x="109.6" y="151.6" transform="matrix(-0.5 -0.866 0.866 -0.5 266.0828 473.3766)" fill="#E535AB" width="320.3" height="16.6"/>
</g>
</g>
<g>
<g>
<rect x="52.5" y="107.5" fill="#E535AB" width="16.6" height="185"/>
</g>
</g>
<g>
<g>
<rect x="330.9" y="107.5" fill="#E535AB" width="16.6" height="185"/>
</g>
</g>
<g>
<g>
<rect x="262.4" y="240.1" transform="matrix(-0.5 -0.866 0.866 -0.5 126.7953 714.2875)" fill="#E535AB" width="14.5" height="160.9"/>
</g>
</g>
<path fill="#E535AB" d="M369.5,297.9c-9.6,16.7-31,22.4-47.7,12.8c-16.7-9.6-22.4-31-12.8-47.7c9.6-16.7,31-22.4,47.7-12.8 C373.5,259.9,379.2,281.2,369.5,297.9"/>
<path fill="#E535AB" d="M90.9,137c-9.6,16.7-31,22.4-47.7,12.8c-16.7-9.6-22.4-31-12.8-47.7c9.6-16.7,31-22.4,47.7-12.8 C94.8,99,100.5,120.3,90.9,137"/>
<path fill="#E535AB" d="M30.5,297.9c-9.6-16.7-3.9-38,12.8-47.7c16.7-9.6,38-3.9,47.7,12.8c9.6,16.7,3.9,38-12.8,47.7 C61.4,320.3,40.1,314.6,30.5,297.9"/>
<path fill="#E535AB" d="M309.1,137c-9.6-16.7-3.9-38,12.8-47.7c16.7-9.6,38-3.9,47.7,12.8c9.6,16.7,3.9,38-12.8,47.7 C340.1,159.4,318.7,153.7,309.1,137"/>
<path fill="#E535AB" d="M200,395.8c-19.3,0-34.9-15.6-34.9-34.9c0-19.3,15.6-34.9,34.9-34.9c19.3,0,34.9,15.6,34.9,34.9 C234.9,380.1,219.3,395.8,200,395.8"/>
<path fill="#E535AB" d="M200,74c-19.3,0-34.9-15.6-34.9-34.9c0-19.3,15.6-34.9,34.9-34.9c19.3,0,34.9,15.6,34.9,34.9 C234.9,58.4,219.3,74,200,74"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 77.62745 102.5">
<path fill="#516baa" d="m31.05548,54.44523v24.1773c.00065.04512-.03164.084-.07611.09164l-23.27949,3.99047c-.05091.0076-.09834-.02751-.10594-.07841-.00256-.01712-.0003-.03461.00653-.05051L30.87996,30.58635c.02242-.04633.07815-.06572.12449-.04331.0316.01529.05193.04704.05259.08214l-.00156,23.82005Zm3.92367-13.93321v38.21148c.00046.04691.03573.08617.08232.09164l34.87031,3.89415c.0512.00527.09698-.03196.10226-.08316.00167-.01616-.00092-.03247-.00751-.04732L35.15623,4.70041c-.02237-.04636-.07809-.0658-.12444-.04343-.03117.01504-.05144.04612-.05264.08071v35.77433Zm34.68546,45.76213l-38.57341,11.57218c-.02155.00797-.04524.00797-.06679,0l-23.309-11.57217c-.04636-.0203-.06749-.07435-.04719-.12071.01513-.03455.04988-.0563.08757-.05481h61.88241c.0508.00825.08531.05613.07706.10693-.00482.0297-.02369.05525-.05066.06859Z"/>
</svg>

After

Width:  |  Height:  |  Size: 901 B

View File

@@ -0,0 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M.113 10.27A13.026 13.026 0 000 11.48h18.23c-.064-.125-.15-.237-.235-.347-3.117-4.027-4.793-3.677-7.19-3.78-.8-.034-1.34-.048-4.524-.048-1.704 0-3.555.005-5.358.01-.234.63-.459 1.24-.567 1.737h9.342v1.216H.113v.002zm18.26 2.426H.009c.02.326.05.645.094.961h16.955c.754 0 1.179-.429 1.315-.96zm-17.318 4.28s2.81 6.902 10.93 7.024c4.855 0 9.027-2.883 10.92-7.024H1.056zM11.988 0C7.5 0 3.593 2.466 1.531 6.108l4.75-.005v-.002c3.71 0 3.849.016 4.573.047l.448.016c1.563.052 3.485.22 4.996 1.364.82.621 2.007 1.99 2.712 2.965.654.902.842 1.94.396 2.934-.408.914-1.289 1.458-2.353 1.458H.391s.099.42.249.886h22.748A12.026 12.026 0 0024 12.005C24 5.377 18.621 0 11.988 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 776 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 590 270">
<path d="M30.36,109.14v.48h0A3.73,3.73,0,0,1,30.36,109.14Z" fill="currentColor" fill-rule="evenodd"/>
<path d="M138.66,28.78C107.2,37.87,57.29,43,30.4,43h0V94.35a.8.8,0,0,0,.19.48c18.35,0,75-6,109.18-15.4a129,129,0,0,0,17.49-5.81c4.18-1.88,6.88-3.86,6.88-5.92V15.91C164.1,20.79,151.39,25.11,138.66,28.78Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M138.66,95.37c-18.83,5.43-44.24,9.47-67.39,11.83-15.54,1.59-30.06,2.42-40.87,2.42h0v51.31a.8.8,0,0,0,.19.48c18.35,0,75-6,109.18-15.39a130.38,130.38,0,0,0,17.49-5.81c4.18-1.89,6.88-3.86,6.88-5.92V82.5C164.1,87.37,151.39,91.69,138.66,95.37Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M138.66,162c-18.83,5.43-44.24,9.46-67.39,11.83-15.56,1.59-30.1,2.42-40.91,2.42V228c18.16,0,75.1-5.95,109.37-15.39,12.63-3.48,24.37-7.44,24.37-11.74V149.08C164.1,154,151.39,158.28,138.66,162Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M30.55,94.83C32.4,97.38,48,102.19,71.27,107.2c23.27,4.46,47.47,22.07,66.29,16.64,12.73-3.68,26.54-36.47,26.54-41.34V82c0-3.4-2.55-6.13-6.88-8.4-17.75-9.07-21.11-12.41-27.69-10.6C95.37,72.43,35.06,67.61,30.55,94.83Z" fill="currentColor" fill-rule="evenodd"/>
<path d="M30.55,161.41C32.4,164,48,168.77,71.27,173.79c26,4.74,48.61,20.19,67.44,14.75,12.73-3.68,25.39-34.58,25.39-39.46v-.48c0-3.39-2.55-6.13-6.88-8.39-13.54-7.2-31.43-15.13-38-13.32C85,136.3,39.26,138.37,30.55,161.41Z" fill="currentColor" fill-rule="evenodd"/>
<path d="M200.7,142.39c6,11.79,15.6,17.6,29.05,17.6,14.44,0,19.59-7.64,19.59-15.11,0-5.15-1.83-8.63-6.64-11.79-4.82-3.32-8.3-4.81-16.93-8-10.63-4-16.77-7-23.41-12.29-6.64-5.48-9.79-13-9.79-22.74a28.28,28.28,0,0,1,10.29-22.58c7-5.81,15.44-8.63,25.56-8.63,15.77,0,27.72,6.31,35.69,18.76L249.34,87.78c-4.48-6.81-11.29-10.3-20.59-10.3-9.13,0-15.77,5.15-15.77,12.29,0,4.81,2,7.14,4.82,10,1.82,1.33,6.47,3.32,8.63,4.48l6,2.32,6.8,2.66c11,4.48,18.76,9.3,23.57,14.44s7.31,12.12,7.31,20.75c0,20.42-14.11,34.2-40.51,34.2-21.41,0-37.18-10-44.48-26.4Z" fill="currentColor"/>
<path d="M354.25,104.71,342,117.49a28.14,28.14,0,0,0-21.24-9.13,25,25,0,0,0-18.43,7.47,27.76,27.76,0,0,0,0,37.52,25,25,0,0,0,18.43,7.47A28.14,28.14,0,0,0,342,151.69l12.29,12.78c-9,9.63-20.09,14.44-33.53,14.44-12.79,0-23.58-4.15-32.37-12.62s-13.12-19.09-13.12-31.7,4.32-23.08,13.12-31.54,19.58-12.78,32.37-12.78C334.16,90.27,345.28,95.08,354.25,104.71Z" fill="currentColor"/>
<path d="M393.88,125.62C408,124.3,413,122.47,413,116c0-5.15-4.64-9.13-13.94-9.13q-13.44,0-22.41,10.95l-12.28-10.46c8.13-11.45,19.58-17.09,34.36-17.09,20.75,0,33.7,10,33.7,27.05v37c0,5.81,2.15,6.48,7,6.48h.5v15.43c-2,1.17-5.15,1.83-9.3,1.83-4.48,0-8-1.33-10.62-4a14.06,14.06,0,0,1-3-5.48c-5.81,6.8-15.27,10.29-28.39,10.29-18.42,0-30.87-10.13-30.87-25.4C357.7,136.41,369.15,127.78,393.88,125.62ZM391.56,162c13.28,0,21.41-6,21.41-16.6v-9.3a9.75,9.75,0,0,1-4.14,2.49c-3.82,1.33-6.31,1.66-14.28,2.49-11.62,1.33-17.43,5-17.43,10.79C377.12,158.33,382.43,162,391.56,162Z" fill="currentColor"/>
<path d="M444.84,60.88h19.92V149.2c0,8.13,2.66,11.62,10,11.62a21.15,21.15,0,0,0,6-.67v17.76a35.56,35.56,0,0,1-9.47,1c-17.59,0-26.39-9-26.39-27.06Z" fill="currentColor"/>
<path d="M521.71,125.62c14.11-1.32,19.09-3.15,19.09-9.62,0-5.15-4.64-9.13-13.94-9.13q-13.44,0-22.41,10.95l-12.28-10.46c8.13-11.45,19.58-17.09,34.36-17.09,20.75,0,33.7,10,33.7,27.05v37c0,5.81,2.15,6.48,7,6.48h.5v15.43c-2,1.17-5.15,1.83-9.3,1.83-4.48,0-8-1.33-10.62-4a13.94,13.94,0,0,1-3-5.48c-5.81,6.8-15.27,10.29-28.39,10.29-18.42,0-30.87-10.13-30.87-25.4C485.53,136.41,497,127.78,521.71,125.62ZM519.39,162c13.28,0,21.41-6,21.41-16.6v-9.3a9.73,9.73,0,0,1-4.15,2.49c-3.81,1.33-6.3,1.66-14.27,2.49-11.62,1.33-17.43,5-17.43,10.79C505,158.33,510.26,162,519.39,162Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,37 @@
<svg viewBox="0 0 254.5 225" xmlns="http://www.w3.org/2000/svg">
<g>
<g>
<g>
<g>
<path fill="#00ACD7" d="M40.2,101.1c-0.4,0-0.5-0.2-0.3-0.5l2.1-2.7c0.2-0.3,0.7-0.5,1.1-0.5l35.7,0c0.4,0,0.5,0.3,0.3,0.6 l-1.7,2.6c-0.2,0.3-0.7,0.6-1,0.6L40.2,101.1z"/>
</g>
</g>
</g>
<g>
<g>
<g>
<path fill="#00ACD7" d="M25.1,110.3c-0.4,0-0.5-0.2-0.3-0.5l2.1-2.7c0.2-0.3,0.7-0.5,1.1-0.5l45.6,0c0.4,0,0.6,0.3,0.5,0.6 l-0.8,2.4c-0.1,0.4-0.5,0.6-0.9,0.6L25.1,110.3z"/>
</g>
</g>
</g>
<g>
<g>
<g>
<path fill="#00ACD7" d="M49.3,119.5c-0.4,0-0.5-0.3-0.3-0.6l1.4-2.5c0.2-0.3,0.6-0.6,1-0.6l20,0c0.4,0,0.6,0.3,0.6,0.7l-0.2,2.4 c0,0.4-0.4,0.7-0.7,0.7L49.3,119.5z"/>
</g>
</g>
</g>
<g>
<g id="CXHf1q_3_">
<g>
<g>
<path fill="#00ACD7" d="M153.1,99.3c-6.3,1.6-10.6,2.8-16.8,4.4c-1.5,0.4-1.6,0.5-2.9-1c-1.5-1.7-2.6-2.8-4.7-3.8 c-6.3-3.1-12.4-2.2-18.1,1.5c-6.8,4.4-10.3,10.9-10.2,19c0.1,8,5.6,14.6,13.5,15.7c6.8,0.9,12.5-1.5,17-6.6 c0.9-1.1,1.7-2.3,2.7-3.7c-3.6,0-8.1,0-19.3,0c-2.1,0-2.6-1.3-1.9-3c1.3-3.1,3.7-8.3,5.1-10.9c0.3-0.6,1-1.6,2.5-1.6 c5.1,0,23.9,0,36.4,0c-0.2,2.7-0.2,5.4-0.6,8.1c-1.1,7.2-3.8,13.8-8.2,19.6c-7.2,9.5-16.6,15.4-28.5,17 c-9.8,1.3-18.9-0.6-26.9-6.6c-7.4-5.6-11.6-13-12.7-22.2c-1.3-10.9,1.9-20.7,8.5-29.3c7.1-9.3,16.5-15.2,28-17.3 c9.4-1.7,18.4-0.6,26.5,4.9c5.3,3.5,9.1,8.3,11.6,14.1C154.7,98.5,154.3,99,153.1,99.3z"/>
</g>
<g>
<path fill="#00ACD7" d="M186.2,154.6c-9.1-0.2-17.4-2.8-24.4-8.8c-5.9-5.1-9.6-11.6-10.8-19.3c-1.8-11.3,1.3-21.3,8.1-30.2 c7.3-9.6,16.1-14.6,28-16.7c10.2-1.8,19.8-0.8,28.5,5.1c7.9,5.4,12.8,12.7,14.1,22.3c1.7,13.5-2.2,24.5-11.5,33.9 c-6.6,6.7-14.7,10.9-24,12.8C191.5,154.2,188.8,154.3,186.2,154.6z M210,114.2c-0.1-1.3-0.1-2.3-0.3-3.3 c-1.8-9.9-10.9-15.5-20.4-13.3c-9.3,2.1-15.3,8-17.5,17.4c-1.8,7.8,2,15.7,9.2,18.9c5.5,2.4,11,2.1,16.3-0.6 C205.2,129.2,209.5,122.8,210,114.2z"/>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -137,7 +137,6 @@ function CreateServiceAccountModal(): JSX.Element {
<AuthZTooltip checks={[SACreatePermission]}>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form="create-sa-form"
variant="solid"
color="primary"

View File

@@ -1,7 +0,0 @@
.dropdown-button {
color: var(--l1-foreground);
}
.dropdown-icon {
font-size: 1.2rem;
}

View File

@@ -1,51 +0,0 @@
import { useState } from 'react';
import { Ellipsis } from '@signozhq/icons';
import { Button, Dropdown, MenuProps } from 'antd';
import './DropDown.styles.scss';
function DropDown({
element,
onDropDownItemClick,
}: {
element: JSX.Element[];
onDropDownItemClick?: MenuProps['onClick'];
}): JSX.Element {
const items: MenuProps['items'] = element.map(
(e: JSX.Element, index: number) => ({
label: e,
key: index,
}),
);
const [isDdOpen, setDdOpen] = useState<boolean>(false);
return (
<Dropdown
menu={{
items,
onMouseEnter: (): void => setDdOpen(true),
onMouseLeave: (): void => setDdOpen(false),
onClick: (item): void => onDropDownItemClick?.(item),
}}
open={isDdOpen}
>
<Button
type="link"
className={`dropdown-button`}
onClick={(e): void => {
e.preventDefault();
setDdOpen(true);
}}
>
<Ellipsis className="dropdown-icon" size={16} />
</Button>
</Dropdown>
);
}
DropDown.defaultProps = {
onDropDownItemClick: (): void => {},
};
export default DropDown;

View File

@@ -1,15 +1,7 @@
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import {
Button,
Col,
Dropdown,
MenuProps,
Popover,
Row,
Select,
Space,
} from 'antd';
import { Button, Col, Popover, Row, Select, Space } from 'antd';
import { DropdownMenuSimple, type MenuProps } from '@signozhq/ui/dropdown-menu';
import { Typography } from '@signozhq/ui/typography';
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';
@@ -241,9 +233,9 @@ function ExplorerCard({
</Popover>
<Share2 onClick={onCopyUrlHandler} size="md" />
{viewKey && (
<Dropdown trigger={['click']} menu={moreOptionMenu}>
<Ellipsis size="md" />
</Dropdown>
<DropdownMenuSimple menu={moreOptionMenu}>
<Button type="text" size="small" icon={<Ellipsis size="md" />} />
</DropdownMenuSimple>
)}
</Space>
</OffSetCol>

View File

@@ -11,9 +11,6 @@ import { GuardAuthZ } from './GuardAuthZ';
describe('GuardAuthZ', () => {
const TestChild = (): ReactElement => <div>Protected Content</div>;
const LoadingFallback = (): ReactElement => <div>Loading...</div>;
const ErrorFallback = (error: Error): ReactElement => (
<div>Error occurred: {error.message}</div>
);
const NoPermissionFallback = (_response: {
requiredPermissionName: BrandedPermission;
}): ReactElement => <div>Access denied</div>;
@@ -90,40 +87,28 @@ describe('GuardAuthZ', () => {
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render fallbackOnError when API error occurs', async () => {
const errorMessage = 'Internal Server Error';
it('should render children when API error occurs and no fallbackOnError provided (fail open)', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: errorMessage }));
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
render(
<GuardAuthZ relation="read" object="role:*" fallbackOnError={ErrorFallback}>
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText(/Error occurred:/)).toBeInTheDocument();
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should pass error object to fallbackOnError function', async () => {
const errorMessage = 'Network request failed';
let receivedError: Error | null = null;
const errorFallbackWithCapture = (error: Error): ReactElement => {
receivedError = error;
return <div>Captured error: {error.message}</div>;
};
it('should render fallbackOnError when API error occurs and fallbackOnError is provided', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: errorMessage }));
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
@@ -131,35 +116,14 @@ describe('GuardAuthZ', () => {
<GuardAuthZ
relation="read"
object="role:*"
fallbackOnError={errorFallbackWithCapture}
fallbackOnError={<div>Custom error fallback</div>}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(receivedError).not.toBeNull();
});
expect(receivedError).toBeInstanceOf(Error);
expect(screen.getByText(/Captured error:/)).toBeInTheDocument();
});
it('should render null when error occurs and no fallbackOnError provided', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
const { container } = render(
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(container.firstChild).toBeNull();
expect(screen.getByText('Custom error fallback')).toBeInTheDocument();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();

View File

@@ -12,7 +12,7 @@ export type GuardAuthZProps<R extends AuthZRelation> = {
relation: R;
object: AuthZObject<R>;
fallbackOnLoading?: JSX.Element;
fallbackOnError?: (error: Error) => JSX.Element;
fallbackOnError?: JSX.Element;
fallbackOnNoPermissions?: (response: {
requiredPermissionName: BrandedPermission;
}) => JSX.Element;
@@ -35,7 +35,7 @@ export function GuardAuthZ<R extends AuthZRelation>({
}
if (error) {
return fallbackOnError?.(error) ?? null;
return fallbackOnError ?? children;
}
if (!permissions?.[permission]?.isGranted) {

View File

@@ -4,7 +4,8 @@ import { useSelector } from 'react-redux';
import { matchPath, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import { Button, Switch } from 'antd';
import { Button } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
@@ -125,9 +126,8 @@ function ShareURLModal(): JSX.Element {
<Info size={14} color={Color.BG_AMBER_600} />
)}
<Switch
checked={enableAbsoluteTime}
value={enableAbsoluteTime}
disabled={!isValidateRelativeTime}
size="small"
onChange={(): void => {
setEnableAbsoluteTime((prev) => !prev);
}}

View File

@@ -0,0 +1,13 @@
.banner {
height: var(--spacing-20);
a {
color: var(--callout-warning-title);
text-decoration: underline;
&:hover {
color: var(--callout-warning-title);
opacity: 0.8;
}
}
}

View File

@@ -0,0 +1,26 @@
import { PersistedAnnouncementBanner } from '@signozhq/ui/announcement-banner';
import styles from './NoAuthBanner.module.scss';
export function NoAuthBanner(): JSX.Element {
return (
<PersistedAnnouncementBanner
type="warning"
storageKey="no-auth-banner-v1"
testId="no-auth-banner"
className={styles.banner}
>
Impersonation mode: authentication is disabled. Anyone with access to this
instance has admin privileges.{' '}
<a
href="https://signoz.io/docs/manage/administrator-guide/configuration/impersonation-mode/"
target="_blank"
rel="noreferrer"
>
Learn more
</a>
</PersistedAnnouncementBanner>
);
}
export default NoAuthBanner;

View File

@@ -0,0 +1,24 @@
import { render, screen } from 'tests/test-utils';
import { NoAuthBanner } from '../NoAuthBanner';
describe('NoAuthBanner', () => {
it('renders the no-auth message', () => {
render(<NoAuthBanner />);
expect(
screen.getByText(/Impersonation mode: authentication is disabled/i),
).toBeInTheDocument();
});
it('renders with the warning test id', () => {
render(<NoAuthBanner />);
expect(screen.getByTestId('no-auth-banner')).toBeInTheDocument();
});
it('renders a docs link that opens in a new tab', () => {
render(<NoAuthBanner />);
const link = screen.getByRole('link', { name: /learn more/i });
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noreferrer');
});
});

View File

@@ -6,7 +6,7 @@ import {
useMemo,
useState,
} from 'react';
import { Dropdown } from 'antd';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import cx from 'classnames';
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -195,7 +195,7 @@ export const QueryV2 = forwardRef(function QueryV2(
)}
{isMultiQueryAllowed && (
<Dropdown
<DropdownMenuSimple
className="query-actions-dropdown"
menu={{
items: [
@@ -217,10 +217,10 @@ export const QueryV2 = forwardRef(function QueryV2(
: []),
],
}}
placement="bottomRight"
align="end"
>
<Ellipsis size={16} />
</Dropdown>
</DropdownMenuSimple>
)}
</div>
</div>

View File

@@ -14,7 +14,8 @@ import {
ComboboxList,
ComboboxTrigger,
} from '@signozhq/ui/combobox';
import { Skeleton, Switch, Tooltip } from 'antd';
import { Skeleton, Tooltip } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
@@ -281,9 +282,8 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
<div className="api-quick-filters-header">
<Typography.Text>Show IP addresses</Typography.Text>
<Switch
size="small"
style={{ marginLeft: 'auto' }}
checked={showIP ?? true}
value={showIP ?? true}
onChange={(checked): void => {
logEvent('API Monitoring: Show IP addresses clicked', {
showIP: checked,

View File

@@ -4,13 +4,14 @@ import type {
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
import { Button, Flex } from 'antd';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { Switch } from '@signozhq/ui/switch';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { SlidersHorizontal } from '@signozhq/icons';
import { popupContainer } from 'utils/selectPopupContainer';
import ResizeTable from './ResizeTable';
import { DynamicColumnTableProps } from './types';
@@ -60,9 +61,7 @@ function DynamicColumnTable({
const onToggleHandler =
(index: number, column: ColumnGroupType<any> | ColumnType<any>) =>
(checked: boolean, event: React.MouseEvent<HTMLButtonElement>): void => {
event.stopPropagation();
(checked: boolean): void => {
if (shouldSendAlertsLogEvent) {
logEvent('Alert: Column toggled', {
column: column?.title,
@@ -85,19 +84,22 @@ function DynamicColumnTable({
);
};
const items: MenuProps['items'] =
const items: MenuItem[] =
dynamicColumns?.map((column, index) => ({
key: String(index),
label: (
<div className="dynamicColumnsTable-items">
<div
className="dynamicColumnsTable-items"
onClick={(e): void => e.stopPropagation()}
role="presentation"
>
<div>{column.title?.toString()}</div>
<Switch
checked={columnsData?.findIndex((c) => c.key === column.key) !== -1}
value={columnsData?.findIndex((c) => c.key === column.key) !== -1}
onChange={onToggleHandler(index, column)}
/>
</div>
),
key: index,
type: 'checkbox',
})) || [];
// Get current page from URL or default to 1
@@ -126,18 +128,14 @@ function DynamicColumnTable({
<Flex justify="flex-end" align="center" gap={8}>
{facingIssueBtn && <LaunchChatSupport {...facingIssueBtn} />}
{dynamicColumns && (
<Dropdown
getPopupContainer={popupContainer}
menu={{ items }}
trigger={['click']}
>
<DropdownMenuSimple menu={{ items }}>
<Button
className="dynamicColumnTable-button filter-btn"
size="middle"
icon={<SlidersHorizontal size={14} />}
data-testid="additional-filters-button"
/>
</Dropdown>
</DropdownMenuSimple>
)}
</Flex>

View File

@@ -127,7 +127,6 @@ function KeyFormPhase({
>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form={FORM_ID}
variant="solid"
color="primary"

View File

@@ -190,7 +190,6 @@ function EditKeyForm({
>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form={FORM_ID}
variant="solid"
color="primary"

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useMemo } from 'react';
import { KeyRound, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Skeleton, Table } from 'antd';
import { Skeleton, Table, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
@@ -110,28 +110,34 @@ function buildColumns({
onClick: (e): void => e.stopPropagation(),
style: { cursor: 'default' },
}),
render: (_, record): JSX.Element => (
<AuthZTooltip
checks={[
buildAPIKeyDeletePermission(record.id),
buildSADetachPermission(accountId),
]}
enabled={!isDisabled && !!accountId}
>
<Button
variant="ghost"
size="sm"
color="destructive"
disabled={isDisabled}
onClick={(): void => {
onRevokeClick(record.id);
}}
className="keys-tab__revoke-btn"
render: (_, record): JSX.Element => {
const tooltipTitle = isDisabled ? 'Service account disabled' : 'Revoke Key';
return (
<AuthZTooltip
checks={[
buildAPIKeyDeletePermission(record.id),
buildSADetachPermission(accountId),
]}
enabled={!isDisabled && !!accountId}
>
<X size={12} />
</Button>
</AuthZTooltip>
),
<Tooltip title={tooltipTitle}>
<Button
variant="ghost"
size="sm"
color="destructive"
disabled={isDisabled}
onClick={(e): void => {
e.stopPropagation();
onRevokeClick(record.id);
}}
className="keys-tab__revoke-btn"
>
<X size={12} />
</Button>
</Tooltip>
</AuthZTooltip>
);
},
},
];
}

View File

@@ -1,6 +1,7 @@
import { Dispatch, SetStateAction, useCallback, useMemo } from 'react';
import { ChevronDown, Globe } from '@signozhq/icons';
import { Button, Dropdown } from 'antd';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import { Button } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import TimeItems, {
timePreferance,
@@ -27,20 +28,17 @@ function TimePreference({
const menu = useMemo(
() => ({
items: menuItems,
onClick: timeMenuItemOnChangeHandler,
items: menuItems.map((item) => ({
...item,
onClick: timeMenuItemOnChangeHandler,
})),
}),
[timeMenuItemOnChangeHandler],
);
return (
<Dropdown
menu={menu}
rootClassName="time-selection-menu"
className="time-selection-target"
trigger={['click']}
>
<Button>
<DropdownMenuSimple menu={menu} className="time-selection-menu">
<Button className="time-selection-target">
<div className="button-selected-text">
<Globe size={14} />
<Typography.Text className="selected-value">
@@ -49,7 +47,7 @@ function TimePreference({
</div>
<ChevronDown size="md" />
</Button>
</Dropdown>
</DropdownMenuSimple>
);
}

View File

@@ -204,7 +204,7 @@ describe('createGuardedRoute', () => {
).not.toBeInTheDocument();
});
it('should render error fallback when API error occurs', async () => {
it('should render the component when API error occurs (fail open)', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
@@ -230,12 +230,8 @@ describe('createGuardedRoute', () => {
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();
});
it('should render no permissions fallback when permission is denied', async () => {

View File

@@ -9,14 +9,11 @@ import { parsePermission } from 'hooks/useAuthZ/utils';
import noDataUrl from '@/assets/Icons/no-data.svg';
import ErrorBoundaryFallback from '../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import AppLoading from '../AppLoading/AppLoading';
import { GuardAuthZ } from '../GuardAuthZ/GuardAuthZ';
import './createGuardedRoute.styles.scss';
const onErrorFallback = (): JSX.Element => <ErrorBoundaryFallback />;
function OnNoPermissionsFallback(response: {
requiredPermissionName: BrandedPermission;
}): ReactElement {
@@ -63,7 +60,6 @@ export function createGuardedRoute<P extends object, R extends AuthZRelation>(
relation={relation}
object={resolvedObject}
fallbackOnLoading={<AppLoading />}
fallbackOnError={onErrorFallback}
fallbackOnNoPermissions={(response): ReactElement => (
<OnNoPermissionsFallback {...response} />
)}

View File

@@ -1,7 +1,8 @@
import { useMemo, useState } from 'react';
import { QueryFunctionContext, useQueries, useQuery } from 'react-query';
import { Spin, Switch, Table, Tooltip } from 'antd';
import { Spin, Table, Tooltip } from 'antd';
import { Info, Loader } from '@signozhq/icons';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import { getQueryRangeV5 } from 'api/v5/queryRange/getQueryRange';
import { MetricRangePayloadV5, ScalarData } from 'api/v5/v5';
@@ -170,11 +171,7 @@ function TopErrors({
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Switch
checked={showStatusCodeErrors}
onChange={setShowStatusCodeErrors}
size="small"
/>
<Switch value={showStatusCodeErrors} onChange={setShowStatusCodeErrors} />
<span style={{ color: 'white', fontSize: '14px' }}>
Status Message Exists
</span>

View File

@@ -1,4 +1,5 @@
import { Button, Flex, SelectProps, Switch } from 'antd';
import { Button, Flex, SelectProps } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import type { BaseOptionType, DefaultOptionType } from 'antd/es/select';
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
@@ -419,8 +420,8 @@ export function RoutingPolicyBanner({
</Typography.Text>
<div className="routing-policies-info-banner-right">
<Switch
checked={notificationSettings.routingPolicies}
data-testid="routing-policies-switch"
value={notificationSettings.routingPolicies}
testId="routing-policies-switch"
onChange={(value): void => {
setNotificationSettings({
type: 'SET_ROUTING_POLICIES',

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { Switch, Tooltip } from 'antd';
import { Tooltip } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import { Info } from '@signozhq/icons';
@@ -49,7 +50,7 @@ function AdvancedOptionItem({
>
{input}
</div>
<Switch onChange={handleOnToggle} checked={showInput} />
<Switch onChange={handleOnToggle} value={showInput} />
</div>
</div>
);

View File

@@ -11,8 +11,13 @@ import {
} from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Callout } from '@signozhq/ui/callout';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@signozhq/ui/dropdown-menu';
import { toast } from '@signozhq/ui/sonner';
import { Dropdown, Skeleton } from 'antd';
import { Skeleton } from 'antd';
import {
RenderErrorResponseDTO,
ZeustypesHostDTO,
@@ -200,10 +205,15 @@ export default function CustomDomainSettings(): JSX.Element {
!workspaceName ? 'workspace-name-hidden' : ''
}`}
>
<Dropdown
trigger={['click']}
disabled={isFetchingHosts}
dropdownRender={(): JSX.Element => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="link" color="none" disabled={isFetchingHosts}>
<Link2 size={12} />
<span>{stripProtocol(activeHost?.url ?? '')}</span>
<ChevronDown size={12} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<div className="workspace-url-dropdown">
<span className="workspace-url-dropdown-header">
All Workspace URLs
@@ -236,14 +246,8 @@ export default function CustomDomainSettings(): JSX.Element {
);
})}
</div>
)}
>
<Button variant="link" color="none">
<Link2 size={12} />
<span>{stripProtocol(activeHost?.url ?? '')}</span>
<ChevronDown size={12} />
</Button>
</Dropdown>
</DropdownMenuContent>
</DropdownMenu>
<span className="custom-domain-card-meta-timezone">
<Clock size={11} />
{timezone.offset}

View File

@@ -1,4 +1,5 @@
import { GetHosts200 } from 'api/generated/services/sigNoz.schemas';
import userEvent from '@testing-library/user-event';
import { rest, server } from 'mocks-server/server';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
@@ -142,12 +143,13 @@ describe('CustomDomainSettings', () => {
});
it('shows all workspace URLs as links in the dropdown', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
// Open the URL dropdown
fireEvent.click(
await user.click(
screen.getByRole('button', { name: /custom-host\.test\.cloud/i }),
);

View File

@@ -5,7 +5,8 @@ import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { orange } from '@ant-design/colors';
import { Color } from '@signozhq/design-tokens';
import { Button, Collapse, Input, Select, Switch, Tag } from 'antd';
import { Button, Collapse, Input, Select, Tag } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import cx from 'classnames';
@@ -763,7 +764,7 @@ function VariableItem({
</Typography>
</LabelContainer>
<Switch
checked={variableMultiSelect}
value={variableMultiSelect}
onChange={(e): void => {
setVariableMultiSelect(e);
if (!e) {
@@ -780,7 +781,7 @@ function VariableItem({
</Typography>
</LabelContainer>
<Switch
checked={variableShowALLOption}
value={variableShowALLOption}
onChange={(e): void => setVariableShowALLOption(e)}
/>
</VariableItemRow>

View File

@@ -0,0 +1,147 @@
import { renderHook } from '@testing-library/react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { usePanelContextMenu } from '../usePanelContextMenu';
// The hook composes `useCoordinates` (popover state) and `useGraphContextMenu`
// (menu items). We mock both so the test focuses on the `enableDrillDown` gate
// rather than the implementation of the menu wiring itself.
const onClickMock = jest.fn();
jest.mock('periscope/components/ContextMenu', () => ({
useCoordinates: (): unknown => ({
coordinates: null,
popoverPosition: null,
clickedData: null,
onClose: jest.fn(),
subMenu: null,
onClick: onClickMock,
setSubMenu: jest.fn(),
}),
}));
jest.mock('container/QueryTable/Drilldown/useGraphContextMenu', () => ({
__esModule: true,
default: (): { menuItemsConfig: { header: string; items: string } } => ({
menuItemsConfig: { header: 'menu-header', items: 'menu-items' },
}),
}));
jest.mock('container/QueryTable/Drilldown/drilldownUtils', () => ({
getUplotClickData: jest.fn(() => ({
coord: { x: 1, y: 2 },
record: { queryName: 'A', filters: [] },
label: 'lbl',
seriesColor: '#abc',
})),
}));
jest.mock('container/PanelWrapper/utils', () => ({
isApmMetric: jest.fn(() => false),
getTimeRangeFromStepInterval: jest.fn(() => ({ start: 0, end: 0 })),
}));
const mockWidget = { id: 'w-1', query: {} } as unknown as Widgets;
const mockQueryResponse = {
data: undefined,
isLoading: false,
} as unknown as UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
describe('usePanelContextMenu', () => {
beforeEach(() => {
onClickMock.mockClear();
});
it('returns empty menuItemsConfig when enableDrillDown is false', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: false,
}),
);
expect(result.current.menuItemsConfig).toStrictEqual({});
});
it('returns wired menuItemsConfig when enableDrillDown is true', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: true,
}),
);
expect(result.current.menuItemsConfig).toStrictEqual({
header: 'menu-header',
items: 'menu-items',
});
});
it('clickHandlerWithContextMenu is a no-op when enableDrillDown is false', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: false,
}),
);
result.current.clickHandlerWithContextMenu(
100, // xValue
200, // yValue
0, // mouseX
0, // mouseY
{ serviceName: 'svc' }, // metric
{ queryName: 'A', inFocusOrNot: true }, // queryData
10, // absoluteMouseX
20, // absoluteMouseY
{}, // axesData
{ seriesIndex: 0, seriesName: 'A', value: 1, color: '#abc' }, // focusedSeries
);
expect(onClickMock).not.toHaveBeenCalled();
});
it('clickHandlerWithContextMenu opens popover when enableDrillDown is true', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: true,
}),
);
result.current.clickHandlerWithContextMenu(
100,
200,
0,
0,
{ serviceName: 'svc' },
{ queryName: 'A', inFocusOrNot: true },
10,
20,
{},
{ seriesIndex: 0, seriesName: 'A', value: 1, color: '#abc' },
);
expect(onClickMock).toHaveBeenCalledTimes(1);
});
it('defaults to disabled when enableDrillDown is not provided', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
}),
);
expect(result.current.menuItemsConfig).toStrictEqual({});
});
});

View File

@@ -21,11 +21,13 @@ interface UseTimeSeriesContextMenuParams {
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
enableDrillDown?: boolean;
}
export const usePanelContextMenu = ({
widget,
queryResponse,
enableDrillDown = false,
}: UseTimeSeriesContextMenuParams): {
coordinates: { x: number; y: number } | null;
popoverPosition: PopoverPosition | null;
@@ -61,6 +63,9 @@ export const usePanelContextMenu = ({
const clickHandlerWithContextMenu = useCallback(
(...args: any[]) => {
if (!enableDrillDown) {
return;
}
const [
xValue,
_yvalue,
@@ -112,14 +117,14 @@ export const usePanelContextMenu = ({
});
}
},
[onClick, queryResponse],
[enableDrillDown, onClick, queryResponse],
);
return {
coordinates,
popoverPosition,
onClose,
menuItemsConfig,
menuItemsConfig: enableDrillDown ? menuItemsConfig : {},
clickHandlerWithContextMenu,
};
};

View File

@@ -31,6 +31,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
isFullViewMode,
onToggleModelHandler,
groupByPerQuery,
enableDrillDown = false,
} = props;
const uPlotRef = useRef<uPlot | null>(null);
const graphRef = useRef<HTMLDivElement>(null);
@@ -61,6 +62,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
} = usePanelContextMenu({
widget,
queryResponse,
enableDrillDown,
});
const config = useMemo(() => {

View File

@@ -31,6 +31,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
isFullViewMode,
onToggleModelHandler,
groupByPerQuery,
enableDrillDown = false,
} = props;
const graphRef = useRef<HTMLDivElement>(null);
const [minTimeScale, setMinTimeScale] = useState<number>();
@@ -60,6 +61,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
} = usePanelContextMenu({
widget,
queryResponse,
enableDrillDown,
});
const chartData = useMemo(() => {

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import { CloudDownload } from '@signozhq/icons';
import { Button, Dropdown, MenuProps, Flex } from 'antd';
import { DropdownMenuSimple, type MenuProps } from '@signozhq/ui/dropdown-menu';
import { Button, Flex } from 'antd';
import { unparse } from 'papaparse';
import { DownloadProps } from './Download.types';
@@ -67,7 +68,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
};
return (
<Dropdown menu={menu} trigger={['click']}>
<DropdownMenuSimple menu={menu}>
<Button
className="download-button"
loading={isLoading || isDownloading}
@@ -79,7 +80,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
Download
</Flex>
</Button>
</Dropdown>
</DropdownMenuSimple>
);
}

View File

@@ -1,6 +1,7 @@
import { Dispatch, ReactElement, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { Form, FormInstance, Input, Select, Switch } from 'antd';
import { Form, FormInstance, Input, Select } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import type { Store } from 'antd/lib/form/interface';
import ROUTES from 'constants/routes';
@@ -82,8 +83,8 @@ function FormAlertChannels({
name="send_resolved"
>
<Switch
defaultChecked={initialValue?.send_resolved}
data-testid="field-send-resolved-checkbox"
defaultValue={initialValue?.send_resolved}
testId="field-send-resolved-checkbox"
onChange={(value): void => {
setSelectedConfig((state) => ({
...state,

View File

@@ -2,7 +2,8 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { Plus } from '@signozhq/icons';
import { Button, Flex, Form, Select, Switch, Tooltip } from 'antd';
import { Button, Flex, Form, Select, Tooltip } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import getAll from 'api/channels/getAll';
import logEvent from 'api/common/logEvent';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
@@ -198,10 +199,10 @@ function BasicInfo({
placement="right"
>
<Switch
checked={shouldBroadCastToAllChannels}
value={shouldBroadCastToAllChannels}
onChange={handleBroadcastToAllChannels}
disabled={noChannels || !!isLoading}
data-testid="alert-broadcast-to-all-channels"
testId="alert-broadcast-to-all-channels"
/>
</Tooltip>
</FormItemMedium>

View File

@@ -1,8 +1,4 @@
import {
Col,
Dropdown as DropDownComponent,
Input as InputComponent,
} from 'antd';
import { Col, Input as InputComponent } from 'antd';
import { Typography as TypographyComponent } from '@signozhq/ui/typography';
import styled from 'styled-components';
@@ -34,16 +30,6 @@ export const ButtonContainer = styled.div`
}
`;
export const Dropdown = styled(DropDownComponent)`
&&& {
display: flex;
justify-content: center;
align-items: center;
max-width: 150px;
min-width: 150px;
}
`;
export const TextContainer = styled.div`
&&& {
min-width: 100px;

View File

@@ -292,6 +292,8 @@ function FullView({
return <Spinner height="100%" size="large" tip="Loading..." />;
}
const showEditBtn = editWidget && dashboardEditView;
return (
<div className="full-view-container">
<OverlayScrollbar>
@@ -306,7 +308,7 @@ function FullView({
Reset Query
</Button>
)}
{editWidget && (
{showEditBtn && (
<Button
className="switch-edit-btn"
disabled={response.isFetching || response.isLoading}

View File

@@ -1,6 +1,7 @@
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { fireEvent, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { AppProvider } from 'providers/App/App';
@@ -176,6 +177,7 @@ jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
describe('WidgetGraphComponent', () => {
it('should show correct menu items when hovering over more options while loading', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const { getByTestId, findByRole, getByText, container } = render(
<MockQueryClientProvider>
<ErrorModalProvider>
@@ -208,7 +210,7 @@ describe('WidgetGraphComponent', () => {
expect(skeleton).toBeInTheDocument();
const moreOptionsButton = getByTestId('widget-header-options');
fireEvent.mouseEnter(moreOptionsButton);
await user.click(moreOptionsButton);
const menu = await findByRole('menu');
expect(menu).toBeInTheDocument();

View File

@@ -54,6 +54,17 @@
visibility: visible;
}
// currently the width of the dropdown menu is set to 100% of the parent container,
// which is not desired. This is a workaround to unset that width and allow the dropdown menu to size based on its content.
// This is necessary because the dropdown menu can contain items with varying widths, and setting it to 100% can cause layout issues and make the menu look unbalanced.
// we should idealy fix this in the dropdown menu component itself, but for now this is a quick fix to ensure the dropdown menu looks correct in the widget header.
[data-radix-popper-content-wrapper]
[data-slot='dropdown-menu-content'].widget-header-dropdown
[data-slot='dropdown-menu-item'] {
width: unset !important;
}
.widget-api-actions {
padding-right: 0.25rem;
}

View File

@@ -467,6 +467,7 @@ describe('WidgetHeader', () => {
describe('Create Alerts Menu Item', () => {
it('renders Create Alerts menu item with external link icon when included in headerMenuList', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<WidgetHeader
title={TEST_WIDGET_TITLE}
@@ -483,7 +484,7 @@ describe('WidgetHeader', () => {
const moreOptionsIcon = await screen.findByTestId(WIDGET_HEADER_OPTIONS_ID);
expect(moreOptionsIcon).toBeInTheDocument();
await userEvent.hover(moreOptionsIcon);
await user.click(moreOptionsIcon);
await screen.findByText(CREATE_ALERTS_TEXT);
@@ -494,6 +495,7 @@ describe('WidgetHeader', () => {
});
it('Create Alerts menu item is enabled and clickable', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockCreateAlertsHandler = jest.fn();
const useCreateAlerts = jest.requireMock(
'hooks/queryBuilder/useCreateAlerts',
@@ -517,12 +519,12 @@ describe('WidgetHeader', () => {
expect(useCreateAlerts).toHaveBeenCalledWith(mockWidget, 'dashboardView');
const moreOptionsIcon = await screen.findByTestId(WIDGET_HEADER_OPTIONS_ID);
await userEvent.hover(moreOptionsIcon);
await user.click(moreOptionsIcon);
const createAlertsMenuItem = await screen.findByText(CREATE_ALERTS_TEXT);
// Verify the menu item is clickable by actually clicking it
await userEvent.click(createAlertsMenuItem);
await user.click(createAlertsMenuItem);
expect(mockCreateAlertsHandler).toHaveBeenCalledTimes(1);
});
});

View File

@@ -15,7 +15,8 @@ import {
X,
} from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Dropdown, Input, MenuProps, Tooltip } from 'antd';
import { Button, Input, Tooltip } from 'antd';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import { Typography } from '@signozhq/ui/typography';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import ErrorPopover from 'components/ErrorPopover/ErrorPopover';
@@ -128,7 +129,7 @@ function WidgetHeader({
],
);
const onMenuItemSelectHandler: MenuProps['onClick'] = useCallback(
const onMenuItemSelectHandler = useCallback(
({ key }: { key: string }): void => {
if (isTWidgetOptions(key)) {
const functionToCall = keyMethodMapping[key];
@@ -188,18 +189,8 @@ function WidgetHeader({
{
key: MenuItemKeys.CreateAlerts,
icon: <Bell size="md" />,
label: (
<span
style={{
display: 'flex',
alignItems: 'baseline',
justifyContent: 'space-between',
}}
>
{MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts]}
<SquareArrowOutUpRight size={10} />
</span>
),
label: MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts],
rightIcon: <SquareArrowOutUpRight size="lg" />,
isVisible: headerMenuList?.includes(MenuItemKeys.CreateAlerts) || false,
disabled: false,
},
@@ -221,8 +212,10 @@ function WidgetHeader({
const menu = useMemo(
() => ({
items: updatedMenuList,
onClick: onMenuItemSelectHandler,
items: updatedMenuList.map((item) => ({
...item,
onClick: onMenuItemSelectHandler,
})),
}),
[updatedMenuList, onMenuItemSelectHandler],
);
@@ -321,7 +314,12 @@ function WidgetHeader({
/>
)}
{menu && Array.isArray(menu.items) && menu.items.length > 0 && (
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
<DropdownMenuSimple
menu={menu}
side="bottom"
align="end"
className="widget-header-dropdown"
>
<Button
data-testid="widget-header-options"
className={`widget-header-more-options ${
@@ -329,7 +327,7 @@ function WidgetHeader({
}`}
icon={<EllipsisVertical size="md" />}
/>
</Dropdown>
</DropdownMenuSimple>
)}
</div>
</>

View File

@@ -6,6 +6,7 @@ export interface MenuItem {
key: MenuItemKeys;
icon: ReactNode;
label: ReactNode;
rightIcon?: ReactNode;
isVisible: boolean;
disabled: boolean;
danger?: boolean;

View File

@@ -1,9 +1,9 @@
import type { MenuItemType } from 'antd/es/menu/hooks/useItems';
import type { MenuItem as DropdownMenuItem } from '@signozhq/ui/dropdown-menu';
import { MenuItemKeys } from './contants';
import { MenuItem } from './types';
export const generateMenuList = (actions: MenuItem[]): MenuItemType[] =>
export const generateMenuList = (actions: MenuItem[]): DropdownMenuItem[] =>
actions
.filter((action: MenuItem) => action.isVisible)
.map(({ key, icon: Icon, label, disabled, ...rest }) => ({

View File

@@ -18,6 +18,8 @@ import listUserPreferences from 'api/v1/user/preferences/list';
import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update';
import Header from 'components/Header/Header';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import NoAuthBanner from 'components/NoAuthBanner/NoAuthBanner';
import { getIsNoAuthMode } from 'utils/noAuthMode';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
@@ -196,7 +198,7 @@ export default function Home(): JSX.Element {
const { mutate: updateUserPreference } = useMutation(updateUserPreferenceAPI, {
onSuccess: () => {
setUpdatingUserPreferences(false);
refetchUserPreferences();
void refetchUserPreferences();
},
onError: () => {
setUpdatingUserPreferences(false);
@@ -204,7 +206,7 @@ export default function Home(): JSX.Element {
});
const handleWillDoThisLater = (): void => {
logEvent('Welcome Checklist: Will do this later clicked', {});
void logEvent('Welcome Checklist: Will do this later clicked', {});
setUpdatingUserPreferences(true);
updateUserPreference({
@@ -271,11 +273,12 @@ export default function Home(): JSX.Element {
}, [metricsOnboardingData, handleUpdateChecklistDoneItem]);
useEffect(() => {
logEvent('Homepage: Visited', {});
void logEvent('Homepage: Visited', {});
}, []);
return (
<div className="home-container">
{getIsNoAuthMode() && <NoAuthBanner />}
<div className="sticky-header">
<Header
leftComponent={
@@ -298,9 +301,9 @@ export default function Home(): JSX.Element {
autoAdjustOverflow
onOpenChange={(visible): void => {
if (visible) {
logEvent('Welcome Checklist: Expanded', {});
void logEvent('Welcome Checklist: Expanded', {});
} else {
logEvent('Welcome Checklist: Minimized', {});
void logEvent('Welcome Checklist: Minimized', {});
}
}}
content={renderWelcomeChecklistModal()}
@@ -353,7 +356,7 @@ export default function Home(): JSX.Element {
className="active-ingestion-card-actions"
onClick={(e: React.MouseEvent): void => {
// eslint-disable-next-line sonarjs/no-duplicate-string
logEvent('Homepage: Ingestion Active Explore clicked', {
void logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Logs',
});
safeNavigate(ROUTES.LOGS_EXPLORER, {
@@ -362,7 +365,7 @@ export default function Home(): JSX.Element {
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
logEvent('Homepage: Ingestion Active Explore clicked', {
void logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Logs',
});
history.push(ROUTES.LOGS_EXPLORER);
@@ -396,7 +399,7 @@ export default function Home(): JSX.Element {
role="button"
tabIndex={0}
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Ingestion Active Explore clicked', {
void logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Traces',
});
safeNavigate(ROUTES.TRACES_EXPLORER, {
@@ -405,7 +408,7 @@ export default function Home(): JSX.Element {
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
logEvent('Homepage: Ingestion Active Explore clicked', {
void logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Traces',
});
history.push(ROUTES.TRACES_EXPLORER);
@@ -439,7 +442,7 @@ export default function Home(): JSX.Element {
role="button"
tabIndex={0}
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Ingestion Active Explore clicked', {
void logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Metrics',
});
safeNavigate(ROUTES.METRICS_EXPLORER, {
@@ -448,7 +451,7 @@ export default function Home(): JSX.Element {
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
logEvent('Homepage: Ingestion Active Explore clicked', {
void logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Metrics',
});
history.push(ROUTES.METRICS_EXPLORER);
@@ -496,7 +499,7 @@ export default function Home(): JSX.Element {
className="periscope-btn secondary"
prefix={<Wrench size={14} />}
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
void logEvent('Homepage: Explore clicked', {
source: 'Logs',
});
safeNavigate(ROUTES.LOGS_EXPLORER, {
@@ -513,7 +516,7 @@ export default function Home(): JSX.Element {
className="periscope-btn secondary"
prefix={<Wrench size={14} />}
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
void logEvent('Homepage: Explore clicked', {
source: 'Traces',
});
safeNavigate(ROUTES.TRACES_EXPLORER, {
@@ -530,7 +533,7 @@ export default function Home(): JSX.Element {
className="periscope-btn secondary"
prefix={<Wrench size={14} />}
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
void logEvent('Homepage: Explore clicked', {
source: 'Metrics',
});
safeNavigate(ROUTES.METRICS_EXPLORER_EXPLORER, {
@@ -569,7 +572,7 @@ export default function Home(): JSX.Element {
className="periscope-btn secondary"
prefix={<Plus size={14} />}
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
void logEvent('Homepage: Explore clicked', {
source: 'Dashboards',
});
safeNavigate(ROUTES.ALL_DASHBOARD, {
@@ -614,7 +617,7 @@ export default function Home(): JSX.Element {
className="periscope-btn secondary"
prefix={<Plus size={14} />}
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
void logEvent('Homepage: Explore clicked', {
source: 'Alerts',
});
safeNavigate(ROUTES.ALERTS_NEW, {

View File

@@ -15,13 +15,13 @@ import {
Modal,
Row,
Select,
Switch,
Table,
TablePaginationConfig,
TableProps as AntDTableProps,
Tag,
Tooltip,
} from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import type { NotificationInstance } from 'antd/es/notification/interface';
import type { CollapseProps } from 'antd/lib';
@@ -1180,8 +1180,7 @@ function MultiIngestionSettings(): JSX.Element {
<div className="limit-enable-disable-toggle">
<Form.Item name="enableDailyLimit">
<Switch
size="small"
checked={activeSignal?.config?.day?.enabled}
value={activeSignal?.config?.day?.enabled}
onChange={(value): void => {
setActiveSignal((prev) =>
prev
@@ -1270,8 +1269,7 @@ function MultiIngestionSettings(): JSX.Element {
<div className="limit-enable-disable-toggle">
<Form.Item name="enableSecondLimit">
<Switch
size="small"
checked={activeSignal?.config?.second?.enabled}
value={activeSignal?.config?.second?.enabled}
onChange={(value): void => {
setActiveSignal((prev) =>
prev

View File

@@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { Button, Flex, Input } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Plus } from '@signozhq/icons';
import { Ellipsis, Plus } from '@signozhq/icons';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import type { ColumnsType } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
@@ -15,7 +16,6 @@ import type {
} from 'api/generated/services/sigNoz.schemas';
import type { ErrorType } from 'api/generatedAPIInstance';
import { AxiosError } from 'axios';
import DropDown from 'components/DropDown/DropDown';
import {
DynamicColumnsKey,
TableDataSource,
@@ -323,55 +323,67 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
dataIndex: 'id',
key: 'action',
width: 10,
render: (id: RuletypesRuleDTO['id'], record): JSX.Element => (
<div data-testid="alert-actions">
<DropDown
onDropDownItemClick={(item): void =>
alertActionLogEvent(item.key, record)
render: (id: RuletypesRuleDTO['id'], record): JSX.Element => {
const actionItems = [
<ToggleAlertState
key="1"
disabled={record.disabled ?? false}
setData={setData}
id={id ?? ''}
/>,
<ColumnButton
key="2"
onClick={(e: React.MouseEvent): void =>
onEditHandler(record, { newTab: isModifierKeyPressed(e) })
}
element={[
<ToggleAlertState
key="1"
disabled={record.disabled ?? false}
setData={setData}
id={id ?? ''}
/>,
<ColumnButton
key="2"
onClick={(e: React.MouseEvent): void =>
onEditHandler(record, { newTab: isModifierKeyPressed(e) })
}
type="link"
loading={editLoader}
>
Edit
</ColumnButton>,
<ColumnButton
key="3-new-tab"
onClick={(): void => onEditHandler(record, { newTab: true })}
type="link"
loading={editLoader}
>
Edit in New Tab
</ColumnButton>,
<ColumnButton
key="3-clone"
onClick={onCloneHandler(record)}
type="link"
loading={cloneLoader}
>
Clone
</ColumnButton>,
<DeleteAlert
key="4"
notifications={notificationsApi}
setData={setData}
id={id ?? ''}
/>,
];
return (
<div data-testid="alert-actions">
<DropdownMenuSimple
menu={{
items: actionItems.map((element, index) => ({
key: String(index),
label: element,
onClick: ({ key }): void => alertActionLogEvent(key, record),
})),
}}
>
<Button
type="link"
loading={editLoader}
>
Edit
</ColumnButton>,
<ColumnButton
key="3"
onClick={(): void => onEditHandler(record, { newTab: true })}
type="link"
loading={editLoader}
>
Edit in New Tab
</ColumnButton>,
<ColumnButton
key="3"
onClick={onCloneHandler(record)}
type="link"
loading={cloneLoader}
>
Clone
</ColumnButton>,
<DeleteAlert
key="4"
notifications={notificationsApi}
setData={setData}
id={id ?? ''}
/>,
]}
/>
</div>
),
style={{ color: 'var(--l1-foreground)' }}
icon={<Ellipsis size={16} />}
/>
</DropdownMenuSimple>
</div>
);
},
});
}

View File

@@ -12,20 +12,19 @@ import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import {
Button,
Dropdown,
Flex,
Input,
MenuProps,
Modal,
Popover,
Skeleton,
Switch,
Table,
Tag,
Tooltip,
} from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import type { TableProps } from 'antd/lib';
import getLocalStorageKey from 'api/browser/localstorage/get';
@@ -553,7 +552,7 @@ function DashboardsList(): JSX.Element {
];
const getCreateDashboardItems = useMemo(() => {
const menuItems: MenuProps['items'] = [
const menuItems: MenuItem[] = [
{
label: (
<div
@@ -711,11 +710,11 @@ function DashboardsList(): JSX.Element {
{createNewDashboard && (
<section className="actions">
<Dropdown
overlayClassName="new-dashboard-menu"
<DropdownMenuSimple
className="new-dashboard-menu"
menu={{ items: getCreateDashboardItems }}
placement="bottomRight"
trigger={['click']}
side="bottom"
align="end"
>
<Button
type="text"
@@ -727,7 +726,7 @@ function DashboardsList(): JSX.Element {
>
New Dashboard
</Button>
</Dropdown>
</DropdownMenuSimple>
<Button
type="text"
className="learn-more"
@@ -756,11 +755,11 @@ function DashboardsList(): JSX.Element {
onChange={handleSearch}
/>
{createNewDashboard && (
<Dropdown
overlayClassName="new-dashboard-menu"
<DropdownMenuSimple
className="new-dashboard-menu"
menu={{ items: getCreateDashboardItems }}
placement="bottomRight"
trigger={['click']}
side="bottom"
align="end"
>
<Button
type="primary"
@@ -773,7 +772,7 @@ function DashboardsList(): JSX.Element {
>
New dashboard
</Button>
</Dropdown>
</DropdownMenuSimple>
)}
</div>
@@ -965,8 +964,7 @@ function DashboardsList(): JSX.Element {
<div className="connection-line" />
<div className="right">
<Switch
size="small"
checked
value
disabled
onChange={(check): void =>
setVisibleColumns((prev) => ({
@@ -985,9 +983,8 @@ function DashboardsList(): JSX.Element {
<div className="connection-line" />
<div className="right">
<Switch
size="small"
disabled
checked
value
onChange={(check): void =>
setVisibleColumns((prev) => ({
...prev,
@@ -1005,8 +1002,7 @@ function DashboardsList(): JSX.Element {
<div className="connection-line" />
<div className="right">
<Switch
size="small"
checked={visibleColumns.updatedAt}
value={visibleColumns.updatedAt}
onChange={(check): void =>
setVisibleColumns((prev) => ({
...prev,
@@ -1024,8 +1020,7 @@ function DashboardsList(): JSX.Element {
<div className="connection-line" />
<div className="right">
<Switch
size="small"
checked={visibleColumns.updatedBy}
value={visibleColumns.updatedBy}
onChange={(check): void =>
setVisibleColumns((prev) => ({
...prev,

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Switch } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
@@ -228,9 +228,8 @@ function LiveLogsContainer({
<div className="live-logs-frequency-chart-view-controller">
<Typography>Frequency chart</Typography>
<Switch
size="small"
checked={showLiveLogsFrequencyChart}
defaultChecked
value={showLiveLogsFrequencyChart}
defaultValue
onChange={handleToggleFrequencyChart}
/>
</div>

View File

@@ -2,7 +2,13 @@ import { useCallback } from 'react';
import { useCopyToClipboard } from 'react-use';
import { orange } from '@ant-design/colors';
import { Settings } from '@signozhq/icons';
import { Dropdown, MenuProps } from 'antd';
import {
type BaseMenuItem,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@signozhq/ui/dropdown-menu';
import {
negateOperator,
OPERATORS,
@@ -135,41 +141,38 @@ function BodyTitleRenderer({
viewName,
]);
const onClickHandler: MenuProps['onClick'] = (props): void => {
const onClickHandler = (key: string): void => {
const mapper = {
[DROPDOWN_KEY.FILTER_IN]: filterHandler(true),
[DROPDOWN_KEY.FILTER_OUT]: filterHandler(false),
[DROPDOWN_KEY.GROUP_BY]: groupByHandler,
};
const handler = mapper[props.key];
const handler = mapper[key];
if (handler) {
handler();
}
};
const menu: MenuProps = {
items: [
{
key: DROPDOWN_KEY.FILTER_IN,
label: `Filter for ${value}`,
},
{
key: DROPDOWN_KEY.FILTER_OUT,
label: `Filter out ${value}`,
},
...(isGroupBySupported
? [
{
key: DROPDOWN_KEY.GROUP_BY,
label: `Group by ${nodeKey}`,
},
]
: []),
],
onClick: onClickHandler,
};
const menuItems: BaseMenuItem[] = [
{
key: DROPDOWN_KEY.FILTER_IN,
label: `Filter for ${value}`,
},
{
key: DROPDOWN_KEY.FILTER_OUT,
label: `Filter out ${value}`,
},
...(isGroupBySupported
? [
{
key: DROPDOWN_KEY.GROUP_BY,
label: `Group by ${nodeKey}`,
},
]
: []),
];
const handleNodeClick = useCallback(
(e: React.MouseEvent): void => {
@@ -218,15 +221,23 @@ function BodyTitleRenderer({
}}
onMouseDown={(e): void => e.preventDefault()}
>
<Dropdown
menu={menu}
trigger={['click']}
dropdownRender={(originNode): React.ReactNode => (
<div data-log-detail-ignore="true">{originNode}</div>
)}
>
<Settings style={{ marginRight: 8 }} className="hover-reveal" />
</Dropdown>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Settings style={{ marginRight: 8 }} className="hover-reveal" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<div data-log-detail-ignore="true">
{menuItems.map((item) => (
<DropdownMenuItem
key={item.key}
onSelect={(): void => onClickHandler(item.key as string)}
>
{item.label}
</DropdownMenuItem>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
</span>
)}
{title.toString()}{' '}

View File

@@ -1,7 +1,7 @@
import { useMemo, useState } from 'react';
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Switch } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -81,7 +81,7 @@ function JSONView({ logData }: JSONViewProps): JSX.Element {
<div className="log-switch">
<div className="wrap-word-switch">
<Typography.Text>Wrap text</Typography.Text>
<Switch checked={isWrapWord} onChange={handleWrapWord} size="small" />
<Switch value={isWrapWord} onChange={handleWrapWord} />
</div>
</div>
</div>

View File

@@ -2,7 +2,8 @@ import { ReactNode, useState } from 'react';
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { Collapse, Divider, Input, Switch, Tag } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Collapse, Divider, Input, Tag } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
@@ -136,7 +137,7 @@ function Overview({
<div className="log-switch">
<div className="wrap-word-switch">
<Typography.Text>Wrap text</Typography.Text>
<Switch checked={isWrapWord} onChange={handleWrapWord} size="small" />
<Switch value={isWrapWord} onChange={handleWrapWord} />
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { Switch } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
@@ -69,9 +69,8 @@ function LogsActionsContainer({
<div className="frequency-chart-view-controller">
<Typography>Frequency chart</Typography>
<Switch
size="small"
checked={showFrequencyChart}
defaultChecked
value={showFrequencyChart}
defaultValue
onChange={handleToggleFrequencyChart}
/>
</div>

View File

@@ -2,9 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Check, ChevronDown, Plus } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { Input } from '@signozhq/ui/input';
import type { MenuProps } from 'antd';
import { Dropdown } from 'antd';
import { useListUsers } from 'api/generated/services/users';
import EditMemberDrawer from 'components/EditMemberDrawer/EditMemberDrawer';
import InviteMembersModal from 'components/InviteMembersModal/InviteMembersModal';
@@ -21,7 +20,6 @@ const PAGE_SIZE = 20;
function MembersSettings(): JSX.Element {
const history = useHistory();
const urlQuery = useUrlQuery();
const pageParam = parseInt(urlQuery.get('page') ?? '1', 10);
const currentPage = Number.isNaN(pageParam) || pageParam < 1 ? 1 : pageParam;
@@ -96,7 +94,7 @@ function MembersSettings(): JSX.Element {
).length;
const totalCount = allMembers.length;
const filterMenuItems: MenuProps['items'] = [
const filterMenuItems: MenuItem[] = [
{
key: FilterMode.All,
label: (
@@ -146,7 +144,7 @@ function MembersSettings(): JSX.Element {
: `Deleted ⎯ ${deletedCount}`;
const handleInviteComplete = useCallback((): void => {
refetchUsers();
void refetchUsers();
}, [refetchUsers]);
const handleRowClick = useCallback((member: MemberRow): void => {
@@ -158,7 +156,7 @@ function MembersSettings(): JSX.Element {
}, []);
const handleMemberEditComplete = useCallback((): void => {
refetchUsers();
void refetchUsers();
}, [refetchUsers]);
return (
@@ -172,10 +170,9 @@ function MembersSettings(): JSX.Element {
</div>
<div className="members-settings__controls">
<Dropdown
<DropdownMenuSimple
menu={{ items: filterMenuItems }}
trigger={['click']}
overlayClassName="members-filter-dropdown"
className="members-filter-dropdown"
>
<Button
variant="solid"
@@ -185,7 +182,7 @@ function MembersSettings(): JSX.Element {
<span>{filterLabel}</span>
<ChevronDown size={12} className="members-filter-trigger__chevron" />
</Button>
</Dropdown>
</DropdownMenuSimple>
<div className="members-settings__search">
<Input

View File

@@ -1,4 +1,5 @@
import type { TypesUserDTO } from 'api/generated/services/sigNoz.schemas';
import userEvent from '@testing-library/user-event';
import { rest, server } from 'mocks-server/server';
import { fireEvent, render, screen } from 'tests/test-utils';
@@ -76,14 +77,15 @@ describe('MembersSettings (integration)', () => {
});
it('filters to pending invites via the filter dropdown', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<MembersSettings />);
await screen.findByText('Alice Smith');
fireEvent.click(screen.getByRole('button', { name: /all members/i }));
await user.click(screen.getByRole('button', { name: /all members/i }));
const pendingOption = await screen.findByText(/pending invites/i);
fireEvent.click(pendingOption);
await user.click(pendingOption);
await screen.findByText('charlie@signoz.io');
expect(screen.queryByText('Alice Smith')).not.toBeInTheDocument();

View File

@@ -30,7 +30,12 @@ import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { GraphTitle, MENU_ITEMS, SERVICE_CHART_ID } from '../constant';
import {
GraphTitle,
MENU_ITEMS,
SERVICE_CHART_ID,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, Row } from '../styles';
import { Button } from './styles';
@@ -206,6 +211,7 @@ function DBCall(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -244,6 +250,7 @@ function DBCall(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>

View File

@@ -32,7 +32,12 @@ import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
import {
GraphTitle,
legend,
MENU_ITEMS,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, Row } from '../styles';
import GraphControlsPanel from './Overview/GraphControlsPanel/GraphControlsPanel';
@@ -279,6 +284,7 @@ function External(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -322,6 +328,7 @@ function External(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -366,6 +373,7 @@ function External(): JSX.Element {
}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -409,6 +417,7 @@ function External(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>

View File

@@ -15,6 +15,7 @@ import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThres
import {
GraphTitle,
SERVICE_CHART_ID,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
@@ -105,6 +106,7 @@ function ApDexMetrics({
threshold={threshold}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
);
}

View File

@@ -8,6 +8,7 @@ import Graph from 'container/GridCardLayout/GridCard';
import {
GraphTitle,
SERVICE_CHART_ID,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
@@ -138,6 +139,7 @@ function ServiceOverview({
onClickHandler={handleGraphClick('Service')}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
)}
</GraphContainer>

View File

@@ -4,6 +4,7 @@ import axios from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ENTITY_VERSION_V4 } from 'constants/app';
import Graph from 'container/GridCardLayout/GridCard';
import { SERVICE_DETAIL_DRILLDOWN_ENABLED } from 'container/MetricsApplication/constant';
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -43,6 +44,7 @@ function TopLevelOperation({
onDragSelect={onDragSelect}
isQueryEnabled={!topLevelOperationsIsLoading}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
)}
</GraphContainer>

View File

@@ -5,11 +5,11 @@ import { useParams } from 'react-router-dom';
import { Search } from '@signozhq/icons';
import {
InputRef,
Switch,
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
Tooltip,
} from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import { ResizeTable } from 'components/ResizeTable';
import TextToolTip from 'components/TextToolTip';
@@ -212,11 +212,7 @@ function TopOperationsTable({
/>
</div>
<div className="top-operation__entry-point">
<Switch
checked={isEntryPoint}
onChange={onEntryPointToggle}
size="small"
/>
<Switch value={isEntryPoint} onChange={onEntryPointToggle} />
<span className="top-operation__entry-point-label">Entrypoint Spans</span>
<TextToolTip
text={entryPointSpanInfo.text}

View File

@@ -25,6 +25,8 @@ export const OPERATION_LEGENDS = ['Operations'];
export const MENU_ITEMS = [MenuItemKeys.View, MenuItemKeys.CreateAlerts];
export const SERVICE_DETAIL_DRILLDOWN_ENABLED = true;
export enum FORMULA {
ERROR_PERCENTAGE = 'A*100/B',
DATABASE_CALLS_AVG_DURATION = 'A/B',

View File

@@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import { Switch, Tooltip } from 'antd';
import { Tooltip } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import logEvent from 'api/common/logEvent';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import WarningPopover from 'components/WarningPopover/WarningPopover';
@@ -358,10 +359,9 @@ function Explorer(): JSX.Element {
title={oneChartPerQueryDisabledTooltip}
>
<Switch
checked={showOneChartPerQuery}
value={showOneChartPerQuery}
onChange={handleToggleShowOneChartPerQuery}
disabled={disableOneChartPerQuery || splitedQueries.length <= 1}
size="small"
/>
</Tooltip>
</div>

View File

@@ -2,7 +2,8 @@ import { useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Color } from '@signozhq/design-tokens';
import { Button, Skeleton, Switch } from 'antd';
import { Button, Skeleton } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import Uplot from 'components/Uplot';
@@ -206,7 +207,7 @@ function GraphView({
</Button.Group>
<div className="view-toggle-button">
<Switch
checked={viewType === 'graph'}
value={viewType === 'graph'}
onChange={(checked): void => {
const newViewType = checked ? 'graph' : 'table';
setViewType(newViewType);

View File

@@ -1,7 +1,8 @@
import { useMemo } from 'react';
import { generatePath } from 'react-router-dom';
import { Color } from '@signozhq/design-tokens';
import { Dropdown, Skeleton } from 'antd';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import { Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import {
useGetMetricAlerts,
@@ -126,12 +127,11 @@ function DashboardsAndAlertsPopover({
return (
<div className="dashboards-and-alerts-popover-container">
{dashboardsPopoverContent && (
<Dropdown
<DropdownMenuSimple
menu={{
items: dashboardsPopoverContent,
}}
placement="bottomLeft"
trigger={['click']}
align="start"
>
<div
className="dashboards-and-alerts-popover dashboards-popover"
@@ -142,15 +142,14 @@ function DashboardsAndAlertsPopover({
{pluralize(dashboards.length, 'dashboard')}
</Typography.Text>
</div>
</Dropdown>
</DropdownMenuSimple>
)}
{alertsPopoverContent && (
<Dropdown
<DropdownMenuSimple
menu={{
items: alertsPopoverContent,
}}
placement="bottomLeft"
trigger={['click']}
align="start"
>
<div
className="dashboards-and-alerts-popover alerts-popover"
@@ -161,7 +160,7 @@ function DashboardsAndAlertsPopover({
{pluralize(alerts.length, 'alert rule')}
</Typography.Text>
</div>
</Dropdown>
</DropdownMenuSimple>
)}
</div>
);

View File

@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Switch } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import logEvent from 'api/common/logEvent';
import { Delete } from '@signozhq/icons';
import { useTimezone } from 'providers/Timezone';
@@ -49,10 +49,10 @@ function TimezoneAdaptation(): JSX.Element {
<div className="timezone-adaption__header">
<h2 className="timezone-adaption__title">Adapt to my timezone</h2>
<Switch
checked={isAdaptationEnabled}
value={isAdaptationEnabled}
onChange={handleSwitchChange}
style={getSwitchStyles()}
data-testid="timezone-adaptation-switch"
testId="timezone-adaptation-switch"
/>
</div>

View File

@@ -127,6 +127,12 @@
flex-direction: column;
gap: 8px;
padding-bottom: 16px;
.password-error-text {
font-size: var(--font-size-xs);
color: var(--bg-cherry-400);
margin-top: 2px;
}
}
.ant-color-picker-trigger {

View File

@@ -1,25 +1,27 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Input, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import {
updateMyPassword,
useUpdateMyUserV2,
} from 'api/generated/services/users';
import { useNotifications } from 'hooks/useNotifications';
import { toast } from '@signozhq/ui/sonner';
import { Check, FileTerminal, Mail, User } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { ErrorV2Resp } from 'types/api';
import { AxiosError } from 'axios';
import '../MySettings.styles.scss';
import './UserInfo.styles.scss';
function UserInfo(): JSX.Element {
const { user, org, updateUser } = useAppContext();
const { t } = useTranslation(['routes', 'settings', 'common']);
const { notifications } = useNotifications();
const { showErrorModal } = useErrorModal();
const { mutateAsync: updateMyUser } = useUpdateMyUserV2();
const [currentPassword, setCurrentPassword] = useState<string>('');
@@ -47,6 +49,8 @@ function UserInfo(): JSX.Element {
const hideResetPasswordModal = (): void => {
setIsResetPasswordModalOpen(false);
setCurrentPassword('');
setUpdatePassword('');
};
const onChangePasswordClickHandler = async (): Promise<void> => {
@@ -57,33 +61,35 @@ function UserInfo(): JSX.Element {
newPassword: updatePassword,
oldPassword: currentPassword,
});
notifications.success({
message: t('success', {
ns: 'common',
}),
});
toast.success('Password updated successfully');
hideResetPasswordModal();
setIsLoading(false);
} catch (error) {
setIsLoading(false);
notifications.error({
message: (error as APIError).error.error.code,
description: (error as APIError).error.error.message,
});
try {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
} catch (apiError) {
showErrorModal(apiError as APIError);
}
}
};
const passwordsMatch =
currentPassword.length > 0 &&
updatePassword.length > 0 &&
currentPassword === updatePassword;
const isResetPasswordDisabled =
isLoading ||
currentPassword.length === 0 ||
updatePassword.length === 0 ||
currentPassword === updatePassword;
passwordsMatch;
const onSaveHandler = async (): Promise<void> => {
logEvent('Account Settings: Name Updated', {
void logEvent('Account Settings: Name Updated', {
name: changedName,
});
logEvent(
void logEvent(
'Account Settings: Name Updated',
{
name: changedName,
@@ -94,11 +100,7 @@ function UserInfo(): JSX.Element {
setIsLoading(true);
await updateMyUser({ data: { displayName: changedName } });
notifications.success({
message: t('success', {
ns: 'common',
}),
});
toast.success('Name updated successfully');
updateUser({
...user,
displayName: changedName,
@@ -106,10 +108,11 @@ function UserInfo(): JSX.Element {
setIsLoading(false);
hideUpdateNameModal();
} catch (error) {
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
try {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
} catch (apiError) {
showErrorModal(apiError as APIError);
}
}
setIsLoading(false);
};
@@ -166,7 +169,7 @@ function UserInfo(): JSX.Element {
type="primary"
icon={<Check size={16} />}
onClick={onSaveHandler}
disabled={isLoading}
loading={isLoading}
data-testid="update-name-btn"
>
Update name
@@ -178,7 +181,11 @@ function UserInfo(): JSX.Element {
<Input
placeholder="e.g. John Doe"
value={changedName}
disabled={isLoading}
onChange={(e): void => setChangedName(e.target.value)}
onPressEnter={(): void => {
void onSaveHandler();
}}
/>
</div>
</Modal>
@@ -188,6 +195,7 @@ function UserInfo(): JSX.Element {
title={<span className="title">Reset password</span>}
open={isResetPasswordModalOpen}
closable
destroyOnClose
onCancel={hideResetPasswordModal}
footer={[
<Button
@@ -197,7 +205,8 @@ function UserInfo(): JSX.Element {
}`}
icon={<Check size={16} />}
onClick={onChangePasswordClickHandler}
disabled={isLoading || isResetPasswordDisabled}
loading={isLoading}
disabled={isResetPasswordDisabled}
data-testid="reset-password-btn"
>
Reset password
@@ -218,6 +227,11 @@ function UserInfo(): JSX.Element {
type="password"
autoComplete="off"
visibilityToggle
onPressEnter={(): void => {
if (!isResetPasswordDisabled) {
void onChangePasswordClickHandler();
}
}}
/>
</div>
@@ -235,7 +249,18 @@ function UserInfo(): JSX.Element {
type="password"
autoComplete="off"
visibilityToggle={false}
status={passwordsMatch ? 'error' : ''}
onPressEnter={(): void => {
if (!isResetPasswordDisabled) {
void onChangePasswordClickHandler();
}
}}
/>
{passwordsMatch && (
<span className="password-error-text">
New password must be different from current password
</span>
)}
</div>
</div>
</Modal>

View File

@@ -8,11 +8,23 @@ import {
waitFor,
within,
} from 'tests/test-utils';
import APIError from 'types/api/error';
import { toast } from '@signozhq/ui/sonner';
const toggleThemeFunction = jest.fn();
const logEventFunction = jest.fn();
const copyToClipboardFn = jest.fn();
const editUserFn = jest.fn();
const updateMyPasswordFn = jest.fn();
const showErrorModalFn = jest.fn();
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: {
success: jest.fn(),
error: jest.fn(),
},
}));
jest.mock('react-use', () => ({
__esModule: true,
@@ -24,12 +36,21 @@ jest.mock('react-use', () => ({
jest.mock('api/generated/services/users', () => ({
...jest.requireActual('api/generated/services/users'),
updateMyPassword: (...args: unknown[]): Promise<unknown> =>
updateMyPasswordFn(...args),
useUpdateMyUserV2: jest.fn(() => ({
mutateAsync: (...args: unknown[]): Promise<unknown> => editUserFn(...args),
isLoading: false,
})),
}));
jest.mock('providers/ErrorModalProvider', () => ({
...jest.requireActual('providers/ErrorModalProvider'),
useErrorModal: jest.fn(() => ({
showErrorModal: showErrorModalFn,
})),
}));
jest.mock('hooks/useDarkMode', () => ({
__esModule: true,
useIsDarkMode: jest.fn(() => true),
@@ -65,12 +86,12 @@ const NEW_PASSWORD_TEST_ID = 'new-password-textbox';
const UPDATE_NAME_BUTTON_TEST_ID = 'update-name-btn';
const RESET_PASSWORD_BUTTON_TEST_ID = 'reset-password-btn';
const UPDATE_NAME_BUTTON_TEXT = 'Update name';
const PASSWORD_VALIDATION_MESSAGE_TEST_ID = 'password-validation-message';
describe('MySettings Flows', () => {
beforeEach(() => {
jest.clearAllMocks();
editUserFn.mockResolvedValue({});
updateMyPasswordFn.mockResolvedValue({});
render(<MySettingsContainer />);
});
@@ -152,9 +173,7 @@ describe('MySettings Flows', () => {
fireEvent.click(modalUpdateNameButton);
await waitFor(() =>
expect(successNotification).toHaveBeenCalledWith({
message: 'success',
}),
expect(toast.success).toHaveBeenCalledWith('Name updated successfully'),
);
});
});
@@ -181,22 +200,131 @@ describe('MySettings Flows', () => {
expect(screen.getByTestId(NEW_PASSWORD_TEST_ID)).toBeInTheDocument();
});
it('Should display validation error if password is less than 8 characters', async () => {
it('Should show inline error when new password matches current password', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
const currentPasswordTextbox = screen.getByTestId(CURRENT_PASSWORD_TEST_ID);
act(() => {
fireEvent.change(currentPasswordTextbox, { target: { value: '123' } });
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'samePassword1' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'samePassword1' },
});
});
expect(
screen.getByText('New password must be different from current password'),
).toBeInTheDocument();
expect(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID)).toBeDisabled();
});
it('Should hide inline error when passwords are changed to be different', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
act(() => {
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'samePassword1' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'samePassword1' },
});
});
act(() => {
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'differentPassword1' },
});
});
expect(
screen.queryByText('New password must be different from current password'),
).not.toBeInTheDocument();
});
it('Should show error modal when password reset API returns an error', async () => {
updateMyPasswordFn.mockRejectedValue(
new Error('Current password is incorrect'),
);
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
act(() => {
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'oldPassword1' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'newPassword1' },
});
});
fireEvent.click(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID));
await waitFor(() => {
// Use getByTestId for the validation message (if present in your modal/component)
if (screen.queryByTestId(PASSWORD_VALIDATION_MESSAGE_TEST_ID)) {
expect(
screen.getByTestId(PASSWORD_VALIDATION_MESSAGE_TEST_ID),
).toBeInTheDocument();
}
expect(showErrorModalFn).toHaveBeenCalledWith(expect.any(APIError));
});
});
it('Should show success toast and close modal on successful password reset', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
act(() => {
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'oldPassword1' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'newPassword1' },
});
});
fireEvent.click(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID));
await waitFor(() => {
expect(toast.success).toHaveBeenCalledWith('Password updated successfully');
expect(
screen.queryByTestId(CURRENT_PASSWORD_TEST_ID),
).not.toBeInTheDocument();
});
});
it('Should clear password fields when modal is cancelled', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
act(() => {
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'somePassword' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'otherPassword' },
});
});
expect(screen.getByTestId(CURRENT_PASSWORD_TEST_ID)).toHaveValue(
'somePassword',
);
// Close the modal
const closeButton = document.querySelector(
'.reset-password-modal .ant-modal-close',
) as HTMLElement;
fireEvent.click(closeButton);
// Reopen the modal
await waitFor(() => {
expect(
screen.queryByTestId(CURRENT_PASSWORD_TEST_ID),
).not.toBeInTheDocument();
});
fireEvent.click(screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT)[0]);
await waitFor(() => {
expect(screen.getByTestId(CURRENT_PASSWORD_TEST_ID)).toHaveValue('');
expect(screen.getByTestId(NEW_PASSWORD_TEST_ID)).toHaveValue('');
});
});

View File

@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { Radio, RadioChangeEvent, Switch, Tag } from 'antd';
import { Radio, RadioChangeEvent, Tag } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import updateUserPreference from 'api/v1/user/preferences/name/update';
@@ -218,10 +219,10 @@ function MySettings(): JSX.Element {
<div className="user-preference-section-content-item-title-action">
Keep the primary sidebar always open{' '}
<Switch
checked={sideNavPinned}
value={sideNavPinned}
onChange={handleSideNavPinnedChange}
loading={isUpdatingUserPreference}
data-testid="side-nav-pinned-switch"
disabled={isUpdatingUserPreference}
testId="side-nav-pinned-switch"
/>
</div>

View File

@@ -7,7 +7,12 @@ import {
DropResult,
} from 'react-beautiful-dnd';
import { Color } from '@signozhq/design-tokens';
import { Button, Divider, Dropdown, Input, MenuProps, Tooltip } from 'antd';
import { Button, Divider, Input, Tooltip } from 'antd';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@signozhq/ui/dropdown-menu';
import { Typography } from '@signozhq/ui/typography';
import { FieldDataType } from 'api/v5/v5';
import { SOMETHING_WENT_WRONG } from 'constants/api';
@@ -159,34 +164,12 @@ function ExplorerColumnsRenderer({
debouncedSetQuerySearchText(e.target.value);
};
const items: MenuProps['items'] = [
{
key: 'search',
label: (
<Input
type="text"
placeholder="Search"
className="explorer-columns-search"
value={searchText}
onChange={handleSearchChange}
prefix={<Search size={16} style={{ padding: '6px' }} />}
/>
),
},
{
key: 'columns',
label: (
<ExplorerAttributeColumns
isLoading={isLoading}
data={data}
searchText={searchText}
isAttributeKeySelected={isAttributeKeySelected}
handleCheckboxChange={handleCheckboxChange}
dataSource={initialDataSource}
/>
),
},
];
const handleOpenChange = (nextOpen: boolean): void => {
setOpen(nextOpen);
if (nextOpen) {
setSearchText('');
}
};
const removeSelectedLogField = (name: string): void => {
if (
@@ -238,13 +221,6 @@ function ExplorerColumnsRenderer({
}
};
const toggleDropdown = (): void => {
setOpen(!open);
if (!open) {
setSearchText('');
}
};
const isDarkMode = useIsDarkMode();
return (
@@ -327,25 +303,38 @@ function ExplorerColumnsRenderer({
</Droppable>
</DragDropContext>
<div>
<Dropdown
menu={{ items }}
arrow
placement="top"
open={open}
overlayClassName="explorer-columns-dropdown"
>
<Button
className="action-btn"
data-testid="add-columns-button"
icon={
<CirclePlus
size={16}
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
/>
}
onClick={toggleDropdown}
/>
</Dropdown>
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
<DropdownMenuTrigger asChild>
<Button
className="action-btn"
data-testid="add-columns-button"
icon={
<CirclePlus
size={16}
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
/>
}
/>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" className="explorer-columns-dropdown">
<Input
type="text"
placeholder="Search"
className="explorer-columns-search"
value={searchText}
onChange={handleSearchChange}
prefix={<Search size={16} style={{ padding: '6px' }} />}
/>
<ExplorerAttributeColumns
isLoading={isLoading}
data={data}
searchText={searchText}
isAttributeKeySelected={isAttributeKeySelected}
handleCheckboxChange={handleCheckboxChange}
dataSource={initialDataSource}
/>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
)}

View File

@@ -146,6 +146,7 @@ describe('ExplorerColumnsRenderer', () => {
});
it('opens and closes the dropdown', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<Wrapper>
<ExplorerColumnsRenderer
@@ -158,12 +159,12 @@ describe('ExplorerColumnsRenderer', () => {
);
const addButton = screen.getByTestId('add-columns-button');
await userEvent.click(addButton);
await user.click(addButton);
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
expect(screen.getByText('attribute1')).toBeInTheDocument();
await userEvent.click(addButton);
await user.click(addButton);
await waitFor(() => {
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
});

View File

@@ -13,7 +13,7 @@ import { Plus, Trash2 } from '@signozhq/icons';
import { ContextLinkProps, Widgets } from 'types/api/dashboard/getAll';
import { getBaseUrl } from 'utils/basePath';
import VariablesDropdown from './VariablesDropdown';
import VariablesPopover from './VariablesPopover';
import './UpdateContextLinks.styles.scss';
@@ -71,7 +71,7 @@ function UpdateContextLinks({
customVariables: fieldVariables,
});
// Transform variables into the format expected by VariablesDropdown
// Transform variables into the format expected by VariablesPopover
const transformedVariables = useMemo(
() => transformContextVariables(variables),
[variables],
@@ -224,7 +224,9 @@ function UpdateContextLinks({
},
]}
>
<VariablesDropdown
{/* TODO: replace with AutoComplete with options for variables and
previously used URLs for better UX */}
<VariablesPopover
onVariableSelect={handleVariableSelect}
variables={transformedVariables}
>
@@ -252,7 +254,7 @@ function UpdateContextLinks({
/>
</div>
)}
</VariablesDropdown>
</VariablesPopover>
</Form.Item>
{/* Remove the separate variables section */}
@@ -282,7 +284,7 @@ function UpdateContextLinks({
/>
</Col>
<Col span={16}>
<VariablesDropdown
<VariablesPopover
onVariableSelect={(variableName, cursorPosition): void =>
handleParamVariableSelect(index, variableName, cursorPosition)
}
@@ -311,7 +313,7 @@ function UpdateContextLinks({
}
/>
)}
</VariablesDropdown>
</VariablesPopover>
</Col>
<Col span={2}>
<Button

View File

@@ -1,26 +0,0 @@
.variables-dropdown-container {
.url-input-trigger {
width: 100%;
.url-input-field {
width: 100%;
}
}
// Override Ant Design dropdown styles
.ant-dropdown-menu {
max-height: 300px;
overflow-y: auto;
padding: 8px 0;
}
}
.variable-row {
display: flex;
justify-content: space-between;
.variable-source {
color: #666;
font-size: 12px;
}
}

View File

@@ -1,93 +0,0 @@
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { Dropdown } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import './VariablesDropdown.styles.scss';
interface VariablesDropdownProps {
onVariableSelect: (variableName: string, cursorPosition?: number) => void;
variables: VariableItem[];
children: (props: {
onVariableSelect: (variableName: string, cursorPosition?: number) => void;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
cursorPosition: number | null;
setCursorPosition: (position: number | null) => void;
}) => ReactNode;
}
interface VariableItem {
name: string;
source: string;
}
function VariablesDropdown({
onVariableSelect,
variables,
children,
}: VariablesDropdownProps): JSX.Element {
const [isOpen, setIsOpen] = useState(false);
const [cursorPosition, setCursorPosition] = useState<number | null>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
// Click outside handler
useEffect(() => {
function handleClickOutside(event: MouseEvent): void {
if (
wrapperRef.current &&
!wrapperRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return (): void => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
const dropdownItems = useMemo(
() =>
variables.map((v) => ({
key: v.name,
label: (
<div className="variable-row">
<Typography.Text className="variable-name">{`{{${v.name}}}`}</Typography.Text>
<Typography.Text className="variable-source">{v.source}</Typography.Text>
</div>
),
})),
[variables],
);
return (
<div className="variables-dropdown-container" ref={wrapperRef}>
<Dropdown
menu={{
items: dropdownItems,
onClick: ({ key }): void => {
const variableName = key as string;
onVariableSelect(`{{${variableName}}}`, cursorPosition || undefined);
setIsOpen(false);
},
}}
open={isOpen}
placement="bottomLeft"
trigger={['click']}
getPopupContainer={(): HTMLElement => wrapperRef.current || document.body}
>
{children({
onVariableSelect,
isOpen,
setIsOpen,
cursorPosition,
setCursorPosition,
})}
</Dropdown>
</div>
);
}
export default VariablesDropdown;

View File

@@ -0,0 +1,74 @@
.variables-popover-container {
.url-input-trigger {
width: 100%;
.url-input-field {
width: 100%;
}
}
.variables-popover-anchor-wrap {
width: 100%;
}
}
.variables-popover-content {
// antd Modal uses z-index ~1000; popover must sit above it.
z-index: 1100;
padding: 4px 0;
max-height: 300px;
overflow-y: auto;
overflow-x: hidden;
min-width: var(--radix-popover-trigger-width);
}
.variables-popover-empty {
padding: 8px 12px;
color: var(--l3-foreground, #999);
font-size: 12px;
font-style: italic;
}
.variables-popover-item {
all: unset;
display: block;
box-sizing: border-box;
width: 100%;
padding: 8px 12px;
cursor: pointer;
color: var(--l1-foreground);
font-size: 13px;
line-height: 1.4;
overflow: hidden;
&:hover,
&:focus {
background-color: var(--l1-background-hover);
}
}
.variable-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-width: 0;
.variable-name,
.variable-source {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.variable-name {
flex: 1 1 auto;
}
.variable-source {
color: #666;
font-size: 12px;
flex: 0 1 auto;
}
}

View File

@@ -0,0 +1,111 @@
// Uses Popover (not DropdownMenu like the rest of the antd-dropdown migration):
// DropdownMenuTrigger preventDefaults pointerdown, breaking input focus and
// dismissing on every keystroke. PopoverAnchor is a passive positioning element.
import { ReactNode, useRef, useState } from 'react';
import { Popover, PopoverAnchor, PopoverContent } from '@signozhq/ui/popover';
import { Typography } from '@signozhq/ui/typography';
import './VariablesPopover.styles.scss';
interface VariablesPopoverProps {
onVariableSelect: (variableName: string, cursorPosition?: number) => void;
variables: VariableItem[];
children: (props: {
onVariableSelect: (variableName: string, cursorPosition?: number) => void;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
cursorPosition: number | null;
setCursorPosition: (position: number | null) => void;
}) => ReactNode;
}
interface VariableItem {
name: string;
source: string;
}
function VariablesPopover({
onVariableSelect,
variables,
children,
}: VariablesPopoverProps): JSX.Element {
const [isOpen, setIsOpen] = useState(false);
const [cursorPosition, setCursorPosition] = useState<number | null>(null);
const anchorRef = useRef<HTMLDivElement>(null);
const handleOpenChange = (open: boolean): void => {
// Accept "close" events from the popover (outside-click, Esc) but ignore
// opens — opening is driven by the input's onFocus in the consumer.
if (!open) {
setIsOpen(false);
}
};
return (
<div className="variables-popover-container">
<Popover open={isOpen} onOpenChange={handleOpenChange} modal={false}>
<PopoverAnchor asChild>
<div className="variables-popover-anchor-wrap" ref={anchorRef}>
{children({
onVariableSelect,
isOpen,
setIsOpen,
cursorPosition,
setCursorPosition,
})}
</div>
</PopoverAnchor>
<PopoverContent
align="start"
sideOffset={4}
className="variables-popover-content"
onOpenAutoFocus={(e): void => e.preventDefault()}
onCloseAutoFocus={(e): void => e.preventDefault()}
onInteractOutside={(e): void => {
// Keep the popover open while interacting with the anchor (the input),
// otherwise typing/clicking the input would close it immediately.
const target = e.target as Node | null;
if (target && anchorRef.current?.contains(target)) {
e.preventDefault();
}
}}
onFocusOutside={(e): void => {
const target = e.target as Node | null;
if (target && anchorRef.current?.contains(target)) {
e.preventDefault();
}
}}
>
{variables.length === 0 ? (
<div className="variables-popover-empty">No variables available</div>
) : (
variables.map((v) => (
<button
key={v.name}
type="button"
className="variables-popover-item"
onMouseDown={(e): void => {
// Prevent the input from losing focus when clicking an item.
e.preventDefault();
}}
onClick={(): void => {
onVariableSelect(`{{${v.name}}}`, cursorPosition || undefined);
setIsOpen(false);
}}
>
<div className="variable-row">
<Typography.Text className="variable-name">{`{{${v.name}}}`}</Typography.Text>
<Typography.Text className="variable-source">
{v.source}
</Typography.Text>
</div>
</button>
))
)}
</PopoverContent>
</Popover>
</div>
);
}
export default VariablesPopover;

View File

@@ -204,7 +204,7 @@ const processContextLinks = (
};
/**
* Transforms context variables into the format expected by VariablesDropdown
* Transforms context variables into the format expected by VariablesPopover
* @param variables - Array of context variables from useContextVariables
* @returns Array of transformed variables with proper source descriptions
*/

View File

@@ -1,5 +1,5 @@
import { Dispatch, SetStateAction } from 'react';
import { Switch } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import {
FillMode,
@@ -73,7 +73,7 @@ export default function ChartAppearanceSection({
Display individual data points on the chart
</Typography.Text>
</div>
<Switch size="small" checked={showPoints} onChange={setShowPoints} />
<Switch value={showPoints} onChange={setShowPoints} />
</section>
)}
{allowSpanGaps && (

View File

@@ -1,5 +1,6 @@
import { Dispatch, SetStateAction } from 'react';
import { InputNumber, Switch } from 'antd';
import { InputNumber } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import SettingsSection from '../../components/SettingsSection/SettingsSection';
@@ -61,8 +62,7 @@ export default function HistogramBucketsSection({
</span>
</Typography.Text>
<Switch
checked={combineHistogram}
size="small"
value={combineHistogram}
onChange={(checked): void => setCombineHistogram(checked)}
/>
</section>

View File

@@ -1,5 +1,6 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { Select, Switch } from 'antd';
import { Select } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import TimePreference from 'components/TimePreferenceDropDown';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -104,8 +105,7 @@ export default function VisualizationSettingsSection({
<section className="stack-chart control-container">
<Typography.Text className="section-heading">Stack series</Typography.Text>
<Switch
checked={stackedBarChart}
size="small"
value={stackedBarChart}
onChange={(checked): void => setStackedBarChart(checked)}
/>
</section>
@@ -120,8 +120,7 @@ export default function VisualizationSettingsSection({
</Typography.Text>
</div>
<Switch
checked={isFillSpans}
size="small"
value={isFillSpans}
onChange={(checked): void => setIsFillSpans(checked)}
/>
</section>

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