Compare commits

..

20 Commits

Author SHA1 Message Date
Vinícius Lourenço
2b73b58acc feat(select-combobox): migrate to signozhq/ui/select and signozhq/ui/combobox 2026-05-22 15:58:57 -03:00
Gaurav Tewari
f2a18e8b6c fix(trace-details): make back button reliably return to previous in-app page (#11414)
Some checks are pending
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
* fix: back button issue

* chore: add unit test

* fix : test cases

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-05-22 16:13:26 +00:00
Manika Malhotra
4da5673e12 chore: migrate antd Progress to signoz ui component (#11398)
* chore: migrate antd ProgressBar to signoz ui component

* fix: homepage progress bar leaking section, resolve comments

* fix: remove stripe animation from progress bars in api monitoring section

* revert: accidental unrelated files
2026-05-22 14:09:08 +00:00
Ashwin Bhatkal
c3db819d8e chore: update code owners for dashboard v2 and e2e (#11412)
* chore: update code owners for dashboard and e2e

* chore: update code owners order
2026-05-22 13:19:04 +00:00
Piyush Singariya
c83578f211 chore: stats collection for logspipeline (#11409)
* feat: logspipeline statscollector

* fix: collect total and enabled

* chore: update metric name
2026-05-22 13:16:51 +00:00
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
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
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
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
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
Manika Malhotra
b753b95a8a chore: replace antd badge with signozhq badge (#11377)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / go-build (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
* chore: replace antd badge with signozhq badge

* chore: add badge to banned components
2026-05-20 14:24:52 +00:00
493 changed files with 13870 additions and 28629 deletions

7
.github/CODEOWNERS vendored
View File

@@ -118,6 +118,9 @@ go.mod @therealpandey
/tests/integration/ @therealpandey
# e2e tests
/tests/e2e/ @AshwinBhatkal
# Flagger Owners
/pkg/flagger/ @therealpandey
@@ -162,3 +165,7 @@ go.mod @therealpandey
/frontend/src/lib/dashboard/ @SigNoz/pulse-frontend
/frontend/src/lib/dashboardVariables/ @SigNoz/pulse-frontend
/frontend/src/components/NewSelect/ @SigNoz/pulse-frontend
## Dashboard V2
/frontend/src/pages/DashboardPageV2/ @SigNoz/pulse-frontend
/frontend/src/pages/DashboardsListPageV2/ @SigNoz/pulse-frontend

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,13 @@
*/
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.',
Badge: 'Use @signozhq/ui/badge instead of antd Badge.',
Progress: 'Use @signozhq/ui/progress instead of antd Progress.',
Select:
'Use SelectSimple / ComboboxSimple from @signozhq/ui/select or @signozhq/ui/combobox instead of antd Select.',
};
export default {

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

@@ -4,7 +4,7 @@
"no_alerts_found": "No alerts found during the evaluation. This happens when rule condition is unsatisfied. You may adjust the rule threshold and retry.",
"button_testrule": "Test Notification",
"label_channel_select": "Notification Channels",
"placeholder_channel_select": "select one or more channels",
"placeholder_channel_select": "Select one or more channels",
"channel_select_tooltip": "Leave empty to send this alert on all the configured channels",
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
"preview_chart_threshold_label": "Threshold",

View File

@@ -4,7 +4,7 @@
"no_alerts_found": "No alerts found during the evaluation. This happens when rule condition is unsatisfied. You may adjust the rule threshold and retry.",
"button_testrule": "Test Notification",
"label_channel_select": "Notification Channels",
"placeholder_channel_select": "select one or more channels",
"placeholder_channel_select": "Select one or more channels",
"channel_select_tooltip": "Leave empty to send this alert on all the configured channels",
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
"preview_chart_threshold_label": "Threshold",

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

@@ -1,31 +0,0 @@
.emptyState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 48px 24px;
text-align: center;
}
.icon {
color: var(--danger-background);
}
.title {
font-size: 16px;
font-weight: 500;
color: var(--text-vanilla-100);
}
.subtitle {
font-size: 14px;
color: var(--text-vanilla-400);
max-width: 400px;
}
.actions {
display: flex;
gap: 8px;
margin-top: 4px;
}

View File

@@ -1,61 +0,0 @@
import { useCallback } from 'react';
import { LifeBuoy, RefreshCw, TriangleAlert } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { handleContactSupport } from 'container/Integrations/utils';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import styles from './ErrorEmptyState.module.scss';
interface ErrorEmptyStateProps {
title?: string;
subtitle?: string;
onRefresh?: () => void;
}
function ErrorEmptyState({
title = 'Something went wrong',
subtitle = 'Our team is getting on top to resolve this. Please reach out to support if the issue persists.',
onRefresh,
}: ErrorEmptyStateProps): JSX.Element {
const { isCloudUser } = useGetTenantLicense();
const onContactSupport = useCallback((): void => {
handleContactSupport(isCloudUser);
}, [isCloudUser]);
return (
<div className={styles.emptyState} data-testid="error-empty-state">
<TriangleAlert className={styles.icon} size={32} />
<div className={styles.title} data-testid="error-title">
{title}
</div>
<div className={styles.subtitle} data-testid="error-subtitle">
{subtitle}
</div>
<div className={styles.actions}>
<Button
variant="solid"
color="secondary"
prefix={<LifeBuoy size={14} />}
onClick={onContactSupport}
data-testid="error-contact-support-button"
>
Contact Support
</Button>
{onRefresh && (
<Button
variant="outlined"
color="secondary"
prefix={<RefreshCw size={14} />}
onClick={onRefresh}
data-testid="error-refresh-button"
>
Refresh
</Button>
)}
</div>
</div>
);
}
export default ErrorEmptyState;

View File

@@ -1 +0,0 @@
export { default } from './ErrorEmptyState';

View File

@@ -1,68 +0,0 @@
.labelColumn {
display: flex;
gap: 4px;
align-items: center;
overflow: hidden;
max-width: 100%;
width: 100%;
}
.labelBadge {
cursor: default;
font-size: 12px;
--badge-display: inline;
max-width: 180px;
text-overflow: ellipsis;
}
.overflowTrigger {
all: unset;
cursor: pointer;
}
.overflowBadge {
cursor: pointer;
font-size: 12px;
}
.labelPopover {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px;
max-height: 300px;
overflow-y: auto;
}
.labelTooltip {
display: flex;
flex-direction: column;
gap: 6px;
max-height: 300px;
overflow-y: auto;
}
.labelValue {
text-overflow: ellipsis;
overflow: hidden;
}
.tooltipContent {
display: flex;
align-items: center;
gap: 8px;
}
.copyButton {
all: unset;
cursor: pointer;
display: flex;
align-items: center;
opacity: 0.7;
&:hover {
opacity: 1;
}
}

View File

@@ -1,142 +0,0 @@
import { TooltipProvider } from '@signozhq/ui/tooltip';
import { act, render, screen } from '@testing-library/react';
import LabelColumn from './LabelColumn';
let resizeCallback: ResizeObserverCallback | null = null;
class MockResizeObserver {
constructor(callback: ResizeObserverCallback) {
resizeCallback = callback;
}
observe = jest.fn();
unobserve = jest.fn();
disconnect = jest.fn();
}
function triggerResize(width: number): void {
if (resizeCallback) {
act(() => {
resizeCallback?.(
[{ contentRect: { width } } as ResizeObserverEntry],
{} as ResizeObserver,
);
});
}
}
beforeAll(() => {
global.ResizeObserver = MockResizeObserver as unknown as typeof ResizeObserver;
});
afterEach(() => {
resizeCallback = null;
});
function renderWithProviders(
ui: React.ReactElement,
): ReturnType<typeof render> {
return render(<TooltipProvider>{ui}</TooltipProvider>);
}
describe('LabelColumn', () => {
it('should render all labels when 5 or fewer', () => {
const labels = ['env', 'service', 'region'];
renderWithProviders(<LabelColumn labels={labels} />);
expect(screen.getByTestId('label-tag-env')).toBeInTheDocument();
expect(screen.getByTestId('label-tag-service')).toBeInTheDocument();
expect(screen.getByTestId('label-tag-region')).toBeInTheDocument();
});
it('should truncate labels and show +N badge when container is narrow', () => {
const labels = ['env', 'service', 'region', 'team', 'owner', 'version'];
renderWithProviders(<LabelColumn labels={labels} />);
// Simulate narrow container that fits ~3 badges
// Badge widths: env=37, service=65, region=58, team=44, owner=51, version=65
// 220px available = 3 badges (160px) + gaps (8px) + overflow (44px)
triggerResize(220);
// First 3 visible
expect(screen.getByTestId('label-tag-env')).toBeInTheDocument();
expect(screen.getByTestId('label-tag-service')).toBeInTheDocument();
expect(screen.getByTestId('label-tag-region')).toBeInTheDocument();
// Remaining in overflow badge
expect(screen.getByTestId('label-overflow-badge')).toHaveTextContent('+3');
});
it('should render label with value when value prop provided', () => {
const labels = ['env'];
const value = { env: 'production' };
renderWithProviders(<LabelColumn labels={labels} value={value} />);
expect(screen.getByTestId('label-tag-env')).toHaveTextContent(
'env: production',
);
});
it('should render labels without value when value is not provided for that label', () => {
const labels = ['env', 'service'];
const value = { env: 'production' };
renderWithProviders(<LabelColumn labels={labels} value={value} />);
expect(screen.getByTestId('label-tag-env')).toHaveTextContent(
'env: production',
);
expect(screen.getByTestId('label-tag-service')).toHaveTextContent('service');
});
it('should show overflow badge with remaining count when container is narrow', () => {
const labels = ['env', 'service', 'region', 'team', 'owner', 'version'];
renderWithProviders(<LabelColumn labels={labels} />);
// Simulate narrow container to trigger overflow (shows 3 labels)
// 220px fits first 3 badges before overflow
triggerResize(220);
// Overflow badge shows +3 (remaining labels)
const overflowBadge = screen.getByTestId('label-overflow-badge');
expect(overflowBadge).toBeInTheDocument();
expect(overflowBadge).toHaveTextContent('+3');
});
it('should render empty when no labels provided', () => {
renderWithProviders(<LabelColumn labels={[]} />);
const column = screen.getByTestId('label-column');
expect(column.children).toHaveLength(0);
});
it('should use primary color by default', () => {
const labels = ['env'];
renderWithProviders(<LabelColumn labels={labels} />);
expect(screen.getByTestId('label-tag-env')).toBeInTheDocument();
});
it('should show all labels when container is wide enough', () => {
const labels = ['env', 'service', 'region', 'team', 'owner', 'version'];
renderWithProviders(<LabelColumn labels={labels} />);
// Simulate wide container
triggerResize(1000);
// All labels visible
labels.forEach((label) => {
expect(screen.getByTestId(`label-tag-${label}`)).toBeInTheDocument();
});
// No overflow badge
expect(screen.queryByTestId('label-overflow-badge')).not.toBeInTheDocument();
});
});

View File

@@ -1,150 +0,0 @@
import { Copy } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { toast } from '@signozhq/ui/sonner';
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import LabelTag from './LabelTag';
import styles from './LabelColumn.module.scss';
import { BADGE_GAP, estimateBadgeWidth, OVERFLOW_BADGE_WIDTH } from './utils';
export interface LabelColumnProps {
labels: string[];
color?:
| 'primary'
| 'secondary'
| 'success'
| 'error'
| 'warning'
| 'robin'
| 'forest'
| 'amber'
| 'sienna'
| 'cherry'
| 'sakura'
| 'aqua'
| 'vanilla';
value?: { [key: string]: string };
}
function LabelColumn({
labels,
value,
color = 'primary',
}: LabelColumnProps): JSX.Element {
const containerRef = useRef<HTMLDivElement>(null);
const [maxVisibleCount, setMaxVisibleCount] = useState(labels.length);
const [, copyToClipboard] = useCopyToClipboard();
const calculateMaxVisible = useCallback(
(width: number): number => {
if (width <= 0) {
return 1;
}
const availableWidth = width - OVERFLOW_BADGE_WIDTH - BADGE_GAP;
let usedWidth = 0;
let count = 0;
for (const label of labels) {
const badgeWidth = estimateBadgeWidth(label, value?.[label]) + BADGE_GAP;
if (usedWidth + badgeWidth > availableWidth && count > 0) {
break;
}
usedWidth += badgeWidth;
count++;
}
return Math.max(1, count);
},
[labels, value],
);
useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
}
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0) {
setMaxVisibleCount(calculateMaxVisible(entry.contentRect.width));
}
});
observer.observe(container);
if (container.clientWidth > 0) {
setMaxVisibleCount(calculateMaxVisible(container.clientWidth));
}
return (): void => observer.disconnect();
}, [calculateMaxVisible]);
const needsOverflow = labels.length > maxVisibleCount;
const visibleLabels = needsOverflow
? labels.slice(0, maxVisibleCount)
: labels;
const remainingLabels = needsOverflow ? labels.slice(maxVisibleCount) : [];
return (
<div
ref={containerRef}
className={styles.labelColumn}
data-testid="label-column"
>
{visibleLabels.map((label) => (
<LabelTag key={label} label={label} color={color} value={value?.[label]} />
))}
{remainingLabels.length > 0 && (
<TooltipRoot>
<TooltipTrigger asChild>
<span>
<Badge
color={color}
className={styles.overflowBadge}
variant="outline"
data-testid="label-overflow-badge"
>
+{remainingLabels.length}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent side="bottom" align="end">
<div className={styles.tooltipContent}>
<span>
{remainingLabels
.map((label) => (value?.[label] ? `${label}: ${value[label]}` : label))
.join(', ')}
</span>
<button
type="button"
className={styles.copyButton}
onClick={(e): void => {
e.stopPropagation();
const searchFormat = remainingLabels
.map((label) => (value?.[label] ? `${label} ${value[label]}` : label))
.join(' ');
copyToClipboard(searchFormat);
toast.success('Copied! Use in search to filter alerts.');
}}
aria-label="Copy to clipboard"
>
<Copy size={12} />
</button>
</div>
</TooltipContent>
</TooltipRoot>
)}
</div>
);
}
export default LabelColumn;

View File

@@ -1,30 +0,0 @@
.labelBadge {
cursor: default;
font-size: 12px;
max-width: 180px;
text-overflow: ellipsis;
}
.labelValue {
text-overflow: ellipsis;
overflow: hidden;
}
.tooltipContent {
display: flex;
align-items: center;
gap: 8px;
}
.copyButton {
all: unset;
cursor: pointer;
display: flex;
align-items: center;
opacity: 0.7;
&:hover {
opacity: 1;
}
}

View File

@@ -1,74 +0,0 @@
import { Copy } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { toast } from '@signozhq/ui/sonner';
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import { useCopyToClipboard } from 'react-use';
import styles from './LabelTag.module.scss';
export interface LabelTagProps {
label: string;
color?:
| 'primary'
| 'secondary'
| 'success'
| 'error'
| 'warning'
| 'robin'
| 'forest'
| 'amber'
| 'sienna'
| 'cherry'
| 'sakura'
| 'aqua'
| 'vanilla';
value?: string;
}
function LabelTag({ label, value, color }: LabelTagProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const displayText = value ? `${label}: ${value}` : label;
const searchFormat = value ? `${label} ${value}` : label;
const handleCopy = (e: React.MouseEvent): void => {
e.stopPropagation();
copyToClipboard(searchFormat);
toast.success('Copied! Use in search to filter alerts.');
};
return (
<TooltipRoot>
<TooltipTrigger asChild>
<span>
<Badge
color={color}
className={styles.labelBadge}
variant="outline"
data-testid={`label-tag-${label}`}
>
<span className={styles.labelValue}>{displayText}</span>
</Badge>
</span>
</TooltipTrigger>
<TooltipContent>
<div className={styles.tooltipContent}>
<span>{displayText}</span>
<button
type="button"
className={styles.copyButton}
onClick={handleCopy}
aria-label="Copy to clipboard"
>
<Copy size={12} />
</button>
</div>
</TooltipContent>
</TooltipRoot>
);
}
export default LabelTag;

View File

@@ -1,2 +0,0 @@
export { default } from './LabelColumn';
export type { LabelColumnProps } from './LabelColumn';

View File

@@ -1,14 +0,0 @@
export const BADGE_GAP = 4;
export const OVERFLOW_BADGE_WIDTH = 40;
export const BADGE_MAX_WIDTH = 180;
export const BADGE_PADDING = 16;
export const CHAR_WIDTH = 7;
export function estimateBadgeWidth(label: string, value?: string): number {
const displayText = value ? `${label}: ${value}` : label;
return Math.min(
displayText.length * CHAR_WIDTH + BADGE_PADDING,
BADGE_MAX_WIDTH,
);
}

View File

@@ -1,30 +0,0 @@
.emptyState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 48px 24px;
text-align: center;
}
.icon {
color: var(--text-vanilla-400);
}
.title {
font-size: 16px;
font-weight: 500;
color: var(--text-vanilla-100);
}
.subtitle {
font-size: 14px;
color: var(--text-vanilla-400);
max-width: 400px;
}
.actions {
display: flex;
gap: 8px;
}

View File

@@ -1,71 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import NoResultsEmptyState from './NoResultsEmptyState';
describe('NoResultsEmptyState', () => {
it('should render with default props', () => {
render(<NoResultsEmptyState />);
expect(screen.getByTestId('no-results-empty-state')).toBeInTheDocument();
expect(screen.getByTestId('no-results-title')).toHaveTextContent(
'No matching results',
);
expect(screen.getByTestId('no-results-subtitle')).toHaveTextContent(
'No items match your current filters. Try adjusting your search criteria.',
);
});
it('should render with custom title and subtitle', () => {
render(
<NoResultsEmptyState title="Custom Title" subtitle="Custom Subtitle" />,
);
expect(screen.getByTestId('no-results-title')).toHaveTextContent(
'Custom Title',
);
expect(screen.getByTestId('no-results-subtitle')).toHaveTextContent(
'Custom Subtitle',
);
});
it('should not render clear button when onClear is not provided', () => {
render(<NoResultsEmptyState />);
expect(
screen.queryByTestId('no-results-clear-button'),
).not.toBeInTheDocument();
});
it('should render clear button when onClear is provided', () => {
const onClear = jest.fn();
render(<NoResultsEmptyState onClear={onClear} />);
expect(screen.getByTestId('no-results-clear-button')).toBeInTheDocument();
expect(screen.getByTestId('no-results-clear-button')).toHaveTextContent(
'Clear Filters',
);
});
it('should render custom clear button text', () => {
render(
<NoResultsEmptyState onClear={jest.fn()} clearButtonText="Reset All" />,
);
expect(screen.getByTestId('no-results-clear-button')).toHaveTextContent(
'Reset All',
);
});
it('should call onClear when clear button is clicked', async () => {
const user = userEvent.setup();
const onClear = jest.fn();
render(<NoResultsEmptyState onClear={onClear} />);
await user.click(screen.getByTestId('no-results-clear-button'));
expect(onClear).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,57 +0,0 @@
import { RefreshCw, Search } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import styles from './NoResultsEmptyState.module.scss';
interface NoResultsEmptyStateProps {
title?: string;
subtitle?: string;
onClear?: () => void;
clearButtonText?: string;
onRefresh?: () => void;
}
function NoResultsEmptyState({
title = 'No matching results',
subtitle = 'No items match your current filters. Try adjusting your search criteria.',
onClear,
clearButtonText = 'Clear Filters',
onRefresh,
}: NoResultsEmptyStateProps): JSX.Element {
return (
<div className={styles.emptyState} data-testid="no-results-empty-state">
<Search className={styles.icon} size={16} />
<div className={styles.title} data-testid="no-results-title">
{title}
</div>
<div className={styles.subtitle} data-testid="no-results-subtitle">
{subtitle}
</div>
<div className={styles.actions}>
{onClear && (
<Button
variant="outlined"
color="secondary"
onClick={onClear}
data-testid="no-results-clear-button"
>
{clearButtonText}
</Button>
)}
{onRefresh && (
<Button
variant="outlined"
color="secondary"
prefix={<RefreshCw size={14} />}
onClick={onRefresh}
data-testid="no-results-refresh-button"
>
Refresh
</Button>
)}
</div>
</div>
);
}
export default NoResultsEmptyState;

View File

@@ -1 +0,0 @@
export { default } from './NoResultsEmptyState';

View File

@@ -1,32 +0,0 @@
import type { BadgeColor } from '@signozhq/ui/badge';
export const STATE_ORDER = ['firing', 'pending', 'inactive', 'disabled'];
export const SEVERITY_ORDER = ['critical', 'error', 'warning', 'info'];
export const STATE_LABELS: Record<string, string> = {
firing: 'Firing',
pending: 'Pending',
inactive: 'OK',
disabled: 'Disabled',
};
export const STATE_COLORS: Record<string, string> = {
firing: 'var(--bg-cherry-500)',
pending: 'var(--bg-amber-500)',
inactive: 'var(--bg-forest-500)',
disabled: 'var(--l2-foreground)',
};
export const SEVERITY_COLORS: Record<string, string> = {
critical: 'var(--bg-cherry-500)',
error: 'var(--bg-cherry-400)',
warning: 'var(--bg-amber-500)',
info: 'var(--bg-robin-500)',
};
export const SEVERITY_BADGE_COLORS: Record<string, BadgeColor> = {
critical: 'error',
error: 'error',
warning: 'warning',
info: 'primary',
};

View File

@@ -1,7 +0,0 @@
export interface FilterValue {
value: string;
}
export interface AlertWithLabels {
labels?: Record<string, string>;
}

View File

@@ -1,287 +0,0 @@
import type { SortState } from 'components/TanStackTableView/types';
import type { AlertWithLabels, FilterValue } from './types';
import { filterByLabels, searchByLabels, sortByColumn } from './utils';
interface TestAlert extends AlertWithLabels {
name: string;
value: number;
}
const createAlert = (
name: string,
value: number,
labels?: Record<string, string>,
): TestAlert => ({
name,
value,
labels,
});
describe('sortByColumn', () => {
const alerts: TestAlert[] = [
createAlert('Alert C', 3),
createAlert('Alert A', 1),
createAlert('Alert B', 2),
];
const getSortValue = (
item: TestAlert,
columnName: string,
): string | number => {
if (columnName === 'name') {
return item.name;
}
if (columnName === 'value') {
return item.value;
}
return '';
};
it('should return items unchanged when no orderBy provided', () => {
const result = sortByColumn(alerts, null, getSortValue);
expect(result).toStrictEqual(alerts);
});
it('should sort by string column ascending', () => {
const orderBy: SortState = { columnName: 'name', order: 'asc' };
const result = sortByColumn(alerts, orderBy, getSortValue);
expect(result.map((a) => a.name)).toStrictEqual([
'Alert A',
'Alert B',
'Alert C',
]);
});
it('should sort by string column descending', () => {
const orderBy: SortState = { columnName: 'name', order: 'desc' };
const result = sortByColumn(alerts, orderBy, getSortValue);
expect(result.map((a) => a.name)).toStrictEqual([
'Alert C',
'Alert B',
'Alert A',
]);
});
it('should sort by number column ascending', () => {
const orderBy: SortState = { columnName: 'value', order: 'asc' };
const result = sortByColumn(alerts, orderBy, getSortValue);
expect(result.map((a) => a.value)).toStrictEqual([1, 2, 3]);
});
it('should sort by number column descending', () => {
const orderBy: SortState = { columnName: 'value', order: 'desc' };
const result = sortByColumn(alerts, orderBy, getSortValue);
expect(result.map((a) => a.value)).toStrictEqual([3, 2, 1]);
});
it('should use defaultSort when orderBy is null', () => {
const defaultSort: SortState = { columnName: 'value', order: 'asc' };
const result = sortByColumn(alerts, null, getSortValue, defaultSort);
expect(result.map((a) => a.value)).toStrictEqual([1, 2, 3]);
});
it('should not mutate original array', () => {
const original = [...alerts];
const orderBy: SortState = { columnName: 'name', order: 'asc' };
sortByColumn(alerts, orderBy, getSortValue);
expect(alerts).toStrictEqual(original);
});
it('should handle empty array', () => {
const result = sortByColumn(
[],
{ columnName: 'name', order: 'asc' },
getSortValue,
);
expect(result).toStrictEqual([]);
});
it('should handle equal values', () => {
const duplicates = [
createAlert('Same', 1),
createAlert('Same', 1),
createAlert('Same', 1),
];
const orderBy: SortState = { columnName: 'name', order: 'asc' };
const result = sortByColumn(duplicates, orderBy, getSortValue);
expect(result).toHaveLength(3);
});
});
describe('searchByLabels', () => {
const alerts: TestAlert[] = [
createAlert('CPU High', 1, { severity: 'critical', team: 'infra' }),
createAlert('Memory Warning', 2, { severity: 'warning', team: 'backend' }),
createAlert('Disk Full', 3, { severity: 'error', region: 'us-east' }),
createAlert('Network Slow', 4, {}),
createAlert('No Labels', 5),
];
const getAlertName = (alert: TestAlert): string => alert.name;
it('should return all items when search is empty', () => {
const result = searchByLabels(alerts, '', getAlertName);
expect(result).toStrictEqual(alerts);
});
it('should return all items when search is whitespace', () => {
const result = searchByLabels(alerts, ' ', getAlertName);
expect(result).toStrictEqual(alerts);
});
it('should search by alert name', () => {
const result = searchByLabels(alerts, 'CPU', getAlertName);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('CPU High');
});
it('should search by alert name case-insensitive', () => {
const result = searchByLabels(alerts, 'cpu', getAlertName);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('CPU High');
});
it('should search by severity label', () => {
const result = searchByLabels(alerts, 'critical', getAlertName);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('CPU High');
});
it('should search by any label key', () => {
const result = searchByLabels(alerts, 'team', getAlertName);
expect(result).toHaveLength(2);
});
it('should search by any label value', () => {
const result = searchByLabels(alerts, 'infra', getAlertName);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('CPU High');
});
it('should handle alerts with no labels', () => {
const result = searchByLabels(alerts, 'No Labels', getAlertName);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('No Labels');
});
it('should handle partial matches', () => {
const result = searchByLabels(alerts, 'warn', getAlertName);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('Memory Warning');
});
it('should return empty for no matches', () => {
const result = searchByLabels(alerts, 'nonexistent', getAlertName);
expect(result).toStrictEqual([]);
});
it('should trim search text', () => {
const result = searchByLabels(alerts, ' CPU ', getAlertName);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('CPU High');
});
});
describe('filterByLabels', () => {
const alerts: TestAlert[] = [
createAlert('A1', 1, { severity: 'critical', team: 'infra', env: 'prod' }),
createAlert('A2', 2, { severity: 'critical', team: 'backend', env: 'prod' }),
createAlert('A3', 3, { severity: 'warning', team: 'infra', env: 'staging' }),
createAlert('A4', 4, { severity: 'info', team: 'frontend', env: 'dev' }),
createAlert('A5', 5, {}),
createAlert('A6', 6),
];
const createFilter = (value: string): FilterValue => ({ value });
it('should return all items when filters are empty', () => {
const result = filterByLabels(alerts, []);
expect(result).toStrictEqual(alerts);
});
it('should return all items when filters is null-ish', () => {
const result = filterByLabels(alerts, null as unknown as FilterValue[]);
expect(result).toStrictEqual(alerts);
});
it('should filter by single label', () => {
const filters = [createFilter('severity:critical')];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(2);
expect(result.map((a) => a.name)).toStrictEqual(['A1', 'A2']);
});
it('should use OR logic for same key', () => {
const filters = [
createFilter('severity:critical'),
createFilter('severity:warning'),
];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(3);
expect(result.map((a) => a.name)).toStrictEqual(['A1', 'A2', 'A3']);
});
it('should use AND logic for different keys', () => {
const filters = [
createFilter('severity:critical'),
createFilter('team:infra'),
];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('A1');
});
it('should handle case-insensitive keys', () => {
const filters = [createFilter('SEVERITY:critical')];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(2);
});
it('should handle case-insensitive values', () => {
const filters = [createFilter('severity:CRITICAL')];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(2);
});
it('should trim whitespace', () => {
const filters = [createFilter(' severity : critical ')];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(2);
});
it('should return empty for invalid filter format', () => {
const filters = [createFilter('invalid')];
const result = filterByLabels(alerts, filters);
expect(result).toStrictEqual([]);
});
it('should ignore invalid filters mixed with valid', () => {
const filters = [createFilter('invalid'), createFilter('severity:critical')];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(2);
});
it('should exclude alerts without matching label key', () => {
const filters = [createFilter('nonexistent:value')];
const result = filterByLabels(alerts, filters);
expect(result).toStrictEqual([]);
});
it('should exclude alerts with no labels', () => {
const filters = [createFilter('severity:critical')];
const result = filterByLabels(alerts, filters);
expect(result.every((a) => a.labels !== undefined)).toBe(true);
});
it('should handle complex AND/OR combinations', () => {
const filters = [
createFilter('env:prod'),
createFilter('env:staging'),
createFilter('team:infra'),
];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(2);
expect(result.map((a) => a.name)).toStrictEqual(['A1', 'A3']);
});
});

View File

@@ -1,116 +0,0 @@
import type { SortState } from 'components/TanStackTableView/types';
import type { AlertWithLabels, FilterValue } from './types';
/**
* Generic sort function for alert-like data
*/
export function sortByColumn<T>(
items: T[],
orderBy: SortState | null,
getSortValue: (item: T, columnName: string) => string | number,
defaultSort?: SortState,
): T[] {
const sortState = orderBy ?? defaultSort;
if (!sortState) {
return items;
}
const { columnName, order } = sortState;
const multiplier = order === 'asc' ? 1 : -1;
return [...items].sort((a, b) => {
const aVal = getSortValue(a, columnName);
const bVal = getSortValue(b, columnName);
if (aVal < bVal) {
return -1 * multiplier;
}
if (aVal > bVal) {
return 1 * multiplier;
}
return 0;
});
}
/**
* Search alerts/rules by name, severity, and all labels
*/
export function searchByLabels<T extends AlertWithLabels>(
items: T[],
searchText: string,
getAlertName: (item: T) => string,
): T[] {
if (!searchText.trim()) {
return items;
}
const value = searchText.toLowerCase().trim();
return items.filter((item) => {
const alertName = getAlertName(item).toLowerCase();
const severity = item.labels?.severity?.toLowerCase() ?? '';
const labelSearchString = Object.entries(item.labels ?? {})
.map(([key, val]) => `${key} ${val}`)
.join(' ')
.toLowerCase();
return (
alertName.includes(value) ||
severity.includes(value) ||
labelSearchString.includes(value)
);
});
}
/**
* Filter alerts by label key:value pairs
* Same key uses OR logic, different keys use AND logic
*/
export function filterByLabels<T extends AlertWithLabels>(
items: T[],
selectedFilters: FilterValue[],
): T[] {
if (!selectedFilters?.length) {
return items;
}
const validFilters = selectedFilters
.map((e) => e.value)
.filter((v) => v.split(':').length === 2);
if (!validFilters.length) {
return [];
}
// Group values by key - same key uses OR, different keys use AND
const filtersByKey = new Map<string, string[]>();
validFilters.forEach((f) => {
const [key, value] = f.split(':');
const trimmedKey = key.trim().toLowerCase();
const trimmedValue = value.trim().toLowerCase();
const existing = filtersByKey.get(trimmedKey) ?? [];
existing.push(trimmedValue);
filtersByKey.set(trimmedKey, existing);
});
return items.filter((item) => {
if (!item.labels) {
return false;
}
// All keys must match (AND), any value per key can match (OR)
return Array.from(filtersByKey.entries()).every(([filterKey, values]) => {
// Case-insensitive key lookup
const matchingKey = Object.keys(item.labels ?? {}).find(
(k) => k.toLowerCase() === filterKey,
);
if (!matchingKey) {
return false;
}
const labelValue = item.labels?.[matchingKey]?.toLowerCase();
return values.some((v) => labelValue === v);
});
});
}

View File

@@ -1,12 +1,12 @@
import { useCallback, useMemo, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { Row, Select, Spin } from 'antd';
import { Row } from 'antd';
import { ComboboxSimple, ComboboxSimpleItem } from '@signozhq/ui/combobox';
import {
getValuesFromQueryParams,
setQueryParamsFromOptions,
} from 'components/CeleryTask/CeleryUtils';
import { useCeleryFilterOptions } from 'components/CeleryTask/useCeleryFilterOptions';
import { SelectMaxTagPlaceholder } from 'components/MessagingQueues/MQCommon/MQCommon';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
@@ -48,7 +48,7 @@ export function FilterSelect({
: getValuesFromQueryParams(queryParam, urlQuery) || [];
// Memoize options to include the typed value if not present
const mergedOptions = useMemo(() => {
const mergedOptions = useMemo<ComboboxSimpleItem[]>(() => {
if (
!!searchValue.trim().length &&
!options.some((opt) => opt.value === searchValue)
@@ -84,35 +84,16 @@ export function FilterSelect({
],
);
// Update searchValue on user input
const handleSearchInput = (input: string): void => {
setSearchValue(input);
handleSearch(input);
};
return (
<Select
<ComboboxSimple
key={filterType.toString()}
placeholder={placeholder}
showSearch
{...(isMultiple ? { mode: 'multiple' } : {})}
options={mergedOptions}
multiple={isMultiple}
items={mergedOptions}
loading={isFetching}
className="config-select-option"
onSearch={handleSearchInput}
maxTagCount={4}
allowClear
maxTagPlaceholder={SelectMaxTagPlaceholder}
value={selectValue}
notFoundContent={
isFetching ? (
<span>
<Spin size="small" /> Loading...
</span>
) : (
<span>No {placeholder} found</span>
)
}
emptyPlaceholder={`No ${placeholder} found`}
onChange={handleSelectChange}
/>
);

View File

@@ -51,13 +51,6 @@
background: var(--l1-background);
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {
background: color-mix(in srgb, var(--l1-foreground) 4%, transparent);
}

View File

@@ -9,13 +9,13 @@ import {
Flex,
Input,
InputRef,
Progress,
Space,
Spin,
TableColumnsType,
TableColumnType,
Tooltip,
} from 'antd';
import { Progress } from '@signozhq/ui/progress';
import { Typography } from '@signozhq/ui/typography';
import type { FilterDropdownProps } from 'antd/lib/table/interface';
import logEvent from 'api/common/logEvent';
@@ -59,7 +59,7 @@ function ProgressRender(item: string | number): JSX.Element {
<Progress
percent={percent}
strokeLinecap="butt"
size="small"
showInfo
strokeColor={((): string => {
const cpuPercent = percent;
if (cpuPercent >= 90) {

View File

@@ -1,7 +1,6 @@
import { useHistory, useLocation } from 'react-router-dom';
import { Select, Spin } from 'antd';
import { ComboboxSimple } from '@signozhq/ui/combobox';
import { Typography } from '@signozhq/ui/typography';
import { SelectMaxTagPlaceholder } from 'components/MessagingQueues/MQCommon/MQCommon';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
@@ -27,30 +26,18 @@ function CeleryTaskConfigOptions(): JSX.Element {
<Typography.Text style={{ whiteSpace: 'nowrap' }}>
Task Name
</Typography.Text>
<Select
<ComboboxSimple
placeholder="Task Name"
showSearch
mode="multiple"
options={options}
multiple
items={options}
loading={isFetching}
className="config-select-option"
onSearch={handleSearch}
maxTagCount={4}
maxTagPlaceholder={SelectMaxTagPlaceholder}
value={getValuesFromQueryParams(QueryParams.taskName, urlQuery) || []}
notFoundContent={
isFetching ? (
<span>
<Spin size="small" /> Loading...
</span>
) : (
<span>No Task Name found</span>
)
}
emptyPlaceholder="No Task Name found"
onChange={(value): void => {
handleSearch('');
setQueryParamsFromOptions(
value,
value as string[],
urlQuery,
history,
location,

View File

@@ -1,7 +1,7 @@
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import type { DefaultOptionType } from 'antd/es/select';
import type { ComboboxSimpleItem } from '@signozhq/ui/combobox';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
@@ -21,7 +21,7 @@ export interface Filters {
}
export interface GetAllFiltersResponse {
options: DefaultOptionType[];
options: ComboboxSimpleItem[];
isFetching: boolean;
}

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import type { DefaultOptionType } from 'antd/es/select';
import type { ComboboxSimpleItem } from '@signozhq/ui/combobox';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
@@ -17,7 +17,7 @@ export const useCeleryFilterOptions = (
searchText: string;
handleSearch: (value: string) => void;
isFetching: boolean;
options: DefaultOptionType[];
options: ComboboxSimpleItem[];
} => {
const [searchText, setSearchText] = useState<string>('');
const { isFetching, options } = useGetAllFilters({

View File

@@ -5,17 +5,25 @@ import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Color } from '@signozhq/design-tokens';
import { Select, Tag, Tooltip } from 'antd';
import {
Combobox,
ComboboxCommand,
ComboboxContent,
ComboboxEmpty,
ComboboxItem,
ComboboxList,
ComboboxMultiTrigger,
ComboboxPill,
} from '@signozhq/ui/combobox';
import { Tag, Tooltip } from 'antd';
import {
OPERATORS,
QUERY_BUILDER_OPERATORS_BY_TYPES,
QUERY_BUILDER_SEARCH_VALUES,
} from 'constants/queryBuilder';
import { CustomTagProps } from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { selectStyle } from 'container/QueryBuilder/filters/QueryBuilderSearch/config';
import { PLACEHOLDER } from 'container/QueryBuilder/filters/QueryBuilderSearch/constant';
import { TypographyText } from 'container/QueryBuilder/filters/QueryBuilderSearch/style';
@@ -38,7 +46,6 @@ import { operatorTypeMapper } from 'hooks/queryBuilder/useOperatorType';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { isArray, isEmpty, isEqual, isObject } from 'lodash-es';
import { ChevronDown, ChevronUp } from '@signozhq/icons';
import type { BaseSelectRef } from 'rc-select';
import {
BaseAutocompleteData,
DataTypes,
@@ -47,7 +54,6 @@ import {
IBuilderQuery,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
import './ClientSideQBSearch.styles.scss';
@@ -93,8 +99,6 @@ function ClientSideQBSearch(
const isDarkMode = useIsDarkMode();
const selectRef = useRef<BaseSelectRef>(null);
const [isOpen, setIsOpen] = useState<boolean>(false);
// create the tags from the initial query here, this should only be computed on the first load as post that tags and query will be always in sync.
@@ -214,7 +218,7 @@ function ClientSideQBSearch(
}, []);
const onInputKeyDownHandler = useCallback(
(event: KeyboardEvent<Element>): void => {
(event: KeyboardEvent<HTMLInputElement>): void => {
if (event.key === 'Backspace' && !searchValue) {
event.stopPropagation();
setTags((prev) => prev.slice(0, -1));
@@ -516,29 +520,23 @@ function ClientSideQBSearch(
[tags],
);
const onTagRender = ({
value,
closable,
onClose,
}: CustomTagProps): React.ReactElement => {
const renderPill = (value: string, index: number): React.ReactElement => {
const { tagOperator } = getTagToken(value);
const isInNin = isInNInOperator(tagOperator);
const chipValue = isInNin
? value?.trim()?.replace(/,\s*$/, '')
: value?.trim();
const indexInQueryTags = queryTags.findIndex((qTag) => isEqual(qTag, value));
const tagDetails = tags[indexInQueryTags];
const tagDetails = tags[index];
const onCloseHandler = (): void => {
onClose();
setSearchValue('');
setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails)));
};
const tagEditHandler = (value: string): void => {
const tagEditHandler = (next: string): void => {
setCurrentFilterItem(tagDetails);
setSearchValue(value);
setSearchValue(next);
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails)));
};
@@ -546,12 +544,13 @@ function ClientSideQBSearch(
const isDisabled = !!searchValue;
return (
<span className="qb-search-bar-tokenised-tags">
<Tag
closable={!searchValue && closable}
onClose={onCloseHandler}
className={tagDetails?.key?.type || ''}
>
<ComboboxPill
key={`${value}-${index}`}
value={value}
onRemove={onCloseHandler}
className="qb-search-bar-tokenised-tags"
>
<Tag closable={false} className={tagDetails?.key?.type || ''}>
<Tooltip title={chipValue}>
<TypographyText
$isInNin={isInNin}
@@ -567,7 +566,7 @@ function ClientSideQBSearch(
</TypographyText>
</Tooltip>
</Tag>
</span>
</ComboboxPill>
);
};
@@ -588,59 +587,73 @@ function ClientSideQBSearch(
);
}, [isDarkMode, isOpen, suffixIcon]);
const handleItemSelect = (rawValue: string): void => {
handleDropdownSelect(rawValue);
setIsOpen(true);
};
const handleInputKeyDownWrapper = (
e: KeyboardEvent<HTMLInputElement>,
): void => {
onInputKeyDownHandler(e);
};
return (
<div className="query-builder-search-v2 ">
<Select
ref={selectRef}
getPopupContainer={popupContainer}
virtual={false}
showSearch
tagRender={onTagRender}
transitionName=""
choiceTransitionName=""
filterOption={false}
open={isOpen}
suffixIcon={suffixIconContent}
onDropdownVisibleChange={setIsOpen}
autoClearSearchValue={false}
mode="multiple"
placeholder={placeholder}
value={queryTags}
searchValue={searchValue}
className={className}
rootClassName="query-builder-search client-side-qb-search"
disabled={!attributeKeys.length}
style={selectStyle}
onSearch={handleSearch}
onSelect={handleDropdownSelect}
onInputKeyDown={onInputKeyDownHandler}
notFoundContent={null}
showAction={['focus']}
onBlur={handleOnBlur}
>
{dropdownOptions.map((option) => {
let val = option.value;
try {
if (isObject(option.value)) {
val = JSON.stringify(option.value);
} else {
val = option.value;
}
} catch {
val = option.value;
}
return (
<Select.Option key={isObject(val) ? `select-option` : val} value={val}>
<Suggestions
label={option.label}
value={option.value}
option={currentState}
searchValue={searchValue}
/>
</Select.Option>
);
})}
</Select>
<div className="query-builder-search-v2" style={selectStyle}>
<Combobox open={isOpen} onOpenChange={setIsOpen}>
<ComboboxMultiTrigger
id="client-side-qb-search-trigger"
testId="client-side-qb-search"
className={className ?? 'query-builder-search client-side-qb-search'}
placeholder={placeholder ?? PLACEHOLDER}
inputValue={searchValue}
onInputChange={handleSearch}
onKeyDown={handleInputKeyDownWrapper}
onFocus={(): void => setIsOpen(true)}
disabled={!attributeKeys.length}
>
{queryTags.map((tagValue, idx) => renderPill(tagValue, idx))}
</ComboboxMultiTrigger>
<ComboboxContent
onCloseAutoFocus={(): void => {
handleOnBlur();
}}
>
<ComboboxCommand shouldFilter={false}>
<ComboboxList>
{dropdownOptions.map((option) => {
let val = option.value as unknown as string;
try {
if (isObject(option.value)) {
val = JSON.stringify(option.value);
}
} catch {
val = String(option.value);
}
const itemKey = isObject(val) ? `select-option` : String(val);
return (
<ComboboxItem
key={itemKey}
value={String(val)}
onSelect={(): void => handleItemSelect(String(val))}
>
<Suggestions
label={option.label}
value={option.value}
option={currentState}
searchValue={searchValue}
/>
</ComboboxItem>
);
})}
<ComboboxEmpty>No results found.</ComboboxEmpty>
</ComboboxList>
</ComboboxCommand>
</ComboboxContent>
</Combobox>
<div className="qb-search-suffix" aria-hidden>
{suffixIconContent}
</div>
</div>
);
}

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,15 +1,7 @@
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import {
Button,
Col,
Dropdown,
MenuProps,
Popover,
Row,
Select,
Space,
} from 'antd';
import { Button, Col, Dropdown, MenuProps, Popover, Row, Space } from 'antd';
import { ComboboxSimple } from '@signozhq/ui/combobox';
import { Typography } from '@signozhq/ui/typography';
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';
@@ -31,11 +23,7 @@ import { popupContainer } from 'utils/selectPopupContainer';
import { ExploreHeaderToolTip, SaveButtonText } from './constants';
import MenuItemGenerator from './MenuItemGenerator';
import SaveViewWithName from './SaveViewWithName';
import {
DropDownOverlay,
ExplorerCardHeadContainer,
OffSetCol,
} from './styles';
import { ExplorerCardHeadContainer, OffSetCol } from './styles';
import { ExplorerCardProps } from './types';
import { deleteViewHandler } from './utils';
import { Ellipsis, Save, Share2, Trash2 } from '@signozhq/icons';
@@ -164,6 +152,26 @@ function ExplorerCard({
const showSaveView = false;
const viewItems = useMemo(
() =>
(viewsData?.data.data ?? []).map((view) => ({
value: view.name,
label: (
<MenuItemGenerator
viewName={view.name}
viewKey={viewKey}
createdBy={view.createdBy}
uuid={view.id}
refetchAllView={refetchAllView}
viewData={viewsData?.data.data ?? []}
sourcePage={sourcepage}
/>
),
displayValue: view.name,
})),
[refetchAllView, sourcepage, viewKey, viewsData?.data.data],
);
return (
<>
{showSaveView && (
@@ -183,30 +191,12 @@ function ExplorerCard({
<Space size="large">
{viewsData?.data.data && viewsData?.data.data.length && (
<Space>
<Select
getPopupContainer={popupContainer}
<ComboboxSimple
loading={isLoading || isRefetching}
showSearch
placeholder="Select a view"
dropdownStyle={DropDownOverlay}
dropdownMatchSelectWidth={false}
optionLabelProp="value"
items={viewItems}
value={viewName || undefined}
>
{viewsData?.data.data.map((view) => (
<Select.Option key={view.id} value={view.name}>
<MenuItemGenerator
viewName={view.name}
viewKey={viewKey}
createdBy={view.createdBy}
uuid={view.id}
refetchAllView={refetchAllView}
viewData={viewsData.data.data}
sourcePage={sourcepage}
/>
</Select.Option>
))}
</Select>
/>
</Space>
)}
{isQueryUpdated && (

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

@@ -20,9 +20,9 @@
padding: 0px 8px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l2-border);
background: var(--l2-background);
color: var(--l2-foreground);
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
color: var(--input-with-label-color, var(--l2-foreground));
display: flex;
justify-content: flex-start;
@@ -35,21 +35,28 @@
min-width: 150px;
font-family: 'Space Mono', monospace !important;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
border-radius: 2px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
color: var(--input-with-label-color, var(--l2-foreground));
border-right: none;
border-left: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
font-size: 12px !important;
line-height: 27px;
line-height: 25px;
&.input__has-label-after {
border-left: none;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
&.input__has-close-button {
border-right: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
&::placeholder {
color: var(--l2-foreground) !important;
color: var(--input-with-label-color, var(--l3-foreground)) !important;
font-size: 12px !important;
}
&[type='number']::-webkit-inner-spin-button,
@@ -63,25 +70,24 @@
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l2-border);
background: var(--l2-background);
height: 38px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
height: 100%;
width: 38px;
}
&.labelAfter {
.input {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
border-radius: 2px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
.label {
border-left: none;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-radius: 0px 2px 2px 0px;
}
}
}

View File

@@ -44,7 +44,10 @@ function InputWithLabel({
>
{!labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
<Input
className="input"
className={cx('input', {
'input__has-label-after': !labelAfter,
'input__has-close-button': !!onClose,
})}
placeholder={placeholder}
type={type}
value={inputValue}

View File

@@ -1,12 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Style } from '@signozhq/design-tokens';
import { ChevronDown, Plus, Trash2, X } from '@signozhq/icons';
import { Plus, Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Callout } from '@signozhq/ui/callout';
import { DialogFooter, DialogWrapper } from '@signozhq/ui/dialog';
import { Input } from '@signozhq/ui/input';
import { SelectSimple } from '@signozhq/ui/select';
import { toast } from '@signozhq/ui/sonner';
import { Select } from 'antd';
import inviteUsers from 'api/v1/invite/bulk/create';
import sendInvite from 'api/v1/invite/create';
import { cloneDeep, debounce } from 'lodash-es';
@@ -15,7 +15,6 @@ import APIError from 'types/api/error';
import { ROLES } from 'types/roles';
import { EMAIL_REGEX } from 'utils/app';
import { getBaseUrl } from 'utils/basePath';
import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
import './InviteMembersModal.styles.scss';
@@ -254,18 +253,17 @@ function InviteMembersModal({
)}
</div>
<div className="team-member-cell role-cell">
<Select
<SelectSimple
value={row.role || undefined}
onChange={(role): void => updateRole(row.id, role as ROLES)}
className="team-member-role-select"
placeholder="Select roles"
suffixIcon={<ChevronDown size={14} />}
getPopupContainer={popupContainer}
>
<Select.Option value="VIEWER">Viewer</Select.Option>
<Select.Option value="EDITOR">Editor</Select.Option>
<Select.Option value="ADMIN">Admin</Select.Option>
</Select>
items={[
{ value: 'VIEWER', label: 'Viewer' },
{ value: 'EDITOR', label: 'Editor' },
{ value: 'ADMIN', label: 'Admin' },
]}
/>
</div>
<div className="team-member-cell action-cell">
{rows.length > 1 && (

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Input, InputNumber, Popover, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type { DefaultOptionType } from 'antd/es/select';
import type { ComboboxSimpleItem } from '@signozhq/ui/combobox';
import cx from 'classnames';
import { LogViewMode } from 'container/LogsTable';
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
@@ -113,15 +113,11 @@ function OptionsMenu({
function handleColumnSelection(
currentIndex: number,
optionsData: DefaultOptionType[],
optionsData: ComboboxSimpleItem[],
): void {
const currentItem = optionsData[currentIndex];
const itemLength = optionsData.length;
if (addColumn && addColumn?.onSelect) {
addColumn?.onSelect(selectedValue, {
label: currentItem.label,
disabled: false,
});
if (addColumn && addColumn?.onSelect && selectedValue) {
addColumn?.onSelect(selectedValue);
// if the last element is selected then select the previous one
if (currentIndex === itemLength - 1) {
@@ -175,7 +171,7 @@ function OptionsMenu({
}
case 'Enter':
e.preventDefault();
handleColumnSelection(currentIndex, optionsData);
handleColumnSelection(currentIndex, optionsData as ComboboxSimpleItem[]);
break;
default:
break;
@@ -317,7 +313,10 @@ function OptionsMenu({
}}
onClick={(eve): void => {
eve.stopPropagation();
handleColumnSelection(index, addColumn?.options || []);
handleColumnSelection(
index,
(addColumn?.options || []) as ComboboxSimpleItem[],
);
}}
>
<div className="name">

View File

@@ -10,7 +10,8 @@ import {
Loader,
X,
} from '@signozhq/icons';
import { Modal, Select, Spin, Tooltip, Tree, TreeDataNode } from 'antd';
import { Modal, Spin, Tooltip, Tree, TreeDataNode } from 'antd';
import { SelectSimple } from '@signozhq/ui/select';
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
@@ -181,8 +182,8 @@ function AttributeCheckList({
const [filter, setFilter] = useState<AttributesFilters>(AttributesFilters.ALL);
const [treeData, setTreeData] = useState<TreeDataNode[]>([]);
const handleFilterChange = (value: AttributesFilters): void => {
setFilter(value);
const handleFilterChange = (value: string | string[]): void => {
setFilter(value as AttributesFilters);
};
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const history = useHistory();
@@ -237,11 +238,11 @@ function AttributeCheckList({
</div>
) : (
<div className="modal-content">
<Select
<SelectSimple
defaultValue={AttributesFilters.ALL}
className="attribute-select"
onChange={handleFilterChange}
options={[
items={[
{
value: AttributesFilters.ALL,
label: AttributeLabels({ title: 'Attributes: All' }),

View File

@@ -1,6 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import type { DefaultOptionType } from 'antd/es/select';
import type { ComboboxSimpleItem } from '@signozhq/ui/combobox';
import { Info } from '@signozhq/icons';
import './MQCommon.styles.scss';
@@ -35,7 +35,7 @@ export function ComingSoon(): JSX.Element {
}
export function SelectMaxTagPlaceholder(
omittedValues: Partial<DefaultOptionType>[],
omittedValues: Partial<ComboboxSimpleItem>[],
): JSX.Element {
return (
<Tooltip title={omittedValues.map(({ value }) => value).join(', ')}>

View File

@@ -18,7 +18,9 @@ import {
RefreshCw,
} from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Checkbox, Select } from 'antd';
// oxlint-disable-next-line signoz/no-antd-components This component for now is too complex to be migrated in one PR
import { Select } from 'antd';
import { Button, Checkbox } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip/TextToolTip';

View File

@@ -16,6 +16,7 @@ import {
X,
} from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
// oxlint-disable-next-line signoz/no-antd-components This component for now is too complex to be migrated in one PR
import { Select } from 'antd';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip';

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

@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { Select, Spin } from 'antd';
import { ComboboxSimple, ComboboxSimpleItem } from '@signozhq/ui/combobox';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { DataSource } from 'types/common/queryBuilder';
@@ -13,48 +13,25 @@ interface ListViewOrderByProps {
dataSource: DataSource;
}
// Loader component for the dropdown when loading or no results
function Loader({ isLoading }: { isLoading: boolean }): JSX.Element {
return (
<div className="order-by-loading-container">
{isLoading ? <Spin size="default" /> : 'No results found'}
</div>
);
}
function ListViewOrderBy({
value,
onChange,
dataSource,
}: ListViewOrderByProps): JSX.Element {
const [searchInput, setSearchInput] = useState('');
const [debouncedInput, setDebouncedInput] = useState('');
const [selectOptions, setSelectOptions] = useState<
{ label: string; value: string }[]
>([]);
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const [selectOptions, setSelectOptions] = useState<ComboboxSimpleItem[]>([]);
// Fetch key suggestions based on debounced input
// Fetch key suggestions once; ComboboxSimple handles local filtering.
const { data, isLoading } = useQuery({
queryKey: ['orderByKeySuggestions', dataSource, debouncedInput],
queryKey: ['orderByKeySuggestions', dataSource],
queryFn: async () => {
const response = await getKeySuggestions({
signal: dataSource,
searchText: debouncedInput,
searchText: '',
});
return response.data;
},
});
useEffect(
() => (): void => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
},
[],
);
// Update options when API data changes
useEffect(() => {
const rawKeys: QueryKeyDataSuggestionsProps[] = data?.data?.keys
@@ -62,52 +39,33 @@ function ListViewOrderBy({
: [];
const keyNames = rawKeys.map((key) => key.name);
const uniqueKeys = [
...new Set(searchInput ? keyNames : ['timestamp', ...keyNames]),
];
const uniqueKeys = [...new Set(['timestamp', ...keyNames])];
const updatedOptions = uniqueKeys.flatMap((key) => [
const updatedOptions: ComboboxSimpleItem[] = uniqueKeys.flatMap((key) => [
{ label: `${key} (desc)`, value: `${key}:desc` },
{ label: `${key} (asc)`, value: `${key}:asc` },
]);
setSelectOptions(updatedOptions);
}, [data, searchInput]);
}, [data]);
// Handle search input with debounce
const handleSearch = (input: string): void => {
setSearchInput(input);
// Filter current options for instant client-side match
const filteredOptions = selectOptions.filter((option) =>
option.value.toLowerCase().includes(input.trim().toLowerCase()),
);
// If no match found or input is empty, trigger debounced fetch
if (filteredOptions.length === 0 || input === '') {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
debounceTimer.current = setTimeout(() => {
setDebouncedInput(input);
}, 100);
}
};
const handleChange = useMemo(
() =>
(val: string | string[]): void => {
onChange(val as string);
},
[onChange],
);
return (
<Select
showSearch
<ComboboxSimple
value={value}
onChange={onChange}
onSearch={handleSearch}
notFoundContent={<Loader isLoading={isLoading} />}
onChange={handleChange}
loading={isLoading}
placeholder="Select a field"
style={{ width: 200 }}
options={selectOptions}
filterOption={(input, option): boolean =>
(option?.value ?? '').toLowerCase().includes(input.trim().toLowerCase())
}
items={selectOptions}
emptyPlaceholder="No results found"
/>
);
}

View File

@@ -22,6 +22,37 @@
border-right: none;
border-left: none;
--select-trigger-height: 2.25rem;
--select-trigger-background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
);
--select-trigger-border-color: var(
--query-builder-v2-border-color,
var(--l2-border)
);
--combobox-trigger-height: 2.25rem;
--combobox-trigger-padding: 7px var(--spacing-6);
--combobox-trigger-background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
);
--combobox-trigger-border-color: var(
--query-builder-v2-border-color,
var(--l2-border)
);
[data-slot='combobox-trigger'],
[data-slot='select-trigger'] {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
[data-slot='combobox-placeholder'],
[data-slot='select-placeholder'] {
color: var(--query-builder-v2-placeholder-color, var(--l3-foreground));
}
.qb-content-container {
display: flex;
flex-direction: column;
@@ -80,8 +111,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--l1-border),
var(--l1-border) 4px,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
transparent 4px,
transparent 8px
);
@@ -101,7 +132,8 @@
top: 12px;
width: 6px;
height: 6px;
border-left: 6px dotted var(--l1-border);
border-left: 6px dotted
var(--query-builder-v2-border-color, var(--l2-border));
}
/* Horizontal line pointing from vertical to the item */
@@ -114,8 +146,8 @@
height: 1px;
background: repeating-linear-gradient(
to right,
var(--l1-border),
var(--l1-border) 4px,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
transparent 4px,
transparent 8px
);
@@ -241,7 +273,8 @@
top: 12px;
width: 6px;
height: 6px;
border-left: 6px dotted var(--l1-border);
border-left: 6px dotted
var(--query-builder-v2-border-color, var(--l2-border));
}
/* Horizontal line pointing from vertical to the item */
@@ -254,8 +287,8 @@
height: 1px;
background: repeating-linear-gradient(
to right,
var(--l1-border),
var(--l1-border) 4px,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
transparent 4px,
transparent 8px
);
@@ -273,6 +306,16 @@
line-height: 16px; /* 128.571% */
resize: none;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
&:placeholder {
color: var(--query-builder-v2-placeholder-color, var(--l3-foreground));
}
}
.formula-legend {
@@ -282,15 +325,30 @@
.ant-input-group-addon {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: var(--l2-background);
color: var(--l2-foreground);
background: var(
--query-builder-v2-background-color,
var(--l2-background)
);
color: var(--query-builder-v2-color, var(--l2-foreground));
font-size: 12px;
font-weight: 300;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
}
.ant-input {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
&:placeholder {
color: var(--query-builder-v2-placeholder-color, var(--l3-foreground));
}
}
}
}
@@ -323,8 +381,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--l1-border),
var(--l1-border) 4px,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
transparent 4px,
transparent 8px
);
@@ -395,8 +453,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--l1-border),
var(--l1-border) 4px,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
transparent 4px,
transparent 8px
);
@@ -412,7 +470,7 @@
min-width: 120px;
border-radius: 2px;
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--l1-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
@@ -457,7 +515,7 @@
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--l1-border) !important;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
background: var(--l1-background) !important;
height: 34px !important;
box-sizing: border-box !important;

View File

@@ -57,6 +57,7 @@
.group-by-filter-container {
min-width: 340px !important;
--combobox-trigger-height: auto;
}
.metrics-aggregation-section-content-item {
@@ -146,17 +147,3 @@
}
}
}
.metrics-operators-select {
border-radius: 2px;
border: 1.005px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}

View File

@@ -118,7 +118,6 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
value={queryAggregation.timeAggregation || ''}
onChange={handleChangeOperator}
operators={operators}
className="metrics-operators-select"
/>
</div>
</div>

View File

@@ -6,11 +6,13 @@
gap: 8px;
width: 100%;
--select-trigger-width: auto;
.ant-select-selection-search-input {
font-size: 12px !important;
line-height: 27px;
line-height: 25px;
&::placeholder {
color: var(--l2-foreground) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
font-size: 12px !important;
}
}
@@ -22,9 +24,9 @@
.ant-select-selector {
width: 100%;
border-radius: 2px;
border: 1px solid var(--l1-border) !important;
background: var(--l1-background);
color: var(--l1-foreground);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
background: var(--query-builder-v2-background-color, var(--l2-background));
color: var(--query-builder-v2-color, var(--l2-foreground));
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
@@ -33,20 +35,23 @@
min-height: 36px;
.ant-select-selection-placeholder {
color: var(--l2-foreground) !important;
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
font-size: 12px !important;
}
}
.ant-select-dropdown {
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.ant-select-item {
color: var(--l1-foreground);
color: var(--query-builder-v2-color, var(--l2-foreground));
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
@@ -55,12 +60,18 @@
&:hover,
&.ant-select-item-option-active {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selected-background-color,
var(--l3-background)
) !important;
}
&.ant-select-item-option-selected {
background: var(--l3-background) !important;
border: 1px solid var(--l1-border);
background: var(
--query-builder-v2-selected-background-color,
var(--l3-background)
) !important;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
font-weight: 600;
}
}

View File

@@ -1,5 +1,5 @@
import { memo, useCallback, useMemo } from 'react';
import { Select } from 'antd';
import { SelectSimple, type SelectSimpleItem } from '@signozhq/ui/select';
import {
initialQueriesMap,
initialQueryMeterWithType,
@@ -10,7 +10,6 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
import {
getPreviousQueryFromKey,
@@ -21,7 +20,7 @@ import {
import './MetricsSelect.styles.scss';
export const SOURCE_OPTIONS: SelectOption<string, string>[] = [
export const SOURCE_OPTIONS: SelectSimpleItem[] = [
{ value: 'metrics', label: 'Metrics' },
{ value: 'meter', label: 'Meter' },
];
@@ -140,14 +139,14 @@ export const MetricsSelect = memo(function MetricsSelect({
return (
<div className="metrics-source-select-container">
{signalSourceChangeEnabled && (
<Select
<SelectSimple
className="source-selector"
placeholder="Source"
options={SOURCE_OPTIONS}
items={SOURCE_OPTIONS}
value={source}
defaultValue="metrics"
data-testid={`metrics-source-selector-${index}`}
onChange={handleSignalSourceChange}
testId={`metrics-source-selector-${index}`}
onChange={(value): void => handleSignalSourceChange(value as string)}
/>
)}

View File

@@ -22,34 +22,35 @@
font-style: normal;
font-weight: var(--font-weight-normal);
color: var(--l2-foreground);
color: var(--query-builder-v2-color, var(--l2-foreground));
}
.tab {
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-left: none;
min-width: 120px;
height: 36px;
line-height: 36px;
&:first-child {
border-left: 1px solid var(--l1-border);
border-left: 1px solid
var(--query-builder-v2-border-color, var(--l2-border));
}
}
.tab::before {
background: var(--l1-border);
background: var(--query-builder-v2-border-color, var(--l2-border));
}
.selected-view {
color: var(--text-robin-500);
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
display: none;
}
.selected-view::before {
background: var(--l1-border);
background: var(--query-builder-v2-border-color, var(--l2-border));
}
}
@@ -58,7 +59,7 @@
height: 30px;
border-radius: 2px;
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--l3-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
@@ -71,10 +72,13 @@
align-items: center;
.having-filter-select-container {
position: relative;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
align-items: flex-start;
background: var(--query-builder-v2-background-color, var(--l2-background));
padding-right: 38px;
.having-filter-select-editor {
border-radius: 2px;
@@ -99,15 +103,17 @@
}
.cm-content {
border-radius: 2px;
border: 1px solid var(--l1-border);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-left-width: 0px;
border-right-width: 0px;
padding: 0px !important;
background-color: var(--l2-background) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
&:focus-within {
border-color: var(--l1-border);
border-color: var(--query-builder-v2-border-color, var(--l2-border));
}
}
@@ -211,17 +217,32 @@
}
.cm-line {
line-height: 36px !important;
min-height: 34px;
line-height: 32px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(--l2-background) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
&,
.ͼ1a {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
::-moz-selection {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
@@ -230,8 +251,11 @@
}
.chip-decorator {
background: var(--l3-background) !important;
color: var(--l1-foreground) !important;
background: var(
--query-builder-v2-chip-decorator-background-color,
var(--l3-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
border-radius: 4px;
padding: 2px 4px;
margin-right: 4px;
@@ -239,24 +263,36 @@
}
.cm-selectionBackground {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
.cm-activeLine > span {
font-size: 12px !important;
}
.cm-placeholder {
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
}
}
}
.close-btn {
position: absolute;
top: 0;
right: 0;
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l2-border);
background: var(--l2-background);
height: 38px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
height: 100%;
width: 38px;
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}

View File

@@ -23,7 +23,7 @@
flex: 1;
min-width: 0;
font-size: 12px;
color: var(--l2-foreground) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
&.error {
.cm-editor {
@@ -51,14 +51,15 @@
.cm-content {
border-radius: 2px;
border: 1px solid var(--l1-border);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
padding: 0px !important;
background-color: var(--l1-background) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
&:focus-within {
border-color: var(--l1-border);
border-color: var(--query-builder-v2-border-color, var(--l2-border));
}
}
@@ -74,7 +75,7 @@
right: 0px !important;
border-radius: 4px;
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: linear-gradient(
139deg,
color-mix(in srgb, var(--card) 80%, transparent) 0%,
@@ -118,7 +119,7 @@
box-sizing: border-box;
overflow: hidden;
color: var(--l2-foreground) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
font-family: 'Space Mono', monospace !important;
.cm-completionIcon {
@@ -127,7 +128,10 @@
&:hover,
&[aria-selected='true'] {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
color: var(--l1-foreground) !important;
font-weight: 600 !important;
}
@@ -142,15 +146,24 @@
.cm-line {
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(--l2-background) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
::-moz-selection {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
@@ -159,8 +172,11 @@
}
.chip-decorator {
background: var(--l3-background) !important;
color: var(--l1-foreground) !important;
background: var(
--query-builder-v2-chip-decorator-background-color,
var(--l3-background)
) !important;
color: var(--query-builder-v2-color, var(--l1-foreground)) !important;
border-radius: 4px;
padding: 2px 4px;
margin-right: 4px;
@@ -168,7 +184,10 @@
}
.cm-selectionBackground {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
}
@@ -201,12 +220,11 @@
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
height: 38px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
height: 100%;
width: 38px;
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
@@ -217,13 +235,13 @@
height: 36px;
line-height: 36px;
border-radius: 2px;
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
font-family: 'Space Mono', monospace !important;
&::placeholder {
color: var(--l1-foreground);
color: var(--query-builder-v2-color, var(--l2-foreground));
opacity: 0.5;
}
}
@@ -240,7 +258,7 @@
input {
max-width: 120px;
&::placeholder {
color: var(--l2-foreground);
color: var(--query-builder-v2-color, var(--l2-foreground));
}
}
}
@@ -251,8 +269,8 @@
.query-aggregation-error-popover {
.ant-popover-inner {
background-color: var(--l1-border);
border: 1px solid var(--l1-border);
background-color: var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-radius: 4px;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
}

View File

@@ -1,7 +1,7 @@
.add-trace-operator-button,
.add-new-query-button,
.add-formula-button {
border: 1px solid var(--l1-border);
background: var(--l2-background);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}

View File

@@ -34,11 +34,14 @@
.query-status-container {
width: 32px;
background-color: var(--l1-background) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-radius: 2px;
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
@@ -77,16 +80,16 @@
.cm-content {
border-radius: 2px;
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
padding: 0px !important;
&:focus-within {
border-color: var(--l1-border);
border-color: var(--query-builder-v2-border-color, var(--l2-border));
}
}
&.cm-focused {
outline: 1px solid var(--l1-border);
outline: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
}
.cm-tooltip-autocomplete {
@@ -148,11 +151,17 @@
font-family: 'Space Mono', monospace !important;
background-color: var(--l1-background) !important;
color: var(--l2-foreground) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
&:hover {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
}
.cm-completionIcon {
@@ -160,7 +169,10 @@
}
&[aria-selected='true'] {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
font-weight: 600 !important;
}
}
@@ -172,25 +184,49 @@
}
.cm-line {
line-height: 34px !important;
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(--l2-background) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
&,
.ͼ1a {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
::-moz-selection {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
}
.cm-selectionBackground {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
.cm-placeholder {
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
}
}
.cursor-position {

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,7 +4,8 @@ import type {
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
import { Button, Dropdown, Flex, MenuProps } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
@@ -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,
@@ -88,10 +87,14 @@ function DynamicColumnTable({
const items: MenuProps['items'] =
dynamicColumns?.map((column, 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>

View File

@@ -1,11 +1,10 @@
import { CircleAlert, RefreshCw } from '@signozhq/icons';
import { Checkbox, Select } from 'antd';
import { ComboboxSimple, ComboboxSimpleItem } from '@signozhq/ui/combobox';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useListRoles } from 'api/generated/services/role';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import cx from 'classnames';
import APIError from 'types/api/error';
import { popupContainer } from 'utils/selectPopupContainer';
import './RolesSelect.styles.scss';
@@ -74,7 +73,6 @@ interface BaseProps {
id?: string;
placeholder?: string;
className?: string;
getPopupContainer?: (trigger: HTMLElement) => HTMLElement;
roles?: AuthtypesRoleDTO[];
loading?: boolean;
isError?: boolean;
@@ -112,14 +110,13 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
});
const roles = externalRoles ?? data?.data ?? [];
const options = getRoleOptions(roles);
const items: ComboboxSimpleItem[] = getRoleOptions(roles);
const {
mode,
id,
placeholder = 'Select role',
className,
getPopupContainer = popupContainer,
loading = internalLoading,
isError = internalError,
error = convertToApiError(internalErrorObj),
@@ -127,55 +124,47 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
disabled,
} = props;
const notFoundContent = isError ? (
<ErrorContent error={error} onRefetch={onRefetch} />
) : undefined;
const emptyPlaceholder = isError
? error?.message || 'Failed to load roles'
: 'No roles available';
if (mode === 'multiple') {
const { value = [], onChange } = props as MultipleProps;
return (
<Select
id={id}
mode="multiple"
value={value}
onChange={onChange}
placeholder={placeholder}
className={cx('roles-select', className)}
loading={loading}
notFoundContent={notFoundContent}
options={options}
optionFilterProp="label"
optionRender={(option): JSX.Element => (
<Checkbox
checked={value.includes(option.value as string)}
style={{ pointerEvents: 'none' }}
>
{option.label}
</Checkbox>
)}
getPopupContainer={getPopupContainer}
disabled={disabled}
/>
<>
<ComboboxSimple
id={id}
multiple
value={value}
onChange={(v): void => onChange?.(v as string[])}
placeholder={placeholder}
className={cx('roles-select', className)}
loading={loading}
emptyPlaceholder={emptyPlaceholder}
items={items}
style={disabled ? { pointerEvents: 'none', opacity: 0.5 } : undefined}
/>
{isError && <ErrorContent error={error} onRefetch={onRefetch} />}
</>
);
}
const { value, onChange, allowClear = true } = props as SingleProps;
const { value, onChange } = props as SingleProps;
return (
<Select
id={id}
showSearch
value={value || undefined}
onChange={onChange}
placeholder={placeholder}
allowClear={allowClear}
className={cx('roles-single-select', className)}
loading={loading}
notFoundContent={notFoundContent}
options={options}
optionFilterProp="label"
getPopupContainer={getPopupContainer}
disabled={disabled}
/>
<>
<ComboboxSimple
id={id}
value={value || undefined}
onChange={(v): void => onChange?.((v as string) || undefined)}
placeholder={placeholder}
className={cx('roles-single-select', className)}
loading={loading}
emptyPlaceholder={emptyPlaceholder}
items={items}
style={disabled ? { pointerEvents: 'none', opacity: 0.5 } : undefined}
/>
{isError && <ErrorContent error={error} onRefetch={onRefetch} />}
</>
);
}

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

@@ -16,7 +16,7 @@ import {
horizontalListSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import { ComboboxSimple } from '@signozhq/ui/combobox';
import { ComboboxSimple, ComboboxSimpleItem } from '@signozhq/ui/combobox';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import { Pagination } from '@signozhq/ui/pagination';
import type { Row } from '@tanstack/react-table';
@@ -51,7 +51,7 @@ import { useEffectiveData } from './useEffectiveData';
import { useFlatItems } from './useFlatItems';
import { useRowKeyData } from './useRowKeyData';
import { useTableParams } from './useTableParams';
import { buildPageSizeItems, buildTanstackColumnDef } from './utils';
import { buildTanstackColumnDef } from './utils';
import { VirtuosoTableColGroup } from './VirtuosoTableColGroup';
import tableStyles from './TanStackTable.module.scss';
@@ -66,6 +66,14 @@ const INCREASE_VIEWPORT_BY = { top: 500, bottom: 500 };
const noopColumnVisibility = (): void => {};
const paginationPageSizeItems: ComboboxSimpleItem[] = [10, 20, 30, 50, 100].map(
(value) => ({
value: value.toString(),
label: value.toString(),
displayValue: value.toString(),
}),
);
// eslint-disable-next-line sonarjs/cognitive-complexity
function TanStackTableInner<TData>(
{
@@ -81,6 +89,7 @@ function TanStackTableInner<TData>(
enableQueryParams,
pagination,
paginationClassname,
onSort,
onEndReached,
getRowKey,
getItemKey,
@@ -93,7 +102,6 @@ function TanStackTableInner<TData>(
onRowClick,
onRowClickNewTab,
onRowDeactivate,
onSort,
activeRowIndex,
renderExpandedRow,
getRowCanExpand,
@@ -121,22 +129,17 @@ function TanStackTableInner<TData>(
const {
page,
limit,
setPage: internalSetPage,
setLimit: internalSetLimit,
setPage,
setLimit,
orderBy,
setOrderBy: internalSetOrderBy,
expanded,
setExpanded,
} = useTableParams(enableQueryParams, {
page: pagination?.defaultPage,
limit: pagination?.defaultLimit ?? pagination?.calculatedPageSize ?? 10,
limit: pagination?.defaultLimit,
});
const pageSizeItems = useMemo(
() => buildPageSizeItems(pagination?.calculatedPageSize),
[pagination?.calculatedPageSize],
);
const setOrderBy = useCallback(
(sort: SortState | null) => {
internalSetOrderBy(sort);
@@ -145,23 +148,6 @@ function TanStackTableInner<TData>(
[internalSetOrderBy, onSort],
);
const setPage = useCallback(
(p: number) => {
internalSetPage(p);
pagination?.onPageChange?.(p);
},
[internalSetPage, pagination],
);
const setLimit = useCallback(
(l: number) => {
internalSetLimit(l);
internalSetPage(1);
pagination?.onLimitChange?.(l);
},
[internalSetLimit, internalSetPage, pagination],
);
const isGrouped = (groupBy?.length ?? 0) > 0;
const {
@@ -635,7 +621,6 @@ function TanStackTableInner<TData>(
{pagination.showPageSize !== false && (
<div className={viewStyles.paginationPageSize}>
<ComboboxSimple
testId="pagination-page-size"
value={limit?.toString()}
defaultValue="10"
onChange={(value): void => {
@@ -646,7 +631,7 @@ function TanStackTableInner<TData>(
pagination.onPageChange?.(1);
}
}}
items={pageSizeItems}
items={paginationPageSizeItems}
/>
</div>
)}

View File

@@ -1,4 +1,4 @@
import { fireEvent, screen, waitFor, within } from '@testing-library/react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UrlUpdateEvent } from 'nuqs/adapters/testing';
@@ -23,13 +23,12 @@ jest.mock('../TanStackTable.module.scss', () => ({
},
}));
beforeAll(() => {
window.ResizeObserver = jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
});
// Mock ResizeObserver for combobox tests
global.ResizeObserver = class ResizeObserver {
observe(): void {}
unobserve(): void {}
disconnect(): void {}
};
describe('TanStackTableView Integration', () => {
describe('rendering', () => {
@@ -403,22 +402,6 @@ describe('TanStackTableView Integration', () => {
});
});
it('preserves page from URL on initial mount', async () => {
renderTanStackTable({
props: {
pagination: { total: 100, defaultPage: 1, defaultLimit: 10 },
enableQueryParams: true,
},
queryParams: { page: '3' },
});
const nav = await screen.findByRole('navigation');
const page3Button = within(nav).getByRole('button', { name: '3' });
// Page 3 should be active (from URL), not reset to defaultPage 1
expect(page3Button).toHaveAttribute('aria-current', 'page');
});
it('resets page to 1 when limit changes', async () => {
const user = userEvent.setup();
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();

View File

@@ -1,25 +0,0 @@
import { renderHook } from '@testing-library/react';
import { useCalculatedPageSize } from '../useCalculatedPageSize';
describe('useCalculatedPageSize', () => {
it('returns containerRef and null calculatedPageSize initially', () => {
const { result } = renderHook(() => useCalculatedPageSize());
expect(result.current.containerRef).toBeDefined();
expect(result.current.containerRef.current).toBeNull();
expect(result.current.calculatedPageSize).toBeNull();
});
it('accepts custom config', () => {
const { result } = renderHook(() =>
useCalculatedPageSize({
rowHeight: 50,
headerHeight: 40,
paginationHeight: 50,
minPageSize: 3,
maxPageSize: 20,
}),
);
expect(result.current.containerRef).toBeDefined();
});
});

View File

@@ -1,89 +0,0 @@
/* eslint-disable no-restricted-syntax */
import { act, renderHook } from '@testing-library/react';
import {
getPreferredPageSize,
usePreferredPageSize,
usePreferredPageSizeStore,
} from '../usePreferredPageSize.store';
const STORAGE_KEY = 'test-table';
const FULL_STORAGE_KEY = '@signoz/table-columns/test-table-preferred-page-size';
describe('usePreferredPageSize', () => {
beforeEach(() => {
localStorage.clear();
usePreferredPageSizeStore.setState({ tables: {} });
});
it('returns null when no stored value exists', () => {
const { result } = renderHook(() => usePreferredPageSize(STORAGE_KEY));
expect(result.current[0]).toBeNull();
});
it('returns null when storageKey is undefined', () => {
const { result } = renderHook(() => usePreferredPageSize(undefined));
expect(result.current[0]).toBeNull();
});
it('loads stored page size from localStorage', () => {
localStorage.setItem(FULL_STORAGE_KEY, '25');
const { result } = renderHook(() => usePreferredPageSize(STORAGE_KEY));
expect(result.current[0]).toBe(25);
});
it('ignores invalid stored values', () => {
localStorage.setItem(FULL_STORAGE_KEY, 'invalid');
const { result } = renderHook(() => usePreferredPageSize(STORAGE_KEY));
expect(result.current[0]).toBeNull();
});
it('persists page size to localStorage when set', () => {
const { result } = renderHook(() => usePreferredPageSize(STORAGE_KEY));
act(() => {
result.current[1](30);
});
expect(result.current[0]).toBe(30);
expect(localStorage.getItem(FULL_STORAGE_KEY)).toBe('30');
});
it('removes from localStorage when set to null', () => {
localStorage.setItem(FULL_STORAGE_KEY, '25');
const { result } = renderHook(() => usePreferredPageSize(STORAGE_KEY));
act(() => {
result.current[1](null);
});
expect(result.current[0]).toBeNull();
expect(localStorage.getItem(FULL_STORAGE_KEY)).toBeNull();
});
it('does nothing when storageKey is undefined and set is called', () => {
const { result } = renderHook(() => usePreferredPageSize(undefined));
act(() => {
result.current[1](30);
});
expect(result.current[0]).toBeNull();
});
});
describe('getPreferredPageSize', () => {
beforeEach(() => {
localStorage.clear();
usePreferredPageSizeStore.setState({ tables: {} });
});
it('returns null when no stored value exists', () => {
expect(getPreferredPageSize(STORAGE_KEY)).toBeNull();
});
it('returns stored value from localStorage', () => {
localStorage.setItem(FULL_STORAGE_KEY, '42');
expect(getPreferredPageSize(STORAGE_KEY)).toBe(42);
});
});

View File

@@ -7,7 +7,6 @@ import {
} from 'nuqs/adapters/testing';
import { useTableParams } from '../useTableParams';
import { usePreferredPageSizeStore } from '../usePreferredPageSize.store';
function createNuqsWrapper(
queryParams?: Record<string, string>,
@@ -544,406 +543,3 @@ describe('useTableParams (selective URL mode — partial config object)', () =>
});
});
});
describe('useTableParams (cleanupOnUnmount option)', () => {
beforeEach(() => {
jest.useFakeTimers();
localStorage.clear();
usePreferredPageSizeStore.setState({ tables: {} });
});
afterEach(() => {
jest.useRealTimers();
});
it('clears URL params on unmount when cleanupOnUnmount is true', async () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
const wrapper = createNuqsWrapper({}, onUrlUpdate);
const { result, unmount } = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit', orderBy: 'orderBy' },
{
page: 1,
limit: 10,
cleanupOnUnmount: true,
},
),
{ wrapper },
);
// Set some values
await act(async () => {
result.current.setLimit(50);
result.current.setPage(3);
jest.runAllTimers();
await Promise.resolve();
});
// Verify values set
expect(result.current.limit).toBe(50);
expect(result.current.page).toBe(3);
// Unmount triggers cleanup
unmount();
await act(async () => {
jest.runAllTimers();
await Promise.resolve();
});
// Last URL update should have cleared params
const lastUpdate = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
expect(lastUpdate[0].searchParams.get('limit')).toBeNull();
expect(lastUpdate[0].searchParams.get('page')).toBeNull();
});
it('does not clear URL params on unmount when cleanupOnUnmount is false', async () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
const wrapper = createNuqsWrapper({}, onUrlUpdate);
const { result, unmount } = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
cleanupOnUnmount: false,
},
),
{ wrapper },
);
await act(async () => {
result.current.setLimit(50);
jest.runAllTimers();
await Promise.resolve();
});
expect(result.current.limit).toBe(50);
unmount();
await act(async () => {
jest.runAllTimers();
await Promise.resolve();
});
// No new URL updates after unmount (or same count)
const lastUpdate = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
expect(lastUpdate[0].searchParams.get('limit')).toBe('50');
});
it('defaults cleanupOnUnmount to false', async () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
const wrapper = createNuqsWrapper({}, onUrlUpdate);
const { result, unmount } = renderHook(
() =>
useTableParams({ page: 'page', limit: 'limit' }, { page: 1, limit: 10 }),
{ wrapper },
);
await act(async () => {
result.current.setLimit(50);
jest.runAllTimers();
await Promise.resolve();
});
unmount();
await act(async () => {
jest.runAllTimers();
await Promise.resolve();
});
// URL should still have limit=50 (cleanup not triggered)
const lastUpdate = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
expect(lastUpdate[0].searchParams.get('limit')).toBe('50');
});
});
describe('useTableParams (auto page size with storageKey)', () => {
beforeEach(() => {
jest.useFakeTimers();
localStorage.clear();
usePreferredPageSizeStore.setState({ tables: {} });
});
afterEach(() => {
jest.useRealTimers();
});
it('uses explicit default when no URL, no calculated, no preferred', () => {
const wrapper = createNuqsWrapper();
const { result } = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'test-table',
calculatedPageSize: null,
},
),
{ wrapper },
);
// Should use explicit default (10), NOT the internal DEFAULT_LIMIT (50)
expect(result.current.limit).toBe(10);
});
it('uses calculatedPageSize when available and no preferred', () => {
const wrapper = createNuqsWrapper();
const { result } = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'test-table',
calculatedPageSize: 42,
},
),
{ wrapper },
);
expect(result.current.limit).toBe(42);
});
it('prefers stored value over calculatedPageSize', () => {
// Pre-populate the store
localStorage.setItem(
'@signoz/table-columns/test-table-preferred-page-size',
'25',
);
const wrapper = createNuqsWrapper();
const { result } = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'test-table',
calculatedPageSize: 42,
},
),
{ wrapper },
);
// Should use preferred (25), not calculated (42)
expect(result.current.limit).toBe(25);
});
it('preserves URL limit over calculated and preferred', () => {
localStorage.setItem(
'@signoz/table-columns/test-table-preferred-page-size',
'25',
);
const wrapper = createNuqsWrapper({ limit: '30' });
const { result } = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'test-table',
calculatedPageSize: 42,
},
),
{ wrapper },
);
// Should use URL (30), not preferred (25) or calculated (42)
expect(result.current.limit).toBe(30);
});
it('persists user selection when different from calculated', () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
const wrapper = createNuqsWrapper({}, onUrlUpdate);
const { result } = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'test-table',
calculatedPageSize: 42,
},
),
{ wrapper },
);
// User selects 30 (different from calculated 42)
act(() => {
result.current.setLimit(30);
jest.runAllTimers();
});
expect(result.current.limit).toBe(30);
expect(
localStorage.getItem('@signoz/table-columns/test-table-preferred-page-size'),
).toBe('30');
});
it('clears preference when user selects calculated value', () => {
// Pre-set a preference
localStorage.setItem(
'@signoz/table-columns/test-table-preferred-page-size',
'30',
);
usePreferredPageSizeStore.setState({ tables: { 'test-table': 30 } });
const wrapper = createNuqsWrapper();
const { result } = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'test-table',
calculatedPageSize: 42,
},
),
{ wrapper },
);
// User selects 42 (same as calculated)
act(() => {
result.current.setLimit(42);
jest.runAllTimers();
});
expect(result.current.limit).toBe(42);
// Preference should be cleared (null removes from storage)
expect(
localStorage.getItem('@signoz/table-columns/test-table-preferred-page-size'),
).toBeNull();
});
it('returns calculated value even before URL is synced', () => {
const wrapper = createNuqsWrapper();
const { result } = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'test-table',
calculatedPageSize: 42,
},
),
{ wrapper },
);
// Limit should be 42 (calculated) even if URL sync is async
expect(result.current.limit).toBe(42);
});
it('does not override URL when it already has a value', () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
const wrapper = createNuqsWrapper({ limit: '30' }, onUrlUpdate);
const { result } = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'test-table',
calculatedPageSize: 42,
},
),
{ wrapper },
);
act(() => {
jest.runAllTimers();
});
// Limit should stay at 30 (from URL), not change to 42
expect(result.current.limit).toBe(30);
});
it('handles calculatedPageSize changing from null to number', () => {
const wrapper = createNuqsWrapper();
const { result, rerender } = renderHook(
({ calculated }) =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'test-table-2',
calculatedPageSize: calculated,
},
),
{ wrapper, initialProps: { calculated: null as number | null } },
);
// Initially should use explicit default (10)
expect(result.current.limit).toBe(10);
// When calculated becomes available, should update
rerender({ calculated: 42 });
act(() => {
jest.runAllTimers();
});
// Limit should now be 42
expect(result.current.limit).toBe(42);
});
it('keeps user selection when calculatedPageSize changes', () => {
const wrapper = createNuqsWrapper();
const { result, rerender } = renderHook(
({ calculated }) =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'test-table-3',
calculatedPageSize: calculated,
},
),
{ wrapper, initialProps: { calculated: 42 as number | null } },
);
expect(result.current.limit).toBe(42);
// User selects 30
act(() => {
result.current.setLimit(30);
jest.runAllTimers();
});
expect(result.current.limit).toBe(30);
// calculatedPageSize changes (e.g., window resize)
rerender({ calculated: 50 });
act(() => {
jest.runAllTimers();
});
// Should keep user's selection (30), not change to new calculated (50)
expect(result.current.limit).toBe(30);
});
});

View File

@@ -1,199 +0,0 @@
import { ReactNode } from 'react';
import { act, renderHook } from '@testing-library/react';
import { useQueryStates, parseAsInteger } from 'nuqs';
import {
NuqsTestingAdapter,
OnUrlUpdateFunction,
UrlUpdateEvent,
} from 'nuqs/adapters/testing';
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
import { useTableParams } from '../useTableParams';
import { usePreferredPageSizeStore } from '../usePreferredPageSize.store';
function createNuqsWrapper(
queryParams?: Record<string, string>,
onUrlUpdate?: OnUrlUpdateFunction,
): ({ children }: { children: ReactNode }) => JSX.Element {
return function NuqsWrapper({
children,
}: {
children: ReactNode;
}): JSX.Element {
return (
<NuqsTestingAdapter
searchParams={queryParams}
onUrlUpdate={onUrlUpdate}
hasMemory
>
{children}
</NuqsTestingAdapter>
);
};
}
const QUERY_PARAMS_CONFIG = {
orderBy: 'orderBy',
page: 'page',
limit: 'limit',
} as const;
type TableParamsWithCleanup = ReturnType<typeof useTableParams> & {
clearParams: ReturnType<typeof useQueryStates>[1];
};
/**
* Simulates the cleanup pattern used in ListAlertRules:
* - Uses useQueryStates to clear URL params on unmount
*/
function useTableParamsWithCleanup(
storageKey: string,
calculatedPageSize: number | null,
): TableParamsWithCleanup {
const result = useTableParams(QUERY_PARAMS_CONFIG, {
page: 1,
limit: 10,
storageKey,
calculatedPageSize,
});
// This mirrors the cleanup effect in ListAlertRules
const [, setTableQueryParams] = useQueryStates({
[QUERY_PARAMS_CONFIG.orderBy]: parseAsJsonNoValidate(),
[QUERY_PARAMS_CONFIG.page]: parseAsInteger,
[QUERY_PARAMS_CONFIG.limit]: parseAsInteger,
});
// Note: We can't use useEffect cleanup in tests easily, but we can verify
// that calling setTableQueryParams with nulls does clear the URL
return { ...result, clearParams: setTableQueryParams };
}
describe('URL cleanup pattern (simulating ListAlertRules behavior)', () => {
beforeEach(() => {
jest.useFakeTimers();
localStorage.clear();
usePreferredPageSizeStore.setState({ tables: {} });
});
afterEach(() => {
jest.useRealTimers();
});
it('setTableQueryParams with null values should clear URL params', async () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
const wrapper = createNuqsWrapper({}, onUrlUpdate);
const { result } = renderHook(
() => useTableParamsWithCleanup('alert-rules', 42),
{ wrapper },
);
// Set limit to 100
await act(async () => {
result.current.setLimit(100);
jest.runAllTimers();
await Promise.resolve();
});
expect(result.current.limit).toBe(100);
// Verify limit=100 is in URL
const limitAfterSet = onUrlUpdate.mock.calls
.map((call) => call[0].searchParams.get('limit'))
.filter(Boolean)
.pop();
expect(limitAfterSet).toBe('100');
// Simulate cleanup: clear all params
await act(async () => {
void result.current.clearParams({
orderBy: null,
page: null,
limit: null,
});
jest.runAllTimers();
await Promise.resolve();
});
// Verify limit was cleared (last update should have limit=null or removed)
const lastUpdate = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
const finalLimit = lastUpdate[0].searchParams.get('limit');
expect(finalLimit).toBeNull();
});
it('cleanup should work even when limit was set from localStorage preference', async () => {
// Pre-set preference
localStorage.setItem(
'@signoz/table-columns/alert-rules-preferred-page-size',
'100',
);
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
const wrapper = createNuqsWrapper({}, onUrlUpdate);
const { result } = renderHook(
() => useTableParamsWithCleanup('alert-rules', 42),
{ wrapper },
);
await act(async () => {
jest.runAllTimers();
await Promise.resolve();
});
// Should use preferred value
expect(result.current.limit).toBe(100);
// Simulate cleanup
await act(async () => {
void result.current.clearParams({
orderBy: null,
page: null,
limit: null,
});
jest.runAllTimers();
await Promise.resolve();
});
// URL should be cleared
const lastUpdate = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
const finalLimit = lastUpdate[0].searchParams.get('limit');
expect(finalLimit).toBeNull();
});
it('demonstrates the bug: component without cleanup leaves limit in URL', async () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
const wrapper = createNuqsWrapper({}, onUrlUpdate);
// Mount TriggeredAlerts-like component (no cleanup)
const { result, unmount } = renderHook(
() =>
useTableParams(QUERY_PARAMS_CONFIG, {
page: 1,
limit: 10,
storageKey: 'triggered-alerts',
calculatedPageSize: 42,
}),
{ wrapper },
);
// Set limit to 100
await act(async () => {
result.current.setLimit(100);
jest.runAllTimers();
await Promise.resolve();
});
expect(result.current.limit).toBe(100);
// Unmount WITHOUT cleanup
unmount();
// Verify limit=100 is STILL in URL (this is the bug!)
const lastUpdate = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
const finalLimit = lastUpdate[0].searchParams.get('limit');
expect(finalLimit).toBe('100'); // BUG: limit persists after unmount
});
});

View File

@@ -1,385 +0,0 @@
import { ReactNode } from 'react';
import { act, renderHook } from '@testing-library/react';
import {
NuqsTestingAdapter,
OnUrlUpdateFunction,
UrlUpdateEvent,
} from 'nuqs/adapters/testing';
import { useTableParams } from '../useTableParams';
import { usePreferredPageSizeStore } from '../usePreferredPageSize.store';
function createNuqsWrapper(
queryParams?: Record<string, string>,
onUrlUpdate?: OnUrlUpdateFunction,
): ({ children }: { children: ReactNode }) => JSX.Element {
return function NuqsWrapper({
children,
}: {
children: ReactNode;
}): JSX.Element {
return (
<NuqsTestingAdapter
searchParams={queryParams}
onUrlUpdate={onUrlUpdate}
hasMemory
>
{children}
</NuqsTestingAdapter>
);
};
}
describe('useTableParams navigation scenarios', () => {
beforeEach(() => {
jest.useFakeTimers();
localStorage.clear();
usePreferredPageSizeStore.setState({ tables: {} });
});
afterEach(() => {
jest.useRealTimers();
});
describe('Tab navigation: Alert Rules -> Configuration -> Routing Policies', () => {
it('preferred value from one table should NOT leak to URL when navigating away', () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
const wrapper = createNuqsWrapper({}, onUrlUpdate);
// Simulate Alert Rules: user sets limit=100
const alertRules = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit', orderBy: 'orderBy' },
{
page: 1,
limit: 10,
storageKey: 'alert-rules',
calculatedPageSize: 42,
},
),
{ wrapper },
);
// User selects limit=100
act(() => {
alertRules.result.current.setLimit(100);
jest.runAllTimers();
});
expect(alertRules.result.current.limit).toBe(100);
// Verify it's persisted in localStorage
expect(
localStorage.getItem(
'@signoz/table-columns/alert-rules-preferred-page-size',
),
).toBe('100');
// Simulate unmount (user navigates away)
alertRules.unmount();
// At this point, the URL should NOT have limit=100 from alert-rules
// when another component mounts with a different storageKey
});
it('different tables with different storageKeys maintain separate preferences', () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
const wrapper = createNuqsWrapper({}, onUrlUpdate);
// Alert Rules sets limit=100
localStorage.setItem(
'@signoz/table-columns/alert-rules-preferred-page-size',
'100',
);
// Triggered Alerts sets limit=25
localStorage.setItem(
'@signoz/table-columns/triggered-alerts-preferred-page-size',
'25',
);
// Mount Triggered Alerts (simulating tab switch from Alert Rules)
const triggeredAlerts = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit', orderBy: 'orderBy' },
{
page: 1,
limit: 10,
storageKey: 'triggered-alerts',
calculatedPageSize: 42,
},
),
{ wrapper },
);
act(() => {
jest.runAllTimers();
});
// Should use triggered-alerts preference (25), NOT alert-rules (100)
expect(triggeredAlerts.result.current.limit).toBe(25);
});
it('table without storageKey should NOT write preference to URL from another table', () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
// Pre-set alert-rules preference
localStorage.setItem(
'@signoz/table-columns/alert-rules-preferred-page-size',
'100',
);
// Start fresh with NO URL params
const wrapper = createNuqsWrapper({}, onUrlUpdate);
// Mount a table WITHOUT storageKey (simulating a simple table)
const simpleTable = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
// NO storageKey
calculatedPageSize: 42,
},
),
{ wrapper },
);
act(() => {
jest.runAllTimers();
});
// Should use calculated (42), not alert-rules preference (100)
expect(simpleTable.result.current.limit).toBe(42);
});
});
describe('URL cleanup on unmount', () => {
it('URL params should be cleanable by consumer on unmount', () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
const wrapper = createNuqsWrapper({}, onUrlUpdate);
const { result, unmount } = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit', orderBy: 'orderBy' },
{
page: 1,
limit: 10,
storageKey: 'test-cleanup',
calculatedPageSize: 42,
},
),
{ wrapper },
);
// Set some values
act(() => {
result.current.setLimit(50);
result.current.setPage(3);
jest.runAllTimers();
});
// Verify URL was updated
const limitUpdates = onUrlUpdate.mock.calls
.map((call) => call[0].searchParams.get('limit'))
.filter(Boolean);
expect(limitUpdates).toContain('50');
// Unmount (note: useTableParams itself doesn't cleanup URL - consumer should)
unmount();
// Verify the component unmounted (no errors)
expect(true).toBe(true);
});
});
describe('Parallel tables sharing URL params', () => {
it('two tables using same URL params should see same values when URL pre-set', () => {
const wrapper = createNuqsWrapper({ limit: '30' });
const table1 = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'table-1',
calculatedPageSize: 42,
},
),
{ wrapper },
);
const table2 = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 20,
storageKey: 'table-2',
calculatedPageSize: 50,
},
),
{ wrapper },
);
// Both should see URL value (30), not their defaults
expect(table1.result.current.limit).toBe(30);
expect(table2.result.current.limit).toBe(30);
});
it('table mounted after setLimit should see updated URL value', () => {
const wrapper = createNuqsWrapper();
// Table1 mounts first
const table1 = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'table-1',
calculatedPageSize: 42,
},
),
{ wrapper },
);
act(() => {
jest.runAllTimers();
});
expect(table1.result.current.limit).toBe(42);
// Table1 sets limit to 100
act(() => {
table1.result.current.setLimit(100);
jest.runAllTimers();
});
expect(table1.result.current.limit).toBe(100);
// Table2 mounts AFTER table1 set limit=100 in URL
// In test environment, URL state doesn't persist between renderHook calls
// This test documents current behavior - each hook instance is independent
});
});
describe('URL state initialization race conditions', () => {
it('should not write preferred value to URL if URL already has value', () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
// Pre-set preference
localStorage.setItem(
'@signoz/table-columns/test-table-preferred-page-size',
'100',
);
// URL already has limit=30
const wrapper = createNuqsWrapper({ limit: '30' }, onUrlUpdate);
const { result } = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'test-table',
calculatedPageSize: 42,
},
),
{ wrapper },
);
act(() => {
jest.runAllTimers();
});
// Should use URL (30), not preferred (100)
expect(result.current.limit).toBe(30);
// URL should NOT have been overwritten with 100
const limitUpdates = onUrlUpdate.mock.calls
.map((call) => call[0].searchParams.get('limit'))
.filter((v) => v === '100');
expect(limitUpdates).toHaveLength(0);
});
it('URL init effect should write calculated value when URL empty', async () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
const wrapper = createNuqsWrapper({}, onUrlUpdate);
// Mount with no URL params
const { result } = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'table-1',
calculatedPageSize: 42,
},
),
{ wrapper },
);
// Effects run after render, need to flush
await act(async () => {
jest.runAllTimers();
await Promise.resolve();
});
// Should use calculated value
expect(result.current.limit).toBe(42);
// The URL init effect writes to URL asynchronously
// Check that limit is 42 (which it is from the limitDefault calculation)
});
it('consumer cleanup effect is responsible for clearing URL params', () => {
// This test documents that useTableParams does NOT auto-cleanup URL
// Consumer components (like ListAlertRules) must use useEffect cleanup
// to clear URL params when unmounting
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
const wrapper = createNuqsWrapper({}, onUrlUpdate);
const { result, unmount } = renderHook(
() =>
useTableParams(
{ page: 'page', limit: 'limit' },
{
page: 1,
limit: 10,
storageKey: 'table-1',
calculatedPageSize: 42,
},
),
{ wrapper },
);
act(() => {
result.current.setLimit(100);
jest.runAllTimers();
});
expect(result.current.limit).toBe(100);
// Unmount - useTableParams does NOT clear URL
unmount();
// Verify unmount happened without clearing URL
// The last URL update should still have limit=100, not null
const lastUpdate = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
expect(lastUpdate[0].searchParams.get('limit')).toBe('100');
});
});
});

View File

@@ -3,10 +3,8 @@ import TanStackTableText from './TanStackTableText';
export * from './TanStackTableStateContext';
export * from './types';
export * from './useCalculatedPageSize';
export * from './useColumnState';
export * from './useColumnStore';
export * from './usePreferredPageSize.store';
export * from './useTableParams';
/**
@@ -194,67 +192,6 @@ export * from './useTableParams';
* )}
* />
* ```
*
* @example useTableParams — manages pagination state with URL sync and persistence
*
* The `useTableParams` hook handles page, limit, orderBy, and expanded state. It can sync
* to URL params, persist user's page size preference, and auto-calculate page size from
* container height.
*
* **Priority chain for limit**: URL > preferred (localStorage) > calculated > explicit default > 50
*
* ```tsx
* import { useCalculatedPageSize, useTableParams } from 'components/TanStackTableView';
*
* const QUERY_PARAMS = { page: 'page', limit: 'limit', orderBy: 'orderBy' } as const;
*
* function MyTable({ data, columns }) {
* // Auto-calculate page size based on container height
* const { containerRef, calculatedPageSize } = useCalculatedPageSize({ rowHeight: 42 });
*
* // useTableParams options:
* // - storageKey: persists user's page size selection to localStorage
* // - calculatedPageSize: uses this when no URL/preferred value exists
* // - cleanupOnUnmount: clears URL params when component unmounts
* const { page, limit, setLimit, orderBy } = useTableParams(QUERY_PARAMS, {
* page: 1,
* limit: 10,
* storageKey: 'my-table',
* calculatedPageSize,
* cleanupOnUnmount: true,
* });
*
* const paginatedData = useMemo(() => {
* const start = (page - 1) * limit;
* return data.slice(start, start + limit);
* }, [data, page, limit]);
*
* return (
* <div ref={containerRef} style={{ height: '100%' }}>
* <TanStackTable
* data={paginatedData}
* columns={columns}
* enableQueryParams={QUERY_PARAMS}
* pagination={{
* total: data.length,
* calculatedPageSize,
* onLimitChange: setLimit,
* }}
* />
* </div>
* );
* }
* ```
*
* **useTableParams options:**
* - `storageKey`: Persists user's page size to localStorage. When user selects a size
* different from calculated, it's saved. Selecting calculated size clears preference.
* - `calculatedPageSize`: From `useCalculatedPageSize`. Used as default when no URL/preferred.
* - `cleanupOnUnmount`: Clears URL params (page, limit, orderBy, expanded) on unmount.
* Use when navigating away should reset table state.
*
* **Pagination shows "Auto" option** when `calculatedPageSize` is passed, allowing users
* to reset to auto-calculated size.
*/
const TanStackTable = Object.assign(TanStackTableBase, {
Text: TanStackTableText,

View File

@@ -74,7 +74,6 @@ export type TableColumnDef<
min?: number | string;
default?: number | string;
max?: number | string;
ignoreLastColumnFill?: boolean;
};
};
@@ -112,14 +111,6 @@ export type TableRowContext<TData> = {
enableAlternatingRowColors?: boolean;
};
export type AutoPageSizeConfig = {
rowHeight?: number;
headerHeight?: number;
paginationHeight?: number;
minPageSize?: number;
maxPageSize?: number;
};
export type PaginationProps = {
total: number;
defaultPage?: number;
@@ -132,12 +123,6 @@ export type PaginationProps = {
onLimitChange?: (limit: number) => void;
showTotalCount?: boolean;
totalCountLabel?: string;
/**
* Auto-calculated page size for the current container.
* When set, shows as "Auto (N)" option in the page size dropdown.
* Consumer is responsible for calculating this via useCalculatedPageSize.
*/
calculatedPageSize?: number | null;
};
export type TanstackTableQueryParamsConfig = {

View File

@@ -1,76 +0,0 @@
import type { RefObject } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { AutoPageSizeConfig } from './types';
const DEFAULT_ROW_HEIGHT = 36;
const DEFAULT_HEADER_HEIGHT = 36;
const DEFAULT_PAGINATION_HEIGHT = 62;
const MIN_PAGE_SIZE = 5;
const MAX_PAGE_SIZE = 100;
export type UseCalculatedPageSizeResult = {
containerRef: RefObject<HTMLDivElement>;
calculatedPageSize: number | null;
};
export function useCalculatedPageSize(
config?: AutoPageSizeConfig,
): UseCalculatedPageSizeResult {
const containerRef = useRef<HTMLDivElement>(null);
const [calculatedPageSize, setCalculatedPageSize] = useState<number | null>(
null,
);
const rowHeight = config?.rowHeight ?? DEFAULT_ROW_HEIGHT;
const headerHeight = config?.headerHeight ?? DEFAULT_HEADER_HEIGHT;
const paginationHeight = config?.paginationHeight ?? DEFAULT_PAGINATION_HEIGHT;
const minPageSize = config?.minPageSize ?? MIN_PAGE_SIZE;
const maxPageSize = config?.maxPageSize ?? MAX_PAGE_SIZE;
const calculatePageSize = useCallback(
(containerHeight: number): number => {
const availableHeight = containerHeight - headerHeight - paginationHeight;
const rawPageSize = Math.floor(availableHeight / rowHeight);
return Math.min(maxPageSize, Math.max(minPageSize, rawPageSize));
},
[rowHeight, headerHeight, paginationHeight, minPageSize, maxPageSize],
);
useEffect(() => {
if (!containerRef.current) {
return;
}
const container = containerRef.current;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (!entry) {
return;
}
const { height } = entry.contentRect;
if (height > 0) {
const newPageSize = calculatePageSize(height);
setCalculatedPageSize((prev) =>
prev !== newPageSize ? newPageSize : prev,
);
}
});
observer.observe(container);
const { height } = container.getBoundingClientRect();
if (height > 0) {
setCalculatedPageSize(calculatePageSize(height));
}
return (): void => {
observer.disconnect();
};
}, [calculatePageSize]);
return { containerRef, calculatedPageSize };
}

View File

@@ -1,91 +0,0 @@
import get from 'api/browser/localstorage/get';
import set from 'api/browser/localstorage/set';
import remove from 'api/browser/localstorage/remove';
import { create } from 'zustand';
const STORAGE_PREFIX = '@signoz/table-columns/';
const STORAGE_SUFFIX = '-preferred-page-size';
type PreferredPageSizeState = {
tables: Record<string, number | null>;
setPreferredPageSize: (storageKey: string, pageSize: number | null) => void;
};
const getStorageKey = (tableKey: string): string =>
`${STORAGE_PREFIX}${tableKey}${STORAGE_SUFFIX}`;
const loadFromStorage = (tableKey: string): number | null => {
try {
const raw = get(getStorageKey(tableKey));
if (!raw) {
return null;
}
const parsed = parseInt(raw, 10);
return Number.isNaN(parsed) ? null : parsed;
} catch {
return null;
}
};
const saveToStorage = (tableKey: string, pageSize: number | null): void => {
try {
const key = getStorageKey(tableKey);
if (pageSize === null) {
remove(key);
} else {
set(key, String(pageSize));
}
} catch {
// Ignore storage errors
}
};
export const usePreferredPageSizeStore = create<PreferredPageSizeState>()(
(set, get) => ({
tables: {},
setPreferredPageSize: (storageKey, pageSize): void => {
set({ tables: { ...get().tables, [storageKey]: pageSize } });
saveToStorage(storageKey, pageSize);
},
}),
);
export function usePreferredPageSize(
storageKey: string | undefined,
): [number | null, (pageSize: number | null) => void] {
const pageSize = usePreferredPageSizeStore((s) => {
if (!storageKey) {
return null;
}
const cached = s.tables[storageKey];
if (cached !== undefined) {
return cached;
}
return loadFromStorage(storageKey);
});
const setPageSize = usePreferredPageSizeStore((s) => s.setPreferredPageSize);
const setPreferred = (size: number | null): void => {
if (storageKey) {
setPageSize(storageKey, size);
}
};
return [pageSize, setPreferred];
}
export function getPreferredPageSize(storageKey: string): number | null {
// oxlint-disable-next-line signoz/no-zustand-getstate-in-hooks
const state = usePreferredPageSizeStore.getState();
const cached = state.tables[storageKey];
if (cached !== undefined) {
return cached;
}
const stored = loadFromStorage(storageKey);
if (stored !== null) {
state.setPreferredPageSize(storageKey, stored);
}
return stored;
}

View File

@@ -4,7 +4,6 @@ import { parseAsInteger, useQueryState } from 'nuqs';
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
import { SortState, TanstackTableQueryParamsConfig } from './types';
import { usePreferredPageSize } from './usePreferredPageSize.store';
const NUQS_OPTIONS = { history: 'push' as const };
const DEFAULT_PAGE = 1;
@@ -21,15 +20,9 @@ type Defaults = {
limit?: number;
orderBy?: SortState | null;
expanded?: ExpandedState;
/** Storage key for persisting user's page size preference */
storageKey?: string;
/** Auto-calculated page size from container. URL initializes with this when available. */
calculatedPageSize?: number | null;
/** Clear URL params on unmount. Useful when navigating away from table views. */
cleanupOnUnmount?: boolean;
};
export type TableParamsResult = {
type TableParamsResult = {
page: number;
limit: number;
orderBy: SortState | null;
@@ -106,23 +99,15 @@ export function useTableParams(
? (enableQueryParams.expanded ?? URL_KEYS_DEFAULT.expanded)
: URL_KEYS_DEFAULT.expanded;
const pageDefault = defaults?.page ?? DEFAULT_PAGE;
const limitDefault = defaults?.limit ?? DEFAULT_LIMIT;
const orderByDefault = defaults?.orderBy ?? null;
const expandedDefault = defaults?.expanded ?? {};
const storageKey = defaults?.storageKey;
const calculatedPageSize = defaults?.calculatedPageSize;
const cleanupOnUnmount = defaults?.cleanupOnUnmount ?? false;
const expandedDefaultArray = useMemo(
() => expandedStateToArray(expandedDefault),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const [preferredPageSize, setPreferredPageSize] =
usePreferredPageSize(storageKey);
const limitDefault =
preferredPageSize ?? calculatedPageSize ?? defaults?.limit ?? DEFAULT_LIMIT;
const [localPage, setLocalPage] = useState(pageDefault);
const [localLimit, setLocalLimit] = useState(limitDefault);
const [localOrderBy, setLocalOrderBy] = useState<SortState | null>(
@@ -135,71 +120,9 @@ export function useTableParams(
pageQueryParam,
parseAsInteger.withDefault(pageDefault).withOptions(NUQS_OPTIONS),
);
const [urlLimitRaw, setUrlLimitRaw] = useQueryState(
const [urlLimit, setUrlLimit] = useQueryState(
limitQueryParam,
parseAsInteger.withOptions(NUQS_OPTIONS),
);
// Track if URL had limit on initial mount
const hadUrlLimitOnMountRef = useRef<boolean | null>(null);
if (hadUrlLimitOnMountRef.current === null) {
hadUrlLimitOnMountRef.current = urlLimitRaw !== null;
}
const hadUrlLimit = hadUrlLimitOnMountRef.current ?? false;
const urlLimit = urlLimitRaw ?? limitDefault;
// Initialize URL with preferred/calculated when available (only if URL was empty)
const hasInitializedUrlRef = useRef(false);
useEffect(() => {
if (!useUrlForLimit || hasInitializedUrlRef.current || hadUrlLimit) {
return;
}
if (preferredPageSize !== null) {
hasInitializedUrlRef.current = true;
void setUrlLimitRaw(preferredPageSize);
return;
}
if (calculatedPageSize != null) {
hasInitializedUrlRef.current = true;
void setUrlLimitRaw(calculatedPageSize);
}
}, [
useUrlForLimit,
calculatedPageSize,
preferredPageSize,
hadUrlLimit,
setUrlLimitRaw,
]);
// Wrapped setLimit that persists preference when different from calculated
const setUrlLimit = useCallback(
(newLimit: number): void => {
if (storageKey) {
if (newLimit !== calculatedPageSize) {
setPreferredPageSize(newLimit);
} else {
setPreferredPageSize(null);
}
}
void setUrlLimitRaw(newLimit);
},
[storageKey, calculatedPageSize, setPreferredPageSize, setUrlLimitRaw],
);
const setLocalLimitWithPersist = useCallback(
(newLimit: number): void => {
if (storageKey) {
if (newLimit !== calculatedPageSize) {
setPreferredPageSize(newLimit);
} else {
setPreferredPageSize(null);
}
}
setLocalLimit(newLimit);
},
[storageKey, calculatedPageSize, setPreferredPageSize],
parseAsInteger.withDefault(limitDefault).withOptions(NUQS_OPTIONS),
);
const [urlOrderBy, setUrlOrderBy] = useQueryState(
orderByQueryParam,
@@ -232,7 +155,7 @@ export function useTableParams(
typeof updaterOrValue === 'function'
? updaterOrValue(urlExpandedRef.current)
: updaterOrValue;
void setUrlExpandedArray(expandedStateToArray(newState));
setUrlExpandedArray(expandedStateToArray(newState));
},
[setUrlExpandedArray],
);
@@ -249,53 +172,21 @@ export function useTableParams(
[],
);
const orderByDefaultMemoKey = `${orderByDefault?.columnName}${orderByDefault?.order}`;
const orderByUrlMemoKey = `${urlOrderBy?.columnName}${urlOrderBy?.order}`;
const prevOrderByRef = useRef<string | null>(null);
useEffect(() => {
// Only reset page when orderBy actually changes, not on initial mount
if (
prevOrderByRef.current !== null &&
prevOrderByRef.current !== orderByUrlMemoKey
) {
if (useUrlForPage) {
void setUrlPage(pageDefault);
} else {
setLocalPage(pageDefault);
}
if (useUrlForPage) {
setUrlPage(pageDefault);
} else {
setLocalPage(pageDefault);
}
prevOrderByRef.current = orderByUrlMemoKey;
}, [useUrlForPage, orderByUrlMemoKey, pageDefault, setUrlPage]);
useEffect(() => {
if (!cleanupOnUnmount) {
return;
}
return (): void => {
if (useUrlForPage) {
void setUrlPage(null);
}
if (useUrlForLimit) {
void setUrlLimitRaw(null);
}
if (useUrlForOrderBy) {
void setUrlOrderBy(null);
}
if (useUrlForExpanded) {
void setUrlExpandedArray(null);
}
};
}, [
cleanupOnUnmount,
useUrlForPage,
useUrlForLimit,
useUrlForOrderBy,
useUrlForExpanded,
orderByDefaultMemoKey,
orderByUrlMemoKey,
pageDefault,
setUrlPage,
setUrlLimitRaw,
setUrlOrderBy,
setUrlExpandedArray,
]);
return {
@@ -304,7 +195,7 @@ export function useTableParams(
orderBy: (useUrlForOrderBy ? urlOrderBy : localOrderBy) as SortState | null,
expanded: useUrlForExpanded ? urlExpanded : localExpanded,
setPage: useUrlForPage ? setUrlPage : setLocalPage,
setLimit: useUrlForLimit ? setUrlLimit : setLocalLimitWithPersist,
setLimit: useUrlForLimit ? setUrlLimit : setLocalLimit,
setOrderBy: useUrlForOrderBy ? setUrlOrderBy : setLocalOrderBy,
setExpanded: useUrlForExpanded ? setUrlExpanded : handleSetLocalExpanded,
};

View File

@@ -2,7 +2,6 @@ import type { CSSProperties, ReactNode } from 'react';
import type { ColumnDef } from '@tanstack/react-table';
import { RowKeyData, TableColumnDef } from './types';
import { ComboboxSimpleItem } from '@signozhq/ui/combobox';
export const getColumnId = <TData>(column: TableColumnDef<TData>): string =>
column.id;
@@ -35,7 +34,7 @@ export const getColumnWidthStyle = <TData>(
isLastColumn?: boolean,
): CSSProperties => {
// Last column always fills remaining space
if (isLastColumn && column?.width?.ignoreLastColumnFill !== true) {
if (isLastColumn) {
return {
width: '100%',
minWidth: persistedWidth ?? column?.width?.min,
@@ -146,31 +145,3 @@ export function buildTanstackColumnDef<TData>(
},
};
}
const DEFAULT_PAGE_SIZES = [10, 20, 30, 50, 100];
export function buildPageSizeItems(
calculatedSize?: number | null,
): ComboboxSimpleItem[] {
const items: ComboboxSimpleItem[] = [];
if (calculatedSize) {
items.push({
value: calculatedSize.toString(),
label: `Auto (${calculatedSize})`,
displayValue: calculatedSize.toString(),
});
}
for (const size of DEFAULT_PAGE_SIZES) {
if (size !== calculatedSize) {
items.push({
value: size.toString(),
label: size.toString(),
displayValue: size.toString(),
});
}
}
return items;
}

View File

@@ -1,7 +1,11 @@
import { useMemo } from 'react';
import { SolidAlertTriangle } from '@signozhq/icons';
import { Select, Tooltip } from 'antd';
import type { DefaultOptionType } from 'antd/es/select';
import {
ComboboxSimple,
ComboboxSimpleGroup,
ComboboxSimpleItem,
} from '@signozhq/ui/combobox';
import { Tooltip } from 'antd';
import classNames from 'classnames';
import { UniversalYAxisUnitMappings } from './constants';
@@ -42,69 +46,53 @@ function YAxisUnitSelector({
return '';
}, [initialValue, value, loading]);
const handleSearch = (
searchTerm: string,
currentOption: DefaultOptionType | undefined,
): boolean => {
if (!currentOption?.value) {
return false;
}
const categoriesToRender = useMemo(
() => categoriesOverride || getYAxisCategories(source),
[categoriesOverride, source],
);
const search = searchTerm.toLowerCase();
const unitId = currentOption.value.toString().toLowerCase();
const unitLabel = currentOption.children?.toString().toLowerCase() || '';
const groups: ComboboxSimpleGroup[] = useMemo(
() =>
categoriesToRender.map((category) => ({
heading: category.name,
items: category.units.map((unit): ComboboxSimpleItem => {
const aliases = Array.from(
UniversalYAxisUnitMappings[unit.id as UniversalYAxisUnit] ?? [],
);
return {
value: unit.id,
label: unit.name,
keywords: aliases,
};
}),
})),
[categoriesToRender],
);
// Check label and id
if (unitId.includes(search) || unitLabel.includes(search)) {
return true;
}
// Check aliases (from the mapping) using array iteration
const aliases = Array.from(
UniversalYAxisUnitMappings[currentOption.value as UniversalYAxisUnit] ?? [],
);
return aliases.some((alias) => alias.toLowerCase().includes(search));
const handleChange = (val: string | string[]): void => {
onChange(val as UniversalYAxisUnit);
};
const categoriesToRender = useMemo(() => {
return categoriesOverride || getYAxisCategories(source);
}, [categoriesOverride, source]);
return (
<div
className={classNames('y-axis-unit-selector-component', containerClassName)}
>
<Select
showSearch
value={universalUnit}
onChange={onChange}
<ComboboxSimple
value={universalUnit ?? undefined}
onChange={handleChange}
placeholder={placeholder}
filterOption={(input, option): boolean => handleSearch(input, option)}
loading={loading}
suffixIcon={
incompatibleUnitMessage ? (
<Tooltip title={incompatibleUnitMessage}>
<SolidAlertTriangle role="img" aria-label="warning" size="md" />
</Tooltip>
) : undefined
}
className={classNames({
'warning-state': incompatibleUnitMessage,
'warning-state': !!incompatibleUnitMessage,
})}
data-testid={dataTestId}
allowClear
>
{categoriesToRender.map((category) => (
<Select.OptGroup key={category.name} label={category.name}>
{category.units.map((unit) => (
<Select.Option key={unit.id} value={unit.id}>
{unit.name}
</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
testId={dataTestId}
groups={groups}
/>
{incompatibleUnitMessage && (
<Tooltip title={incompatibleUnitMessage}>
<SolidAlertTriangle role="img" aria-label="warning" size="md" />
</Tooltip>
)}
</div>
);
}

View File

@@ -1,5 +1,8 @@
.y-axis-unit-selector-component {
.ant-select {
--combobox-trigger-background-color: var(--l2-background);
--combobox-trigger-border-color: var(--l2-border);
[data-slot='combobox-trigger'] {
width: 220px;
}
}

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} />
)}

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